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

635 lines (489 loc) 19.8 kB
// # Shape path calculation // The function, and its helper functions, translates SVG path data string - example: `M0,0H100V100H-100V-100z` - into data which can be used to construct the a Path2D object which can be consumed by the canvasRenderingContext2D engine's `fill`, `stroke` and `isPointInPath` functions // + TODO: this file is in the wrong place: it's not a mixin; it exports a single function currently imported only by ./factory.shape.js; and it duplicates Vector code // + Code has been separated out into its own file because it is a potential candidate for hiving off to a web worker // #### Imports import { releaseArray, requestArray } from './array-pool.js'; // Shared constants import { _atan2, _cos, _isFinite, _max, _min, _pow, _sin, _sqrt, BEZIER, CLOSE, LINEAR, MOVE, QUADRATIC, UNKNOWN, ZERO_STR } from './shared-vars.js'; // Local constants const GET_BEZIER = 'getBezierXY', GET_QUADRATIC = 'getQuadraticXY'; // We only use one pathCalcObject, but treat it like a pool of such objects for resetting to defaults const pathCalcObjectPool = []; // Exported helper functions export const requestPathCalcObject = function () { if (!pathCalcObjectPool.length) pathCalcObjectPool.push({ localPath: null, length: 0, maxX: 0, maxY: 0, minX: 0, minY: 0, unitLengths: [], unitPartials: [], units: [], unitPositions: [], unitProgression: [], xRange: [], yRange: [], }); return pathCalcObjectPool.shift(); }; export const releasePathCalcObject = function (a) { a.localPath = null; a.length = 0; a.units.length = 0; a.maxX = 0; a.maxY = 0; a.minX = 0; a.minY = 0; a.unitLengths.length = 0; a.unitPartials.length = 0; a.unitPositions.length = 0; a.unitProgression.length = 0; a.xRange.length = 0; a.yRange.length = 0; pathCalcObjectPool.push(a); }; // #### Export function export const calculatePath = (d, scale, start, useAsPath, precision, result) => { // Setup local variables const points = requestArray(), myData = requestArray(), xPoints = requestArray(), yPoints = requestArray(), units = result.units, unitLengths = result.unitLengths, unitPartials = result.unitPartials, progression = result.unitProgression, positions = result.unitPositions, mySet = d.match(/([A-Za-z][0-9. ,-]*)/g), localMatch = /(-?[0-9.]+\b)/g; let command = ZERO_STR, localPath = ZERO_STR, myLen = 0, i, iz, j, jz, checkMatch, curX = 0, curY = 0, oldX = 0, oldY = 0, reflectX = 0, reflectY = 0; // Local function to populate the temporary myData array with data for every path partial const buildArrays = (thesePoints) => { myData.push({ c: command.toLowerCase(), p: thesePoints || null, x: oldX, y: oldY, cx: curX, cy: curY, rx: reflectX, ry: reflectY }); if (!useAsPath) { xPoints.push(curX); yPoints.push(curY); } oldX = curX; oldY = curY; }; // The purpose of this loop is to // 1. convert all point values from strings to floats // 2. scale every value // 3. relativize every value to the last stated cursor position // 4. populate the temporary myData array with data which can be used for all subsequent calculations for (i = 0, iz = mySet.length; i < iz; i++) { command = mySet[i][0]; points.length = 0; checkMatch = mySet[i].match(localMatch); if (checkMatch) points.push(...checkMatch); if (points.length) { for (j = 0, jz = points.length; j < jz; j++) { points[j] = parseFloat(points[j]); } if (i === 0) { if (command === 'M') { oldX = (points[0] * scale) - start.x; oldY = (points[1] * scale) - start.y; command = 'm'; } } else { oldX = curX; oldY = curY; } switch (command) { case 'H': for (j = 0, jz = points.length; j < jz; j++) { points[j] = (points[j] * scale) - oldX; curX += points[j]; reflectX = reflectY = 0; buildArrays(points.slice(j, j + 1)); } break; case 'V': for (j = 0, jz = points.length; j < jz; j++) { points[j] = (points[j] * scale) - oldY; curY += points[j]; reflectX = reflectY = 0; buildArrays(points.slice(j, j + 1)); } break; case 'M': for (j = 0, jz = points.length; j < jz; j += 2) { points[j] = (points[j] * scale) - oldX; points[j + 1] = (points[j + 1] * scale) - oldY; curX += points[j]; curY += points[j + 1]; reflectX = reflectY = 0; buildArrays(points.slice(j, j + 2)); } break; case 'L': case 'T': for (j = 0, jz = points.length; j < jz; j += 2) { points[j] = (points[j] * scale) - oldX; points[j + 1] = (points[j + 1] * scale) - oldY; curX += points[j]; curY += points[j + 1]; if (command === 'T') { reflectX = points[j] + oldX; reflectY = points[j + 1] + oldY; } else { reflectX = reflectY = 0; } buildArrays(points.slice(j, j + 2)); } break; case 'Q': case 'S': for (j = 0, jz = points.length; j < jz; j += 4) { points[j] = (points[j] * scale) - oldX; points[j + 1] = (points[j + 1] * scale) - oldY; points[j + 2] = (points[j + 2] * scale) - oldX; points[j + 3] = (points[j + 3] * scale) - oldY; curX += points[j + 2]; curY += points[j + 3]; reflectX = points[j] + oldX; reflectY = points[j + 1] + oldY; buildArrays(points.slice(j, j + 4)); } break; case 'C': for (j = 0, jz = points.length; j < jz; j += 6) { points[j] = (points[j] * scale) - oldX; points[j + 1] = (points[j + 1] * scale) - oldY; points[j + 2] = (points[j + 2] * scale) - oldX; points[j + 3] = (points[j + 3] * scale) - oldY; points[j + 4] = (points[j + 4] * scale) - oldX; points[j + 5] = (points[j + 5] * scale) - oldY; curX += points[j + 4]; curY += points[j + 5]; reflectX = points[j + 2] + oldX; reflectY = points[j + 3] + oldY; buildArrays(points.slice(j, j + 6)); } break; case 'A': for (j = 0, jz = points.length; j < jz; j += 7) { points[j + 5] = (points[j + 5] * scale) - oldX; points[j + 6] = (points[j + 6] * scale) - oldY; curX += points[j + 5]; curY += points[j + 6]; reflectX = reflectY = 0; buildArrays(points.slice(j, j + 7)); } break; case 'h': for (j = 0, jz = points.length; j < jz; j++) { points[j] *= scale; curX += points[j]; reflectX = reflectY = 0; buildArrays(points.slice(j, j + 1)); } break; case 'v': for (j = 0, jz = points.length; j < jz; j++) { points[j] *= scale; curY += points[j]; reflectX = reflectY = 0; buildArrays(points.slice(j, j + 1)); } break; case 'm': case 'l': case 't': for (j = 0, jz = points.length; j < jz; j += 2) { points[j] *= scale; points[j + 1] *= scale; curX += points[j]; curY += points[j + 1]; if (command === 't') { reflectX = points[j] + oldX; reflectY = points[j + 1] + oldY; } else { reflectX = reflectY = 0; } buildArrays(points.slice(j, j + 2)); } break; case 'q': case 's': for (j = 0, jz = points.length; j < jz; j += 4) { points[j] *= scale; points[j + 1] *= scale; points[j + 2] *= scale; points[j + 3] *= scale; curX += points[j + 2]; curY += points[j + 3]; reflectX = points[j] + oldX; reflectY = points[j + 1] + oldY; buildArrays(points.slice(j, j + 4)); } break; case 'c': for (j = 0, jz = points.length; j < jz; j += 6) { points[j] *= scale; points[j + 1] *= scale; points[j + 2] *= scale; points[j + 3] *= scale; points[j + 4] *= scale; points[j + 5] *= scale; curX += points[j + 4]; curY += points[j + 5]; reflectX = points[j + 2] + oldX; reflectY = points[j + 3] + oldY; buildArrays(points.slice(j, j + 6)); } break; case 'a': for (j = 0, jz = points.length; j < jz; j += 7) { points[j] *= scale; points[j + 1] *= scale; points[j + 5] *= scale; points[j + 6] *= scale; curX += points[j + 5]; curY += points[j + 6]; reflectX = reflectY = 0; buildArrays(points.slice(j, j + 7)); } break; } } else { reflectX = reflectY = 0; buildArrays(); } } // This loop builds the local path string for (i = 0, iz = myData.length; i < iz; i++) { const curData = myData[i], myPts = curData.p; if (myPts) localPath += `${curData.c}${curData.p.join()}`; else localPath += `${curData.c}`; } result.localPath = localPath; // Calculates unit lengths and sum of lengths, alongside obtaining data to build a more accurate bounding box if (useAsPath) { // Request a vector - used for reflection points const v = vector; let curData, prevData, c, p, x, y, cx, cy; // This loop calculates this.units array data // + because the lengths calculations requires absolute coordinates // + and TtSs path units use reflective coordinates for (i = 0, iz = myData.length; i < iz; i++) { curData = myData[i]; prevData = (i > 0) ? myData[i - 1] : false; ({c, p, x, y, cx, cy} = curData); if (p) { switch (c) { case 'h' : units[i] = [LINEAR, x, y, p[0] + x, y]; break; case 'v' : units[i] = [LINEAR, x, y, x, p[0] + y]; break; case 'm' : units[i] = [MOVE, x, y]; break; case 'l' : units[i] = [LINEAR, x, y, p[0] + x, p[1] + y]; break; case 't' : if (prevData && (prevData.rx || prevData.ry)) { setVector(v, prevData.rx - cx, prevData.ry - cy); rotateVector(v, 180); units[i] = [QUADRATIC, x, y, v.x + cx, v.y + cy, p[0] + x, p[1] + y]; } else units[i] = [QUADRATIC, x, y, x, y, p[0] + x, p[1] + y]; break; case 'q' : units[i] = [QUADRATIC, x, y, p[0] + x, p[1] + y, p[2] + x, p[3] + y]; break; case 's' : if (prevData && (prevData.rx || prevData.ry)) { setVector(v, prevData.rx - cx, prevData.ry - cy); rotateVector(v, 180); units[i] = [BEZIER, x, y, v.x + cx, v.y + cy, p[0] + x, p[1] + y, p[2] + x, p[3] + y]; } else units[i] = [BEZIER, x, y, x, y, p[0] + x, p[1] + y, p[2] + x, p[3] + y]; break; case 'c' : units[i] = [BEZIER, x, y, p[0] + x, p[1] + y, p[2] + x, p[3] + y, p[4] + x, p[5] + y]; break; case 'a' : units[i] = [LINEAR, x, y, p[5] + x, p[6] + y]; break; case 'z' : if (!_isFinite(x)) x = 0; if (!_isFinite(y)) y = 0; units[i] = [CLOSE, x, y]; break; default : if (!_isFinite(x)) x = 0; if (!_isFinite(y)) y = 0; units[i] = [UNKNOWN, x, y]; } } else units[i] = [`no-points-${c}`, x, y]; } for (i = 0, iz = units.length; i < iz; i++) { const [spec, ...data] = units[i]; let localResults; switch (spec) { case LINEAR : case QUADRATIC : case BEZIER : localResults = getShapeUnitMetaData(spec, precision, data); unitLengths[i] = localResults.length; xPoints.push(...localResults.xPoints); yPoints.push(...localResults.yPoints); progression.push(localResults.progression); positions.push(localResults.positions); break; default : unitLengths[i] = 0; } } myLen = unitLengths.reduce((a, v) => a + v, 0); let mySum = 0; for (i = 0, iz = unitLengths.length; i < iz; i++) { mySum += unitLengths[i] / myLen; unitPartials[i] = mySum; } } result.length = myLen; // calculate bounding box dimensions result.maxX = _max(...xPoints); result.maxY = _max(...yPoints); result.minX = _min(...xPoints); result.minY = _min(...yPoints); result.xRange.push(...xPoints); result.yRange.push(...yPoints); releaseArray(points, myData, xPoints, yPoints); return result; }; // #### Locally defined Vector // + TODO: consider whether we could swap out this vector stuff for a pool coordinate? const vector = { x: 0, y: 0 }; const setVector = function (v, x, y) { v.x = x; v.y = y; }; const rotateVector = function (v, angle) { let arg = _atan2(v.y, v.x); arg += (angle * 0.01745329251); const mag = _sqrt((v.x * v.x) + (v.y * v.y)); v.x = mag * _cos(arg); v.y = mag * _sin(arg); }; // #### Helper functions // `getShapeUnitMetaData` const getShapeUnitMetaData = function (species, precision, args) { let xPts = [], yPts = [], len = 0, w, h; const progression = [], positions = []; // We want to separate out linear species before going into the while loop // + because these calculations will be simple if (species === LINEAR) { const [sx, sy, ex, ey] = args; w = ex - sx; h = ey - sy; len = _sqrt((w * w) + (h * h)); xPts = xPts.concat([sx, ex]); yPts = yPts.concat([sy, ey]); } else if (species === BEZIER || (species === QUADRATIC)) { const func = (species === BEZIER) ? GET_BEZIER : GET_QUADRATIC; let flag = false, step = 0.25, newLength = 0, oldX, oldY, x, y, t, res; while (!flag) { xPts.length = 0; yPts.length = 0; newLength = 0; progression.length = 0; positions.length = 0; res = getXY[func](0, ...args); oldX = res.x; oldY = res.y; xPts.push(oldX); yPts.push(oldY); for (t = step; t <= 1; t += step) { res = getXY[func](t, ...args); ({x, y} = res) xPts.push(x); yPts.push(y); w = x - oldX; h = y - oldY; newLength += _sqrt((w * w) + (h * h)); oldX = x; oldY = y; progression.push(newLength); positions.push(t); } // Stop the while loop if we're getting close to the true length of the curve if (newLength < len + precision) flag = true; len = newLength; step /= 2; // Stop the while loop after checking a maximum of 129 points along the curve if (step < 0.004) flag = true; } } return { length: len, xPoints: xPts, yPoints: yPts, positions: positions, progression: progression, }; }; // `getXY` const getXY = { [GET_BEZIER]: 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) }; }, [GET_QUADRATIC]: 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 }; }, };