kute.js/src/components/svgMorph.js
thednp 933d61de19 Changes:
* added ESLint and updated all code base 
* updated SVGPathCommander, CubicBezier, shorter-js, minifill
* updated polyfills
* minor CSS fixes
2021-03-30 09:23:29 +00:00

327 lines
8.9 KiB
JavaScript

import pathToCurve from 'svg-path-commander/src/convert/pathToCurve.js';
import pathToString from 'svg-path-commander/src/convert/pathToString.js';
import normalizePath from 'svg-path-commander/src/process/normalizePath.js';
import splitPath from 'svg-path-commander/src/process/splitPath.js';
import roundPath from 'svg-path-commander/src/process/roundPath.js';
import invalidPathValue from 'svg-path-commander/src/util/invalidPathValue.js';
import getPathLength from 'svg-path-commander/src/util/getPathLength.js';
import getPointAtLength from 'svg-path-commander/src/util/getPointAtLength.js';
import getDrawDirection from 'svg-path-commander/src/util/getDrawDirection.js';
import epsilon from 'svg-path-commander/src/math/epsilon.js';
import midPoint from 'svg-path-commander/src/math/midPoint.js';
import distanceSquareRoot from 'svg-path-commander/src/math/distanceSquareRoot.js';
import { onStartSVGMorph } from './svgMorphBase.js';
import coords from '../interpolation/coords.js';
import defaultOptions from '../objects/defaultOptions.js';
import selector from '../util/selector.js';
/* SVGMorph = {
property: 'path',
defaultValue: [],
interpolators: {numbers,coords},
functions = { prepareStart, prepareProperty, onStart, crossCheck }
} */
// Component Interpolation
// function function(array1, array2, length, progress)
// Component Util
// original script flubber
// https://github.com/veltman/flubber
function polygonLength(ring) {
return ring.reduce((length, point, i) => (i
? length + distanceSquareRoot(ring[i - 1], point)
: 0), 0);
}
function exactRing(pathArray) {
const ring = [];
const pathlen = pathArray.length;
let segment = [];
let pathCommand = '';
let pathLength = 0;
if (!pathArray.length || pathArray[0][0] !== 'M') {
return false;
}
for (let i = 0; i < pathlen; i += 1) {
segment = pathArray[i];
[pathCommand] = segment;
if ((pathCommand === 'M' && i) || pathCommand === 'Z') {
break; // !!
} else if ('ML'.indexOf(pathCommand) > -1) {
ring.push([segment[1], segment[2]]);
} else {
return false;
}
}
pathLength = polygonLength(ring);
return pathlen ? { ring, pathLength } : false;
}
function approximateRing(parsed, maxSegmentLength) {
const ringPath = splitPath(pathToString(parsed))[0];
const curvePath = pathToCurve(ringPath, 4);
const pathLength = getPathLength(curvePath);
const ring = [];
let numPoints = 3;
let point;
if (maxSegmentLength && !Number.isNaN(maxSegmentLength) && +maxSegmentLength > 0) {
numPoints = Math.max(numPoints, Math.ceil(pathLength / maxSegmentLength));
}
for (let i = 0; i < numPoints; i += 1) {
point = getPointAtLength(curvePath, (pathLength * i) / numPoints);
ring.push([point.x, point.y]);
}
// Make all rings clockwise
if (!getDrawDirection(curvePath)) {
ring.reverse();
}
return {
pathLength,
ring,
skipBisect: true,
};
}
function pathStringToRing(str, maxSegmentLength) {
const parsed = normalizePath(str, 0);
return exactRing(parsed) || approximateRing(parsed, maxSegmentLength);
}
function rotateRing(ring, vs) {
const len = ring.length;
let min = Infinity;
let bestOffset;
let sumOfSquares = 0;
let spliced;
let d;
let p;
for (let offset = 0; offset < len; offset += 1) {
sumOfSquares = 0;
// vs.forEach((p, i) => {
// const d = distanceSquareRoot(ring[(offset + i) % len], p);
// sumOfSquares += d * d;
// });
for (let i = 0; i < vs.length; i += 1) {
p = vs[i];
d = distanceSquareRoot(ring[(offset + i) % len], p);
sumOfSquares += d * d;
}
if (sumOfSquares < min) {
min = sumOfSquares;
bestOffset = offset;
}
}
if (bestOffset) {
spliced = ring.splice(0, bestOffset);
ring.splice(ring.length, 0, ...spliced);
}
}
function addPoints(ring, numPoints) {
const desiredLength = ring.length + numPoints;
// const step = ring.pathLength / numPoints;
const step = polygonLength(ring) / numPoints;
let i = 0;
let cursor = 0;
let insertAt = step / 2;
let a;
let b;
let segment;
while (ring.length < desiredLength) {
a = ring[i];
b = ring[(i + 1) % ring.length];
segment = distanceSquareRoot(a, b);
if (insertAt <= cursor + segment) {
ring.splice(i + 1, 0, segment
? midPoint(a, b, (insertAt - cursor) / segment)
: a.slice(0));
insertAt += step;
} else {
cursor += segment;
i += 1;
}
}
}
function bisect(ring, maxSegmentLength = Infinity) {
let a = [];
let b = [];
for (let i = 0; i < ring.length; i += 1) {
a = ring[i];
b = i === ring.length - 1 ? ring[0] : ring[i + 1];
// Could splice the whole set for a segment instead, but a bit messy
while (distanceSquareRoot(a, b) > maxSegmentLength) {
b = midPoint(a, b, 0.5);
ring.splice(i + 1, 0, b);
}
}
}
function validRing(ring) {
return Array.isArray(ring)
&& ring.every((point) => Array.isArray(point)
&& point.length === 2
&& !Number.isNaN(point[0])
&& !Number.isNaN(point[1]));
}
function normalizeRing(input, maxSegmentLength) {
let skipBisect;
let pathLength;
let ring = input;
if (typeof (ring) === 'string') {
const converted = pathStringToRing(ring, maxSegmentLength);
ring = converted.ring;
skipBisect = converted.skipBisect;
pathLength = converted.pathLength;
} else if (!Array.isArray(ring)) {
throw Error(`${invalidPathValue}: ${ring}`);
}
const points = ring.slice(0);
points.pathLength = pathLength;
if (!validRing(points)) {
throw Error(`${invalidPathValue}: ${points}`);
}
// TODO skip this test to avoid scale issues?
// Chosen epsilon (1e-6) is problematic for small coordinate range, we now use 1e-9
if (points.length > 1 && distanceSquareRoot(points[0], points[points.length - 1]) < epsilon) {
points.pop();
}
if (!skipBisect && maxSegmentLength
&& !Number.isNaN(maxSegmentLength) && (+maxSegmentLength) > 0) {
bisect(points, maxSegmentLength);
}
return points;
}
function getInterpolationPoints(pathArray1, pathArray2, precision) {
const morphPrecision = precision || defaultOptions.morphPrecision;
const fromRing = normalizeRing(pathArray1, morphPrecision);
const toRing = normalizeRing(pathArray2, morphPrecision);
const diff = fromRing.length - toRing.length;
addPoints(fromRing, diff < 0 ? diff * -1 : 0);
addPoints(toRing, diff > 0 ? diff : 0);
rotateRing(fromRing, toRing);
return [roundPath(fromRing), roundPath(toRing)];
}
// Component functions
function getSVGMorph(/* tweenProp */) {
return this.element.getAttribute('d');
}
function prepareSVGMorph(tweenProp, value) {
const pathObject = {};
// remove newlines, they brake JSON strings sometimes
const pathReg = new RegExp('\\n', 'ig');
let elem = null;
if (value instanceof SVGElement) {
elem = value;
} else if (/^\.|^#/.test(value)) {
elem = selector(value);
}
// first make sure we return pre-processed values
if (typeof (value) === 'object' && value.pathArray) {
return value;
} if (elem && ['path', 'glyph'].includes(elem.tagName)) {
pathObject.original = elem.getAttribute('d').replace(pathReg, '');
// maybe it's a string path already
} else if (!elem && typeof (value) === 'string') {
pathObject.original = value.replace(pathReg, '');
}
return pathObject;
}
function crossCheckSVGMorph(prop) {
if (this.valuesEnd[prop]) {
const pathArray1 = this.valuesStart[prop].pathArray;
const pathArray2 = this.valuesEnd[prop].pathArray;
// skip already processed paths
// allow the component to work with pre-processed values
if (!pathArray1 || !pathArray2
|| (pathArray1 && pathArray2 && pathArray1.length !== pathArray2.length)) {
const p1 = this.valuesStart[prop].original;
const p2 = this.valuesEnd[prop].original;
// process morphPrecision
const morphPrecision = this._morphPrecision
? parseInt(this._morphPrecision, 10)
: defaultOptions.morphPrecision;
const [path1, path2] = getInterpolationPoints(p1, p2, morphPrecision);
this.valuesStart[prop].pathArray = path1;
this.valuesEnd[prop].pathArray = path2;
}
}
}
// All Component Functions
const svgMorphFunctions = {
prepareStart: getSVGMorph,
prepareProperty: prepareSVGMorph,
onStart: onStartSVGMorph,
crossCheck: crossCheckSVGMorph,
};
// Component Full
const svgMorph = {
component: 'svgMorph',
property: 'path',
defaultValue: [],
Interpolate: coords,
defaultOptions: { morphPrecision: 10, morphIndex: 0 },
functions: svgMorphFunctions,
// Export utils to global for faster execution
Util: {
addPoints,
bisect,
normalizeRing,
validRing, // component
getInterpolationPoints,
pathStringToRing,
distanceSquareRoot,
midPoint,
approximateRing,
rotateRing,
pathToString,
pathToCurve, // svg-path-commander
getPathLength,
getPointAtLength,
getDrawDirection,
roundPath,
},
};
export default svgMorph;