UNPKG

lottie-web

Version:

After Effects plugin for exporting animations to SVG + JavaScript or canvas + JavaScript

307 lines (260 loc) 8.35 kB
import { roundCorner, } from '../common'; import { extendPrototype, } from '../functionExtensions'; import PropertyFactory from '../PropertyFactory'; import shapePool from '../pooling/shape_pool'; import { ShapeModifier, } from './ShapeModifiers'; import { PolynomialBezier, polarOffset, lineIntersection, pointDistance, pointEqual, floatEqual, } from '../PolynomialBezier'; function linearOffset(p1, p2, amount) { var angle = Math.atan2(p2[0] - p1[0], p2[1] - p1[1]); return [ polarOffset(p1, angle, amount), polarOffset(p2, angle, amount), ]; } function offsetSegment(segment, amount) { var p0; var p1a; var p1b; var p2b; var p2a; var p3; var e; e = linearOffset(segment.points[0], segment.points[1], amount); p0 = e[0]; p1a = e[1]; e = linearOffset(segment.points[1], segment.points[2], amount); p1b = e[0]; p2b = e[1]; e = linearOffset(segment.points[2], segment.points[3], amount); p2a = e[0]; p3 = e[1]; var p1 = lineIntersection(p0, p1a, p1b, p2b); if (p1 === null) p1 = p1a; var p2 = lineIntersection(p2a, p3, p1b, p2b); if (p2 === null) p2 = p2a; return new PolynomialBezier(p0, p1, p2, p3); } function joinLines(outputBezier, seg1, seg2, lineJoin, miterLimit) { var p0 = seg1.points[3]; var p1 = seg2.points[0]; // Bevel if (lineJoin === 3) return p0; // Connected, they don't need a joint if (pointEqual(p0, p1)) return p0; // Round if (lineJoin === 2) { var angleOut = -seg1.tangentAngle(1); var angleIn = -seg2.tangentAngle(0) + Math.PI; var center = lineIntersection( p0, polarOffset(p0, angleOut + Math.PI / 2, 100), p1, polarOffset(p1, angleOut + Math.PI / 2, 100) ); var radius = center ? pointDistance(center, p0) : pointDistance(p0, p1) / 2; var tan = polarOffset(p0, angleOut, 2 * radius * roundCorner); outputBezier.setXYAt(tan[0], tan[1], 'o', outputBezier.length() - 1); tan = polarOffset(p1, angleIn, 2 * radius * roundCorner); outputBezier.setTripleAt(p1[0], p1[1], p1[0], p1[1], tan[0], tan[1], outputBezier.length()); return p1; } // Miter var t0 = pointEqual(p0, seg1.points[2]) ? seg1.points[0] : seg1.points[2]; var t1 = pointEqual(p1, seg2.points[1]) ? seg2.points[3] : seg2.points[1]; var intersection = lineIntersection(t0, p0, p1, t1); if (intersection && pointDistance(intersection, p0) < miterLimit) { outputBezier.setTripleAt( intersection[0], intersection[1], intersection[0], intersection[1], intersection[0], intersection[1], outputBezier.length() ); return intersection; } return p0; } function getIntersection(a, b) { const intersect = a.intersections(b); if (intersect.length && floatEqual(intersect[0][0], 1)) intersect.shift(); if (intersect.length) return intersect[0]; return null; } function pruneSegmentIntersection(a, b) { var outa = a.slice(); var outb = b.slice(); var intersect = getIntersection(a[a.length - 1], b[0]); if (intersect) { outa[a.length - 1] = a[a.length - 1].split(intersect[0])[0]; outb[0] = b[0].split(intersect[1])[1]; } if (a.length > 1 && b.length > 1) { intersect = getIntersection(a[0], b[b.length - 1]); if (intersect) { return [ [a[0].split(intersect[0])[0]], [b[b.length - 1].split(intersect[1])[1]], ]; } } return [outa, outb]; } function pruneIntersections(segments) { var e; for (var i = 1; i < segments.length; i += 1) { e = pruneSegmentIntersection(segments[i - 1], segments[i]); segments[i - 1] = e[0]; segments[i] = e[1]; } if (segments.length > 1) { e = pruneSegmentIntersection(segments[segments.length - 1], segments[0]); segments[segments.length - 1] = e[0]; segments[0] = e[1]; } return segments; } function offsetSegmentSplit(segment, amount) { /* We split each bezier segment into smaller pieces based on inflection points, this ensures the control point polygon is convex. (A cubic bezier can have none, one, or two inflection points) */ var flex = segment.inflectionPoints(); var left; var right; var split; var mid; if (flex.length === 0) { return [offsetSegment(segment, amount)]; } if (flex.length === 1 || floatEqual(flex[1], 1)) { split = segment.split(flex[0]); left = split[0]; right = split[1]; return [ offsetSegment(left, amount), offsetSegment(right, amount), ]; } split = segment.split(flex[0]); left = split[0]; var t = (flex[1] - flex[0]) / (1 - flex[0]); split = split[1].split(t); mid = split[0]; right = split[1]; return [ offsetSegment(left, amount), offsetSegment(mid, amount), offsetSegment(right, amount), ]; } function OffsetPathModifier() {} extendPrototype([ShapeModifier], OffsetPathModifier); OffsetPathModifier.prototype.initModifierProperties = function (elem, data) { this.getValue = this.processKeys; this.amount = PropertyFactory.getProp(elem, data.a, 0, null, this); this.miterLimit = PropertyFactory.getProp(elem, data.ml, 0, null, this); this.lineJoin = data.lj; this._isAnimated = this.amount.effectsSequence.length !== 0; }; OffsetPathModifier.prototype.processPath = function (inputBezier, amount, lineJoin, miterLimit) { var outputBezier = shapePool.newElement(); outputBezier.c = inputBezier.c; var count = inputBezier.length(); if (!inputBezier.c) { count -= 1; } var i; var j; var segment; var multiSegments = []; for (i = 0; i < count; i += 1) { segment = PolynomialBezier.shapeSegment(inputBezier, i); multiSegments.push(offsetSegmentSplit(segment, amount)); } if (!inputBezier.c) { for (i = count - 1; i >= 0; i -= 1) { segment = PolynomialBezier.shapeSegmentInverted(inputBezier, i); multiSegments.push(offsetSegmentSplit(segment, amount)); } } multiSegments = pruneIntersections(multiSegments); // Add bezier segments to the output and apply line joints var lastPoint = null; var lastSeg = null; for (i = 0; i < multiSegments.length; i += 1) { var multiSegment = multiSegments[i]; if (lastSeg) lastPoint = joinLines(outputBezier, lastSeg, multiSegment[0], lineJoin, miterLimit); lastSeg = multiSegment[multiSegment.length - 1]; for (j = 0; j < multiSegment.length; j += 1) { segment = multiSegment[j]; if (lastPoint && pointEqual(segment.points[0], lastPoint)) { outputBezier.setXYAt(segment.points[1][0], segment.points[1][1], 'o', outputBezier.length() - 1); } else { outputBezier.setTripleAt( segment.points[0][0], segment.points[0][1], segment.points[1][0], segment.points[1][1], segment.points[0][0], segment.points[0][1], outputBezier.length() ); } outputBezier.setTripleAt( segment.points[3][0], segment.points[3][1], segment.points[3][0], segment.points[3][1], segment.points[2][0], segment.points[2][1], outputBezier.length() ); lastPoint = segment.points[3]; } } if (multiSegments.length) joinLines(outputBezier, lastSeg, multiSegments[0][0], lineJoin, miterLimit); return outputBezier; }; OffsetPathModifier.prototype.processShapes = function (_isFirstFrame) { var shapePaths; var i; var len = this.shapes.length; var j; var jLen; var amount = this.amount.v; var miterLimit = this.miterLimit.v; var lineJoin = this.lineJoin; if (amount !== 0) { var shapeData; var localShapeCollection; for (i = 0; i < len; i += 1) { shapeData = this.shapes[i]; localShapeCollection = shapeData.localShapeCollection; if (!(!shapeData.shape._mdf && !this._mdf && !_isFirstFrame)) { localShapeCollection.releaseShapes(); shapeData.shape._mdf = true; shapePaths = shapeData.shape.paths.shapes; jLen = shapeData.shape.paths._length; for (j = 0; j < jLen; j += 1) { localShapeCollection.addShape(this.processPath(shapePaths[j], amount, lineJoin, miterLimit)); } } shapeData.shape.paths = shapeData.localShapeCollection; } } if (!this.dynamicProperties.length) { this._mdf = false; } }; export default OffsetPathModifier;