Scrawl-canvas objects overview

The <canvas> element, alongside the Canvas API used to program it, is an immediate mode graphics system. What this means in practical terms is that all Canvas API drawing instructions have to be set out in a JavaScript file, or written directly in a <script> element, and run in their entirety to generate the canvas display. For animation, this needs to be repeated for every frame of the animation.

Most canvas libraries – including Scrawl-canvas – act as a buffer between the dev-user and the Canvas API by introducing a stateful scene graph composed of objects, and groups of objects, which define a set of entities to be drawn onto the canvas. The scene graph gets rendered onto the canvas once, then re-rendered only when changes to its state require it. Some canvas libraries are sophisticated enough to only re-render those parts of their scene graph that change, thus minimising the work the browser needs to do. They are, effectively, retained mode graphics systems.

Object creation and the SC library

When a dev-user develops an SC canvas display they start by defining the set of artefacts and entitys they will use in the display, using factory functions supplied by SC for that purpose. These factory functions are easy to identify because they are all named along the lines of makeSomething:

// Factory functions always return the object they've just created
const redBlock = scrawl.makeBlock({
  name: 'my-red-block',
  width: 200,
  height: 150,
  startX: 50,
  startY: 80,
  fillStyle: 'red',
  method: 'fill',
});

Factory functions are each defined in their own JavaScript files in the repo. Looking at the repo structure we can see that most of these files are located in the factory folder:

| - scrawl.js                       Entry file
| - scrawl.d.ts                     TS types definitions for the dev-user API
|
| - asset-management
|   | - [...]
|
| - core
|   | - library.js                  <- the library file
|   | - [...]
|
| - factory
|   | - block.js                    <- makeBlock()
|   | - color.js                    <- makeColor()
|   | - filter.js                   <- makeFilter()
|   | - gradient.js                 <- makeGradient()
|   | - group.js                    <- makeGroup()
|   | - picture.js                  <- makePicture()
|   | - render-animation.js         <- makeRender()
|   | - wheel.js                    <- makeWheel()
|   | - [...]
|
| - helper
|   | - [...]
|
| - mixin
|   | - base.js                     <- the base file
|   | - [...]
|
| - untracked-factory
|   | - drag-zone.js                <- makeDragZone()
|   | - keyboard-zone.js            <- makeKeyboardZone()
|   | - [...]

The base mixin

The key difference between the files in the factory and untracked-factory folders is that the objects created by the factory folder files get tracked in the SC library. They also share critical functionality defined in the mixin/base.js file. This functionality includes:

Object naming

Every tracked SC object needs a name – if the dev-user fails to provide the factory function with a name attribute SC will assign the object a random (and very ugly) name.

Object names need to be (mostly) unique because SC uses the name attribute as that object's key in the SC library. The dev-user should always be able to locate an object in the library, and retrieve it, if they know its name.

tl;dr: namespacing objects in the SC library is highly recommended! Because the SC library can quickly fill with objects, keeping the library tidy becomes a priority – particularly when objects are no longer required for the <canvas> display.

A simple way for the dev-user to namespace objects is to use a small piece of boilerplate code in their project files, along the lines of:

const namespace = `${canvas.name}-somenamespacestring`;
const name = (n) => `${namespace}-${n}`;

// Object creation
scrawl.makeBlock({
  name: name('red-block'),
  ...
}).clone({
  name: name('blue-block'),
  ...
});

// Purging objects from the SC library
scrawl.purge(namespace);

The Scrawl-canvas library

The SC library comprises a set of {key: value} objects where we store all tracked objects created by the SC factory functions. The library is divided into the following sections:

anchor
  |-- Anchor object instances
  |-- Button object instances
  |
animation
  |-- Animation object instances
  |-- RenderAnimation object instances
  |
  |-- (Animation SC-core-filters-cleanup-action)
  |-- (Animation SC-core-gradient-delta-animation)
  |-- (Animation SC-core-listeners-tracker)
  |-- (Animation SC-core-tickers-animation)
  |-- (Animation SC-core-workstore-hygiene)
  |
animationtickers
  |-- Ticker object instances
  |
