Skip to content

packages/core/src/lib/geometry/path-transforms.ts

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

1 import { PathGeometry } from './path-geometry.js';
2 import {
3   bendSpans,
4   bendSpanBounds,
5   splitBendSpan,
6   type BendSpan,
7 } from './path-bend-spans.js';
8 import {
9   absI,
10   constant,
11   add,
12   sub,
13   scale,
14   mul,
15   lerp,
16   reciprocal,
17   type Pair,
18 } from './transform-math.js';
19 import {
20   snapshotGuide,
21   sampleGuide,
22   type PathGuide,
23 } from './transform-guide.js';
24 import {
25   warpPath,
26   type PathWarpCorners,
27   type PathWarpMode,
28 } from './path-warp.js';
29 
30 /**
31  * A point in source or destination path coordinates.
32  *
33  * @see {@link PathTransform}
34  * @see {@link PathRect}
35  */
36 export interface PathPoint {
37   /**
38    * Horizontal coordinate or displacement in the containing coordinate system. See
39    * {@link PathPoint}.
40    */
41   readonly x: number;
42   /**
43    * Vertical coordinate or displacement in the containing coordinate system. See {@link PathPoint}
44    * .
45    */
46   readonly y: number;
47 }
48 /**
49  * A source rectangle with an origin and finite positive dimensions.
50  *
51  * @see {@link PathPoint}
52  * @see {@link QuadTransformOptions}
53  * @see {@link EnvelopeTransformOptions}
54  */
55 export interface PathRect extends PathPoint {
56   /** Horizontal extent in the units of the containing geometry or surface. See {@link PathRect}. */
57   readonly width: number;
58   /** Vertical extent in the units of the containing geometry or surface. See {@link PathRect}. */
59   readonly height: number;
60 }
61 declare const transformBrand: unique symbol;
62 /**
63  * A constructor-created, reusable coordinate mapping for path transformation.
64  *
65  * @see {@link PathPoint}
66  * @see {@link transformPath}
67  * @see {@link composeTransforms}
68  * @see {@link affineTransform}
69  */
70 export interface PathTransform {
71   readonly [transformBrand]: true;
72   /**
73    * Evaluate in ordinary source/destination coordinates. Guide approximation is bounded by
74    * tolerance.
75    * @param point - Source point in the transform's input coordinate system. See {@link PathPoint}.
76    * @param options - Optional approximation tolerance for evaluating the mapping.
77    * @returns The mapped point in the transform's output coordinate system. See {@link PathPoint}.
78    */
79   mapPoint(
80     point: PathPoint,
81     options?: {
82       /** Positive maximum geometric approximation error. See {@link PathTransform}. */
83       readonly tolerance?: number;
84     },
85   ): PathPoint;
86 }
87 /**
88  * Accuracy and output-segment limits for applying a path transform.
89  *
90  * @see {@link transformPath}
91  */
92 export interface TransformPathOptions {
93   /** Positive maximum geometric approximation error. See {@link TransformPathOptions}. */
94   readonly tolerance: number;
95   /** Upper bound on the number of output path segments. See {@link TransformPathOptions}. */
96   readonly maxSegments?: number;
97 }
98 /**
99  * The four destination corners used by a quadrilateral path transform.
100  *
101  * @see {@link QuadTransformOptions}
102  */
103 export type PathQuadCorners = PathWarpCorners;
104 /**
105  * Selects bilinear or perspective interpolation for a quadrilateral transform.
106  *
107  * @see {@link QuadTransformOptions}
108  */
109 export type PathQuadMode = PathWarpMode;
110 /**
111  * Source rectangle, destination corners, and interpolation mode for a quadrilateral transform.
112  *
113  * @see {@link PathRect}
114  * @see {@link PathQuadCorners}
115  * @see {@link PathQuadMode}
116  * @see {@link quadTransform}
117  */
118 export interface QuadTransformOptions {
119   /** Source rectangle mapped to the destination quadrilateral. See {@link PathRect}. */
120   readonly source: PathRect;
121   /** Destination corners into which the source rectangle maps. See {@link PathQuadCorners}. */
122   readonly destination: PathQuadCorners;
123   /** Selects the supported mapping, placement, or result policy. See {@link PathQuadMode}. */
124   readonly mode?: PathQuadMode;
125 }
126 /**
127  * A guide path positioned at a normalized station across an envelope.
128  *
129  * @see {@link PathGuide}
130  * @see {@link EnvelopeTransformOptions}
131  */
132 export interface EnvelopeGuide {
133   /** Normalized station position. See {@link EnvelopeGuide}. */
134   readonly at: number;
135   /** Path geometry used by this operation. See {@link PathGuide}. */
136   readonly path: PathGuide;
137 }
138 /**
139  * Source rectangle and either a top/bottom pair or ordered envelope guides.
140  *
141  * @see {@link PathRect}
142  * @see {@link PathGuide}
143  * @see {@link EnvelopeGuide}
144  * @see {@link envelopeTransform}
145  */
146 export type EnvelopeTransformOptions = {
147   /** Source rectangle mapped between the envelope guides. See {@link PathRect}. */
148   readonly source: PathRect;
149   /** Interpolation between successive guide paths; only linear interpolation is supported. See {@link EnvelopeTransformOptions}. */
150   readonly interpolation?: 'linear';
151 } & (
152   | {
153       /** Top edge value or top envelope guide. See {@link PathGuide}. */
154       readonly top: PathGuide;
155       /** Bottom edge value or bottom envelope guide. See {@link PathGuide}. */
156       readonly bottom: PathGuide;
157       /**
158        * Not accepted in this variant; use the alternative fields instead. See
159        * {@link EnvelopeTransformOptions}.
160        */
161       readonly guides?: never;
162     }
163   | {
164       /**
165        * Ordered guide paths defining the envelope from its first to last station. See
166        * {@link EnvelopeGuide}.
167        */
168       readonly guides: readonly EnvelopeGuide[];
169       /**
170        * Not accepted in this variant; use the alternative fields instead. See
171        * {@link EnvelopeTransformOptions}.
172        */
173       readonly top?: never;
174       /**
175        * Not accepted in this variant; use the alternative fields instead. See
176        * {@link EnvelopeTransformOptions}.
177        */
178       readonly bottom?: never;
179     }
180 );
181 export type {
182   PathGuide,
183   PathGuideOptions,
184   PathGuideStation,
185 } from './transform-guide.js';
186 
187 export interface Stage {
188   apply?(path: PathGeometry, options: TransformPathOptions): PathGeometry;
189   evaluate(input: Pair): Pair;
190   error(input: Pair): number;
191   domain?(point: Pair, margin: number): boolean;
192   affine?: Affine;
193 }
194 export type Factory = (accuracy: number) => Stage;
195 const definitions = new WeakMap<PathTransform, readonly Factory[]>();
196 const MAX_WORK = 2_000_000;
197 export const coordinate = (value: number, label = 'coordinate') => {
198   if (typeof value !== 'number')
199     throw new TypeError(`${label} must be a number.`);
200   if (!Number.isFinite(value)) throw new RangeError(`${label} must be finite.`);
201   return value;
202 };
203 export const positive = (value: number, label: string) => {
204   coordinate(value, label);
205   if (value <= 0) throw new RangeError(`${label} must be positive.`);
206   return value;
207 };
208 export function rectangle(input: PathRect): PathRect {
209   return {
210     x: coordinate(input?.x),
211     y: coordinate(input?.y),
212     width: positive(input?.width, 'source.width'),
213     height: positive(input?.height, 'source.height'),
214   };
215 }
216 export const normalize = (input: Pair, r: PathRect): Pair => [
217   scale(sub(input[0], constant(r.x)), 1 / r.width),
218   scale(sub(input[1], constant(r.y)), 1 / r.height),
219 ];
220 export function make(factories: readonly Factory[]): PathTransform {
221   // One cached preparation only; drag sessions cannot grow a tolerance cache indefinitely.
222   let cachedAccuracy = NaN,
223     cached: readonly Stage[] = [];
224   const prepare = (accuracy: number) => {
225     if (accuracy !== cachedAccuracy) {
226       cached = factories.map((f) => f(accuracy));
227       cachedAccuracy = accuracy;
228     }
229     return cached;
230   };
231   const value = Object.freeze({
232     mapPoint(point: PathPoint, options: { tolerance?: number } = {}) {
233       const tolerance = positive(options.tolerance ?? 0.0001, 'tolerance');
234       const input: Pair = [
235         constant(coordinate(point?.x)),
236         constant(coordinate(point?.y)),
237       ];
238       for (
239         let accuracy = tolerance / 16, attempt = 0;
240         attempt < 9;
241         attempt++, accuracy /= 16
242       ) {
243         const result = evaluate(prepare(accuracy), input);
244         const x = result.value[0].v[0],
245           y = result.value[1].v[0];
246         if (!result.domain)
247           throw new RangeError(
248             'Point lies outside a transformation source rectangle.',
249           );
250         if (result.error + roundoff(result.value) <= tolerance)
251           return { x: coordinate(x), y: coordinate(y) };
252       }
253       throw new RangeError(
254         'Cannot certify point accuracy at available precision.',
255       );
256     },
257   }) as PathTransform;
258   definitions.set(value, factories);
259   return value;
260 }
261 function definition(value: PathTransform): readonly Factory[] {
262   const result = definitions.get(value);
263   if (!result)
264     throw new TypeError('Expected a constructor-created PathTransform.');
265   return result;
266 }
267 /**
268  * Apply mappings in the order written. Empty composition is identity.
269  *
270  * @param transforms - Point mappings in the order they should be applied. See
271  * {@link PathTransform} .
272  * @returns One mapping that applies each supplied transform in order. See {@link PathTransform}.
273  *
274  * @see {@link PathTransform}
275  */
276 export function composeTransforms(
277   ...transforms: readonly PathTransform[]
278 ): PathTransform {
279   return make(transforms.flatMap((t) => [...definition(t)]));
280 }
281 /**
282  * Creates a reusable 2D affine mapping; an omitted matrix produces identity.
283  *
284  * @param matrix - Affine matrix coefficients; omitted coefficients use the identity matrix.
285  * @returns A point mapping applying the matrix. See {@link PathTransform}.
286  *
287  * @see {@link PathTransform}
288  */
289 export function affineTransform(matrix: DOMMatrix2DInit = {}): PathTransform {
290   const m = affineMatrix(matrix);
291   return make([
292     () => ({
293       affine: m,
294       error: () => 0,
295       evaluate: ([x, y]) => [
296         add(add(scale(x, m.a), scale(y, m.c)), constant(m.e)),
297         add(add(scale(x, m.b), scale(y, m.d)), constant(m.f)),
298       ],
299     }),
300   ]);
301 }
302 /**
303  * Maps a source rectangle into four destination corners using bilinear or perspective
304  * interpolation.
305  *
306  * @param options - Source rectangle and destination corner positions. See
307  * {@link QuadTransformOptions} .
308  * @returns A point mapping from the source rectangle to the destination quadrilateral. See
309  * {@link PathTransform} .
310  *
311  * @see {@link QuadTransformOptions}
312  * @see {@link PathTransform}
313  */
314 export function quadTransform(options: QuadTransformOptions): PathTransform {
315   const source = rectangle(options.source),
316     mode = options.mode === undefined ? 'bilinear' : options.mode;
317   const names = ['topLeft', 'topRight', 'bottomLeft', 'bottomRight'] as const;
318   const corners = names.map((n) => ({
319     x: coordinate(options.destination?.[n]?.x),
320     y: coordinate(options.destination?.[n]?.y),
321   }));
322   const destination = Object.fromEntries(
323     names.map((n, i) => [n, corners[i]]),
324   ) as unknown as PathWarpCorners;
325   // Reuse the existing convexity/pole validation, including empty geometry validation.
326   warpPath(new PathGeometry(), { source, destination, mode, tolerance: 1 });
327   const [a, b, c, d] = corners;
328   const dx1 = b.x - d.x,
329     dx2 = c.x - d.x,
330     dx3 = a.x - b.x - c.x + d.x;
331   const dy1 = b.y - d.y,
332     dy2 = c.y - d.y,
333     dy3 = a.y - b.y - c.y + d.y;
334   const det = dx1 * dy2 - dx2 * dy1;
335   const g = mode === 'perspective' ? (dx3 * dy2 - dx2 * dy3) / det : 0;
336   const h = mode === 'perspective' ? (dx1 * dy3 - dx3 * dy1) / det : 0;
337   return make([
338     () => ({
339       apply: (path, quality) =>
340         warpPath(path, {
341           source,
342           destination,
343           mode,
344           tolerance: quality.tolerance,
345           maxSegments: quality.maxSegments,
346         }),
347       error: () => 0,
348       evaluate: (input) => {
349         const [u, v] = normalize(input, source);
350         if (mode === 'bilinear')
351           return [
352             lerp(
353               lerp(constant(a.x), constant(b.x), u),
354               lerp(constant(c.x), constant(d.x), u),
355               v,
356             ),
357             lerp(
358               lerp(constant(a.y), constant(b.y), u),
359               lerp(constant(c.y), constant(d.y), u),
360               v,
361             ),
362           ];
363         const w = reciprocal(add(add(scale(u, g), scale(v, h)), constant(1)));
364         return [
365           mul(
366             add(
367               add(scale(u, b.x - a.x + g * b.x), scale(v, c.x - a.x + h * c.x)),
368               constant(a.x),
369             ),
370             w,
371           ),
372           mul(
373             add(
374               add(scale(u, b.y - a.y + g * b.y), scale(v, c.y - a.y + h * c.y)),
375               constant(a.y),
376             ),
377             w,
378           ),
379         ];
380       },
381     }),
382   ]);
383 }
384 /**
385  * Maps a source rectangle between ordered guide paths using linear interpolation.
386  *
387  * @param options - Source bounds and envelope guides defining the deformation. See
388  * {@link EnvelopeTransformOptions} .
389  * @returns A point mapping that deforms the source into the envelope. See {@link PathTransform}.
390  *
391  * @see {@link EnvelopeTransformOptions}
392  * @see {@link PathTransform}
393  */
394 export function envelopeTransform(
395   options: EnvelopeTransformOptions,
396 ): PathTransform {
397   const source = rectangle(options.source);
398   if (options.interpolation !== undefined && options.interpolation !== 'linear')
399     throw new TypeError('Unsupported envelope interpolation.');
400   if (options.guides && (options.top || options.bottom))
401     throw new TypeError('Specify guides or top/bottom, not both.');
402   const guides = (
403     options.guides ?? [
404       { at: 0, path: options.top! },
405       { at: 1, path: options.bottom! },
406     ]
407   ).map((g) => ({ at: coordinate(g.at), guide: snapshotGuide(g.path) }));
408   if (
409     guides.length < 2 ||
410     guides[0].at !== 0 ||
411     guides.at(-1)!.at !== 1 ||
412     guides.some((g, i) => i > 0 && g.at <= guides[i - 1].at)
413   )
414     throw new RangeError('Envelope guides must strictly increase from 0 to 1.');
415   return make([
416     (accuracy) => {
417       const sampled = guides.map((g) => sampleGuide(g.guide, accuracy));
418       const error = Math.max(...sampled.map((g) => g.error));
419       const evaluateBand = (input: Pair, i: number): Pair => {
420         const [u, v] = normalize(input, source),
421           a = sampled[i].evaluate(u),
422           b = sampled[i + 1].evaluate(u);
423         const t = scale(
424           sub(v, constant(guides[i].at)),
425           1 / (guides[i + 1].at - guides[i].at),
426         );
427         return [lerp(a[0], b[0], t), lerp(a[1], b[1], t)];
428       };
429       return {
430         error: () => error,
431         domain: (input, margin) =>
432           input[0].v[0] >= source.x - margin &&
433           input[0].v[1] <= source.x + source.width + margin &&
434           input[1].v[0] >= source.y - margin &&
435           input[1].v[1] <= source.y + source.height + margin,
436         evaluate: (input) => {
437           const v0 = (input[1].v[0] - source.y) / source.height,
438             v1 = (input[1].v[1] - source.y) / source.height;
439           let first = 0;
440           while (first < guides.length - 2 && guides[first + 1].at <= v0)
441             first++;
442           let last = first;
443           while (last < guides.length - 2 && guides[last + 1].at < v1) last++;
444           if (first === last) return evaluateBand(input, first);
445           const outputs: Pair[] = [];
446           for (let i = first; i <= last; i++) {
447             const y = {
448               ...input[1],
449               v: [
450                 Math.max(
451                   input[1].v[0],
452                   source.y + guides[i].at * source.height,
453                 ),
454                 Math.min(
455                   input[1].v[1],
456                   source.y + guides[i + 1].at * source.height,
457                 ),
458               ] as const,
459             };
460             outputs.push(evaluateBand([input[0], y], i));
461           }
462           return [0, 1].map((axis) => ({
463             v: [
464               Math.min(...outputs.map((p) => p[axis].v[0])),
465               Math.max(...outputs.map((p) => p[axis].v[1])),
466             ],
467             d: [
468               Math.min(...outputs.map((p) => p[axis].d[0])),
469               Math.max(...outputs.map((p) => p[axis].d[1])),
470             ],
471             dd: [-Infinity, Infinity],
472           })) as unknown as Pair;
473         },
474       };
475     },
476   ]);
477 }
478 function roundoff(pair: Pair): number {
479   return Number.EPSILON * 512 * Math.max(1, ...pair.map((p) => absI(p.v)));
480 }
481 function evaluate(
482   stages: readonly Stage[],
483   input: Pair,
484 ): { value: Pair; error: number; domain: boolean } {
485   let value = input,
486     error = 0,
487     domain = true;
488   for (const stage of stages) {
489     // Bound propagation of guide approximation through every later map via its Jacobian.
490     if (error > 0) {
491       const box = value.map((p) => ({
492         v: [p.v[0] - error, p.v[1] + error] as const,
493         d: [0, 0] as const,
494         dd: [0, 0] as const,
495       })) as unknown as Pair;
496       const dx = stage.evaluate([{ ...box[0], d: [1, 1] }, box[1]]);
497       const dy = stage.evaluate([box[0], { ...box[1], d: [1, 1] }]);
498       error *= Math.hypot(
499         absI(dx[0].d),
500         absI(dx[1].d),
501         absI(dy[0].d),
502         absI(dy[1].d),
503       );
504     }
505     domain &&= stage.domain?.(value, roundoff(value)) ?? true;
506     error += stage.error(value);
507     value = stage.evaluate(value);
508   }
509   return { value, error, domain };
510 }
511 /**
512  * Return independent geometry. Nonlinear mappings approximate the final composed curve once.
513  *
514  * @param path - Source geometry; it is not modified. See {@link PathGeometry}.
515  * @param transform - Point mapping to apply to the source. See {@link PathTransform}.
516  * @param options - Approximation tolerance and work limits for transformed curves. See
517  * {@link TransformPathOptions} .
518  * @returns Independent geometry containing the transformed path. See {@link PathGeometry}.
519  *
520  * @see {@link PathGeometry}
521  * @see {@link PathTransform}
522  * @see {@link TransformPathOptions}
523  */
524 export function transformPath(
525   path: PathGeometry,
526   transform: PathTransform,
527   options: TransformPathOptions,
528 ): PathGeometry {
529   if (!(path instanceof PathGeometry))
530     throw new TypeError('transformPath requires PathGeometry.');
531   const factories = definition(transform),
532     tolerance = positive(options?.tolerance, 'tolerance');
533   const maxSegments = positive(
534     options.maxSegments === undefined ? 1_000_000 : options.maxSegments,
535     'maxSegments',
536   );
537   if (!Number.isSafeInteger(maxSegments))
538     throw new RangeError('maxSegments must be a safe integer.');
539   let work = 0;
540   for (
541     let attempt = 0, accuracy = tolerance / 32;
542     attempt < 9;
543     attempt++, accuracy /= 16
544   ) {
545     const stages = factories.map((f) => f(accuracy));
546     if (stages.length === 1 && stages[0].apply)
547       return stages[0].apply(path, options);
548     if (stages.every((s) => s.affine)) {
549       if (path.segmentCount > maxSegments)
550         throw new RangeError('transformPath exceeded maxSegments.');
551       let result = path.clone();
552       for (const s of stages) result = applyAffine(result, s.affine!);
553       return result;
554     }
555     const output = new PathGeometry();
556     let retry = false;
557     const reserve = () => {
558       if (output.segmentCount >= maxSegments)
559         throw new RangeError('transformPath exceeded maxSegments.');
560     };
561     for (const item of bendSpans(path)) {
562       if (item.type === 'close') {
563         reserve();
564         output.closePath();
565         continue;
566       }
567       if (item.type === 'move') {
568         const r = evaluate(stages, [
569           constant(item.point[0]),
570           constant(item.point[1]),
571         ]);
572         if (!r.domain)
573           throw new RangeError(
574             'Path lies outside a transformation source rectangle.',
575           );
576         if (r.error > tolerance / 4) {
577           retry = true;
578           break;
579         }
580         if (r.error + roundoff(r.value) > tolerance)
581           throw new RangeError('Cannot certify transform precision.');
582         reserve();
583         output.moveTo(coordinate(r.value[0].v[0]), coordinate(r.value[1].v[0]));
584         continue;
585       }
586       const stack: { span: BendSpan; depth: number }[] = [
587         { span: item.span, depth: 0 },
588       ];
589       while (stack.length) {
590         if (++work > MAX_WORK)
591           throw new RangeError('transformPath exceeded its work limit.');
592         const { span, depth } = stack.pop()!,
593           b = bendSpanBounds(span);
594         const r = evaluate(stages, [
595           { v: [b.xmin, b.xmax], d: [-b.dx, b.dx], dd: [-b.ddx, b.ddx] },
596           { v: [b.ymin, b.ymax], d: [-b.dy, b.dy], dd: [-b.ddy, b.ddy] },
597         ]);
598         if (r.error > tolerance / 4) {
599           retry = true;
600           break;
601         }
602         const second = Math.hypot(absI(r.value[0].dd), absI(r.value[1].dd)) / 8;
603         // A Lipschitz bound also covers piecewise-smooth guides and crossed cell boundaries.
604         const first = Math.hypot(absI(r.value[0].d), absI(r.value[1].d)) / 2;
605         const bound = Math.min(second, first) + 2 * r.error + roundoff(r.value);
606         if (r.domain && bound <= tolerance) {
607           const end =
608             'points' in span
609               ? span.points.at(-1)!
610               : [
611                   span.arc.cx +
612                     span.arc.ux *
613                       Math.cos(span.arc.startAngle + span.arc.sweep) +
614                     span.arc.vx *
615                       Math.sin(span.arc.startAngle + span.arc.sweep),
616                   span.arc.cy +
617                     span.arc.uy *
618                       Math.cos(span.arc.startAngle + span.arc.sweep) +
619                     span.arc.vy *
620                       Math.sin(span.arc.startAngle + span.arc.sweep),
621                 ];
622           const p = evaluate(stages, [constant(end[0]), constant(end[1])]);
623           reserve();
624           output.lineTo(
625             coordinate(p.value[0].v[0]),
626             coordinate(p.value[1].v[0]),
627           );
628         } else {
629           if (depth >= 52)
630             throw new RangeError(
631               'Cannot certify transform tolerance or source domain.',
632             );
633           const [left, right] = splitBendSpan(span);
634           stack.push(
635             { span: right, depth: depth + 1 },
636             { span: left, depth: depth + 1 },
637           );
638         }
639       }
640       if (retry) break;
641     }
642     if (!retry) return output;
643   }
644   throw new RangeError('Cannot certify composed guide accuracy.');
645 }
646 
647 /** @internal Merge cell enclosures; derivatives may jump at the shared edge. */
648 export function mergePairs(outputs: readonly Pair[]): Pair {
649   return [0, 1].map((axis) => ({
650     v: [
651       Math.min(...outputs.map((p) => p[axis].v[0])),
652       Math.max(...outputs.map((p) => p[axis].v[1])),
653     ],
654     d: [
655       Math.min(...outputs.map((p) => p[axis].d[0])),
656       Math.max(...outputs.map((p) => p[axis].d[1])),
657     ],
658     dd: [-Infinity, Infinity],
659   })) as unknown as Pair;
660 }
661 export function sourceDomain(
662   input: Pair,
663   source: PathRect,
664   margin: number,
665 ): boolean {
666   return (
667     input[0].v[0] >= source.x - margin &&
668     input[0].v[1] <= source.x + source.width + margin &&
669     input[1].v[0] >= source.y - margin &&
670     input[1].v[1] <= source.y + source.height + margin
671   );
672 }
673 export { meshTransform, type MeshTransformOptions } from './transform-mesh.js';
674 export {
675   arcBendTransform,
676   pathBendTransform,
677   type ArcBendTransformOptions,
678   type PathBendTransformOptions,
679   type BendTransformOptions,
680 } from './transform-bend.js';
681 
682 interface Affine {
683   readonly a: number;
684   readonly b: number;
685   readonly c: number;
686   readonly d: number;
687   readonly e: number;
688   readonly f: number;
689 }
690 function affineMatrix(matrix: DOMMatrix2DInit): Affine {
691   if (!matrix || typeof matrix !== 'object')
692     throw new TypeError('Expected a finite 2D affine matrix.');
693   const value = matrix as DOMMatrixInit;
694   if (
695     value.is2D === false ||
696     ['m13', 'm14', 'm23', 'm24', 'm31', 'm32', 'm34', 'm43'].some(
697       (k) =>
698         value[k as keyof DOMMatrixInit] !== undefined &&
699         value[k as keyof DOMMatrixInit] !== 0,
700     ) ||
701     [value.m33, value.m44].some((n) => n !== undefined && n !== 1)
702   )
703     throw new RangeError('Expected a finite 2D affine matrix.');
704   const pairs = [
705     ['a', 'm11', 1],
706     ['b', 'm12', 0],
707     ['c', 'm21', 0],
708     ['d', 'm22', 1],
709     ['e', 'm41', 0],
710     ['f', 'm42', 0],
711   ] as const;
712   const result = {} as Record<string, number>;
713   for (const [key, alias, fallback] of pairs) {
714     const a = value[key],
715       b = value[alias];
716     if (a !== undefined && b !== undefined && a !== b)
717       throw new TypeError('Conflicting affine matrix aliases.');
718     result[key] = coordinate(
719       a === undefined ? (b === undefined ? fallback : b) : a,
720     );
721   }
722   return result as unknown as Affine;
723 }
724 function applyAffine(path: PathGeometry, m: Affine): PathGeometry {
725   const p = (x: number, y: number, vector = false) => ({
726     x: coordinate(m.a * x + m.c * y + (vector ? 0 : m.e)),
727     y: coordinate(m.b * x + m.d * y + (vector ? 0 : m.f)),
728   });
729   const segments = Array.from(path, (segment) => {
730     switch (segment.type) {
731       case 'close':
732         return segment;
733       case 'move':
734       case 'line':
735         return { ...segment, ...p(segment.x, segment.y) };
736       case 'quadratic': {
737         const control = p(segment.cpx, segment.cpy);
738         return {
739           ...segment,
740           ...p(segment.x, segment.y),
741           cpx: control.x,
742           cpy: control.y,
743         };
744       }
745       case 'cubic': {
746         const a = p(segment.cp1x, segment.cp1y),
747           b = p(segment.cp2x, segment.cp2y);
748         return {
749           ...segment,
750           ...p(segment.x, segment.y),
751           cp1x: a.x,
752           cp1y: a.y,
753           cp2x: b.x,
754           cp2y: b.y,
755         };
756       }
757       case 'arc': {
758         const center = p(segment.cx, segment.cy),
759           u = p(segment.ux, segment.uy, true),
760           v = p(segment.vx, segment.vy, true);
761         return {
762           ...segment,
763           cx: center.x,
764           cy: center.y,
765           ux: u.x,
766           uy: u.y,
767           vx: v.x,
768           vy: v.y,
769         };
770       }
771     }
772   });
773   const result = new PathGeometry();
774   result.spliceSegments(0, 0, segments);
775   return result;
776 }
777 

Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.