The Scrawl-canvas scene graph

Scrawl-canvas works by generating a retained mode description – an object model of graphical primitives (called entitys) – for a scene which displays in a <canvas> element. SC builds this scene graph using Group and Cell objects, alongside entity objects which get gathered into the Group objects that have been created for the scene.

tl;dr: SC is not a game engine! The SC scene graph does not use the classic tree structure approach to build out a top-down hierarchy of layers and nodes to describe the scene. Rather, SC uses a more bottom-up approach to creating the scene graph where entity objects control how, where and when they will appear in the canvas display. Using a tree structure for the scene graph may have been a more efficient design choice, but SC is not a game engine!

Dev-users should be aware that this – somewhat different – approach may take a bit of getting used to but, once the concepts are in place, it should be relatively simple to work with.

SC scene graph hierarchy

The following code creates a canvas display with this output. Note that the code is creating a deliberately complex scene graph, for demonstration purposes; few of the test demos generate scene graphs as complex as this one:

Code output

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="ie=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
      canvas { margin: 1em auto; }
    </style>
  </head>
  <body>
    <canvas 
      id="my-canvas"
      width="600"
      height="400"
      data-scrawl-canvas
      data-base-background-color="slategray"
    ></canvas>

    <script type="module">
      // Import SC
      import * as scrawl from 'scrawl-canvas';

      // Get a handle to the Canvas artefact
      const canvas = scrawl.findCanvas('my-canvas');

      // Namespacing boilerplate
      const namespace = `${canvas.name}-placehold`;
      const name = (n) => `${namespace}-${n}`;

      // The Canvas artefact includes a hidden Cell, used as a Pattern style by an entity
      canvas.buildCell({
          name: name('my-hidden-cell'),
          shown: false,
          dimensions: [80, 80],
          backgroundColor: 'lightgray',
      });

      // A yellow Block in the hidden Cell
      scrawl.makeBlock({
          name: name('yellow-block'),
          group: name('my-hidden-cell'),
          dimensions: ['50%', '50%'],
          start: ['center', 'center'],
          handle: ['center', 'center'],
          roll: 30,
          fillStyle: 'yellow',
          lineWidth: 2,
          method: 'fillThenDraw',
      });

      // An extra Cell that will appear towards the bottom-right of the canvas display
      canvas.buildCell({
          name: name('my-extra-cell'),
          dimensions: ['50%', '50%'],
          start: ['70%', '70%'],
          handle: ['50%', '50%'],
          roll: 45,
          backgroundColor: 'aliceblue',
      });

      // The extra Cell has an additional Group as well as its namesake Group
      scrawl.makeGroup({
          name: name('my-additional-group'),
          host: name('my-extra-cell'),
      });

      // Part of the additional Group
      scrawl.makeBlock({
          name: name('blue-block'),
          group: name('my-additional-group'),
          dimensions: ['100%', '60%'],
          offsetY: 20,
          fillStyle: 'blue',
          lineWidth: 2,
          method: 'fillThenDraw',
      });

      // For this scene, the extra Cell's namesake Group
      // needs to compile after its additional Group
      scrawl.findGroup(name('my-extra-cell')).set({ order: 1 });

      // Part of the extra Cell's namesake Group
      scrawl.makeBlock({
          name: name('red-block'),
          group: name('my-extra-cell'),
          pivot: name('blue-block'),
          lockTo: 'pivot',
          dimensions: ['40%', '40%'],
          fillStyle: 'red',
          lineWidth: 2,
          method: 'fillThenDraw',
      });

      // Part of the extra Cell's namesake Group
      // - using the hidden Cell as its fillStyle value
      scrawl.makeBlock({
          name: name('last-block'),
          group: name('my-extra-cell'),
          fillStyle: name('my-hidden-cell'),
          dimensions: ['75%', '75%'],
          start: ['25%', '25%'],
          lineWidth: 2,
          method: 'fillThenDraw',
      });

      // Part of the Canvas artefact's base Cell's namesake Group
      scrawl.makeBlock({
          name: name('orange-block'),
          dimensions: ['40%', '40%'],
          start: ['30%', '30%'],
          handle: ['center', 'center'],
          fillStyle: 'orange',
          lineWidth: 2,
          method: 'fillThenDraw',
      });

      // Part of the Canvas artefact's base Cell's namesake Group
      scrawl.makeBlock({
          name: name('brown-block'),
          dimensions: ['30%', '30%'],
          pivot: name('orange-block'),
          lockTo: 'pivot',
          handle: ['center', 'center'],
          fillStyle: 'brown',
          lineWidth: 2,
          method: 'fillThenDraw',
      });

      canvas.render();
    </script>
  </body>
</html>

The scene graph for the above code looks like this:

