UNPKG

scrawl-canvas

Version:

Responsive, interactive and more accessible HTML5 canvas elements. Scrawl-canvas is a JavaScript library designed to make using the HTML5 canvas element easier, and more fun

665 lines (447 loc) 19.5 kB
// # Shape-basic mixin // This mixin defines the key attributes and functionality for all Scrawl-canvas __path-defined entitys__. // #### Imports import { artefact } from '../core/library.js'; import { mergeOver, pushUnique, xt, λnull, Ωempty } from '../helper/utilities.js'; import { releaseVector, requestVector } from '../untracked-factory/vector.js'; import { releaseArray, requestArray } from '../helper/array-pool.js'; import { calculatePath, releasePathCalcObject, requestPathCalcObject } from '../helper/shape-path-calculation.js'; import entityMix from './entity.js'; // Shared constants import { _atan2, _isFinite, _parse, _piHalf, _pow, _radian, BEZIER, CLOSE, DESTINATION_OUT, LINEAR, MOUSE, MOVE, PARTICLE, QUADRATIC, SOURCE_OVER, T_BEZIER, T_LINE, T_POLYLINE, T_QUADRATIC, UNKNOWN, ZERO_STR } from '../helper/shared-vars.js'; // Local constants const HALFTRANS = 'rgb(0 0 0 / 0.5)'; // #### Export function export default function (P = Ωempty) { // #### Mixins // + [entity](../mixin/entity.html) entityMix(P); // #### Shared attributes const defaultAttributes = { species: ZERO_STR, useAsPath: false, precision: 10, pathDefinition: ZERO_STR, showBoundingBox: false, boundingBoxColor: HALFTRANS, minimumBoundingBoxDimensions: 20, }; P.defs = mergeOver(P.defs, defaultAttributes); // #### Packet management P.packetExclusions = pushUnique(P.packetExclusions, ['dimensions', 'pathed']); P.finalizePacketOut = function (copy, items) { const stateCopy = _parse(this.state.saveAsPacket(items))[3]; copy = mergeOver(copy, stateCopy); copy = this.handlePacketAnchor(copy, items); return copy; }; // #### Clone management // No additional clone functionality defined here // #### Kill management // No additional kill functionality defined here // #### Get, Set, deltaSet const S = P.setters, D = P.deltaSetters; S.species = function (item) { if (xt(item)) { this.species = item; this.updateDirty(); } }; S.precision = function (item) { if (xt(item) && item.toFixed) { this.precision = item; this.updateDirty(); } }; // Invalidate __dimensions__ setters - dimensions are an emergent property of shapes, not a defining property S.width = λnull; S.height = λnull; S.dimensions = λnull; D.width = λnull; D.height = λnull; D.dimensions = λnull; // __pathDefinition__ S.pathDefinition = function (item) { if (item.substring) this.pathDefinition = item; this.pathCalculatedOnce = false; this.dirtyPathObject = true; }; // #### Prototype functions // `updateDirty` - internal setter helper function P.updateDirty = function () { this.dirtySpecies = true; this.dirtyPathObject = true; this.dirtyFilterIdentifier = true; }; // `shapeInit` - internal constructor helper function P.shapeInit = function (items) { this.units = []; this.unitLengths = []; this.unitPartials = []; this.pathed = []; this.localBox = []; this.localPath = null; this.length = 0; this.unitProgression = []; this.unitPositions = []; this.entityInit(items); }; // #### Path-related functionality // `positionPointOnPath` P.positionPointOnPath = function (vals) { const v = requestVector(vals); v.vectorSubtract(this.currentStampHandlePosition); if(this.flipReverse) v.x = -v.x; if(this.flipUpend) v.y = -v.y; v.rotate(this.roll); v.vectorAdd(this.currentStampPosition); const res = { x: v.x, y: v.y } releaseVector(v); return res; }; // `getBezierXY` P.getBezierXY = function (t, sx, sy, cp1x, cp1y, cp2x, cp2y, ex, ey) { const T = 1 - t; return { x: (_pow(T, 3) * sx) + (3 * t * _pow(T, 2) * cp1x) + (3 * t * t * T * cp2x) + (t * t * t * ex), y: (_pow(T, 3) * sy) + (3 * t * _pow(T, 2) * cp1y) + (3 * t * t * T * cp2y) + (t * t * t * ey) }; }; // `getQuadraticXY` P.getQuadraticXY = function (t, sx, sy, cp1x, cp1y, ex, ey) { const T = 1 - t; return { x: T * T * sx + 2 * T * t * cp1x + t * t * ex, y: T * T * sy + 2 * T * t * cp1y + t * t * ey }; }; // `getLinearXY` P.getLinearXY = function (t, sx, sy, ex, ey) { return { x: sx + ((ex - sx) * t), y: sy + ((ey - sy) * t) }; }; // `getBezierAngle` P.getBezierAngle = function (t, sx, sy, cp1x, cp1y, cp2x, cp2y, ex, ey) { const T = 1 - t, dx = _pow(T, 2) * (cp1x - sx) + 2 * t * T * (cp2x - cp1x) + t * t * (ex - cp2x), dy = _pow(T, 2) * (cp1y - sy) + 2 * t * T * (cp2y - cp1y) + t * t * (ey - cp2y); return (-_atan2(dx, dy) + _piHalf) / _radian; }; // `getQuadraticAngle` P.getQuadraticAngle = function (t, sx, sy, cp1x, cp1y, ex, ey) { const T = 1 - t, dx = 2 * T * (cp1x - sx) + 2 * t * (ex - cp1x), dy = 2 * T * (cp1y - sy) + 2 * t * (ey - cp1y); return (-_atan2(dx, dy) + _piHalf) / _radian; }; // `getLinearAngle` P.getLinearAngle = function (t, sx, sy, ex, ey) { const dx = ex - sx, dy = ey - sy; return (-_atan2(dx, dy) + _piHalf) / _radian; }; // `getConstantPosition` - internal function called by `getPathPositionData` P.getConstantPosition = function (pos) { if (!_isFinite(pos)) return 0; if (pos >= 1) return 0.9999; const { unitPositions, unitProgression, length } = this; if (unitPositions && unitPositions.length) { const arraysLen = unitPositions.length; let index = 0, steadyDistance = 0, dynamicDistance = 0, sectionRatio; for (let i = 0; i < arraysLen; i++) { sectionRatio = unitProgression[i] / length; if (pos > sectionRatio) { steadyDistance = unitPositions[i]; dynamicDistance = sectionRatio; index++; } } const remainingDynamicDistance = (index) ? (pos - dynamicDistance) : pos; const dynamicSegmentLength = (index) ? (unitProgression[index] - unitProgression[index - 1]) / length : unitProgression[index] / length; const steadySegmentLength = (index) ? (unitPositions[index] - unitPositions[index - 1]) : unitPositions[index]; const steadyToDynamicRatio = steadySegmentLength / dynamicSegmentLength; steadyDistance += (remainingDynamicDistance * steadyToDynamicRatio); return steadyDistance; } else return pos; }; // `buildPathPositionObject` - internal function called by `getPathPositionData` P.buildPathPositionObject = function (unit, myLen) { if (unit) { const [unitSpecies, ...vars] = unit; let myPoint, angle; switch (unitSpecies) { case LINEAR : myPoint = this.positionPointOnPath(this.getLinearXY(myLen, ...vars)); angle = this.getLinearAngle(myLen, ...vars); break; case QUADRATIC : myPoint = this.positionPointOnPath(this.getQuadraticXY(myLen, ...vars)); angle = this.getQuadraticAngle(myLen, ...vars); break; case BEZIER : myPoint = this.positionPointOnPath(this.getBezierXY(myLen, ...vars)); angle = this.getBezierAngle(myLen, ...vars); break; } let flipAngle = 0 if (this.flipReverse) flipAngle++; if (this.flipUpend) flipAngle++; if (flipAngle === 1) angle = -angle; angle += this.roll; myPoint.angle = angle; return myPoint; } return false; } // `getPathPositionData` // + Also useful in user code to retrieve the Cell-relative coordinates of any point (measured as a float Number between `0` and `1` along the path) // + The second argument - a Boolean - rectifies for constant speed P.getPathPositionData = function (pos, constantSpeed = false) { if (this.useAsPath && xt(pos) && pos.toFixed) { const unitPartials = this.unitPartials; let previousLen = 0, remainder = pos % 1; let stoppingLen, myLen, i, iz, unit, species; // ... because sometimes everything doesn't all add up to 1 if (pos === 0) remainder = 0; else if (pos === 1) remainder = 0.9999; if (constantSpeed) remainder = this.getConstantPosition(remainder); // 1. Determine the pertinent subpath to use for calculation for (i = 0, iz = unitPartials.length; i < iz; i++) { species = this.units[i][0]; if (species === MOVE || species === CLOSE || species === UNKNOWN) continue; stoppingLen = unitPartials[i]; if (remainder <= stoppingLen) { // 2. Calculate point along the subpath the pos value represents unit = this.units[i]; myLen = (remainder - previousLen) / (stoppingLen - previousLen); break; } previousLen = stoppingLen; } return this.buildPathPositionObject(unit, myLen); } return false; }; // #### Display cycle functionality // `prepareStamp` - the purpose of most of these actions is described in the [entity mixin function](http://localhost:8080/docs/source/mixin/entity.html#section-31) that this function overwrites P.prepareStamp = function() { if (this.dirtyHost) this.dirtyHost = false; if (this.dirtyScale || this.dirtySpecies || this.dirtyDimensions || this.dirtyStart || this.dirtyHandle) { this.dirtyPathObject = true; if (this.dirtyScale || this.dirtySpecies) this.pathCalculatedOnce = false; } if (this.isBeingDragged || this.lockTo.includes(MOUSE) || this.lockTo.includes(PARTICLE)) this.dirtyStampPositions = true; if (this.dirtyScale) this.cleanScale(); if (this.dirtyStart) this.cleanStart(); if (this.dirtyOffset) this.cleanOffset(); if (this.dirtyRotation) this.cleanRotation(); if (this.dirtyStampPositions) this.cleanStampPositions(); if (this.dirtySpecies) this.cleanSpecies(); if (this.dirtyPathObject) this.cleanPathObject(); if (this.dirtyPositionSubscribers) this.updatePositionSubscribers(); // `prepareStampTabsHelper` is defined in the `mixin/hidden-dom-elements.js` file - handles updates to anchor and button objects this.prepareStampTabsHelper(); }; // `cleanDimensions` - internal helper function called by `prepareStamp` // + Dimensional data has no meaning in the context of Shape entitys (beyond positioning handle Coordinates): width and height are emergent properties that cannot be set on the entity. P.cleanDimensions = function () { this.dirtyDimensions = false; this.dirtyStart = true; this.dirtyHandle = true; this.dirtyOffset = true; }; // `cleanPathObject` - internal helper function - called by `prepareStamp` P.cleanPathObject = function () { this.dirtyPathObject = false; if (!this.noPathUpdates || !this.pathObject) { if (this.dirtyDimensions) { this.cleanSpecies(); this.pathCalculatedOnce = false; } this.calculateLocalPath(this.pathDefinition); if (this.dirtyDimensions) this.cleanDimensions(); if (this.dirtyHandle) this.cleanHandle(); if (this.dirtyStampHandlePositions) this.cleanStampHandlePositions(); const handle = this.currentStampHandlePosition; this.pathObject = new Path2D(`m${-handle[0]},${-handle[1]}${this.localPath}`); } }; // `calculateLocalPath` - internal helper function - called by `cleanPathObject` P.calculateLocalPath = function (d, isCalledFromAdditionalActions) { let res; if (!this.pathCalculatedOnce) { res = calculatePath(d, this.currentScale, this.currentStart, this.useAsPath, this.precision, requestPathCalcObject()); this.pathCalculatedOnce = true; } if (res) { this.localPath = res.localPath; this.length = res.length; const maxX = res.maxX, maxY = res.maxY, minX = res.minX, minY = res.minY; const dims = this.dimensions, currentDims = this.currentDimensions, box = this.localBox; dims[0] = maxX - minX; dims[1] = maxY - minY; if(dims[0] !== currentDims[0] || dims[1] !== currentDims[1]) { currentDims[0] = dims[0]; currentDims[1] = dims[1]; this.dirtyHandle = true; } box.length = 0; box.push(minX, minY, dims[0], dims[1]); if (this.useAsPath) { // we can do work here to flatten some of these arrays const {units, unitLengths, unitPartials, unitProgression, unitPositions} = res; const flatProgression = requestArray(), flatPositions = requestArray(); let lastLength = 0, currentPartial, lastPartial, progression, positions, i, iz, j, jz, l, p; for (i = 0, iz = unitLengths.length; i < iz; i++) { lastLength += unitLengths[i]; progression = unitProgression[i]; if (progression) { lastPartial = unitPartials[i]; currentPartial = unitPartials[i + 1] - lastPartial; positions = unitPositions[i]; for (j = 0, jz = progression.length; j < jz; j++) { l = lastLength + progression[j]; flatProgression.push(l); p = lastPartial + (positions[j] * currentPartial); flatPositions.push(p); } } } this.units.length = 0; this.units.push(...units); this.unitLengths.length = 0; this.unitLengths.push(...unitLengths); this.unitPartials.length = 0; this.unitPartials.push(...unitPartials); if (!this.unitProgression) this.unitProgression = []; this.unitProgression.length = 0; this.unitProgression.push(...flatProgression); if (!this.unitPositions) this.unitPositions = []; this.unitPositions.length = 0; this.unitPositions.push(...flatPositions); releaseArray(flatProgression, flatPositions); } releasePathCalcObject(res); if (!isCalledFromAdditionalActions) this.calculateLocalPathAdditionalActions(); } }; P.calculateLocalPathAdditionalActions = λnull; // `updatePathSubscribers` P.updatePathSubscribers = function () { let art; this.pathed.forEach(name => { art = artefact[name]; if (art) { art.currentPathData = false; art.dirtyStart = true; if (art.addPathHandle) art.dirtyHandle = true; if (art.addPathOffset) art.dirtyOffset = true; if (art.addPathRotation) art.dirtyRotation = true; if (art.type === T_POLYLINE) art.dirtyPins = true; else if (art.type === T_LINE || art.type === T_QUADRATIC || art.type === T_BEZIER) art.dirtyPins.push(this.name); } }, this); }; // #### Stamp methods // All actual drawing is achieved using the entity's pre-calculated [Path2D object](https://developer.mozilla.org/en-US/docs/Web/API/Path2D). // `draw` P.draw = function (engine) { engine.stroke(this.pathObject); if (this.showBoundingBox) this.drawBoundingBox(engine); }; // `fill` P.fill = function (engine) { engine.fill(this.pathObject, this.winding); if (this.showBoundingBox) this.drawBoundingBox(engine); }; // `drawAndFill` P.drawAndFill = function (engine) { const p = this.pathObject; engine.stroke(p); this.currentHost.clearShadow(); engine.fill(p, this.winding); if (this.showBoundingBox) this.drawBoundingBox(engine); }; // `fillAndDraw` P.fillAndDraw = function (engine) { const p = this.pathObject; engine.stroke(p); this.currentHost.clearShadow(); engine.fill(p, this.winding); engine.stroke(p); if (this.showBoundingBox) this.drawBoundingBox(engine); }; // `drawThenFill` P.drawThenFill = function (engine) { const p = this.pathObject; engine.stroke(p); engine.fill(p, this.winding); if (this.showBoundingBox) this.drawBoundingBox(engine); }; // `fillThenDraw` P.fillThenDraw = function (engine) { const p = this.pathObject; engine.fill(p, this.winding); engine.stroke(p); if (this.showBoundingBox) this.drawBoundingBox(engine); }; // `clear` P.clear = function (engine) { const gco = engine.globalCompositeOperation; engine.globalCompositeOperation = DESTINATION_OUT; engine.fill(this.pathObject, this.winding); engine.globalCompositeOperation = gco; if (this.showBoundingBox) this.drawBoundingBox(engine); }; // `drawBoundingBox` P.drawBoundingBox = function (engine) { engine.save(); engine.strokeStyle = this.boundingBoxColor; engine.lineWidth = 1; engine.globalCompositeOperation = SOURCE_OVER; engine.globalAlpha = 1; engine.shadowOffsetX = 0; engine.shadowOffsetY = 0; engine.shadowBlur = 0; engine.strokeRect(...this.getBoundingBox()); engine.restore(); }; // `getBoundingBox` P.getBoundingBox = function () { const minDims = this.minimumBoundingBoxDimensions; /* eslint-disable-next-line */ let [x, y, w, h] = this.localBox; const [hX, hY] = this.currentStampHandlePosition; const [sX, sY] = this.currentStampPosition; // Pad out excessively thin widths and heights if (w < minDims) w = minDims; if (h < minDims) h = minDims; return [x - hX, y - hY, w, h, sX, sY]; }; }