The Scrawl-canvas positioning system
The key purpose of SC is to position graphical entitys onto a <canvas> element, in a given order, so that those entitys build some form of graphical representation, chart, imagery, infographic, artwork (etc) that can be displayed as part of a web page.
Most of the code associated with the SC positioning system can be found in the mixin/position.js mixin file.
Background
As background knowledge, we need to understand that a DOM <canvas> element, and SC's representation of that element (as Canvas and Cell wrapper artefacts) are very different things.
DOM
<canvas>elements are part of the HTML5 specification. The element includes attributes –height=,width=– used to define a coordinate space (measured in CSS pixels) within which drawing operations can take place. The visual representation of the canvas element in the web page can be styled using CSS; note that when the element's CSS styling dimensions diverge from its coordinate space dimensions, browsers will scale the coordinate space (ignoring aspect ratio) to fit into the styled dimensions. Drawing operations are, for 2D graphics, defined by the Canvas API; these operations are managed by a context interface.SC Canvas artefacts wrap DOM
<canvas>elements and bring them into the SC ecosystem. They include handles to the DOM element itself and its context interface, though dev-users should generally avoid directly interacting with them. Instead, graphical operations are handled by Cell artefacts, of which each Canvas wrapper will have at least one – the base Cell. Dev-users can control how the base cell will display in the DOM<canvas>element, allowing us to build real-time responsive graphical displays.SC Cell artefacts wrap regular (auto-generated)
<canvas>elements that are not added to the web page's DOM. Every Canvas wrapper includes, at a minimum, a base Cell, and can include additional Cell artefacts as necessary. Note that these additional<canvas>elements are nothing special: SC does not make use of the OffscreenCanvas interface or, indeed, Web Workers.
tl;dr: We only need to care about Cell artefact dimensions. All SC graphical operations happen in Cell artefacts and end up in the Canvas artefact's base Cell. Every Cell we create will have its own dimensions – a cell coordinate space – which can diverge from the associated DOM
<canvas>element's dimensions. Canvas artefacts handle the transfer of graphical data from their base Cell to their DOM<canvas>element automatically.
From this point on:
- When we refer to
Cellwe (generally) mean an SC Canvas artefact's base Cell artefact. - A
host coordinate systemis the Cell artefact's coordinate space. Entitysrefer to graphical objects which can be stamped onto a Cell.- A
local coordinate systemis a coordinate system specific to a given entity.
Furthermore, positioning refers to the location of an entity on the Cell, alongside that entity's dimensions, scale (relative to its default dimensions) and rotation (relative to the Cell's coordinate space axes).
Also, while we concentrate on the positioning of graphical entitys in this document, SC extends the positioning system to include DOM elements within an SC "stack" element.
The rotation-reflection point
When we position an entity, we are in fact positioning that entity's rotation-reflection point, around which the entity will stamp itself onto the Cell.
By default, the rotation-reflection point is located at the top-left corner of the entity, and represents the [0, 0] coordinate of that entity's local coordinate system.
We position this rotation-reflection point in the Cell's host coordinate system, which starts at the Cell's top left corner and extends the width (x axis) and height (y axis) of its drawing space.
SC allows us to position an entity's rotation-reflection point in several different ways:
Absolute positioning using pixel coordinates (
[x, y]) relative to the Cell's top-left corner – for example[50, 50]represents a position 50px from the Cell's left border and 50px from the top border.Relative positioning using percentage ratio coordinates (
['x%', 'y%']) relative to the Cell's current width and height dimensions – for example['50%', '50%']represents a position at the centre of the Cell.Positioning by reference where an entity can use the current position of another entity to calculate its own position on the Cell.
Absolute and relative positioning
The most direct way to position an entity is to give it start, offset and handle values, from which it will calculate its position on the Cell. Each of these attributes are Coordinates with a default value of [0,0]. Each attribute also comes with a set of pseudo-attributes which allow the dev-user to set and get the x and y components of the Coordinate separately.
The start artefact attribute
We set an artefact's rotation-reflection point using its start attribute. For convenience, we can also set each part of the point's coordinate using the startX and startY pseudo-attributes.
We can mix-and-match absolute and relative values in a coordinate: [200, '40%'] is a legitimate coordinate, as is ['40%', 200].
Setting an artefact's start attribute will also set the artefact's dirtyStart boolean flag to true. At the start of the next Display cycle the SC system performs a check across all entitys and, for those marked dirty, will perform calculations to clean their start attribute, placing the result (measured in cell coordinate space pixels) into the private currentStart attribute.
When we get an artefact's start coordinate, the currentStart attribute will be returned. Note that any attempt to set the currentStart will have unexpected effects on the artefact.
Directly setting an artefact's start or (worse!) currentStart value will lead to unexpected behaviours and bugs. Always use the artefact.set({ start: [x, y]}) or artefact.deltaSet({ start: [x, y]}) functionality!
The offset and position artefact attributes
An artefact's start (and currentStart) value does not fully represent its rotation-reflection point. We are able to offset the rotation-reflection point from the start coordinate using the artefact's offset attribute. See Demo Canvas-002 for an example of this functionality.
These offset values, like start values, can be absolute (measured in pixels) or relative (as a percentage of the host Cell's current dimensions). When we set the artefact's offset (offsetX, offsetY) value, we also set its dirtyOffset boolean flag to true. Similar to the start attribute, entitys will clean their dirty offsets and store the result in the private currentOffset attribute.
When we get an artefact's offset coordinate, the currentOffset value will be returned. Note that any attempt to set the currentOffset will have unexpected effects on the artefact.
We can get an artefact's current rotation-reflection coordinate at any time using the position pseudo-attribute; the positionX and positionY pseudo-attributes are also supported. Note that any attempt to set these pseudo-attributes will have no effect on the artefact.
{0,0} host coordinate system
.---------|---------|---------|---------|---------.
| |
| start offset handle | @: start coordinate
| [10,5] [5,2] [0,0] |
| | o: rotation-reflection point
| @ | = currentStart + currentOffset
| | = [10,5] + [5,2]
| o---------+ | = [15,7]
| | | artefact width: 10 |
| | | artefact height: 5 |
- | | roll: 0 -
| | | scale: 1 |
| +---------+ |
| | *: path-start-coordinate
| currentStart currentOffset currentHandle | = rot-ref-point - (currentHandle * scale)
| [10,5] [5,2] [0,0] | = [15,7] - [0*1,0*1]
| | = [15,7] - [0,0]
| | = [15,7]
| |
| |
.---------|---------|---------|---------|---------.
{50,20}
{0,0} host coordinate system
.---------|---------|---------|---------|---------.
| |
| start offset handle | @: start coordinate
| ['20%','50%'] ['10%','10%'] [0,0] |
| | o: rotation-reflection point
| @ | = currentStart + currentOffset
| | = [10,5] + [5,2]
| o---------+ | = [15,7]
| | | artefact width: 10 |
| | | artefact height: 5 |
- | | roll: 0 -
| | | scale: 1 |
| +---------+ |
| | *: path-start-coordinate
| currentStart currentOffset currentHandle | = rot-ref-point - (currentHandle * scale)
| [10,5] [5,2] [0,0] | = [15,7] - [0*1,0*1]
| | = [15,7] - [0,0]
| | = [15,7]
| |
| |
.---------|---------|---------|---------|---------.
{50,20}
The handle artefact attribute.
In brief, SC paints an artefact onto the canvas using the following protocol:
- If necessary, clean the artefact's dirty attributes and recalculate its rotation-reflection point.
- If necessary, recalculate the artefact's path2D object, which will be used during the painting step to
filland/orstrokethe artefact onto the Cell. - Position and rotate the host Cell's context engine using the Canvas API
setTransform()function – it is at this moment that we move the Cell's engine's coordinate system origin point –[0,0]– to match the artefact's rotation-reflection point. - Update the Cell's engine state to match the artefact's engine state.
- Stamp the artefact onto the Cell's DOM
<canvas>element.
The start and offset attributes discussed above both feed into the first step of the protocol.
Every artefact has a path2D object, which SC uses for stroke/fill painting operations as well as the artefact's hit functionality (for example: hover, and drag-and-drop, operations).
When building the artefact's path2D object, SC takes into account the artefact's dimensions and scale. It also includes a (scaled) local displacement value which has the apparent effect of moving the rotation-reflection point away from the artefact's top-left corner. Dev-users can set this displacement value in the artefact's handle attribute.
Similar to the start and offset values, handle values can be absolute (measured in pixels) or relative (as a percentage of the artefact's current scaled dimensions). When we set the artefact's handle (handleX, handleY) value, we also set its dirtyHandle boolean flag to true. After cleaning, the handle's calculated values get stored in the private currentHandle attribute.
When we get an artefact's handle coordinate, the currentHandle value will be returned. Note that any attempt to set the currentHandle will have unexpected effects on the artefact.
{0,0} host coordinate system
.---------|---------|---------|---------|---------.
| |
| start offset handle | @: start coordinate
| [10,5] [5,2] [-4,1] |
| | o: rotation-reflection point
| @ | = currentStart + currentOffset
| *---------+ | = [10,5] + [5,2]
| o | | artefact width: 10 | = [15,7]
| | | artefact height: 5 |
- | | roll: 0 |
| | | scale: 1 -
| +---------+ |
| |
| | *: path-start-coordinate
| currentStart currentOffset currentHandle | = rot-ref-point - (currentHandle * scale)
| [10,5] [5,2] [-4,1] | = [15,7] - [-4*1,1*1]
| | = [15,7] - [-4,1]
| | = [19,6]
| |
| |
.---------|---------|---------|---------|---------.
{50,20}
{0,0} host coordinate system
.---------|---------|---------|---------|---------.
| |
| start offset handle | @: start coordinate
| ['20%','50%'] ['10%','10%'] ['-40%','20%'] |
| | o: rotation-reflection point
| @ | = currentStart + currentOffset
| *---------+ | = [10,5] + [5,2]
| o | | artefact width: 10 | = [15,7]
| | | artefact height: 5 |
- | | roll: 0 |
| | | scale: 1 -
| +---------+ |
| |
| | *: path-start-coordinate
| currentStart currentOffset currentHandle | = rot-ref-point - (currentHandle * scale)
| [10,5] [5,2] [-4,1] | = [15,7] - [-4*1,1*1]
| | = [15,7] - [-4,1]
| | = [19,6]
| |
| |
.---------|---------|---------|---------|---------.
{50,20}
Positioning by reference
A foundational tenet of the SC positioning system is that any artefact can position itself on the Cell by referencing any other artefact.
In essence, this means that instead of using its own currentStart values when calculating the value of its rotation-reflection point, our artefact will instead use the referenced artefact's currentStart values. SC manages this through a system of locks, alongside a (bespoke, and rudimentary) signals system.
The lockTo artefact attribute
The lockTo attribute is an Array containing two String values. Each value indicates how the artefact wants to calculate its position along the Cell's x and y axes – ['x-axis-string', 'y-axis-string']. The default value is ['start', 'start'], indicating that the artefact wishes both parts of its start coordinate to use absolute or relative positioning as described above.
Like the other coordinate-like attributes, lockTo comes with a set of pseudo-attributes – lockXTo, lockYTo – which dev-users can use to set the individual elements of the attribute.
The following String values can be used in the lockTo attribute's Array:
start– (default): use absolute or relative positioningpivot: use the referenced artefact'scurrentStartvalues to calculate the rotation-reflection point. Dev-users can reference an artefact by setting the artefact'spivotattribute to the artefact's name String, or the artefact itself.mimic: use the referenced artefact'scurrentStartvalues to calculate the rotation-reflection point. Dev-users can reference an artefact by setting the artefact'smimicattribute to the artefact's name String, or the artefact itself, alongside setting itsuseMimicStartflag totrue.path: use a given position's coordinates along the referenced artefact's path to calculate the rotation-reflection point. Dev-users can reference a path-based entity by setting our artefact'spathattribute to the referenced entity's name String, or the referenced entity itself. The position along the path is a float Number between0and1set on our artefact'spathPositionattribute; note that this position can be affected by the value of theconstantSpeedAlongPathboolean attribute – see demo Canvas-030 for an example of this in action.particle: use the referenced particle's current position to calculate the rotation-reflection point.mouse: use the mouse cursor's calculated position relative to the Cell to calculate the rotation-reflection point
Dev-users are able to set an artefact to reference multiple artefacts, one each for the pivot, mimic, path and particle attributes. These attributes can be updated at any time. It is the lockTo attribute which determines which reference will be used to position the artefact.
Pivot specifics
If the referenced artefact is an Element artefact, the artefact is able to pivot to either the Element's start value, or to the position of any of the Element's current corner positions, depending on the value set on the artefact's
pivotCornerattribute.If the referenced artefact is a Polyline entity, the artefact is able to pivot to any of the Polyline's pins, set on the artefact's
pivotPinattribute.If the referenced artefact is an EnhancedLabel entity, the artefact is able to pivot to the EnhancedLabel's template artefact's start value, or to the position of a given textUnit within the EnhancedLabel, depending on the value set on the artefact's
pivotIndexattribute.If the referenced artefact is a Grid entity, the artefact is able to pivot to a specified tile within the Grid, determined by the value set on the artefact's
pivotIndexattribute.If the artefact's
addPivotRotationboolean flag is set totrue, the artefact will add the referenced artefact's rotation value to its own rotation value.If the artefact's
addPivotOffsetboolean flag is set totrue, the artefact will add the referenced artefact'scurrentOffsetvalue to its own offset value.If the artefact's
addPivotHandleboolean flag is set totrue, the artefact will add the referenced artefact'scurrentHandlevalue to its own handle value.
Mimic specifics
Mimic functionality allows an artefact to mimic a range of the referenced artefacts attributes, as follows:
start– settinguseMimicStarttotruemakes the artefact use the referenced artefact's start attribute; settingaddOwnStartToMimicwill add together both the artefact's and the referenced artefact's start values to generate the final result.offset– settinguseMimicOffsettotruemakes the artefact use the referenced artefact's offset attribute; settingaddOwnOffsetToMimicwill add together both the artefact's and the referenced artefact's offset values to generate the final result.handle– settinguseMimicHandletotruemakes the artefact use the referenced artefact's handle attribute; settingaddOwnHandleToMimicwill add together both the artefact's and the referenced artefact's handle values to generate the final result.roll– settinguseMimicRotationtotruemakes the artefact use the referenced artefact's roll attribute; settingaddOwnRotationToMimicwill add together both the artefact's and the referenced artefact's roll values to generate the final result.dimensions– settinguseMimicDimensionstotruemakes the artefact use the referenced artefact's dimensions attribute; settingaddOwnDimensionsToMimicwill add together both the artefact's and the referenced artefact's dimensions values to generate the final result.scale– settinguseMimicScaletotruemakes the artefact use the referenced artefact's scale attribute; settingaddOwnScaleToMimicwill add together both the artefact's and the referenced artefact's scale values to generate the final result.flipReverseandflipUpend– settinguseMimicFliptotruemakes the artefact use the referenced artefact's flipReverse and flipUpend boolean flags as part of its calculations.
Path specifics
If the artefact's
addPathRotationboolean flag is set totrue, the artefact will add the referenced path-based entity's rotation value to its own rotation value.If the artefact's
addPathOffsetboolean flag is set totrue, the artefact will add the referenced path-based entity'scurrentOffsetvalue to its own offset value.If the artefact's
addPathHandleboolean flag is set totrue, the artefact will add the referenced path-based entity'scurrentHandlevalue to its own handle value.