- Canvas artefact 'my-canvas'
  |
  |- base Cell object 'my-canvas_base'
  |  |
  |  |- namesake Group object 'my-canvas_base'
  |     |
  |     |- Block entity 'orange-block'
  |     |- Block entity 'brown-block' (position dependency on 'orange-block')
  |
  |- hidden Cell object 'my-hidden-cell'
  |  |
  |  |- namesake Group object 'my-hidden-cell'
  |     |
  |     |- Block entity 'yellow-block'
  |
  |- extra Cell object 'my-extra-cell'
     |
     |- additional Group object 'my-additional-group'
     |  |
     |  |- Block entity 'blue-block'
     |
     |- namesake Group object 'my-extra-cell'
        |
        |- Block entity 'red-block' (position dependency on 'blue-block')
        |- Block entity 'last-block' (style dependency on 'my-hidden-cell')

The scene graph described above demonstrates these properties:

SC Group objects

SC uses Group objects for a range of functionalities across the repo code base:

tl;dr: Group objects represent a collection of SC artefact (including entity) objects – and that is all they are!

Most of the code relating to Group object functionality can be found in the following files:

Create, serialize, clone and kill Group objects

For the most part, Group objects act like regular SC objects, with a few quirks …

Create

A new Group object can be created using the scrawl.makeGroup({key: value, ...}) factory function:

Serialize

Group objects can be serialized using the group.saveAsPacket() function. The serialized packet will include all the currently associated artefact/entity object name strings in the artefacts attribute, but will not clone the artefacts themselves.

(TODO: Repo-devs need to review how Group serialization should work with respect to associated artefacts and entitys – should there be a mechanism in the functionality to tell the group.saveAsPacket() invocation to serialize associated objects at the same time as it serializes the Group object?)

Clone

Group objects can be created from another Group by cloning: group.clone({key: value, ...}).

The cloned Group object will include the currently associated artefact/entity object name strings in the artefacts attribute, but will not clone those objects themselves.

This may lead to issues if the Group is involved in the Display cycle as those objects will get stamped twice: once through association with the original Group, and again through association with the clone Group.

Kill

Use the group.kill() function. Unlike (most) other SC objects, the Group kill function can take 1-2 boolean arguments which, when set to true, will also kill all the artefact/entity objects currently associated with the group:

Group objects also include functionality to kill all their currently associated artefact/entity objects while keeping the Group object itself intact:

Group discovery in the SC library, and beyond

Group objects are tracked in the SC library, in the library.group section. If the dev-user needs to retrieve a handle to a Group object, and they know the object's name attribute, they can do this using scrawl.findGroup('name-string').

SC also includes functionality for the dev-user to retrieve Group objects via their Stack/Canvas artefact host object, and from Cell objects:

Add Group objects to Stack/Cell objects, and remove them

There's three approaches to associating a Group object with a Stack artefact or Cell object. The simplest method is during Group instantiation, by including a host attribute in the scrawl.makeGroup() factory function's argument object.

Dev-users can also use the stack.addGroup(...items) and cell.addGroup(...items) functions, where either the name attribute strings of the Group objects, or the Group objects themselves, are used as the function's arguments. Similarly, remove Group objects using the stack.removeGroup(...items) and cell.removeGroup(...items) functions.

SC recommends that dev-users avoid the stack.set({ group }), cell.set({ group }) options because: only one group can be handled using this approach; and the operation will clear out all other associated Groups (including the namesake Group) before associating the new Group object.

Group visibility, order and sorting

The following only applies to Group objects associated with Cell objects that are part of the Display cycle.

Group objects include a visibility attribute which, when set to false, will cause the Display cycle to skip over processing all of the artefacts/entitys associated with that Group – they will not recalculate their state, and they will not be stamped into the final Canvas display.

Group objects also include an order attribute, which should be set to a positive integer value (default: 0). Entitys associated with a Group will all be batch processed on a per-group basis, with the artefacts in a Group with a lower order value completing their processing before the next group of entitys start their processing.

Cell objects store the name strings of the Group objects associated with them in an Array keyed to their cell.groups attribute. As part of any sorting operation, the Cell object will retrieve the Group objects from the SC library, sort these objects in ascending order values and then store references to the now sorted Group objects in an internal cell.groupBucket Array. The groups Array should never itself be sorted as this represents the order in which Groups get associated with the Cell – which is itself determined in the the code written by dev-users.

Cell objects only re-sort their Group objects when they have to:

Whenever any of these things happen, the Cell object's batchResort flag will be set to true, thus triggering a re-sort on the next Display cycle. SC uses a simple bucket sort algorithm for the sorting operation. Much of this functionality gets defined in the mixin/cascade.js file.

