packages/core/src/lib/geometry/path-transforms.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
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 version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.