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

683 lines (467 loc) 17.2 kB
// # Polyline factory // A factory for generating an open or closed line/curve based shape entity, using a set of pins to mark the course the line // #### Imports import { artefact, constructors, particle } from '../core/library.js'; import { addStrings, correctForZero, doCreate, isa_boolean, isa_obj, mergeOver, pushUnique, removeItem, xt, xta, Ωempty } from '../helper/utilities.js'; import { makeCoordinate } from '../untracked-factory/coordinate.js'; import { releaseArray, requestArray } from '../helper/array-pool.js'; import baseMix from '../mixin/base.js'; import shapeMix from '../mixin/shape-basic.js'; // Shared constants import { _floor, _isArray, _keys, _parse, _pow, _sqrt, BOTTOM, CENTER, ENTITY, LEFT, MOUSE, PARTICLE, PIVOT, RIGHT, START, T_POLYLINE, TOP, ZERO_PATH } from '../helper/shared-vars.js'; // Local constants const PINS = 'pins', POLYLINE = 'polyline'; // #### Polyline constructor const Polyline = function (items = Ωempty) { this.pins = []; this.currentPins = []; this.controlledLineOffset = makeCoordinate(); this.shapeInit(items); return this; }; // #### Polyline prototype const P = Polyline.prototype = doCreate(); P.type = T_POLYLINE; P.lib = ENTITY; P.isArtefact = true; P.isAsset = false; // #### Mixins baseMix(P); shapeMix(P); // #### Polyline attributes const defaultAttributes = { // The __pins__ attribute takes an array with elements which are: // + [x, y] coordinate arrays, where values can be absolute (Numbers) and/or relative (String%) values // + Artefact objects, or their name-String values // + (`set` function will also accept an object with attributes: `index, x, y` - to be used by Tweens) pins: null, // __tension__ - gives us the curviness of the line: // + 0 - straight lines connecting the pins // + 0.4ish - reasonable looking curves // + 1 - exaggerated curves // + negative values - add loops at the pins tension: 0, // __closed__ - whether to connect all the pins (true), or run from first to last pin only (false) closed: false, mapToPins: false, // __useParticlesAsPins__ - when true, all pins should map directly to Particle objects (as supplied in a Net entity) useParticlesAsPins: false, }; P.defs = mergeOver(P.defs, defaultAttributes); // #### Packet management P.packetExclusions = pushUnique(P.packetExclusions, ['controlledLineOffset']); P.finalizePacketOut = function (copy, items) { const stateCopy = _parse(this.state.saveAsPacket(items))[3]; copy = mergeOver(copy, stateCopy); copy = this.handlePacketAnchor(copy, items); _keys(copy).forEach(key => { if (key === PINS) { const temp = []; copy.pins.forEach(pin => { if (isa_obj(pin)) temp.push(pin.name); else if (_isArray(pin)) temp.push([].concat(pin)); else temp.push(pin); }); copy.pins = temp; } }); return copy; }; // #### Clone management // No additional clone functionality required // #### Kill management // No additional kill functionality required // #### Get, Set, deltaSet const G = P.getters, S = P.setters, D = P.deltaSetters; G.pins = function (item) { if (xt(item)) return this.getPinAt(item); return this.currentPins.concat(); }; S.pins = function (item) { if (xt(item)) { const pins = this.pins; if (_isArray(item)) { pins.forEach((item, index) => this.removePinAt(index)); pins.length = 0; pins.push(...item); this.updateDirty(); } else if (isa_obj(item) && xt(item.index)) { const element = pins[item.index]; if (_isArray(element)) { if (xt(item.x)) element[0] = item.x; if (xt(item.y)) element[1] = item.y; this.updateDirty(); } } } }; D.pins = function (item) { if (xt(item)) { const pins = this.pins; if (isa_obj(item) && xt(item.index)) { const element = pins[item.index]; if (_isArray(element)) { if (xt(item.x)) element[0] = addStrings(element[0], item.x); if (xt(item.y)) element[1] = addStrings(element[1], item.y); this.updateDirty(); } } } }; S.tension = function (item) { if (item.toFixed) { this.tension = item; this.updateDirty(); } }; D.tension = function (item) { if (item.toFixed) { this.tension += item; this.updateDirty(); } }; S.closed = function (item) { this.closed = item; this.updateDirty(); }; S.mapToPins = function (item) { this.mapToPins = item; this.updateDirty(); }; // __flipUpend__ S.flipUpend = function (item) { this.flipUpend = item; this.updateDirty(); }; // __flipReverse__ S.flipReverse = function (item) { this.flipReverse = item; this.updateDirty(); }; // __flipReverse__ S.useAsPath = function (item) { this.useAsPath = item; this.updateDirty(); }; // __pivot__ S.pivot = function (item) { if (isa_boolean(item) && !item) { this.pivot = null; if (this.lockTo[0] === PIVOT) this.lockTo[0] = START; if (this.lockTo[1] === PIVOT) this.lockTo[1] = START; this.dirtyStampPositions = true; this.dirtyStampHandlePositions = true; } else { const oldPivot = this.pivot, newPivot = (item.substring) ? artefact[item] : item, name = this.name; if (newPivot && newPivot.name) { if (oldPivot && oldPivot.name !== newPivot.name) removeItem(oldPivot.pivoted, name); pushUnique(newPivot.pivoted, name); this.pivot = newPivot; this.dirtyStampPositions = true; this.dirtyStampHandlePositions = true; } } this.updateDirty(); }; // #### Prototype functions // `updateDirty` - internal setter helper function P.updateDirty = function () { this.dirtySpecies = true; this.dirtyPathObject = true; this.dirtyPins = true; }; // `getPinAt` - P.getPinAt = function (index) { const i = _floor(index); if (this.useAsPath) { const pos = this.getPathPositionData(this.unitPartials[i]); return [pos.x, pos.y]; } else { const pins = this.currentPins, pin = pins[i]; const [x, y] = this.localBox; const [px, py] = pin; const [ox] = pins[0]; const [lx, ly] = this.localOffset; const [sx, sy] = this.currentStampPosition; let dx, dy; if (this.mapToPins) { dx = px - ox + x; dy = py - ox + y; } else { dx = px - lx; dy = py - ly; } return [sx + dx, sy + dy]; } }; // `updatePinAt` - P.updatePinAt = function (item, index) { if (xta(item, index)) { index = _floor(index); const pins = this.pins; if (index < pins.length && index >= 0) { const oldPin = pins[index]; if (isa_obj(oldPin) && oldPin.pivoted) removeItem(oldPin.pivoted, this.name); pins[index] = item; this.updateDirty(); } } }; // `removePinAt` - P.removePinAt = function (index) { index = _floor(index); const pins = this.pins; if (index < pins.length && index >= 0) { const oldPin = pins[index]; if (isa_obj(oldPin) && oldPin.pivoted) removeItem(oldPin.pivoted, this.name); pins[index] = null; this.updateDirty(); } }; // `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.useParticlesAsPins) this.dirtyPins = true; if (this.dirtyPins || this.dirtyLock) this.dirtySpecies = true; 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(); this.updatePathSubscribers(); } 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(); }; // `cleanSpecies` - internal helper function - called by `prepareStamp` P.cleanSpecies = function () { this.dirtySpecies = false; this.pathDefinition = this.makePolylinePath(); }; // `getPathParts` - internal helper function - called by `makePolylinePath` P.getPathParts = function (x0, y0, x1, y1, x2, y2, t) { const d01 = _sqrt(_pow(x1 - x0, 2) + _pow(y1 - y0, 2)), d12 = _sqrt(_pow(x2 - x1, 2) + _pow(y2 - y1, 2)), fa = t * d01 / (d01 + d12), fb = t * d12 / (d01 + d12), p1x = x1 - fa * (x2 - x0), p1y = y1 - fa * (y2 - y0), p2x = x1 + fb * (x2 - x0), p2y = y1 + fb * (y2 - y0); return [p1x, p1y, x1, y1, p2x, p2y]; }; // `buildLine` - internal helper function - called by `makePolylinePath` P.buildLine = function (x, y, coords) { let p = `${ZERO_PATH}l`; for (let i = 2; i < coords.length; i += 6) { p += `${correctForZero(coords[i] - x)},${correctForZero(coords[i + 1] - y)} `; x = coords[i]; y = coords[i + 1]; } return p; }; // `buildCurve` - internal helper function - called by `makePolylinePath` P.buildCurve = function (x, y, coords) { let p = `${ZERO_PATH}c`, counter = 0; for (let i = 0; i < coords.length; i += 2) { p += `${correctForZero(coords[i] - x)},${correctForZero(coords[i + 1] - y)} `; counter++; if (counter > 2) { x = coords[i]; y = coords[i + 1]; counter = 0; } } return p; }; // `cleanCoordinate` - internal helper function - called by `cleanPinsArray` P.cleanCoordinate = function (coord, dim) { if (coord.toFixed) return coord; if (coord === LEFT || coord === TOP) return 0; if (coord === RIGHT || coord === BOTTOM) return dim; if (coord === CENTER) return dim / 2; return (parseFloat(coord) / 100) * dim; }; // `cleanPinsArray` - internal helper function - called by `makePolylinePath` P.cleanPinsArray = function () { this.dirtyPins = false; const pins = this.pins, current = this.currentPins; current.length = 0; if (this.useParticlesAsPins) { pins.forEach((part, index) => { let temp; if (part && part.substring) { temp = particle[part]; if (temp) pins[index] = temp; } else temp = part; const pos = (temp && temp.position) ? temp.position : false; if (pos) current.push([pos.x, pos.y]); }); if (!current.length) this.dirtyPins = true; } else { const host = this.getHost(), clean = this.cleanCoordinate; let w = 1, h = 1, x, y, dims; if (host) { dims = host.currentDimensions; if (dims) { [w, h] = dims; } } pins.forEach((item, index) => { let temp; if (item && item.substring) { temp = artefact[item]; pins[index] = temp; } else temp = item; if (temp) { if (_isArray(temp)) { [x, y] = temp; current.push([clean(x, w), clean(y, h)]); } else if (isa_obj(temp) && temp.currentStart) { const name = this.name; if (!temp.pivoted.includes(name)) pushUnique(temp.pivoted, name); current.push([...temp.currentStampPosition]); } } }); } if (current.length) { // Calculate the local offset let mx = current[0][0], my = current[0][1]; current.forEach(e => { if (e[0] < mx) mx = e[0]; if (e[1] < my) my = e[1]; }) this.localOffset = [mx, my]; this.updatePivotSubscribers(); } }; // `makePolylinePath` - internal helper function - called by `cleanSpecies` P.makePolylinePath = function () { const getPathParts = this.getPathParts, buildLine = this.buildLine, buildCurve = this.buildCurve, cPin = this.currentPins, tension = this.tension, closed = this.closed; // 1. go through the pins array and get current values for each, pushed into currentPins array if (this.dirtyPins) this.cleanPinsArray(); if (cPin.length) { // 2. build the line const cLen = cPin.length, first = cPin[0], last = cPin[cLen - 1]; const calc = requestArray(); let result = ZERO_PATH, i; if (closed) { const startPoint = requestArray(); startPoint.push(...getPathParts(...last, ...first, ...cPin[1], tension)); for (i = 0; i < cLen - 2; i++) { calc.push(...getPathParts(...cPin[i], ...cPin[i + 1], ...cPin[i + 2], tension)); } calc.push(...getPathParts(...cPin[cLen - 2], ...last, ...first, tension)); calc.unshift(startPoint[4], startPoint[5]); calc.push(startPoint[0], startPoint[1], startPoint[2], startPoint[3]); if (tension) result = buildCurve(first[0], first[1], calc) + 'z'; else result = buildLine(first[0], first[1], calc) + 'z'; releaseArray(startPoint); } else { calc.push(first[0], first[1]); for (i = 0; i < cLen - 2; i++) { calc.push(...getPathParts(...cPin[i], ...cPin[i + 1], ...cPin[i + 2], tension)); } calc.push(last[0], last[1], last[0], last[1]); if (tension) result = buildCurve(first[0], first[1], calc); else result = buildLine(first[0], first[1], calc); } releaseArray(calc); return result; } return ZERO_PATH; }; P.calculateLocalPathAdditionalActions = function () { const [x, y] = this.localBox, def = this.pathDefinition; if (this.mapToPins) { this.set({ start: this.currentPins[0], }); } else this.pathDefinition = def.replace(ZERO_PATH, `m${-x},${-y}`); this.pathCalculatedOnce = false; // ALWAYS, when invoking `calculateLocalPath` from `calculateLocalPathAdditionalActions`, include the second argument, set to `true`! Failure to do this leads to an infinite loop which will make your machine weep. // + We need to recalculate the local path to take into account the offset required to put the Oval entity's start coordinates at the top-left of the local box, and to recalculate the data used by other artefacts to place themselves on, or move along, its path. this.calculateLocalPath(this.pathDefinition, true); }; // `updatePathSubscribers` P.updatePathSubscribers = function () { this.pathed.forEach(name => { const instance = artefact[name]; if (instance) instance.dirtyStart = true; }); }; // #### Factories // ##### makePolyline // Accepts argument with attributes: // + __pins__ (required) - an Array of either coordinate (`[x, y]`) arrays with coordinates defined as absolute (Number) or relative (String%) values; or artefact objects (or their name-String values). // + __tension__ float Number representing the bendiness of the line - for example: `0` (straight lines); `0.3` (a reasonably curved line). // + __closed__ Boolean - when set, the start and end pins will be joined to complete the shape // + __mapToPins__ Boolean - when set, the line will map to its initial pin coordinate // // ``` // scrawl.makePolyline({ // // name: 'my-polyline', // // pins: [[10, 10], ['20%', '90%'], [120, 'center']], // // tension: 0.3, // closed: true, // mapToPins: true, // // strokeStyle: 'orange', // lineWidth: 6, // lineCap: 'round', // lineJoin: 'round', // shadowColor: 'black', // // method: 'draw', // }); // ``` export const makePolyline = function (items) { if (!items) return false; items.species = POLYLINE; return new Polyline(items); }; constructors.Polyline = Polyline;