(Repo-dev note: current functionality is that when a Group object's order value changes – via a group.set({order: newValue}) invocation – the Group will only signal the change to its current host Cell object. This may cause unexpected outcomes (edge cases) for more complex dev-user projects and may need to be revisited at some point.)

(Repo-dev note: Group object visibility for Stack Element artefacts has not been investigated or tested, even though the functionality is – in theory – present. Needs review.)

Add artefact/entity objects to Group objects, and remove them

Artefact/entity objects can be added to, and removed from, a Group object after it has been created by using the following functions:

These operations have to be invoked on the Group object itself; SC does not supply convenience functions for the Stack artefact or Cell object to feed through arguments to their namesake Group objects.

Dev-users can retrieve a specified artefact/entity object from a Group object using the group.getArtefact('name-string') function.

Artefact sorting

The functionality previously described for Group ordering and sorting within Cell objects also applies to sorting artefact/entity objects within Group objects:

Group objects only sort their artefact/entity objects when they have to, signalled through the group.batchResort flag. This flag gets set to true when:

SC uses a bucket-sort algorithm to perform these sort operations. Both sorts are handled in a single internal function – group.sortArtefacts() – defined in the factory/group.js file.

(Repo-dev note: current functionality is that when an artefact/entity object's calculateOrder or stampOrder value changes, the artefact/entity will only signal the change to its current host Group object. This may cause unexpected outcomes (edge cases) for more complex dev-user projects and may need to be revisited at some point.)

Batch update artefact/entity object attributes using Group object functions

Any SC tracked object (an object that inherits functionality from the mixin/base.js file) can have its attributes updated at any time using its .set({key: value, ...}) and .setDelta({key: value, ...}) functions.

SC Group objects include functions that allow such updates to be applied to all of their artefact/entity objects in a single invocation:

tl;dr: The difference between set and setDelta is:

Delta manipulations

SC delta animation is explained in the Animation and Display cycle page of this Runbook. Group objects can be used to trigger delta updates across all of their associated artefact/entity objects using the following functions – even when delta animation for that artefact/entity object has been disabled:

Artefact class manipulations

Specifically for associated artefact objects, dev-users can add or remove CSS class labels to/from those objects' DOM elements using the following Group object functions – note that the function argument is a String of space-separated classNames:

The actual update to the DOM elements doesn't happen straight away. Instead a dirtyClasses flag is set to true, which then gets actioned during the show operation of the Display cycle.

Stack artefact and Cell object equivalent functionality

The mixin/cascade.js file, consumed by the Stack and Cell factory files, provides an equivalent set of manipulation functions which the dev-user can invoke on Stack artefact and Cell object instances. The functions pass their argument through to all of the Group objects currently associated with that Stack or Cell:

Apply visual filters to Groups containing entity objects

SC filters can be applied to entity objects at the Cell, Group and entity object level of the Display cycle. See test demo Canvas-007 for an example of this functionality in action. Details about filter operations can be found in the SC filter engine page of this Runbook.

If the same set of filters need to be applied to several entity objects at the same time, those objects can be gathered together into their own Group object for processing. However dev-users should be aware of the limitations surrounding this approach:

Note that while the Group object filtering functionality is very similar to Cell object and entity object functionality, it differs from them in several small-yet-key areas. Repo-devs need to be aware that changes in filter functionality in one of these three areas of the code base may need to be reflected in the other two areas.

Cell entity, and Stack artefact, end-user interactions

Group objects come with a set of attributes and functions which, for end-users interacting with a Stack or Canvas element in a desktop (screen + mouse) environment, can quickly detect when the end-user's cursor is hovering over an Element artefact or entity object and take actions accordingly to change the Stack/Canvas display. Note that once these attributes are set, SC will handle the associated functionality automatically for the dev-user:

For dev-user convenience, these attributes can also be set via the Canvas artefact and Cell objects:

Beyond hovering, dev-users can obtain a list of entitys currently under the cursor's location by invoking the following functions. The function argument will generally be an appropriate here object (see below for details on here objects):

SC Cell objects

SC Cell objects wrap HTML <canvas> elements, and add functionality for managing and manipulating that canvas display. SC Canvas artefact objects also wrap HTML <canvas> elements, with added functionality.

To understand the SC ecosystem, developers need to understand the differences between the SC Canvas and Cell objects:

                                             Canvas artefact        Cell object
--------------------------------------------------------------------------------------------
<canvas> element is part of the DOM?         Yes                    No
<canvas> element can be styled with CSS?     Yes                    No
Events can be added to <canvas> element?     Yes                    No
Responsible for accessibility behaviour?     Yes                    No
Responsible for responsiveness behaviour?    No                     Yes
Involvement in the SC Display cycle?         Final output only      Everything else
Display can be directly manipulated?         No                     Yes
Display can be filtered?                     CSS/SVG filters        CSS/SVG and SC filters

tl;dr: When SC is instructed to manage a <canvas> element, it will generate the following:

Most SC functionality revolves around the base Cell, with the display canvas limited to acting as the final destination for the base Cell's graphical output. By taking most canvas manipulation functionality away from the DOM <canvas> element, SC is able to achieve significant improvements in canvas-related performance, while minimising the impact of canvas-based displays and animations on web page performance.

Note that the hidden <canvas> elements that SC generates as part of its work are just normal <canvas> elements, created using the browser's document.createElement('canvas') function. It is because of this reliance on access to the document object (alongside a number of other things such as the SC event system requiring access to the web page DOM) that SC is limited, by design, to work only in the frontend browser.

Types of Cell objects

SC uses hidden <canvas> elements for a variety of different purposes, and codes them up in different ways:

Base Cells

Base Cell objects acts in concert with their display canvas; SC creates a base Cell every time it wraps a <canvas> element into a Canvas artefact wrapper and assigns the Cell object to the canvas.base attribute. The base Cell is the default painting area for the display canvas and, at the end of each Display cycle, copies itself into the display canvas.

Base Cells share a lot of functionality with layer Cells, with code defined in the factory/cell.js and mixin/cell-key-functions.js files.

Dev-users are strongly advised to not interfere with base Cell objects, unless they enjoy frustration!

Pool Cells

SC maintains a pool of hidden <canvas> elements, wrapped in CellFragment objects, for various internal purposes. These pool Cells are not tracked in the SC library, and are not available for dev-user use.

CellFragment objects contain only the minimum functionality required so the Cells can perform their various jobs around the code base. The code for CellFragment objects, alongside the pool infrastructure, can be found in the untracked-factory/cell-fragment.js file, with additional, shared functionality defined in the mixin/cell-key-functions.js file.

SC uses pool Cell objects extensively through the code base. Repo-devs retrieve a Cell from the pool using the requestCell() function, and return it using the releaseCell(object) function. When returned to the pool, the CellFragment objects and their <canvas> elements will be cleaned back into a default state. Note that failing to return a pool Cell will lead to memory leaks:

Layer Cells

A canvas scene can include multiple Cell objects alongside the base Cell. Each of these layer Cell objects will include a namesake Group object with which entity objects can associate themselves, to be included in the layer's output which will be stamped onto the base Cell as part of the show operation of the Display cycle.

Dev-users can add a new layer Cell to a canvas at any time using the canvas.buildCell({key: value, ...}) function. Be aware that the base Cell will stamp its own entitys into its display before stamping the layer Cell output on top.

Layer Cells are highly versatile containers. Dev-users can:

Create, serialize, clone and kill Cell objects

SC handles the creation of base and pool Cells internally, as required.

Dev-users can create new layer Cell objects using the canvas.buildCell({key: value, ...}) function. While layer Cells can, in theory, be moved between Canvas artefacts, Repo-devs currently work on the assumption that layers will be created for a specific canvas display and will not be transferred or shared between displays.

Cell objects cannot, at this time, be serialized or cloned. Repo-devs need to address serialization work at some point.

For the kill functionality, Cell objects will check Canvas artefact objects to break any associations, and also check through all entity objects to make sure any fillStyle and strokeStyle references to the Cell get set back to default values. The Cell's namesake Group's kill function is then invoked with no arguments: terminating the Cell object will not terminate any entitys associated-by-proxy with that Cell.

Cell discovery in the SC library, and beyond

Base and layer Cell objects are tracked objects in the SC library. They have their own section – library.cell – but also get included in the library.asset section. For this reason, Cell object and Asset/Gradient/Pattern object names should never clash.

Dev users can retrieve Cell objects from the library (as long as they know the object's name) using the scrawl.findCell('name-string') function, and also through the scrawl.findAsset('name-string') function.

Base Cell objects can also be retrieved using the Canvas artefact object canvas.getBase() function. If only the base Cell object's name is required, use canvas.get('baseName').

For dev-user convenience, base Cell object attributes can be updated using the canvas.setBase({key: value, ...}) and canvas.deltaSetBase({key: value, ...}) function (with apologies for the function naming discrepancy here).

Pool Cell objects are untracked objects – repo-devs can get a pool Cell using the requestCell() function and return it to the pool using the releaseCell(object) function.

The Cell engine

When the Cell object wraps a <canvas> element, it will register handles to both the DOM element, and the element's CanvasRenderingContext2D Interface:

tl;dr: Note that while the canvas specification refers to the CanvasRenderingContext2D object as context, many articles and explainers across the internet (including the MDN reference pages) will call it ctx. In the SC repo code base, this object is always referred to as: engine.

Much of the functionality of SC revolves around translating the SC scene graph into Canvas API instructions which then get invoked on the Cell element using the Cell engine's functions.

Cell dimensions

Cell dimensions are set on the cell.element DOM element, using that element's width and height attributes. Dev-users should never need to set these attributes directly!

Internally, Cell objects keep dimension details in the cell.dimensions attribute, whose value is an Array comprising of [width, height] data. These values can be either absolute Number values, or relative 'string%' values, where the value is a percentage of either the host Canvas artefact's dimensions, or (for layer Cells) the base Cell object's dimensions.

Cell objects have enforced minimum dimensions of [1, 1] – the Canvas API's functionality will generally throw errors whenever it encounters a <canvas> element whose width or height is less than 1px.

These values will be recalculated into pixel values each time the dimensions change, with the results stored in the cell.currentDimensions attribute – another Array. Recalculation gets triggered whenever the SC signals system sets the Cell object's cell.dirtyDimensions flag to true.

For base and pool Cell objects, all of this functionality is automated by SC.

Layer Cells get created by dev-users, thus setting their dimensions is a dev-user responsibility. For the most part layer Cells act like Element artefacts or Block entitys with respect to dimensions management. Cell dimensions values – which can also be set individually using the width and height pseudo-attributes – can be:

These dimensions values can be changed at any time using the cell.set({...}) or cell.setDelta({...}) functions:

Cell backgrounds

For the display canvas, the <canvas> element background can be set in the same way as for any other DOM element, using normal CSS styling. SC does not control, nor does it care about, this setting.

SC pool Cells play no direct part in the Display cycle, thus don't need to concern themselves with backgrounds.

For layer and base Cells, the Cell background is set as part of the Display cycle clear operation. Depending on the values of the Cell object attributes, it will be set as follows:

See test demo Canvas-002 for an example of this functionality.

The backgroundColor and clearAlpha attributes can be set for base and layer Cells in the normal way, using the cell.set({...}) function.

Dev-users can also set the base background color via HTML by adding the data-base-background-color="color-value" and data-base-clear-alpha="number" attributes to the <canvas> markup.

Cell display manipulation – the splitShift() function

SC offers a function – cell.splitShift() which gives direct access to a Cell object's engine to perform an animation effect where a given number of rows/columns on one side of the display get copied over to the other side of the display, with the remaining part of the display shifted to take up the vacated space. Code for this function can be found in the factory/cell.js file.

Dev-users wanting to use this functionality should be aware that it works best on layer Cell objects whose cleared and compiled flags have been set to false. Example test demos include Canvas-069 and Canvas-070.

Cell data manipulation – the getCellData() and paintCellData() functions

SC includes two Cell functions – cell.getCellData() and cell.paintCellData() – which give dev-users direct access to a Cell object's pixel data.

The getCellData(opaqueFlag = false) function returns an object with two attributes:

{
  // Index pointer (positive integer number) into the iData.data array 
  index

  // Pixel color channel values 
  // + integer numbers between 0 and 255
  red
  green
  blue
  alpha

  // The pixel's cardinal coordinates, measured in pixels
  // + from the display's top-left corner
  row
  col

  // The pixel's polar coordinates from the display's center
  // + distance measured in pixels; angle measured in turns (0.0 - 1.0)
  // + with angle 0 directly above the center (clock 12, or due north)
  distance
  angle
}

Dev users can then manipulate the pixelState object – for instance, run functions across the pixelState data to change their color channel values – and then apply them to the Cell display using the paintCellData(obj) function. Note that the object returned by getCellData must be the argument supplied to paintCellData.

Code for this function can be found in the factory/cell.js file. Dev-users wanting to use this functionality should be aware that it works best on layer Cell objects whose cleared and compiled flags have been set to false. Example test demos include Canvas-071, Canvas-072 and Canvas-073.

Display cycle considerations

Cell objects are central to the SC Display cycle, which is covered in detail in the Animation and Display cycle page of this Runbook.

Cell objects react to the Display cycle in different ways:

All code for Cell Display cycle functionality can be found in the factory/cell.js file.

Cell clear functionality

Cell objects will clear their display using different functions, dependant on the settings of their backgroundColor and clearAlpha attribute values. SC imposes a hierarchy on which clear method gets used as follows:

Dev-users can stop a Cell object clearing itself by setting the Cell's cleared flag to false.

Cell compile functionality

The role of Cell objects during the compile operation is to instruct its associated Group objects to tell their associated entity objects to stamp themselves into the Cell. Dev-users can stop a Cell object compiling itself by setting the Cell's compiled flag to false.

The order in which a Cell object compiles can become important for the final display <canvas> output. Canvas artefacts sort Cell objects in ascending order according to their compileOrder attributes. By default:

Layer Cell show functionality

Layer Cell functionality includes the ability to stamp themselves onto base Cell displays during the Display cycle show operation. The order in which layer Cells stamp themselves is determined by their showOrder attribute. By default layer Cells will display; dev-users can prevent this by setting the Cell's shown attribute to false.

Each layer Cell can be positioned in, and animated across, their base Cell just like artefact objects in Stacks and entity objects in Cells – see the positioning system page in this Runbook for details, and test demo Canvas-036 for a working example.

Note that layer Cells cannot be dragged-and-dropped like entity objects. Instead they need to be pivoted to an entity object which can be dragged. Repo-devs are welcome to investigate this issue further and – perhaps – come up with a better solution.

Base Cell show functionality

SC includes a (rough) emulation of CSS object-fit functionality, which gets actioned when the base Cell stamps itself into its display <canvas> as the last step in the Display cycle. The base Cell reads the Canvas artefact object's fit attribute and positions itself into the display accordingly. This functionality is defined in the factory/cell.js file – specifically the show() function.

Cell opacity, composition and filters

When a layer Cell object stamps itself into the base Cell, and the base Cell stamps itself into the display <canvas>, they will take into account their globalAlpha, globalCompositeOperation, filter and filters attributes.

Cell interactions

Scrawl artefact objects can include a here object, which includes real-time-updated details of the environment in which the artefact's DOM element lives. Further details of the here object can be found in the User interaction and the here object section of the "Artefacts and the DOM" page in this Runbook.

Cell objects have a similar object – cell.here – which, when populated with data, will include a much smaller set of attributes:

{
// Current Cell dimensions
  w: Number – Cell element width (px)
  h: Number – Cell element height (px)

// Current cursor position relative to Cell element top-left corner
  x: Number – cursor x position 
  y: Number – cursor y position 

  active: Boolean – is the cursor over the Cell element?
}

For base Cells, the here object will be automatically updated at the start of every Display cycle by the Cell object's host Canvas artefact. The code for this update can be found in the Cell factory file's cell.updateBaseHere() function.

For layer Cells, the update process is manual – dev-users will need to include a call to the Cell object's cell.updateHere() function whenever they need up-to-date data. Examples of this can be found in the test demos Canvas-039 and Canvas-059.

Pool Cells never get shown in <canvas> displays, thus never need to have their cell.here objects updated.

Cell, and entity, state

The Cell engine (which is the <canvas> element's CanvasRenderingContext2D object) consists of a set of properties (attributes) that describe the engine's current state, and a group of methods (functions) for manipulating that state as well as performing painting operations on the element's drawing surface.

The engine is an immediate mode object. Setting an engine property value – for instance: engine.strokeStyle = 'red' – will be actioned immediately; every change requires some time to complete. While a single update is insignificant in itself, many such changes during a Display cycle have the potential to impact canvas rendering speed resulting in a sub-optimal performance.

To help manage this issue, SC supplies every Cell object with a virtual State object which keeps track of the engine object's properties. Entity objects also have a state object containing the values the entity requires the engine to have when it stamps itself onto the Cell. Rather than apply those values directly to the engine, SC will perform a bespoke diff operation before every entity stamp and only update those engine properties that need to be updated.

Create, serialize, clone and kill State objects

State objects are untracked, thus not referenced in the SC library. They get created at the same time as their Cell or entity object instantiates, and they will be serialized, cloned and killed alongside their host objects. This functionality is all handled internally by SC.

The State object's factory code can be found in the untracked-factory/state.js file. Note that the diff function lives on the State object – state.getChanges(). The code to apply diff changes to the Cell object's engine can be found in the mixin/cell-key-functions.js file, in the cell.setEngine(entity) function.

State object attributes

State objects track the following engine properties:

Property Type Default
------------------------------------------------------------ ------------------------- -----------------------------
Fills and Strokes
fillStyle (various) 'rgb(0 0 0 / 1)'
strokeStyle (various) 'rgb(0 0 0 / 1)'
------------------------------------------------------------ ------------------------- -----------------------------
Composition
globalAlpha Number 1
globalCompositeOperation CSS GCO String 'source-over'
------------------------------------------------------------ ------------------------- -----------------------------
Line styling
lineWidth Number 1
lineCap String 'butt'
lineJoin String 'miter'
lineDash Number Array []
lineDashOffset Number 0
miterLimit Number 10
------------------------------------------------------------ ------------------------- -----------------------------
Shadows
shadowOffsetX Number 0
shadowOffsetY Number 0
shadowBlur Number 0
shadowColor CSS color String 'rgb(0 0 0 / 1)'
------------------------------------------------------------ ------------------------- -----------------------------
Text styling
font CSS font String '12px sans-serif'
direction String 'ltr'
fontKerning String 'normal'
textRendering String 'auto'
letterSpacing CSS length String '0px'
wordSpacing CSS length String '0px'
fontStretch String 'normal'
fontVariantCaps String 'normal'
------------------------------------------------------------ ------------------------- -----------------------------
Unactioned text styling
textAlign String 'left'
textBaseline String 'top'
------------------------------------------------------------ ------------------------- -----------------------------
CSS/SVG filters
filter String 'none'
------------------------------------------------------------ ------------------------- -----------------------------
Image smoothing
imageSmoothingEnabled Boolean true
imageSmoothingQuality String 'high'

Updating State object attributes

The functionality for updating State object attributes is tightly linked to entity object update code. This code is defined in the mixin/entity.js file, but a number of factory functions overwrite those get(), set() and setDelta() functions to extend them in various ways.

tl;dr: Dev-users should never update State objects attributes directly. Instead they can be updated via their entity object's entity.set({ key: value, ... }) and entity.setDelta({ key: value, ... }) functions.

Fill and stroke details

The State object's fillStyle and strokeStyle attributes can be set to the following:

Line styling details

The State object's lineWidth attribute represents the width of the line when the entity scale attribute is set to 1. By default, the line width will scale relative to the entity object's current scale.

Dev-users who want a constant line width regardless of entity scale can set the entity object's scaleOutline flag to false.

Shadow styling details

The State object's shadowOffsetX, shadowOffsetY and shadowBlur attributes are, by default, constant – regardless of the entity object's scale attribute's value. The values applied when scale === 1 are the same as when scale === 2.

Dev-users can make the shadow scale with the entity by setting the entity object's scaleShadow flag to true.

Text styling details

The State object's (many) text styling attributes are relevant only to Label and EnhancedLabel entitys – details of how each attribute affects the displayed text is covered in more detail in the text-based entitys page of this Runbook.

CSS/SVG filter details

SC supports the application of CSS/SVG filters at both the entity and the Cell level (but not the Group level), where the filter(s) will be applied:

tl;dr: do not confuse the filter and filters attributes!

Note that any SC filters will be applied to the stamped output first then, if present, the CSS/SVG filters will be applied afterwards.

Using Cells as Pattern styles

Cell objects can act as State object fillStyle and strokeStyle attribute patterns because they include the functionality defined in the mixin/pattern.js file.

Further details about Pattern styles objects can be found in the Styles management and use page of this Runbook.

Object processing order within the scene graph

When the dev-user adds pivot, mimic, and path references into their SC code (see the positioning system page for details), they also introduce artefact dependencies: if an artefact depends on another artefact to calculate some part of its own display (position, rotation, dimensions, scale), then the need arises for the referenced artefacts to complete their calculations for those attributes before the dependent artefact begins its own calculations.

tl;dr: – SC includes no functionality to internally construct and maintain a dependency graph describing which artefacts need to calculate values before dependent artefact can calculate theirs. It is up to the dev-user to tell SC the order in which artefacts should calculate/update their state.

The SC Display cycle comprises the following steps:

1: Clear operation

2: Compile operation
   2.1: Calculate phase
   2.2: Stamp phase

3: Show operation

A number of attributes are used across the code base to describe ordering; it's important not to confuse them:

To understand ordering, consider the code presented in the scene graph section above. But this time the factory functions have been rearranged in the file as shown:

canvas.buildCell({
  name: name('my-extra-cell'),
  backgroundColor: 'aliceblue',
  ...
});

scrawl.makeGroup({
  name: name('my-additional-group'),
  host: name('my-extra-cell'),
});

canvas.buildCell({
  name: name('my-hidden-cell'),
  shown: false,
  backgroundColor: 'lightgray',
  ...
});

scrawl.makeBlock({
  name: name('yellow-block'),
  group: name('my-hidden-cell'),
  fillStyle: 'yellow',
  ...
});

scrawl.makeBlock({
  name: name('brown-block'),
  pivot: name('orange-block'),
  fillStyle: 'brown',
  ...
});

scrawl.makeBlock({
  name: name('orange-block'),
  fillStyle: 'orange',
  ...
});

scrawl.makeBlock({
  name: name('blue-block'),
  group: name('my-additional-group'),
  fillStyle: 'blue',
  ...
});

scrawl.makeBlock({
  name: name('red-block'),
  group: name('my-extra-cell'),
  pivot: name('blue-block'),
  fillStyle: 'red',
  ...
});

scrawl.makeBlock({
  name: name('last-block'),
  group: name('my-extra-cell'),
  fillStyle: name('my-hidden-cell'),
  ...
});

The SC Display cycle will process the above code in the following order:

Canvas {name: 'my-canvas'}

  Cell {name: 'my-extra-cell', compileOrder: 0, shown: true}

    Group {name: 'my-extra-cell', order: 0}

      Block {
        name: 'red-block', lockTo: 'pivot', pivot: 'blue-block', 
        calculateOrder: 0, stampOrder: 0,
      }

      Block {
        name: 'last-block', lockTo: 'start', fillStyle: 'my-hidden-cell', 
        calculateOrder: 0, stampOrder: 0,
      }

    Group {name: 'my-additional-group', order: 0}

      Block {
        name: 'blue-block', lockTo: 'start', 
        calculateOrder: 0, stampOrder: 0,
      }

  Cell {name: 'my-hidden-cell', compileOrder: 0, shown: false}

    Group {name: 'my-hidden-cell', order: 0}

      Block {
        name: 'yellow-block', lockTo: 'start', 
        calculateOrder: 0, stampOrder: 0,
      }

  Cell {name: 'my-canvas_base', compileOrder: 9999}

    Group {name: 'my-canvas_base'}

      Block {
        name: 'brown-block', lockTo: 'pivot', pivot: 'orange-block', 
        calculateOrder: 0, stampOrder: 0,
      }

      Block {
        name: 'orange-block', lockTo: 'start', 
        calculateOrder: 0, stampOrder: 0,
      }

my-extra-cell

  1. red-block – has a pivot dependency on blue-block
  2. last-block – has a stamp dependency on my-hidden-cell
  3. blue-block – has no dependencies

my-hidden-cell

  1. yellow-block – has no dependencies

my-canvas_base

  1. brown-block – has a pivot dependency on orange-block
  2. orange-block – has no dependencies

… Which will lead to an incorrect canvas output:

Original code output Rearranged code output
Original code output Rearranged code output

To fix this, the dev-user can either:

// The 'my-extra-cell' Cell needs to compile after 'my-hidden-cell'
canvas.buildCell({
  name: name('my-extra-cell'),
  ...
  compileOrder: 1,
});

// The 'my-extra-cell' namesake Group needs to compile after 'my-additional-group'
scrawl.findGroup('my-extra-cell').set({ order: 1 });

scrawl.makeGroup({
  name: name('my-additional-group'),
  host: name('my-extra-cell'),
});

canvas.buildCell({
  name: name('my-hidden-cell'),
  ...
});

scrawl.makeBlock({
  name: name('yellow-block'),
  group: name('my-hidden-cell'),
  fillStyle: 'yellow',
  ...
});

// This block needs to calculate and stamp after its pivot
scrawl.makeBlock({
  name: name('brown-block'),
  pivot: name('orange-block'),
  ...
  order: 1,
});

scrawl.makeBlock({
  name: name('orange-block'),
  fillStyle: 'orange',
  ...
});

scrawl.makeBlock({
  name: name('blue-block'),
  group: name('my-additional-group'),
  fillStyle: 'blue',
  ...
});

scrawl.makeBlock({
  name: name('red-block'),
  group: name('my-extra-cell'),
  pivot: name('blue-block'),
  fillStyle: 'red',
  ...
});

scrawl.makeBlock({
  name: name('last-block'),
  group: name('my-extra-cell'),
  fillStyle: name('my-hidden-cell'),
  ...
});

Now the SC Display cycle processes the code like this:

Canvas {name: 'my-canvas'}

  Cell {name: 'my-hidden-cell', compileOrder: 0, shown: false}

    Group {name: 'my-hidden-cell', order: 0}

      Block {
        name: 'yellow-block', lockTo: 'start', 
        calculateOrder: 0, stampOrder: 0,
      }

  Cell {name: 'my-extra-cell', compileOrder: 1, shown: true}

    Group {name: 'my-additional-group', order: 0}

      Block {
        name: 'blue-block', lockTo: 'start', 
        calculateOrder: 0, stampOrder: 0,
      }

    Group {name: 'my-extra-cell', order: 1}

      Block {
        name: 'red-block', lockTo: 'pivot', pivot: 'blue-block', 
        calculateOrder: 0, stampOrder: 0,
      }

      Block {
        name: 'last-block', lockTo: 'start', fillStyle: 'my-hidden-cell', 
        calculateOrder: 0, stampOrder: 0,
      }

  Cell {name: 'my-canvas_base', compileOrder: 9999}

    Group {name: 'my-canvas_base'}

      Block {
        name: 'orange-block', lockTo: 'start', 
        calculateOrder: 0, stampOrder: 0,
      }

      Block {
        name: 'brown-block', lockTo: 'pivot', pivot: 'orange-block', 
        calculateOrder: 1, stampOrder: 1
      }

Which will lead to the expected outcome:

my-hidden-cell

  1. yellow-block – has no dependencies

my-extra-cell

  1. blue-block – has no dependencies
  2. red-block – has a pivot dependency on blue-block
  3. last-block – has a stamp dependency on my-hidden-cell

my-canvas_base

  1. orange-block – has no dependencies
  2. brown-block – has a pivot dependency on orange-block