kute.js/src/components/svgCubicMorph.js
thednp 2a5bac2bb3 Changes V2.2.0:
* major JSDoc write up
* removed ESLint `no-bitwise` exception, it only applies to specific functions and not the entire code
* the `SVGCubicMorph` component will remove un-necessary `Z` path commands when is the case for better out of the box animation
* fixed a minor disambiguation with `filterEffects` and `drop-shadow` property and its `dropshadow` interpolation function
* TypeScript strong: all files are modules, easy to implement in any third party app
* updated `CubicBezier` and SVGPathCommander
* code cleanup
2021-12-08 23:43:31 +02:00

229 lines
7.2 KiB
JavaScript

import parsePathString from 'svg-path-commander/src/parser/parsePathString';
import pathToAbsolute from 'svg-path-commander/src/convert/pathToAbsolute';
import pathToCurve from 'svg-path-commander/src/convert/pathToCurve';
import pathToString from 'svg-path-commander/src/convert/pathToString';
import reverseCurve from 'svg-path-commander/src/process/reverseCurve';
import getDrawDirection from 'svg-path-commander/src/util/getDrawDirection';
import clonePath from 'svg-path-commander/src/process/clonePath';
import splitCubic from 'svg-path-commander/src/process/splitCubic';
import splitPath from 'svg-path-commander/src/process/splitPath';
import fixPath from 'svg-path-commander/src/process/fixPath';
import getSegCubicLength from 'svg-path-commander/src/util/getSegCubicLength';
import distanceSquareRoot from 'svg-path-commander/src/math/distanceSquareRoot';
import { onStartCubicMorph } from './svgCubicMorphBase';
import numbers from '../interpolation/numbers';
import selector from '../util/selector';
// Component Util
/**
* Returns first `pathArray` from multi-paths path.
* @param {SVGPathCommander.pathArray | string} source the source `pathArray` or string
* @returns {KUTE.curveSpecs[]} an `Array` with a custom tuple for `equalizeSegments`
*/
function getCurveArray(source) {
return pathToCurve(splitPath(source)[0])
.map((segment, i, pathArray) => {
const segmentData = i && [...pathArray[i - 1].slice(-2), ...segment.slice(1)];
const curveLength = i ? getSegCubicLength(...segmentData) : 0;
let subsegs;
if (i) {
// must be [segment,segment]
subsegs = curveLength ? splitCubic(segmentData) : [segment, segment];
} else {
subsegs = [segment];
}
return {
s: segment,
ss: subsegs,
l: curveLength,
};
});
}
/**
* Returns two `curveArray` with same amount of segments.
* @param {SVGPathCommander.curveArray} path1 the first `curveArray`
* @param {SVGPathCommander.curveArray} path2 the second `curveArray`
* @param {number} TL the maximum `curveArray` length
* @returns {SVGPathCommander.curveArray[]} equalized segments
*/
function equalizeSegments(path1, path2, TL) {
const c1 = getCurveArray(path1);
const c2 = getCurveArray(path2);
const L1 = c1.length;
const L2 = c2.length;
const l1 = c1.filter((x) => x.l).length;
const l2 = c2.filter((x) => x.l).length;
const m1 = c1.filter((x) => x.l).reduce((a, { l }) => a + l, 0) / l1 || 0;
const m2 = c2.filter((x) => x.l).reduce((a, { l }) => a + l, 0) / l2 || 0;
const tl = TL || Math.max(L1, L2);
const mm = [m1, m2];
const dif = [tl - L1, tl - L2];
let canSplit = 0;
const result = [c1, c2]
.map((x, i) => (x.l === tl
? x.map((y) => y.s)
: x.map((y, j) => {
canSplit = j && dif[i] && y.l >= mm[i];
dif[i] -= canSplit ? 1 : 0;
return canSplit ? y.ss : [y.s];
}).flat()));
return result[0].length === result[1].length
? result
: equalizeSegments(result[0], result[1], tl);
}
/**
* Returns all possible path rotations for `curveArray`.
* @param {SVGPathCommander.curveArray} a the source `curveArray`
* @returns {SVGPathCommander.curveArray[]} all rotations for source
*/
function getRotations(a) {
const segCount = a.length;
const pointCount = segCount - 1;
return a.map((_, idx) => a.map((__, i) => {
let oldSegIdx = idx + i;
let seg;
if (i === 0 || (a[oldSegIdx] && a[oldSegIdx][0] === 'M')) {
seg = a[oldSegIdx];
return ['M', ...seg.slice(-2)];
}
if (oldSegIdx >= segCount) oldSegIdx -= pointCount;
return a[oldSegIdx];
}));
}
/**
* Returns the `curveArray` rotation for the best morphing animation.
* @param {SVGPathCommander.curveArray} a the target `curveArray`
* @param {SVGPathCommander.curveArray} b the reference `curveArray`
* @returns {SVGPathCommander.curveArray} the best `a` rotation
*/
function getRotatedCurve(a, b) {
const segCount = a.length - 1;
const lineLengths = [];
let computedIndex = 0;
let sumLensSqrd = 0;
const rotations = getRotations(a);
rotations.forEach((_, i) => {
a.slice(1).forEach((__, j) => {
sumLensSqrd += distanceSquareRoot(a[(i + j) % segCount].slice(-2), b[j % segCount].slice(-2));
});
lineLengths[i] = sumLensSqrd;
sumLensSqrd = 0;
});
computedIndex = lineLengths.indexOf(Math.min.apply(null, lineLengths));
return rotations[computedIndex];
}
// Component Functions
/**
* Returns the current `d` attribute value.
* @returns {string}
*/
function getCubicMorph(/* tweenProp, value */) {
return this.element.getAttribute('d');
}
/**
* Returns the property tween object.
* @see KUTE.curveObject
*
* @param {string} _ is the `path` property name, not needed
* @param {string | KUTE.curveObject} value the `path` property value
* @returns {KUTE.curveObject}
*/
function prepareCubicMorph(/* tweenProp, */_, value) {
// get path d attribute or create a path from string value
const pathObject = {};
// remove newlines, they break some JSON strings
const pathReg = new RegExp('\\n', 'ig');
let el = null;
if (value instanceof SVGElement) {
el = value;
} else if (/^\.|^#/.test(value)) {
el = selector(value);
}
// make sure to return pre-processed values
if (typeof (value) === 'object' && value.curve) {
return value;
} if (el && /path|glyph/.test(el.tagName)) {
pathObject.original = el.getAttribute('d').replace(pathReg, '');
// maybe it's a string path already
} else if (!el && typeof (value) === 'string') {
pathObject.original = value.replace(pathReg, '');
}
return pathObject;
}
/**
* Enables the `to()` method by preparing the tween object in advance.
* @param {string} tweenProp is `path` tween property, but it's not needed
*/
function crossCheckCubicMorph(tweenProp/** , value */) {
if (this.valuesEnd[tweenProp]) {
const pathCurve1 = this.valuesStart[tweenProp].curve;
const pathCurve2 = this.valuesEnd[tweenProp].curve;
if (!pathCurve1 || !pathCurve2
|| (pathCurve1 && pathCurve2 && pathCurve1[0][0] === 'M' && pathCurve1.length !== pathCurve2.length)) {
const path1 = this.valuesStart[tweenProp].original;
const path2 = this.valuesEnd[tweenProp].original;
const curves = equalizeSegments(path1, path2);
const curve0 = getDrawDirection(curves[0]) !== getDrawDirection(curves[1])
? reverseCurve(curves[0])
: clonePath(curves[0]);
this.valuesStart[tweenProp].curve = curve0;
this.valuesEnd[tweenProp].curve = getRotatedCurve(curves[1], curve0);
}
}
}
// All Component Functions
const svgCubicMorphFunctions = {
prepareStart: getCubicMorph,
prepareProperty: prepareCubicMorph,
onStart: onStartCubicMorph,
crossCheck: crossCheckCubicMorph,
};
// Component Full
const svgCubicMorph = {
component: 'svgCubicMorph',
property: 'path',
defaultValue: [],
Interpolate: { numbers, pathToString },
functions: svgCubicMorphFunctions,
// export utils to global for faster execution
Util: {
pathToCurve,
pathToAbsolute,
pathToString,
parsePathString,
getRotatedCurve,
getRotations,
equalizeSegments,
reverseCurve,
clonePath,
getDrawDirection,
splitCubic,
splitPath,
fixPath,
getCurveArray,
},
};
export default svgCubicMorph;