@remotion/paths
Version:
Utilities for working with SVG paths
276 lines (275 loc) • 9.66 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.removeATSHVQInstructions = void 0;
const convert_q_to_c_instruction_1 = require("./convert-q-to-c-instruction");
const iterate_1 = require("./iterate");
const TAU = Math.PI * 2;
function approximate_unit_arc(theta1, delta_theta) {
const alpha = (4 / 3) * Math.tan(delta_theta / 4);
const x1 = Math.cos(theta1);
const y1 = Math.sin(theta1);
const x2 = Math.cos(theta1 + delta_theta);
const y2 = Math.sin(theta1 + delta_theta);
return [
x1,
y1,
x1 - y1 * alpha,
y1 + x1 * alpha,
x2 + y2 * alpha,
y2 - x2 * alpha,
x2,
y2,
];
}
function unit_vector_angle(ux, uy, vx, vy) {
const sign = ux * vy - uy * vx < 0 ? -1 : 1;
let dot = ux * vx + uy * vy;
// Add this to work with arbitrary vectors:
// dot /= Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy);
// rounding errors, e.g. -1.0000000000000002 can screw up this
if (dot > 1.0) {
dot = 1.0;
}
if (dot < -1.0) {
dot = -1.0;
}
return sign * Math.acos(dot);
}
function get_arc_center({ x1, y1, x2, y2, largeArcFlag, sweepFlag, rx, ry, sin_phi, cos_phi, }) {
// Step 1.
//
// Moving an ellipse so origin will be the middlepoint between our two
// points. After that, rotate it to line up ellipse axes with coordinate
// axes.
//
const x1p = (cos_phi * (x1 - x2)) / 2 + (sin_phi * (y1 - y2)) / 2;
const y1p = (-sin_phi * (x1 - x2)) / 2 + (cos_phi * (y1 - y2)) / 2;
const rx_sq = rx * rx;
const ry_sq = ry * ry;
const x1p_sq = x1p * x1p;
const y1p_sq = y1p * y1p;
// Step 2.
//
// Compute coordinates of the centre of this ellipse (cx', cy')
// in the new coordinate system.
//
let radicant = rx_sq * ry_sq - rx_sq * y1p_sq - ry_sq * x1p_sq;
if (radicant < 0) {
// due to rounding errors it might be e.g. -1.3877787807814457e-17
radicant = 0;
}
radicant /= rx_sq * y1p_sq + ry_sq * x1p_sq;
radicant = Math.sqrt(radicant) * (largeArcFlag === sweepFlag ? -1 : 1);
const cxp = ((radicant * rx) / ry) * y1p;
const cyp = ((radicant * -ry) / rx) * x1p;
// Step 3.
//
// Transform back to get centre coordinates (cx, cy) in the original
// coordinate system.
//
const cx = cos_phi * cxp - sin_phi * cyp + (x1 + x2) / 2;
const cy = sin_phi * cxp + cos_phi * cyp + (y1 + y2) / 2;
// Step 4.
//
// Compute angles (theta1, delta_theta).
//
const v1x = (x1p - cxp) / rx;
const v1y = (y1p - cyp) / ry;
const v2x = (-x1p - cxp) / rx;
const v2y = (-y1p - cyp) / ry;
const theta1 = unit_vector_angle(1, 0, v1x, v1y);
let delta_theta = unit_vector_angle(v1x, v1y, v2x, v2y);
if (sweepFlag === false && delta_theta > 0) {
delta_theta -= TAU;
}
if (sweepFlag === true && delta_theta < 0) {
delta_theta += TAU;
}
return [cx, cy, theta1, delta_theta];
}
function arcToCircle({ x1, y1, x2, y2, largeArcFlag, sweepFlag, rx, ry, phi, }) {
const sin_phi = Math.sin((phi * TAU) / 360);
const cos_phi = Math.cos((phi * TAU) / 360);
// Make sure radii are valid
//
const x1p = (cos_phi * (x1 - x2)) / 2 + (sin_phi * (y1 - y2)) / 2;
const y1p = (-sin_phi * (x1 - x2)) / 2 + (cos_phi * (y1 - y2)) / 2;
if (x1p === 0 && y1p === 0) {
// we're asked to draw line to itself
return [];
}
if (rx === 0 || ry === 0) {
// one of the radii is zero
return [];
}
// Compensate out-of-range radii
//
rx = Math.abs(rx);
ry = Math.abs(ry);
const lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
if (lambda > 1) {
rx *= Math.sqrt(lambda);
ry *= Math.sqrt(lambda);
}
// Get center parameters (cx, cy, theta1, delta_theta)
//
const cc = get_arc_center({
x1,
y1,
x2,
y2,
largeArcFlag,
sweepFlag,
rx,
ry,
sin_phi,
cos_phi,
});
const result = [];
let theta1 = cc[2];
let delta_theta = cc[3];
// Split an arc to multiple segments, so each segment
// will be less than τ/4 (= 90°)
//
const segments = Math.max(Math.ceil(Math.abs(delta_theta) / (TAU / 4)), 1);
delta_theta /= segments;
for (let i = 0; i < segments; i++) {
result.push(approximate_unit_arc(theta1, delta_theta));
theta1 += delta_theta;
}
// We have a bezier approximation of a unit circle,
// now need to transform back to the original ellipse
//
return result.map((curve) => {
for (let i = 0; i < curve.length; i += 2) {
let x = curve[i + 0];
let y = curve[i + 1];
// scale
x *= rx;
y *= ry;
// rotate
const xp = cos_phi * x - sin_phi * y;
const yp = sin_phi * x + cos_phi * y;
// translate
curve[i + 0] = xp + cc[0];
curve[i + 1] = yp + cc[1];
}
return curve;
});
}
// Requires path to be normalized
const removeATSHVQInstructions = (segments) => {
return (0, iterate_1.iterateOverSegments)({
segments,
iterate: ({ segment, prevSegment, x, y, cpX, cpY }) => {
if (segment.type === 'H') {
return [{ type: 'L', x: segment.x, y }];
}
if (segment.type === 'V') {
return [{ type: 'L', x, y: segment.y }];
}
if (segment.type === 'A') {
const nextX = segment.x;
const nextY = segment.y;
const new_segments = arcToCircle({
x1: x,
y1: y,
x2: nextX,
y2: nextY,
largeArcFlag: segment.largeArcFlag,
sweepFlag: segment.sweepFlag,
rx: segment.rx,
ry: segment.ry,
phi: segment.xAxisRotation,
});
// Degenerated arcs can be ignored by renderer, but should not be dropped
// to avoid collisions with `S A S` and so on. Replace with empty line.
if (new_segments.length === 0) {
return [
{
type: 'L',
x: segment.x,
y: segment.y,
},
];
}
const result = new_segments.map((_s) => {
return {
type: 'C',
cp1x: _s[2],
cp1y: _s[3],
cp2x: _s[4],
cp2y: _s[5],
x: _s[6],
y: _s[7],
};
});
return result;
}
if (segment.type === 'T') {
let prevControlX = 0;
let prevControlY = 0;
if (prevSegment &&
(prevSegment.type === 'Q' || prevSegment.type === 'T')) {
prevControlX = cpX;
prevControlY = cpY;
}
else {
prevControlX = x;
prevControlY = y;
}
// New first control point is reflection of previous second control point
const vectorX = prevControlX - x;
const vectorY = prevControlY - y;
const newControlX = x - vectorX;
const newControlY = y - vectorY;
return [
(0, convert_q_to_c_instruction_1.convertQToCInstruction)({
type: 'Q',
cpx: newControlX,
cpy: newControlY,
x: segment.x,
y: segment.y,
}, { x, y }),
];
}
if (segment.type === 'S') {
let prevControlX = 0;
let prevControlY = 0;
if (prevSegment && prevSegment.type === 'C') {
prevControlX = prevSegment.cp2x;
prevControlY = prevSegment.cp2y;
}
else if (prevSegment && prevSegment.type === 'S') {
prevControlX = prevSegment.cpx;
prevControlY = prevSegment.cpy;
}
else {
prevControlX = x;
prevControlY = y;
}
// New first control point is reflection of previous second control point
const vectorX = prevControlX - x;
const vectorY = prevControlY - y;
const newControlX = x - vectorX;
const newControlY = y - vectorY;
return [
{
type: 'C',
cp1x: newControlX,
cp1y: newControlY,
cp2x: segment.cpx,
cp2y: segment.cpy,
x: segment.x,
y: segment.y,
},
];
}
if (segment.type === 'Q') {
return [(0, convert_q_to_c_instruction_1.convertQToCInstruction)(segment, { x, y })];
}
return [segment];
},
});
};
exports.removeATSHVQInstructions = removeATSHVQInstructions;