artefact
  |-- Canvas artefact instances
  |-- Element artefact instances
  |-- Stack artefact instances
  |
  |-- Bezier entity instances
  |-- Block entity instances
  |-- Cog entity instances
  |-- Crescent entity instances
  |-- Emitter entity instances [physics engine]
  |-- EnhancedLabel entity instances
  |-- Grid entity instances
  |-- Label entity instances
  |-- LineSpiral entity instances
  |-- Line entity instances
  |-- Loom entity instances
  |-- Mesh entity instances [physics-related]
  |-- Net entity instances [physics engine]
  |-- Oval entity instances
  |-- Picture entity instances
  |-- Polygon entity instances
  |-- Polyline entity instances
  |-- Quadratic entity instances
  |-- Rectangle entity instances
  |-- Shape entity instances
  |-- Spiral entity instances
  |-- Star entity instances
  |-- Tetragon entity instances
  |-- Tracer entity instances [physics engine]
  |-- Wheel entity instances
  |
asset
  |-- ImageAsset object instances
  |-- NoiseAsset object instances
  |-- RawAsset object instances
  |-- RdAsset object instances
  |-- VideoAsset object instances
  |
  |-- Cell object instances
  |
canvas
  |-- Canvas artefact instances
  |
cell
  |-- Cell object instances
  |
element
  |-- Element artefact instances
  |
entity
  |-- Bezier entity instances
  |-- Block entity instances
  |-- Cog entity instances
  |-- Crescent entity instances
  |-- Emitter entity instances [physics engine]
  |-- EnhancedLabel entity instances
  |-- Grid entity instances
  |-- Label entity instances
  |-- LineSpiral entity instances
  |-- Line entity instances
  |-- Loom entity instances
  |-- Mesh entity instances [physics-related]
  |-- Net entity instances [physics engine]
  |-- Oval entity instances
  |-- Picture entity instances
  |-- Polygon entity instances
  |-- Polyline entity instances
  |-- Quadratic entity instances
  |-- Rectangle entity instances
  |-- Shape entity instances
  |-- Spiral entity instances
  |-- Star entity instances
  |-- Tetragon entity instances
  |-- Tracer entity instances [physics engine]
  |-- Wheel entity instances
  |
filter
  |-- Filter object instances
  |
fontfamilymetadata
  |-- Font data objects [text layout engine]
  |
force
  |-- Force object instances [physics engine]
  |
  |-- (Force gravity)
  |
group
  |-- Group object instances
  |
particle
  |-- Particle object instances [physics engine]
  |
spring
  |-- Spring object instances [physics engine]
  |
stack
  |-- Stack artefact instances
  |
styles
  |-- Color object instances
  |-- ConicGradient object instances
  |-- Gradient object instances
  |-- Pattern object instances
  |-- RadialGradient object instances
  |
  |-- (Color SC-core-color-engine)
  |
tween
  |-- Action object instances
  |-- Tween object instances
  |
unstackedelement
  |-- UnstackedElement object instances [snippets]
  |
world
  |-- World object instances [physics engine]

Some points of interest:

Locating objects in the SC library

The SC library provides helper functions for locating objects in the library. Each function targets a specific section, and they all follow the same pattern: findSomething('object-name')

// Object creation
scrawl.makeBlock({
  name: name('red-block'),
  ...
});

// Object retrieval
const myRedBlock = scrawl.findEntity(name('red-block'));

Alternatively, dev-users can get the object directly from the library using dot.notation:

const myRedBlock = scrawl.library.entity[name('red-block')];

Deleting objects, and SC library hygiene

The easiest way to delete an object is to invoke its kill function: myRedBlock.kill(). This will delete all references to the object across the SC system and remove it from the SC library.

However there may be times when a dev-user wants to delete many objects, for instance if they have set up a static background and want to free up some memory for other tasks. This is where namespacing the objects becomes really useful as SC exports a function – scrawl.purge('namespace-string') – to purge all objects whose names start with that namespace string from the system. See Demo Canvas-046 for a working example.

Getting and setting object attributes

While the SC (bespoke) property accessor functionality may seem over-complicated on first glance, the system has been built in this way for reasons that have evolved over the course of the repo's history:

tl;dr: SC does not encourage the use of JavaScript property accessors (dot-notation or bracket-notation syntax) for getting or setting values on factory-generated object instances in product code. Use the .get('attribute'), .set({key: value, ...}) and .setDelta({key: value, ...}) functions instead.

