Scrawl-canvas page load initialization
Scrawl-canvas is a modular javascript repo, which needs to be imported into other TypeScript/JavaScript code. For instance, if the repo has been added via npm install or yarn add then dev-users can import the repo into code like this:
import * as scrawl from 'scrawl-canvas';
While such import statements may be encountered many times across different JS/TS files in a web page, the SC code itself will run only once, during page load, when the browser first finds the import statement. For every subsequent encounter, the browser just returns the SC object (scrawl) that was generated on that first meeting. This has implications when using SC in framework environments such as React, Vue, Angular, Svelte, etc.
tl;dr: It is strongly recommended that code using SC only runs after the page's HTML download completes. This is because SC initialization code will interrogate the web page's DOM, looking for
<canvas>and<div>stack elements that dev-users want it to manage … but this only happens once per page load!
The scrawl.js file
The package.json file has the following structure:
{
"name": "scrawl-canvas",
"version": [current version number],
"description": [current version number and release date],
"main": "./min/scrawl.js",
"type": "module",
"types": "./source/scrawl.d.ts",
"scripts": [ ... various script definitions ],
"keywords": [ ... various keywords ],
"repository": { ... repository details },
"license": "MIT",
"homepage": "https://github.com/KaliedaRik/Scrawl-canvas#readme",
"devDependencies": { ... note: no direct or peer dependencies permitted! },
"packageManager": "yarn@4.0.1"
}
The repo's entry point is the min/scrawl.js file (which includes all of the repo's source folder's code). Note that the minified file has not been tree-shaken – a known issue that has not yet been fixed.
tl;dr: If tree shaking is essential then dev-users can edit the
scrawl.jsfile, commenting out the functionality they do not need in their project. They will have to rebuild the repo locally, and be aware that updating the repo at any point will destroy their prior work with this file (unless they take action in their dev toolchain to somehow preserve the file between updates).
The source/scrawl.js file looks like this:
// ## Initialize Scrawl-canvas
import { init as _init } from './core/init.js';
export const init = _init;
if (typeof window !== 'undefined') _init();
// ## Export Scrawl-canvas module functions
export {
startCoreAnimationLoop,
stopCoreAnimationLoop,
} from './core/animation-loop.js';
export {
clear,
compile,
show,
render,
} from './core/display-cycle.js';
export { recalculateFonts } from './core/document.js';
export {
addListener,
removeListener,
addNativeListener,
removeNativeListener,
makeAnimationObserver,
} from './core/events.js';
export * as library from './core/library.js';
[ ... etc ]
When the browser's JS engine runs this code it will at the same time construct all of the factory and other objects used by the SC system.
The core/init.js file
The init file is the first piece of functionality to run when importing the SC repo. Note that the code will not run if it finds itself in an environment which doesn't include a global window object.
import { startCoreAnimationLoop } from './animation-loop.js';
import { getCanvases } from '../factory/canvas.js';
import { getStacks } from '../factory/stack.js';
import {
startCoreListeners,
applyCoreResizeListener,
applyCoreScrollListener,
} from './user-interaction.js';
export const init = function () {
// Discovery phase
getStacks();
getCanvases();
// Start the core animation loop
startCoreAnimationLoop();
// Start the core listeners on the window object
applyCoreResizeListener();
applyCoreScrollListener();
startCoreListeners();
};
Imports
For a project whose only action relating to SC is to import the repo into code, where dev-users have commented out all the export lines in the scrawl.js file except the first four, the browser will read and run code from the following repo modules:
asset-management/image-asset.jscore/animation-loop.jscore/document.jscore/events.jscore/init.jscore/library.jscore/user-interaction.jsfactory/anchor.jsfactory/animation.jsfactory/button.jsfactory/canvas.jsfactory/cell.jsfactory/color.jsfactory/element.jsfactory/group.jsfactory/stack.jshelper/array-pool.jshelper/color-factory.jshelper/document-root-elements.jshelper/filter-engine-bluenoise-data.jshelper/filter-engine.jshelper/random-seed.jshelper/shared-vars.jshelper/system-flags.jshelper/utilities.jshelper/workstore.jsmixin/anchor.jsmixin/asset.jsmixin/base.jsmixin/button.jsmixin/cascade.jsmixin/cell-key-functionality.jsmixin/delta.jsmixin/display-shape.jsmixin/dom.jsmixin/filter.jsmixin/hidden-dom-elements.jsmixin/mimic.jsmixin/path.jsmixin/pattern.jsmixin/pivot.jsmixin/position.jsuntracked-factory/cell-fragment.jsuntracked-factory/coordinate.jsuntracked-factory/quaternion.jsuntracked-factory/state.jsuntracked-factory/vector.js
All other SC modules are loaded as a consequence of being included as exports from the scrawl.js file.
Discovery activity
The init() function, when it runs, invokes two discovery operations:
getStacks()– to find and wrap all DOM elements marked with adata-scrawl-stackattribute into Stack artefact objects. Additionally, all direct child elements of the stack element will be wrapped in Element artefact objectsgetCanvases()– to find and wrap all<canvas>elements marked with adata-scrawl-canvasattribute into Canvas artefact objects
Because this discovery activity happens as soon as the SC code loads, and only happens once per page load, it is imperative that the code does not run until the browser has downloaded the HTML file and constructed its Document Object Model from the file's content.
It's also important to remember that this discovery activity will only find <canvas> elements (and stacks) that already exist in (have been hard-coded into) the HTML file. Any <canvas> element added to the DOM after page load – for instance through framework client-side hydration or an Islands architecture pattern – will not be found during the discovery phase and thus will not have been wrapped into the SC library.
This has implications for when dev-users want to retrieve the generated artefacts from the SC library for further use:
- If the artefact was created as part of the discovery process, dev-users can use the
scrawl.findArtefact('element-id'),scrawl.findCanvas('element-id')orscrawl.findStack('element-id')functions to retrieve them. Most of the SC demo tests include this functionality - If, however, the HTML element was added to the DOM after the initial load - as happens in various component-based frameworks such as React, Angular, Vue, Svelte, etc – then dev-users need to use the
scrawl.getCanvas('element-id')andscrawl.getStack('element-id')functions, which perform a post-load discovery operation and wrap the element in a Canvas or Stack wrapper artefact. This functionality can be seen in demo DOM-017
Note that when SC wraps <canvas>, <div> stacks and stack elements into SC artefact objects, it will mutate those DOM elements. Further details about this can be found in the SC artefacts and the DOM page in the Runbook.
The Scrawl-canvas animation loop
The SC system runs a single RequestAnimationFrame animation loop, which gets started as part of the initialization work. More details about the animation loop can be found in the core.animation-loop.js file.
More information can be found in the Animation and Display cycle page of this Runbook.
Core constants, flags and event listeners
Two of the key drivers for SC is to make <canvas> elements responsive to their environments, and offer high-level functionality for end-user interactions with those responsive elements. Much of the code that handles this functionality can be found in the core/user-interaction.js file.
The code in this file makes heavy use of shared constants and system flags. As a result, these also need to be instantiated as part of the initialization process.
Shared constants
The thinking behind shared constants is that SC should minimize the work required to create throwaway strings and arrays used in loops or conditional tests – especially when repo-devs find themselves using the same string or array across different files.
To solve this issue SC defines and exports a common set of strings and arrays in the helper/shared-vars.js file. Examples of exports in the file include:
export const _piDouble = Math.PI * 2;
export const _piHalf = Math.PI * 0.5;
export const _pow = Math.pow;
export const _radian = Math.PI / 180;
export const _random = Math.random;
...
export const ACCEPTED_WRAPPERS = ['Canvas', 'Stack'];
export const ADD_EVENT_LISTENER = 'addEventListener';
...
export const INT_COLOR_SPACES = ['RGB', 'HSL', 'HWB', 'XYZ', 'LAB', 'LCH', 'OKLAB', 'OKLCH'];
export const INVERT_CHANNELS = 'invert-channels';
export const LEAVE = 'leave';
...
export const T_CELL = 'Cell';
export const T_COLOR = 'Color';
export const T_COORDINATE = 'Coordinate';
export const T_ENHANCED_LABEL = 'EnhancedLabel';
...
export const WHITE = 'rgb(255 255 255 / 1)';
export const WIDTH = 'width';
export const ZERO_PATH = 'M0,0';
export const ZERO_STR = '';
Note that repo-devs don't have any proof that this helps cut down on unnecessary work (in fact various LLMs are adamant that this approach offers no clear efficiency benefits). It's just part of the SC ethos to only define constants once, and to define/export a constant from this file if they find themselves using it in different modules.
Shared functions
Similar to shared constants, SC defines a set of exported shared variables in the helper/utilities.js file. These include:
- Array manipulation functions –
pushUnique(),removeItem() - Correction functions –
correctAngle(),correctForZero(),constrain(),interpolate() - Default functions (differentiated by what they return) –
λnull(),λfirstArg(),λcloneError() - Easing functions, which are all gathered in a single object –
easeEngines.out(),easeEngines.in() - Existence check functions – exists
xt(), all existxta(), one existsxto(), return the first existingxtGet() - Generation functions –
generateUuid(),generateUniqueString() - Manipulation functions –
addStrings(),convertTime() - Object manipulation functions –
mergeOver(),mergeDiscard() - Type checking functions, which all begin with
isa_–isa_boolean(),isa_fn(),isa_canvas()
System flags
SC uses flags – commonly Boolean – for much of its internal signalling and communications work. Many of the flags related to system state get defined in the helper/system-flags.js file. For example:
let mouseChanged = false;
export const getMouseChanged = () => mouseChanged;
export const setMouseChanged = (val) => mouseChanged = val;
let viewportChanged = false;
export const getViewportChanged = () => viewportChanged;
export const setViewportChanged = (val) => viewportChanged = val;
let prefersContrastChanged = false;
export const getPrefersContrastChanged = () => prefersContrastChanged;
export const setPrefersContrastChanged = (val) => prefersContrastChanged = val;
The currentCorePosition object
SC keeps track of system state, and changes to that state, in its currentCorePosition object:
export const currentCorePosition = {
x: 0,
y: 0,
scrollX: 0,
scrollY: 0,
w: 0,
h: 0,
type: MOUSE,
prefersReducedMotion: false,
prefersDarkColorScheme: false,
prefersReduceTransparency: false,
prefersContrast: false,
prefersReduceData: false,
displaySupportsP3Color: false,
canvasSupportsP3Color: false,
devicePixelRatio: 0,
rawTouches: [],
};
SC checks for changes to system state using an animation object – SC-core-listeners-tracker – that runs once at the start of each animation loop. When changes are detected Canvas and Stack artefacts will be informed (via dirty flags). When they in turn run their Display cycle functionality they will cascade that information to their constituent objects which will, if necessary, update their state to reflect the changed environment.
Browser mouse/touch/pointer, scroll, and resize events
During initialization SC will add a set of event listeners to the window object which react to various mouse/touch/pointer events. When such events occur the listeners will set the appropriate system flags to true (ie: something has changed) and store the cursor's current position data in the currentCorePosition object. Functionality to react to these changes is deferred until the next animation loop runs.
Similarly, SC sets event listeners on the window object to listen for browser resize and scroll events.
After initialization completes the dev-user can stop and restart these core listeners by invoking the scrawl.stopCoreListeners() and scrawl.startCoreListeners() functions. See demo test DOM-009 for an example of this functionality in action.
End-user preferences media queries and events
SC uses evented media queries to listen out for changes in various system settings. For example:
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
colorSchemeMediaQuery.addEventListener(CHANGE, () => {
const res = colorSchemeMediaQuery.matches;
if (currentCorePosition.prefersDarkColorScheme !== res) {
currentCorePosition.prefersDarkColorScheme = res;
setPrefersDarkColorSchemeChanged(true);
}
});
currentCorePosition.prefersDarkColorScheme = colorSchemeMediaQuery.matches;
SC tracks the following system settings:
forced-colorsinverted-colorsprefers-color-schemeprefers-contrastprefers-reduced-dataprefers-reduced-motionprefers-reduced-transparency
SC, by default, doesn't react to changes in these settings. It's up to the dev-user to add hook functions to each Canvas wrapper object to supply the appropriate functionality for that canvas's output when changes occur. See the Objects overview page in the Runbook for further details.
SC also tracks the current pixel-ratio and color-gamut: p3 settings for the device/screen on which the browser is displaying. This can change when, for instance, an end-user drags the browser window between screens. Such changes are handled by SC internally with little need for additional dev-user intervention.
Scrawl-canvas pools
SC code needs to run fast. For this reason functional programming approaches, where new objects get created rather than existing objects mutated, adds computational weight (and excessive garbage collection) which is best avoided.
Instead, SC code makes use of a set of pooled objects and arrays for much of its functionality. These pools get initialized when the browser first imports the relevant modules, and start as empty arrays. While some of these pools are made available to the dev-user via scrawl functions, others are strictly internal.
Internal pooled objects/arrays include:
- Generic zero-length arrays, from helper/array-pool.js; note that the pool will be periodically culled –
requestArray(),releaseArray() - SC basic Cell objects from untracked-factory/cell-fragment.js, wrapping unattached
<canvas>elements and their associated 2D context engines –requestCell(),releaseCell() - SC particle objects from factory/particle.js; note that the pool will be periodically culled –
requestParticle(),releaseParticle() - SC particle history arrays from untracked-factory/particle-history.js; note that the pool will be periodically culled –
requestParticleHistory(),releaseParticleHistory()
Scrawl-exported pooled objects/arrays, which can be used by dev-users, include:
- SC coordinate arrays, from untracked-factory/coordinate.js –
requestCoordinate(),releaseCoordinate() - SC quaternion objects, from untracked-factory/quaternion.js –
requestQuaternion(),releaseQuaternion() - SC vector objects, from untracked-factory/vector.js –
requestVector(),releaseVector()
tl;dr: Always release requested pooled objects/arrays! Failure to release them will lead to less efficient code and (potentially) slow memory leaks.
Scrawl-canvas filter engine
SC comes with a sophisticated set of graphical filters which can be applied at the entity, Group or Cell level – see demo Canvas-007 for an example.
The functionality to generate these filters and apply them to entitys, Groups and Cells is housed in a single filter engine object whose code can be found in the helper/filter-engine.js file. This singleton object is instantiated as part of the SC initialization process as a by-product of both the Cell and Group modules importing it.
The filter engine module will also set up a permanent animation object – SC-core-filters-cleanup-action – to run once at the end of every animation loop iteration, to reset filter object dirtyFilterIdentifier flags back to false.
Scrawl-canvas color engine
SC will, on initialization, instantiate a single color engine object which includes code to convert, manipulate and cache color data. This code can be found in the helper/color-engine.js file.
Note that the filter engine will instantiate an internal Color object – SC-core-color-engine – during initialization. All color objects have direct access to the color engine.
Scrawl-canvas workstore
The SC workstore is an internal mechanism for caching various computationally-intensive objects; it is used in particular by the singleton filter engine but is available for use by other SC modules as required.
Every object cached in the workstore includes a timestamp indicating the last time it was accessed; the workstore actively purges objects that haven't been recently accessed. This action is handled by an animation object – SC-core-workstore-hygiene – which gets instantiated at the same time as the workstore.
More details about workstore functionality can be found in the helper/workstore.js file.