The SC property accessor functionality is initially defined in the base mixin file. However each factory function generates differently shaped instance objects so some mixin files and factory functions will overwrite the accessor functions to make sure they meet the needs of the instance. For example, the .get() function gets overwritten in the following files:

(For future consideration: there may be some merit to reworking the SC property accessor functionality, to use Object.defineProperty() – it could simplify the code base and allow dev-users to more safely use dot-notation/bracket-notation syntax in their product code. This would, however, be a significant amount of work for – potentially! – minimal benefit.)

Object serialization and cloning

SC objects can be serialized into strings – for storage, transmission over networks, etc – and deserialized from those strings back into objects. This includes serializing functions assigned to object attributes.

JavaScript does not tell a good story when it comes to serializing functions. By necessity, SC has to use the JavaScript Function() constructor as part of the deserialization process. Some dev-users may consider this to be a security risk but, compared to using eval() to deserialize the function, the risk is minimal (but not eliminated).

Because of the issues surrounding JavaScript object serialization, SC implements its own serialization functionality called packet management. Much of this functionality gets defined in the base mixin file using the following prototype attributes and functions:

Much of the complexity in the packet management system arises from the shapes of the object instances produced by the factory functions, and the need to (as far as possible) minimize the string lengths of the packets produced by the system. For this reason mixin files and factory functions will add data to the base mixin's attributes and, on occasion, overwrite functions – in particular finalizePacketOut().

tl;dr: The packet management system is still a work-in-progress – it works, but it can work better. Some SC objects have not yet been included in the system (such as: Canvas); and there's more work to be done around string length minimization.

An example of a packet string created by SC object serialization looks like this:

const block = scrawl.makeBlock({
    name: name('block-fill'),
    width: 100,
    height: 100,
    startX: 25,
    startY: 25,
    fillStyle: 'green',
    strokeStyle: 'gold',
    lineWidth: 6,
    lineJoin: 'round',
    shadowOffsetX: 4,
    shadowOffsetY: 4,
    shadowBlur: 2,
    shadowColor: 'black',
});

console.log(block.saveAsPacket());

// Results
["mycanvas-block-fill","Block","entity",{"name":"mycanvas-block-fill","dimensions":[100,10
0],"start":[25,25],"delta":{},"deltaConstraints":{},"pivot":null,"mimic":null,"filters":[]
,"visibility":true,"calculateOrder":0,"stampOrder":0,"bringToFrontOnDrag":true,"ignoreDrag
ForX":false,"ignoreDragForY":false,"scale":1,"roll":0,"noUserInteraction":false,"noPositio
nDependencies":false,"noCanvasEngineUpdates":false,"noFilters":false,"noPathUpdates":false
,"noDeltaUpdates":false,"checkDeltaConstraints":false,"performDeltaChecks":false,"pivotPin
":0,"pivotIndex":-1,"addPivotHandle":false,"addPivotOffset":true,"addPivotRotation":false,
"useMimicDimensions":false,"useMimicScale":false,"useMimicStart":false,"useMimicHandle":fa
lse,"useMimicOffset":false,"useMimicRotation":false,"useMimicFlip":false,"addOwnDimensions
ToMimic":false,"addOwnScaleToMimic":false,"addOwnStartToMimic":false,"addOwnHandleToMimic"
:false,"addOwnOffsetToMimic":false,"addOwnRotationToMimic":false,"pathPosition":0,"addPath
Handle":false,"addPathOffset":true,"addPathRotation":false,"constantSpeedAlongPath":false,
"isStencil":false,"memoizeFilterOutput":false,"method":"fill","winding":"nonzero","flipRev
erse":false,"flipUpend":false,"scaleOutline":true,"scaleShadow":false,"lockFillStyleToEnti
ty":false,"lockStrokeStyleToEntity":false,"onEnter":"~~~","onLeave":"~~~","onDown":"~~~","
onUp":"~~~","onOtherInteraction":"~~~","group":"mycanvas_base","fillStyle":"green","stroke
Style":"gold","lineWidth":6,"lineJoin":"round","shadowOffsetX":4,"shadowOffsetY":4,"shadow
Blur":2,"shadowColor":"black"}]

Because packet management is central to SC functionality it gets tested in a number of the demos. More dedicated packet management testing happens in demos Packets-001 and Packets-002.

Clone functionality

The SC cloning functionality is tied closely to the packet functionality. To create a clone of an object SC will first serialize the object, and then deserialize the resulting string to create a new object, which can in turn action the updates described in the .clone() function's argument object.

Most SC clone functionality is coded into the base mixin file .clone({key: value, ...}) function. Various mixin and factory functions will finesse that functionality to better match their instance object shape requirements by overwriting the base mixin's postCloneAction() function.

Exceptions to the outlined processes

The ecosystem of mixin and factory functions which make up the SC stateful scene graph is necessarily complex, partly to meet the requirements of a (hopefully) fast and memory-efficient code base, but also to make the creation of <canvas> displays with SC as simple as possible for dev-users (good UX).

Most factory function and mixin files follow the above outlined processes; a few do not. Brief details of these divergent files are given below.

Canvas and Stack factory functions

The Canvas and Stack factory functions do not contribute (for the most part) to the SC scene graph. Instead they are the mechanisms by which the scene graph deploys to the web page. Because of this intimate connection to the web page's Document Object Model, these functions do not export a makeCanvas or makeStack function.

There's three ways a dev-user can add an SC-controlled <canvas> or <div> stack to a web page:

For the moment, SC does not support packet management and cloning functionality for Canvas and Stack instances. This may change in the future.

The myStack.kill() and myCanvas.kill() functions will not only remove the Canvas and Stack object instances from the SC ecosystem, but also kill any SC objects associated with the Canvas and Stack instances. Additionally, the kill functions will delete the <canvas> and <div> stack elements from the DOM.

Cell factory function

The Cell factory function does export a makeCell() function, but this is only used internally by other modules in the repo code.

Instead, dev-users have to create new cells against the Canvas wrapper object which will host the Cell object: myCanvas.buildCell({name: string, key: value, ...}).

For the moment, SC does not support packet management and cloning functionality for Cell instances. This may change in the future.

The myCell.kill() function does work, removing both the Cell instance and its associated Group instance but not any artefacts or entitys associated with that Group.

Animation and RenderAnimation factory functions

The Animation and RenderAnimation factory functions export scrawl.makeAnimation() and scrawl.makeRender() functions for instance creation.

For the moment, SC does not support packet management and cloning functionality for Animation or RenderAnimation instances. This may change in the future.

Animation instances include an .onKill attribute which accepts a function as its value. When the myAnimation.kill() function is invoked this additional kill function will run immediately prior to the instance's removal from the SC system.

Loom and Mesh entity factory functions

The Loom and Mesh entity factory functions differ from other entity factories in that they do not use the mixin/entity.js mixin. This is because they rely on other entitys to define their positions in a canvas display.

Packet management support has been coded for these entitys, but tests have not yet been written for the functionality. Cloning functionality has, for the time being, been disabled. Kill functionality has been enabled but, again, not properly tested.

Image-based asset management factory functions

Image-based asset code does not include factory functions to create instances of the assets. This is because image-based assets are often closely tied to image files which need to be fetched across the network, or to <img> and <video> elements in the DOM.

To fetch and wrap an image or video file, dev-users can set the file URL in a .source attribute when creating Picture entity or Pattern style instances.

Alternatively, dev-users can invoke scrawl.importImage('url-string'), scrawl.importSprite('url-string') and scrawl.importVideo('url-string') to load these assets from remote files.

However the best approach for managing visual assets is to define them in the DOM and then import them using the scrawl.importDomImage('css-query-string') and scrawl.importDomVideo('css-query-string') functions. This allows the dev-user to define these assets in a responsive manner, and to import multiple assets in a single invocation. The associated SC files include functionality to manage responsive image and video elements (TODO code up similar functionality for sprite assets):

Dev-users can, in a similar way, import browser media streams and screen capture streams for use in canvas displays by invoking the scrawl.importMediaStream() and scrawl.importScreenCapture() functions. Note that browsers will impose an end-user consent check before the asset can be imported into the SC environment.

SC does not support packet management and cloning functionality for any asset instances (including NoiseAsset, RawAsset and ReactionDiffusionAsset). Kill functionality works as expected.

Pool-based factory functions

Dev-users can request the following pool-based objects. It is imperative that if the dev-user requests an object, they must release it when done with it – failure to do so may lead to slow memory leaks and degraded performance over time:

Note that these pooled objects are not tracked in the SC library.