@underlying/svg 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) underlyi.ng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ <p align="center">
2
+ <img alt="underlying" src="https://underlyi.ng/wordmark-sapin.svg" width="280" />
3
+ </p>
4
+
5
+ <p align="center">
6
+ <strong>Ride an element along a path, or draw a stroke on - on live physics.</strong>
7
+ </p>
8
+
9
+ <p align="center">
10
+ <a href="https://underlyi.ng"><img alt="docs" src="https://img.shields.io/badge/docs-underlyi.ng-1C3426" /></a>
11
+ <img alt="svg gzip" src="https://img.shields.io/badge/svg-~1.6%20kB%20gzip-1C3426" />
12
+ <img alt="built on" src="https://img.shields.io/badge/built%20on-%40underlying%2Fcore-1C3426" />
13
+ <img alt="license" src="https://img.shields.io/badge/license-MIT-1C3426" />
14
+ </p>
15
+
16
+ > Beta. The API may still move before 1.0.
17
+
18
+ SVG path animation for [`@underlying/core`](https://github.com/underlyingjs/underlying/tree/main/packages/core) - MotionPath, DrawSVG, and a resampling morph. The progress of each is a single live value, so where other libraries bake a path tween, here you can flick a marker down a path and let it settle, interrupt a stroke mid-draw, scrub a morph, or hand the same progress to scroll. No new engine: it samples the path with the native `getPointAtLength`/`getTotalLength` and drives core's `animatable`.
19
+
20
+ ```sh
21
+ npm install @underlying/svg @underlying/core
22
+ ```
23
+
24
+ ## `motionPath()`
25
+
26
+ Ride an element along a path. Progress `t` is a live `Animatable`, so spring it, flick it, or retarget it mid-flight - velocity is conserved across the change. `autoRotate` turns the element to face along the path.
27
+
28
+ ```ts
29
+ import { motionPath } from '@underlying/svg'
30
+
31
+ const ride = motionPath(marker, '#track', { autoRotate: true })
32
+
33
+ ride.spring(1) // travel to the end with a real spring
34
+ ride.flick(2.4) // fling it down the path; it decays to a stop
35
+ ride.spring(0.4) // retarget mid-flight, momentum kept
36
+
37
+ ride.t // the live 0..1 Animatable - compose it (scroll, timeline)
38
+ ride.revert() // unbind and restore the element's transform
39
+ ```
40
+
41
+ ## `draw()`
42
+
43
+ Draw a stroke on (0 = hidden, 1 = fully drawn). The fraction is a live `Animatable` too - it can overshoot, and you can interrupt it mid-draw.
44
+
45
+ ```ts
46
+ import { draw } from '@underlying/svg'
47
+
48
+ const line = draw('#signature')
49
+ line.spring(1) // draw it on
50
+ line.to(0, { duration: 400 })// erase with a timed tween, still interruptible
51
+ ```
52
+
53
+ ## `morph()`
54
+
55
+ Turn one shape into another. Both outlines are resampled into points along their length and interpolated, so *any* two paths morph - you do not have to match their commands by hand. The fraction is live, so you can scrub it or grab it mid-morph.
56
+
57
+ ```ts
58
+ import { morph } from '@underlying/svg'
59
+
60
+ const m = morph(blob, starPathData, { closed: true }) // target: a `d` string or an element
61
+ m.spring(1) // morph to the star
62
+ m.spring(0) // morph back - interruptible
63
+ m.set(0.5) // hold it halfway
64
+ ```
65
+
66
+ This is a resampling morph (smooth, handles arbitrary shapes); full command-preserving MorphSVG - sharp-corner fidelity and `shapeIndex` - is future work.
67
+
68
+ ## The familiar one-call form
69
+
70
+ Both accept a `{ to }` kickoff that reads like `gsap.to(el, { motionPath })` or `{ drawSVG }` - but springs under the hood, and the handle is still there for the live wins.
71
+
72
+ ```ts
73
+ motionPath(marker, '#track', { to: 1, autoRotate: true })
74
+ draw('#signature', { to: 1 })
75
+ ```
76
+
77
+ ## Composing - drive the same path from scroll or a timeline
78
+
79
+ `motionPath` and `draw` own a driver `Animatable` (`.t` / `.fraction`) you can hand to anything that drives a value.
80
+
81
+ ```ts
82
+ import { motionPath } from '@underlying/svg'
83
+ import { createScroll } from '@underlying/scroll'
84
+
85
+ const ride = motionPath(marker, '#track', { autoRotate: true })
86
+ createScroll({ scroller }).scrub(ride.t) // marker follows the path as you scroll
87
+ ```
88
+
89
+ ## Bring your own driver
90
+
91
+ The handles are built on thin binders. If you already have an `Animatable` (or want to control the value yourself, GSAP-style), bind it directly:
92
+
93
+ ```ts
94
+ import { bindPath, bindDraw, samplePath } from '@underlying/svg'
95
+ import { animatable } from '@underlying/core'
96
+
97
+ const t = animatable(0)
98
+ bindPath(marker, '#track', t, { autoRotate: true }) // maps t -> transform
99
+ t.decay({ velocity: 2.4 })
100
+
101
+ samplePath('#track').at(0.5) // low-level: { x, y, angle } at progress 0.5
102
+ ```
103
+
104
+ ## Notes
105
+
106
+ - **Reduced motion** is inherited from core: a `spring`/`decay`/`to` on the driver auto-degrades under `prefers-reduced-motion`, so the element jumps to the target with no travel.
107
+ - **Coordinate space.** `motionPath` writes the sampled point straight to the element's `transform`, so the element and the path should share a coordinate space (e.g. both inside the same SVG, or the element absolutely positioned over it).
108
+ - **SSR.** Sampling needs the DOM; pass an element rather than a selector on the server, or call from an effect.
109
+ - **Morph** here resamples both outlines into points and interpolates - it handles any two shapes, but it is not yet the full command-preserving MorphSVG (sharp corners can soften; raise `samples` for fidelity).
110
+
111
+ ## License
112
+
113
+ MIT (c) underlyi.ng
package/dist/draw.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { type Animatable, type Scheduler } from '@underlying/core';
2
+ import { type ScalarControls } from './handle';
3
+ /** An element that can be stroke-drawn: reports its length and has a writable style. */
4
+ export interface DrawElement {
5
+ getTotalLength(): number;
6
+ style: {
7
+ strokeDasharray: string;
8
+ strokeDashoffset: string;
9
+ };
10
+ }
11
+ /** A draw source: a CSS selector or a stroked geometry element. */
12
+ export type DrawInput = string | DrawElement;
13
+ export interface DrawOptions {
14
+ /** Scheduler driving the value and the style flush. Defaults to the shared one. */
15
+ scheduler?: Scheduler;
16
+ /** Initial draw fraction, 0..1 (0 hidden, 1 drawn). Default 0. */
17
+ from?: number;
18
+ /** Spring to this fraction on creation - the GSAP-familiar one-call form. */
19
+ to?: number;
20
+ }
21
+ /**
22
+ * Low-level binder: map a driver Animatable (0..1) onto an element's stroke
23
+ * draw-on (0 = hidden, 1 = fully drawn) via stroke-dasharray/offset. You own the
24
+ * driver. Returns an unbind fn that restores the original dash properties.
25
+ */
26
+ export declare function bindDraw(path: DrawInput, source: Animatable, options?: DrawOptions): () => void;
27
+ export interface Draw extends ScalarControls {
28
+ /** The live 0..1 draw fraction (0 hidden, 1 drawn). Compose it anywhere. */
29
+ readonly fraction: Animatable;
30
+ }
31
+ /**
32
+ * Draw a stroke on, physics-first. The fraction is a live Animatable - spring it
33
+ * in (it can overshoot), interrupt it mid-draw, flick it, or scrub it from
34
+ * scroll. Restores the original stroke-dash properties on revert().
35
+ */
36
+ export declare function draw(path: DrawInput, options?: DrawOptions): Draw;
37
+ //# sourceMappingURL=draw.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draw.d.ts","sourceRoot":"","sources":["../src/draw.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAClG,OAAO,EAAkB,KAAK,cAAc,EAAE,MAAM,UAAU,CAAA;AAE9D,wFAAwF;AACxF,MAAM,WAAW,WAAW;IAC1B,cAAc,IAAI,MAAM,CAAA;IACxB,KAAK,EAAE;QAAE,eAAe,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,CAAA;CAC7D;AAED,mEAAmE;AACnE,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,WAAW,CAAA;AAE5C,MAAM,WAAW,WAAW;IAC1B,mFAAmF;IACnF,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,6EAA6E;IAC7E,EAAE,CAAC,EAAE,MAAM,CAAA;CACZ;AAcD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,GAAE,WAAgB,GAAG,MAAM,IAAI,CAuCnG;AAED,MAAM,WAAW,IAAK,SAAQ,cAAc;IAC1C,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAA;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,GAAE,WAAgB,GAAG,IAAI,CAgBrE"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A subset of SVGGeometryElement: anything that can report its length and sample
3
+ * a point at a given arc distance. Real `<path>`/`<line>`/`<polyline>` elements
4
+ * satisfy it natively; tests pass a plain object.
5
+ */
6
+ export interface PathGeometry {
7
+ getTotalLength(): number;
8
+ getPointAtLength(distance: number): {
9
+ x: number;
10
+ y: number;
11
+ };
12
+ }
13
+ /** A path source: a CSS selector or any geometry element/object. */
14
+ export type PathInput = string | PathGeometry;
15
+ /** A sampled point on a path: position plus the tangent direction (degrees). */
16
+ export interface PathPoint {
17
+ x: number;
18
+ y: number;
19
+ /** tangent direction in degrees, fed to autoRotate */
20
+ angle: number;
21
+ }
22
+ export interface SamplePathOptions {
23
+ /**
24
+ * Distance, as a fraction of total length, used to estimate the tangent angle
25
+ * from two neighbouring samples. Smaller is sharper but noisier. Default 0.001.
26
+ */
27
+ tangentEpsilon?: number;
28
+ }
29
+ export interface PathSampler {
30
+ /** Total path length, in the path's user units. */
31
+ readonly length: number;
32
+ /** Point at progress t (clamped to 0..1), with the tangent angle. */
33
+ at(t: number): PathPoint;
34
+ }
35
+ /** Resolve a {@link PathInput} to a geometry source (querySelector for strings). */
36
+ export declare function resolvePathGeometry(path: PathInput): PathGeometry;
37
+ /**
38
+ * Wrap a path in a length-normalized sampler: feed it t in 0..1 and read back the
39
+ * point plus tangent angle. Total length is measured once; each `at()` is two
40
+ * getPointAtLength() reads (the point, and a neighbour for the tangent).
41
+ */
42
+ export declare function samplePath(path: PathInput, options?: SamplePathOptions): PathSampler;
43
+ //# sourceMappingURL=geometry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry.d.ts","sourceRoot":"","sources":["../src/geometry.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,cAAc,IAAI,MAAM,CAAA;IACxB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAC7D;AAED,oEAAoE;AACpE,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,YAAY,CAAA;AAE7C,gFAAgF;AAChF,MAAM,WAAW,SAAS;IACxB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,mDAAmD;IACnD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,qEAAqE;IACrE,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACzB;AAID,oFAAoF;AACpF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,SAAS,GAAG,YAAY,CAQjE;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,GAAE,iBAAsB,GAAG,WAAW,CAgBxF"}
@@ -0,0 +1,27 @@
1
+ import type { Animatable, AnimationHandle, DecayOptions, SpringOptions, ToOptions } from '@underlying/core';
2
+ /**
3
+ * The physics verbs every @underlying/svg handle shares. They delegate straight
4
+ * to the live driver Animatable, so a motionPath or a draw is interruptible and
5
+ * retargetable at any moment - the same engine as the rest of core.
6
+ */
7
+ export interface ScalarControls {
8
+ /** Spring the driver toward a target (0..1), interruptible, velocity conserved. */
9
+ spring(target: number, options?: SpringOptions): AnimationHandle;
10
+ /** Glide on inertia from the current velocity, clamped to 0..1. */
11
+ decay(options?: DecayOptions): AnimationHandle;
12
+ /** Duration/easing escape hatch - still interruptible. */
13
+ to(target: number, options?: ToOptions): AnimationHandle;
14
+ /** Teleport the driver (no animation). */
15
+ set(value: number): void;
16
+ /** Flick: seed a velocity (0..1 units/s) and let it decay to a stop on the path. */
17
+ flick(velocity: number, options?: Omit<DecayOptions, 'velocity'>): AnimationHandle;
18
+ /** Freeze in place; position and velocity stay readable. */
19
+ stop(): void;
20
+ /** Read the driver, 0..1. */
21
+ progress(): number;
22
+ /** Stop, unbind, and restore the element to its pre-bind state. */
23
+ revert(): void;
24
+ }
25
+ /** Build the shared verb surface around a driver value, plus a revert. */
26
+ export declare function scalarControls(value: Animatable, revert: () => void): ScalarControls;
27
+ //# sourceMappingURL=handle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handle.d.ts","sourceRoot":"","sources":["../src/handle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAE3G;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,mFAAmF;IACnF,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,eAAe,CAAA;IAChE,mEAAmE;IACnE,KAAK,CAAC,OAAO,CAAC,EAAE,YAAY,GAAG,eAAe,CAAA;IAC9C,0DAA0D;IAC1D,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,GAAG,eAAe,CAAA;IACxD,0CAA0C;IAC1C,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,oFAAoF;IACpF,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,GAAG,eAAe,CAAA;IAClF,4DAA4D;IAC5D,IAAI,IAAI,IAAI,CAAA;IACZ,6BAA6B;IAC7B,QAAQ,IAAI,MAAM,CAAA;IAClB,mEAAmE;IACnE,MAAM,IAAI,IAAI,CAAA;CACf;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,IAAI,GAAG,cAAc,CAWpF"}
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const h=require("@underlying/core"),L=t=>t<0?0:t>1?1:t;function $(t){if(typeof t!="string")return t;if(typeof document>"u")throw new Error("@underlying/svg: a selector needs a DOM; pass an element on the server");const r=document.querySelector(t);if(r===null)throw new Error(`@underlying/svg: no element matches "${t}"`);return r}function M(t,r={}){const e=$(t),n=e.getTotalLength(),s=(r.tangentEpsilon??.001)*n||1e-4;return{length:n,at(l){const a=L(l)*n,d=e.getPointAtLength(a),u=e.getPointAtLength(Math.max(0,a-s)),o=e.getPointAtLength(Math.min(n,a+s)),c=Math.atan2(o.y-u.y,o.x-u.x)*180/Math.PI;return{x:d.x,y:d.y,angle:c}}}}function x(t,r){return{spring:(e,n)=>t.spring(e,n),decay:e=>t.decay(e),to:(e,n)=>t.to(e,n),set:e=>t.set(e),flick:(e,n)=>t.decay({min:0,max:1,...n,velocity:e}),stop:()=>t.stop(),progress:()=>t.get(),revert:r}}function A(t,r,e,n={}){const s=n.scheduler??h.getSharedScheduler(),l=M(r,n),a=n.autoRotate??!1,d=a!==!1,u=typeof a=="number"?a:0;let o=!1,c=null;const i=()=>{const g=l.at(e.get()),m=`translate3d(${g.x}px, ${g.y}px, 0)`;t.style.transform=d?`${m} rotate(${g.angle+u}deg)`:m},f=()=>{c===null&&(c=s.subscribe(()=>{c==null||c(),c=null,o&&(o=!1,i())},"render"))},v=e.on("change",()=>{o=!0,f()});return i(),()=>{v(),c==null||c(),c=null}}function k(t,r,e={}){const n=h.animatable(e.from??0,e.scheduler!==void 0?{scheduler:e.scheduler}:void 0),s=t.style.transform,l=A(t,r,n,e),a=()=>{n.stop(),l(),n.dispose(),t.style.transform=s};return e.to!==void 0&&n.spring(e.to),{t:n,...x(n,a)}}const T=t=>t<0?0:t>1?1:t;function O(t){if(typeof t!="string")return t;if(typeof document>"u")throw new Error("@underlying/svg: a selector needs a DOM; pass an element on the server");const r=document.querySelector(t);if(r===null)throw new Error(`@underlying/svg: no element matches "${t}"`);return r}function E(t,r,e={}){const n=e.scheduler??h.getSharedScheduler(),s=O(t),l=s.getTotalLength(),a=s.style.strokeDasharray,d=s.style.strokeDashoffset;s.style.strokeDasharray=String(l);let u=!1,o=null;const c=()=>{s.style.strokeDashoffset=String(l*(1-T(r.get())))},i=()=>{o===null&&(o=n.subscribe(()=>{o==null||o(),o=null,u&&(u=!1,c())},"render"))},f=r.on("change",()=>{u=!0,i()});return c(),()=>{f(),o==null||o(),o=null,s.style.strokeDasharray=a,s.style.strokeDashoffset=d}}function q(t,r={}){const e=h.animatable(r.from??0,r.scheduler!==void 0?{scheduler:r.scheduler}:void 0),n=E(t,e,r),s=()=>{e.stop(),n(),e.dispose()};return r.to!==void 0&&e.spring(r.to),{fraction:e,...x(e,s)}}const p=t=>Math.round(t*100)/100,S=(t,r)=>{const e=t.getTotalLength(),n=r>1?r-1:1,s=[];for(let l=0;l<r;l+=1){const a=t.getPointAtLength(l/n*e);s.push({x:a.x,y:a.y})}return s},G=(t,r)=>{const e=t[0];if(e===void 0)return"";let n=`M ${p(e.x)} ${p(e.y)}`;for(let s=1;s<t.length;s+=1){const l=t[s];l!==void 0&&(n+=` L ${p(l.x)} ${p(l.y)}`)}return r?`${n} Z`:n},R=t=>{if(typeof t!="string")return t;if(typeof document>"u")throw new Error("@underlying/svg: morph target path data needs a DOM");const r=document.createElementNS("http://www.w3.org/2000/svg","path");return r.setAttribute("d",t),r};function j(t,r,e={}){const n=Math.max(2,e.samples??64),s=e.closed??!1,l=S(t,n),a=S(R(r),n),d=t.getAttribute("d"),u=e.scheduler??h.getSharedScheduler(),o=h.animatable(e.from??0,e.scheduler!==void 0?{scheduler:e.scheduler}:void 0);let c=!1,i=null;const f=()=>{const P=o.get(),D=[];for(let b=0;b<n;b+=1){const y=l[b],w=a[b];y===void 0||w===void 0||D.push({x:y.x+(w.x-y.x)*P,y:y.y+(w.y-y.y)*P})}t.setAttribute("d",G(D,s))},v=()=>{i===null&&(i=u.subscribe(()=>{i==null||i(),i=null,c&&(c=!1,f())},"render"))},g=o.on("change",()=>{c=!0,v()});f();const m=()=>{o.stop(),g(),i==null||i(),i=null,d!==null&&t.setAttribute("d",d),o.dispose()};return e.to!==void 0&&o.spring(e.to),{fraction:o,...x(o,m)}}exports.bindDraw=E;exports.bindPath=A;exports.draw=q;exports.morph=j;exports.motionPath=k;exports.resolvePathGeometry=$;exports.samplePath=M;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../src/geometry.ts","../src/handle.ts","../src/motion-path.ts","../src/draw.ts","../src/morph.ts"],"sourcesContent":["/**\n * A subset of SVGGeometryElement: anything that can report its length and sample\n * a point at a given arc distance. Real `<path>`/`<line>`/`<polyline>` elements\n * satisfy it natively; tests pass a plain object.\n */\nexport interface PathGeometry {\n getTotalLength(): number\n getPointAtLength(distance: number): { x: number; y: number }\n}\n\n/** A path source: a CSS selector or any geometry element/object. */\nexport type PathInput = string | PathGeometry\n\n/** A sampled point on a path: position plus the tangent direction (degrees). */\nexport interface PathPoint {\n x: number\n y: number\n /** tangent direction in degrees, fed to autoRotate */\n angle: number\n}\n\nexport interface SamplePathOptions {\n /**\n * Distance, as a fraction of total length, used to estimate the tangent angle\n * from two neighbouring samples. Smaller is sharper but noisier. Default 0.001.\n */\n tangentEpsilon?: number\n}\n\nexport interface PathSampler {\n /** Total path length, in the path's user units. */\n readonly length: number\n /** Point at progress t (clamped to 0..1), with the tangent angle. */\n at(t: number): PathPoint\n}\n\nconst clamp01 = (t: number): number => (t < 0 ? 0 : t > 1 ? 1 : t)\n\n/** Resolve a {@link PathInput} to a geometry source (querySelector for strings). */\nexport function resolvePathGeometry(path: PathInput): PathGeometry {\n if (typeof path !== 'string') return path\n if (typeof document === 'undefined') {\n throw new Error('@underlying/svg: a selector needs a DOM; pass an element on the server')\n }\n const element = document.querySelector(path)\n if (element === null) throw new Error(`@underlying/svg: no element matches \"${path}\"`)\n return element as unknown as PathGeometry\n}\n\n/**\n * Wrap a path in a length-normalized sampler: feed it t in 0..1 and read back the\n * point plus tangent angle. Total length is measured once; each `at()` is two\n * getPointAtLength() reads (the point, and a neighbour for the tangent).\n */\nexport function samplePath(path: PathInput, options: SamplePathOptions = {}): PathSampler {\n const geometry = resolvePathGeometry(path)\n const length = geometry.getTotalLength()\n const eps = (options.tangentEpsilon ?? 0.001) * length || 0.0001\n\n return {\n length,\n at(t) {\n const distance = clamp01(t) * length\n const point = geometry.getPointAtLength(distance)\n const behind = geometry.getPointAtLength(Math.max(0, distance - eps))\n const ahead = geometry.getPointAtLength(Math.min(length, distance + eps))\n const angle = (Math.atan2(ahead.y - behind.y, ahead.x - behind.x) * 180) / Math.PI\n return { x: point.x, y: point.y, angle }\n },\n }\n}\n","import type { Animatable, AnimationHandle, DecayOptions, SpringOptions, ToOptions } from '@underlying/core'\n\n/**\n * The physics verbs every @underlying/svg handle shares. They delegate straight\n * to the live driver Animatable, so a motionPath or a draw is interruptible and\n * retargetable at any moment - the same engine as the rest of core.\n */\nexport interface ScalarControls {\n /** Spring the driver toward a target (0..1), interruptible, velocity conserved. */\n spring(target: number, options?: SpringOptions): AnimationHandle\n /** Glide on inertia from the current velocity, clamped to 0..1. */\n decay(options?: DecayOptions): AnimationHandle\n /** Duration/easing escape hatch - still interruptible. */\n to(target: number, options?: ToOptions): AnimationHandle\n /** Teleport the driver (no animation). */\n set(value: number): void\n /** Flick: seed a velocity (0..1 units/s) and let it decay to a stop on the path. */\n flick(velocity: number, options?: Omit<DecayOptions, 'velocity'>): AnimationHandle\n /** Freeze in place; position and velocity stay readable. */\n stop(): void\n /** Read the driver, 0..1. */\n progress(): number\n /** Stop, unbind, and restore the element to its pre-bind state. */\n revert(): void\n}\n\n/** Build the shared verb surface around a driver value, plus a revert. */\nexport function scalarControls(value: Animatable, revert: () => void): ScalarControls {\n return {\n spring: (target, options) => value.spring(target, options),\n decay: (options) => value.decay(options),\n to: (target, options) => value.to(target, options),\n set: (next) => value.set(next),\n flick: (velocity, options) => value.decay({ min: 0, max: 1, ...options, velocity }),\n stop: () => value.stop(),\n progress: () => value.get(),\n revert,\n }\n}\n","import { animatable, getSharedScheduler, type Animatable, type Scheduler } from '@underlying/core'\nimport { samplePath, type PathInput, type SamplePathOptions } from './geometry'\nimport { scalarControls, type ScalarControls } from './handle'\n\nexport interface PathBindOptions extends SamplePathOptions {\n /**\n * Turn the element to face along the path. `true` aligns to the tangent;\n * a number adds that many degrees of offset (e.g. `90` for a north-up icon).\n */\n autoRotate?: boolean | number\n /** Scheduler driving the value and the style flush. Defaults to the shared one. */\n scheduler?: Scheduler\n}\n\nexport interface MotionPathOptions extends PathBindOptions {\n /** Initial progress, 0..1. Default 0. */\n from?: number\n /** Spring to this progress on creation - the GSAP-familiar one-call form. */\n to?: number\n}\n\n/**\n * Low-level binder: map a driver Animatable (0..1) onto an element's transform\n * along a path. You own the driver - spring, decay, or scrub it from scroll or a\n * timeline. Writes the current point synchronously at bind; returns an unbind fn.\n */\nexport function bindPath(\n element: HTMLElement | SVGElement,\n path: PathInput,\n source: Animatable,\n options: PathBindOptions = {},\n): () => void {\n const scheduler = options.scheduler ?? getSharedScheduler()\n const sampler = samplePath(path, options)\n const autoRotate = options.autoRotate ?? false\n const rotates = autoRotate !== false\n const offset = typeof autoRotate === 'number' ? autoRotate : 0\n\n let dirty = false\n let cancelFlush: (() => void) | null = null\n\n const write = (): void => {\n const point = sampler.at(source.get())\n const move = `translate3d(${point.x}px, ${point.y}px, 0)`\n element.style.transform = rotates ? `${move} rotate(${point.angle + offset}deg)` : move\n }\n\n // One-shot render subscription per change burst: flush once on the next frame,\n // then let the scheduler sleep - same pattern as core's bindStyle.\n const scheduleFlush = (): void => {\n if (cancelFlush !== null) return\n cancelFlush = scheduler.subscribe(() => {\n cancelFlush?.()\n cancelFlush = null\n if (dirty) {\n dirty = false\n write()\n }\n }, 'render')\n }\n\n const unsubscribe = source.on('change', () => {\n dirty = true\n scheduleFlush()\n })\n write()\n\n return () => {\n unsubscribe()\n cancelFlush?.()\n cancelFlush = null\n }\n}\n\nexport interface MotionPath extends ScalarControls {\n /** The live 0..1 driver. Compose it: scroll.scrub(mp.t), a timeline, a sequence. */\n readonly t: Animatable\n}\n\n/**\n * Ride an element along an SVG path, physics-first. Progress `t` is a live\n * Animatable, so you can spring it, flick it down the path and let it settle,\n * retarget it mid-flight (velocity conserved), or hand `t` to scroll/timeline.\n * `autoRotate` turns the element to face along the path.\n */\nexport function motionPath(\n element: HTMLElement | SVGElement,\n path: PathInput,\n options: MotionPathOptions = {},\n): MotionPath {\n const t = animatable(\n options.from ?? 0,\n options.scheduler !== undefined ? { scheduler: options.scheduler } : undefined,\n )\n const previousTransform = element.style.transform\n const unbind = bindPath(element, path, t, options)\n\n const revert = (): void => {\n t.stop()\n unbind()\n t.dispose()\n element.style.transform = previousTransform\n }\n\n if (options.to !== undefined) t.spring(options.to)\n\n return { t, ...scalarControls(t, revert) }\n}\n","import { animatable, getSharedScheduler, type Animatable, type Scheduler } from '@underlying/core'\nimport { scalarControls, type ScalarControls } from './handle'\n\n/** An element that can be stroke-drawn: reports its length and has a writable style. */\nexport interface DrawElement {\n getTotalLength(): number\n style: { strokeDasharray: string; strokeDashoffset: string }\n}\n\n/** A draw source: a CSS selector or a stroked geometry element. */\nexport type DrawInput = string | DrawElement\n\nexport interface DrawOptions {\n /** Scheduler driving the value and the style flush. Defaults to the shared one. */\n scheduler?: Scheduler\n /** Initial draw fraction, 0..1 (0 hidden, 1 drawn). Default 0. */\n from?: number\n /** Spring to this fraction on creation - the GSAP-familiar one-call form. */\n to?: number\n}\n\nconst clamp01 = (v: number): number => (v < 0 ? 0 : v > 1 ? 1 : v)\n\nfunction resolveDrawElement(path: DrawInput): DrawElement {\n if (typeof path !== 'string') return path\n if (typeof document === 'undefined') {\n throw new Error('@underlying/svg: a selector needs a DOM; pass an element on the server')\n }\n const element = document.querySelector(path)\n if (element === null) throw new Error(`@underlying/svg: no element matches \"${path}\"`)\n return element as unknown as DrawElement\n}\n\n/**\n * Low-level binder: map a driver Animatable (0..1) onto an element's stroke\n * draw-on (0 = hidden, 1 = fully drawn) via stroke-dasharray/offset. You own the\n * driver. Returns an unbind fn that restores the original dash properties.\n */\nexport function bindDraw(path: DrawInput, source: Animatable, options: DrawOptions = {}): () => void {\n const scheduler = options.scheduler ?? getSharedScheduler()\n const element = resolveDrawElement(path)\n const length = element.getTotalLength()\n const previousDasharray = element.style.strokeDasharray\n const previousDashoffset = element.style.strokeDashoffset\n element.style.strokeDasharray = String(length)\n\n let dirty = false\n let cancelFlush: (() => void) | null = null\n\n const write = (): void => {\n element.style.strokeDashoffset = String(length * (1 - clamp01(source.get())))\n }\n const scheduleFlush = (): void => {\n if (cancelFlush !== null) return\n cancelFlush = scheduler.subscribe(() => {\n cancelFlush?.()\n cancelFlush = null\n if (dirty) {\n dirty = false\n write()\n }\n }, 'render')\n }\n\n const unsubscribe = source.on('change', () => {\n dirty = true\n scheduleFlush()\n })\n write()\n\n return () => {\n unsubscribe()\n cancelFlush?.()\n cancelFlush = null\n element.style.strokeDasharray = previousDasharray\n element.style.strokeDashoffset = previousDashoffset\n }\n}\n\nexport interface Draw extends ScalarControls {\n /** The live 0..1 draw fraction (0 hidden, 1 drawn). Compose it anywhere. */\n readonly fraction: Animatable\n}\n\n/**\n * Draw a stroke on, physics-first. The fraction is a live Animatable - spring it\n * in (it can overshoot), interrupt it mid-draw, flick it, or scrub it from\n * scroll. Restores the original stroke-dash properties on revert().\n */\nexport function draw(path: DrawInput, options: DrawOptions = {}): Draw {\n const fraction = animatable(\n options.from ?? 0,\n options.scheduler !== undefined ? { scheduler: options.scheduler } : undefined,\n )\n const unbind = bindDraw(path, fraction, options)\n\n const revert = (): void => {\n fraction.stop()\n unbind()\n fraction.dispose()\n }\n\n if (options.to !== undefined) fraction.spring(options.to)\n\n return { fraction, ...scalarControls(fraction, revert) }\n}\n","import { animatable, getSharedScheduler, type Animatable, type Scheduler } from '@underlying/core'\nimport type { PathGeometry } from './geometry'\nimport { scalarControls, type ScalarControls } from './handle'\n\n/** A path element whose shape this package rewrites: geometry plus the `d` attribute. */\nexport interface MorphElement extends PathGeometry {\n getAttribute(name: string): string | null\n setAttribute(name: string, value: string): void\n}\n\n/** The shape to morph toward: raw path data (`\"M ...\"`) or any geometry/element. */\nexport type MorphTarget = string | PathGeometry\n\nexport interface MorphOptions {\n scheduler?: Scheduler\n /** Points sampled along each outline; more is smoother (and heavier). Default 64. */\n samples?: number\n /** Close the interpolated outline - for closed shapes (a star, a blob). */\n closed?: boolean\n /** Initial morph fraction, 0..1 (0 = original shape, 1 = target). Default 0. */\n from?: number\n /** Spring to this fraction on creation. */\n to?: number\n}\n\ninterface Point {\n x: number\n y: number\n}\n\nconst round = (n: number): number => Math.round(n * 100) / 100\n\n// Resample an outline into `count` points evenly spaced by arc length. This is\n// what lets ANY two shapes morph: both become the same-length point list, no\n// matching of bezier commands required.\nconst samplePoints = (geometry: PathGeometry, count: number): Point[] => {\n const length = geometry.getTotalLength()\n const divisor = count > 1 ? count - 1 : 1\n const points: Point[] = []\n for (let i = 0; i < count; i += 1) {\n const p = geometry.getPointAtLength((i / divisor) * length)\n points.push({ x: p.x, y: p.y })\n }\n return points\n}\n\nconst toPathData = (points: Point[], closed: boolean): string => {\n const first = points[0]\n if (first === undefined) return ''\n let d = `M ${round(first.x)} ${round(first.y)}`\n for (let i = 1; i < points.length; i += 1) {\n const p = points[i]\n if (p !== undefined) d += ` L ${round(p.x)} ${round(p.y)}`\n }\n return closed ? `${d} Z` : d\n}\n\nconst targetGeometry = (target: MorphTarget): PathGeometry => {\n if (typeof target !== 'string') return target\n if (typeof document === 'undefined') {\n throw new Error('@underlying/svg: morph target path data needs a DOM')\n }\n const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')\n path.setAttribute('d', target)\n return path as unknown as PathGeometry\n}\n\nexport interface Morph extends ScalarControls {\n /** The live 0..1 morph fraction (0 original, 1 target). Compose it anywhere. */\n readonly fraction: Animatable\n}\n\n/**\n * Morph one path into another, physics-first. Both outlines are resampled into\n * `samples` points along their length and interpolated, so any two shapes morph\n * (no matching command structure needed). The fraction is a live Animatable -\n * spring it, scrub it, or grab it mid-morph. `revert()` restores the original `d`.\n */\nexport function morph(element: MorphElement, target: MorphTarget, options: MorphOptions = {}): Morph {\n const count = Math.max(2, options.samples ?? 64)\n const closed = options.closed ?? false\n const fromPoints = samplePoints(element, count)\n const toPoints = samplePoints(targetGeometry(target), count)\n const originalD = element.getAttribute('d')\n\n const scheduler = options.scheduler ?? getSharedScheduler()\n const fraction = animatable(\n options.from ?? 0,\n options.scheduler !== undefined ? { scheduler: options.scheduler } : undefined,\n )\n\n let dirty = false\n let cancelFlush: (() => void) | null = null\n\n const write = (): void => {\n const f = fraction.get()\n const blended: Point[] = []\n for (let i = 0; i < count; i += 1) {\n const a = fromPoints[i]\n const b = toPoints[i]\n if (a === undefined || b === undefined) continue\n blended.push({ x: a.x + (b.x - a.x) * f, y: a.y + (b.y - a.y) * f })\n }\n element.setAttribute('d', toPathData(blended, closed))\n }\n const scheduleFlush = (): void => {\n if (cancelFlush !== null) return\n cancelFlush = scheduler.subscribe(() => {\n cancelFlush?.()\n cancelFlush = null\n if (dirty) {\n dirty = false\n write()\n }\n }, 'render')\n }\n\n const unsubscribe = fraction.on('change', () => {\n dirty = true\n scheduleFlush()\n })\n write()\n\n const revert = (): void => {\n fraction.stop()\n unsubscribe()\n cancelFlush?.()\n cancelFlush = null\n if (originalD !== null) element.setAttribute('d', originalD)\n fraction.dispose()\n }\n\n if (options.to !== undefined) fraction.spring(options.to)\n\n return { fraction, ...scalarControls(fraction, revert) }\n}\n"],"names":["clamp01","resolvePathGeometry","path","element","samplePath","options","geometry","length","eps","t","distance","point","behind","ahead","angle","scalarControls","value","revert","target","next","velocity","bindPath","source","scheduler","getSharedScheduler","sampler","autoRotate","rotates","offset","dirty","cancelFlush","write","move","scheduleFlush","unsubscribe","motionPath","animatable","previousTransform","unbind","v","resolveDrawElement","bindDraw","previousDasharray","previousDashoffset","draw","fraction","round","n","samplePoints","count","divisor","points","i","p","toPathData","closed","first","d","targetGeometry","morph","fromPoints","toPoints","originalD","f","blended","a","b"],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export { samplePath, resolvePathGeometry } from './geometry';
2
+ export type { PathGeometry, PathInput, PathPoint, PathSampler, SamplePathOptions } from './geometry';
3
+ export { bindPath, motionPath } from './motion-path';
4
+ export type { MotionPath, MotionPathOptions, PathBindOptions } from './motion-path';
5
+ export { bindDraw, draw } from './draw';
6
+ export type { Draw, DrawElement, DrawInput, DrawOptions } from './draw';
7
+ export { morph } from './morph';
8
+ export type { Morph, MorphElement, MorphOptions, MorphTarget } from './morph';
9
+ export type { ScalarControls } from './handle';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAC5D,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AACpG,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AACpD,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AACnF,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACvC,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAA;AACvE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAC7E,YAAY,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,152 @@
1
+ import { getSharedScheduler as w, animatable as x } from "@underlying/core";
2
+ const A = (t) => t < 0 ? 0 : t > 1 ? 1 : t;
3
+ function E(t) {
4
+ if (typeof t != "string") return t;
5
+ if (typeof document > "u")
6
+ throw new Error("@underlying/svg: a selector needs a DOM; pass an element on the server");
7
+ const r = document.querySelector(t);
8
+ if (r === null) throw new Error(`@underlying/svg: no element matches "${t}"`);
9
+ return r;
10
+ }
11
+ function L(t, r = {}) {
12
+ const e = E(t), n = e.getTotalLength(), s = (r.tangentEpsilon ?? 1e-3) * n || 1e-4;
13
+ return {
14
+ length: n,
15
+ at(i) {
16
+ const l = A(i) * n, a = e.getPointAtLength(l), d = e.getPointAtLength(Math.max(0, l - s)), o = e.getPointAtLength(Math.min(n, l + s)), c = Math.atan2(o.y - d.y, o.x - d.x) * 180 / Math.PI;
17
+ return { x: a.x, y: a.y, angle: c };
18
+ }
19
+ };
20
+ }
21
+ function D(t, r) {
22
+ return {
23
+ spring: (e, n) => t.spring(e, n),
24
+ decay: (e) => t.decay(e),
25
+ to: (e, n) => t.to(e, n),
26
+ set: (e) => t.set(e),
27
+ flick: (e, n) => t.decay({ min: 0, max: 1, ...n, velocity: e }),
28
+ stop: () => t.stop(),
29
+ progress: () => t.get(),
30
+ revert: r
31
+ };
32
+ }
33
+ function k(t, r, e, n = {}) {
34
+ const s = n.scheduler ?? w(), i = L(r, n), l = n.autoRotate ?? !1, a = l !== !1, d = typeof l == "number" ? l : 0;
35
+ let o = !1, c = null;
36
+ const u = () => {
37
+ const h = i.at(e.get()), y = `translate3d(${h.x}px, ${h.y}px, 0)`;
38
+ t.style.transform = a ? `${y} rotate(${h.angle + d}deg)` : y;
39
+ }, f = () => {
40
+ c === null && (c = s.subscribe(() => {
41
+ c == null || c(), c = null, o && (o = !1, u());
42
+ }, "render"));
43
+ }, p = e.on("change", () => {
44
+ o = !0, f();
45
+ });
46
+ return u(), () => {
47
+ p(), c == null || c(), c = null;
48
+ };
49
+ }
50
+ function C(t, r, e = {}) {
51
+ const n = x(
52
+ e.from ?? 0,
53
+ e.scheduler !== void 0 ? { scheduler: e.scheduler } : void 0
54
+ ), s = t.style.transform, i = k(t, r, n, e), l = () => {
55
+ n.stop(), i(), n.dispose(), t.style.transform = s;
56
+ };
57
+ return e.to !== void 0 && n.spring(e.to), { t: n, ...D(n, l) };
58
+ }
59
+ const S = (t) => t < 0 ? 0 : t > 1 ? 1 : t;
60
+ function T(t) {
61
+ if (typeof t != "string") return t;
62
+ if (typeof document > "u")
63
+ throw new Error("@underlying/svg: a selector needs a DOM; pass an element on the server");
64
+ const r = document.querySelector(t);
65
+ if (r === null) throw new Error(`@underlying/svg: no element matches "${t}"`);
66
+ return r;
67
+ }
68
+ function O(t, r, e = {}) {
69
+ const n = e.scheduler ?? w(), s = T(t), i = s.getTotalLength(), l = s.style.strokeDasharray, a = s.style.strokeDashoffset;
70
+ s.style.strokeDasharray = String(i);
71
+ let d = !1, o = null;
72
+ const c = () => {
73
+ s.style.strokeDashoffset = String(i * (1 - S(r.get())));
74
+ }, u = () => {
75
+ o === null && (o = n.subscribe(() => {
76
+ o == null || o(), o = null, d && (d = !1, c());
77
+ }, "render"));
78
+ }, f = r.on("change", () => {
79
+ d = !0, u();
80
+ });
81
+ return c(), () => {
82
+ f(), o == null || o(), o = null, s.style.strokeDasharray = l, s.style.strokeDashoffset = a;
83
+ };
84
+ }
85
+ function I(t, r = {}) {
86
+ const e = x(
87
+ r.from ?? 0,
88
+ r.scheduler !== void 0 ? { scheduler: r.scheduler } : void 0
89
+ ), n = O(t, e, r), s = () => {
90
+ e.stop(), n(), e.dispose();
91
+ };
92
+ return r.to !== void 0 && e.spring(r.to), { fraction: e, ...D(e, s) };
93
+ }
94
+ const b = (t) => Math.round(t * 100) / 100, M = (t, r) => {
95
+ const e = t.getTotalLength(), n = r > 1 ? r - 1 : 1, s = [];
96
+ for (let i = 0; i < r; i += 1) {
97
+ const l = t.getPointAtLength(i / n * e);
98
+ s.push({ x: l.x, y: l.y });
99
+ }
100
+ return s;
101
+ }, q = (t, r) => {
102
+ const e = t[0];
103
+ if (e === void 0) return "";
104
+ let n = `M ${b(e.x)} ${b(e.y)}`;
105
+ for (let s = 1; s < t.length; s += 1) {
106
+ const i = t[s];
107
+ i !== void 0 && (n += ` L ${b(i.x)} ${b(i.y)}`);
108
+ }
109
+ return r ? `${n} Z` : n;
110
+ }, G = (t) => {
111
+ if (typeof t != "string") return t;
112
+ if (typeof document > "u")
113
+ throw new Error("@underlying/svg: morph target path data needs a DOM");
114
+ const r = document.createElementNS("http://www.w3.org/2000/svg", "path");
115
+ return r.setAttribute("d", t), r;
116
+ };
117
+ function N(t, r, e = {}) {
118
+ const n = Math.max(2, e.samples ?? 64), s = e.closed ?? !1, i = M(t, n), l = M(G(r), n), a = t.getAttribute("d"), d = e.scheduler ?? w(), o = x(
119
+ e.from ?? 0,
120
+ e.scheduler !== void 0 ? { scheduler: e.scheduler } : void 0
121
+ );
122
+ let c = !1, u = null;
123
+ const f = () => {
124
+ const P = o.get(), $ = [];
125
+ for (let m = 0; m < n; m += 1) {
126
+ const g = i[m], v = l[m];
127
+ g === void 0 || v === void 0 || $.push({ x: g.x + (v.x - g.x) * P, y: g.y + (v.y - g.y) * P });
128
+ }
129
+ t.setAttribute("d", q($, s));
130
+ }, p = () => {
131
+ u === null && (u = d.subscribe(() => {
132
+ u == null || u(), u = null, c && (c = !1, f());
133
+ }, "render"));
134
+ }, h = o.on("change", () => {
135
+ c = !0, p();
136
+ });
137
+ f();
138
+ const y = () => {
139
+ o.stop(), h(), u == null || u(), u = null, a !== null && t.setAttribute("d", a), o.dispose();
140
+ };
141
+ return e.to !== void 0 && o.spring(e.to), { fraction: o, ...D(o, y) };
142
+ }
143
+ export {
144
+ O as bindDraw,
145
+ k as bindPath,
146
+ I as draw,
147
+ N as morph,
148
+ C as motionPath,
149
+ E as resolvePathGeometry,
150
+ L as samplePath
151
+ };
152
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/geometry.ts","../src/handle.ts","../src/motion-path.ts","../src/draw.ts","../src/morph.ts"],"sourcesContent":["/**\n * A subset of SVGGeometryElement: anything that can report its length and sample\n * a point at a given arc distance. Real `<path>`/`<line>`/`<polyline>` elements\n * satisfy it natively; tests pass a plain object.\n */\nexport interface PathGeometry {\n getTotalLength(): number\n getPointAtLength(distance: number): { x: number; y: number }\n}\n\n/** A path source: a CSS selector or any geometry element/object. */\nexport type PathInput = string | PathGeometry\n\n/** A sampled point on a path: position plus the tangent direction (degrees). */\nexport interface PathPoint {\n x: number\n y: number\n /** tangent direction in degrees, fed to autoRotate */\n angle: number\n}\n\nexport interface SamplePathOptions {\n /**\n * Distance, as a fraction of total length, used to estimate the tangent angle\n * from two neighbouring samples. Smaller is sharper but noisier. Default 0.001.\n */\n tangentEpsilon?: number\n}\n\nexport interface PathSampler {\n /** Total path length, in the path's user units. */\n readonly length: number\n /** Point at progress t (clamped to 0..1), with the tangent angle. */\n at(t: number): PathPoint\n}\n\nconst clamp01 = (t: number): number => (t < 0 ? 0 : t > 1 ? 1 : t)\n\n/** Resolve a {@link PathInput} to a geometry source (querySelector for strings). */\nexport function resolvePathGeometry(path: PathInput): PathGeometry {\n if (typeof path !== 'string') return path\n if (typeof document === 'undefined') {\n throw new Error('@underlying/svg: a selector needs a DOM; pass an element on the server')\n }\n const element = document.querySelector(path)\n if (element === null) throw new Error(`@underlying/svg: no element matches \"${path}\"`)\n return element as unknown as PathGeometry\n}\n\n/**\n * Wrap a path in a length-normalized sampler: feed it t in 0..1 and read back the\n * point plus tangent angle. Total length is measured once; each `at()` is two\n * getPointAtLength() reads (the point, and a neighbour for the tangent).\n */\nexport function samplePath(path: PathInput, options: SamplePathOptions = {}): PathSampler {\n const geometry = resolvePathGeometry(path)\n const length = geometry.getTotalLength()\n const eps = (options.tangentEpsilon ?? 0.001) * length || 0.0001\n\n return {\n length,\n at(t) {\n const distance = clamp01(t) * length\n const point = geometry.getPointAtLength(distance)\n const behind = geometry.getPointAtLength(Math.max(0, distance - eps))\n const ahead = geometry.getPointAtLength(Math.min(length, distance + eps))\n const angle = (Math.atan2(ahead.y - behind.y, ahead.x - behind.x) * 180) / Math.PI\n return { x: point.x, y: point.y, angle }\n },\n }\n}\n","import type { Animatable, AnimationHandle, DecayOptions, SpringOptions, ToOptions } from '@underlying/core'\n\n/**\n * The physics verbs every @underlying/svg handle shares. They delegate straight\n * to the live driver Animatable, so a motionPath or a draw is interruptible and\n * retargetable at any moment - the same engine as the rest of core.\n */\nexport interface ScalarControls {\n /** Spring the driver toward a target (0..1), interruptible, velocity conserved. */\n spring(target: number, options?: SpringOptions): AnimationHandle\n /** Glide on inertia from the current velocity, clamped to 0..1. */\n decay(options?: DecayOptions): AnimationHandle\n /** Duration/easing escape hatch - still interruptible. */\n to(target: number, options?: ToOptions): AnimationHandle\n /** Teleport the driver (no animation). */\n set(value: number): void\n /** Flick: seed a velocity (0..1 units/s) and let it decay to a stop on the path. */\n flick(velocity: number, options?: Omit<DecayOptions, 'velocity'>): AnimationHandle\n /** Freeze in place; position and velocity stay readable. */\n stop(): void\n /** Read the driver, 0..1. */\n progress(): number\n /** Stop, unbind, and restore the element to its pre-bind state. */\n revert(): void\n}\n\n/** Build the shared verb surface around a driver value, plus a revert. */\nexport function scalarControls(value: Animatable, revert: () => void): ScalarControls {\n return {\n spring: (target, options) => value.spring(target, options),\n decay: (options) => value.decay(options),\n to: (target, options) => value.to(target, options),\n set: (next) => value.set(next),\n flick: (velocity, options) => value.decay({ min: 0, max: 1, ...options, velocity }),\n stop: () => value.stop(),\n progress: () => value.get(),\n revert,\n }\n}\n","import { animatable, getSharedScheduler, type Animatable, type Scheduler } from '@underlying/core'\nimport { samplePath, type PathInput, type SamplePathOptions } from './geometry'\nimport { scalarControls, type ScalarControls } from './handle'\n\nexport interface PathBindOptions extends SamplePathOptions {\n /**\n * Turn the element to face along the path. `true` aligns to the tangent;\n * a number adds that many degrees of offset (e.g. `90` for a north-up icon).\n */\n autoRotate?: boolean | number\n /** Scheduler driving the value and the style flush. Defaults to the shared one. */\n scheduler?: Scheduler\n}\n\nexport interface MotionPathOptions extends PathBindOptions {\n /** Initial progress, 0..1. Default 0. */\n from?: number\n /** Spring to this progress on creation - the GSAP-familiar one-call form. */\n to?: number\n}\n\n/**\n * Low-level binder: map a driver Animatable (0..1) onto an element's transform\n * along a path. You own the driver - spring, decay, or scrub it from scroll or a\n * timeline. Writes the current point synchronously at bind; returns an unbind fn.\n */\nexport function bindPath(\n element: HTMLElement | SVGElement,\n path: PathInput,\n source: Animatable,\n options: PathBindOptions = {},\n): () => void {\n const scheduler = options.scheduler ?? getSharedScheduler()\n const sampler = samplePath(path, options)\n const autoRotate = options.autoRotate ?? false\n const rotates = autoRotate !== false\n const offset = typeof autoRotate === 'number' ? autoRotate : 0\n\n let dirty = false\n let cancelFlush: (() => void) | null = null\n\n const write = (): void => {\n const point = sampler.at(source.get())\n const move = `translate3d(${point.x}px, ${point.y}px, 0)`\n element.style.transform = rotates ? `${move} rotate(${point.angle + offset}deg)` : move\n }\n\n // One-shot render subscription per change burst: flush once on the next frame,\n // then let the scheduler sleep - same pattern as core's bindStyle.\n const scheduleFlush = (): void => {\n if (cancelFlush !== null) return\n cancelFlush = scheduler.subscribe(() => {\n cancelFlush?.()\n cancelFlush = null\n if (dirty) {\n dirty = false\n write()\n }\n }, 'render')\n }\n\n const unsubscribe = source.on('change', () => {\n dirty = true\n scheduleFlush()\n })\n write()\n\n return () => {\n unsubscribe()\n cancelFlush?.()\n cancelFlush = null\n }\n}\n\nexport interface MotionPath extends ScalarControls {\n /** The live 0..1 driver. Compose it: scroll.scrub(mp.t), a timeline, a sequence. */\n readonly t: Animatable\n}\n\n/**\n * Ride an element along an SVG path, physics-first. Progress `t` is a live\n * Animatable, so you can spring it, flick it down the path and let it settle,\n * retarget it mid-flight (velocity conserved), or hand `t` to scroll/timeline.\n * `autoRotate` turns the element to face along the path.\n */\nexport function motionPath(\n element: HTMLElement | SVGElement,\n path: PathInput,\n options: MotionPathOptions = {},\n): MotionPath {\n const t = animatable(\n options.from ?? 0,\n options.scheduler !== undefined ? { scheduler: options.scheduler } : undefined,\n )\n const previousTransform = element.style.transform\n const unbind = bindPath(element, path, t, options)\n\n const revert = (): void => {\n t.stop()\n unbind()\n t.dispose()\n element.style.transform = previousTransform\n }\n\n if (options.to !== undefined) t.spring(options.to)\n\n return { t, ...scalarControls(t, revert) }\n}\n","import { animatable, getSharedScheduler, type Animatable, type Scheduler } from '@underlying/core'\nimport { scalarControls, type ScalarControls } from './handle'\n\n/** An element that can be stroke-drawn: reports its length and has a writable style. */\nexport interface DrawElement {\n getTotalLength(): number\n style: { strokeDasharray: string; strokeDashoffset: string }\n}\n\n/** A draw source: a CSS selector or a stroked geometry element. */\nexport type DrawInput = string | DrawElement\n\nexport interface DrawOptions {\n /** Scheduler driving the value and the style flush. Defaults to the shared one. */\n scheduler?: Scheduler\n /** Initial draw fraction, 0..1 (0 hidden, 1 drawn). Default 0. */\n from?: number\n /** Spring to this fraction on creation - the GSAP-familiar one-call form. */\n to?: number\n}\n\nconst clamp01 = (v: number): number => (v < 0 ? 0 : v > 1 ? 1 : v)\n\nfunction resolveDrawElement(path: DrawInput): DrawElement {\n if (typeof path !== 'string') return path\n if (typeof document === 'undefined') {\n throw new Error('@underlying/svg: a selector needs a DOM; pass an element on the server')\n }\n const element = document.querySelector(path)\n if (element === null) throw new Error(`@underlying/svg: no element matches \"${path}\"`)\n return element as unknown as DrawElement\n}\n\n/**\n * Low-level binder: map a driver Animatable (0..1) onto an element's stroke\n * draw-on (0 = hidden, 1 = fully drawn) via stroke-dasharray/offset. You own the\n * driver. Returns an unbind fn that restores the original dash properties.\n */\nexport function bindDraw(path: DrawInput, source: Animatable, options: DrawOptions = {}): () => void {\n const scheduler = options.scheduler ?? getSharedScheduler()\n const element = resolveDrawElement(path)\n const length = element.getTotalLength()\n const previousDasharray = element.style.strokeDasharray\n const previousDashoffset = element.style.strokeDashoffset\n element.style.strokeDasharray = String(length)\n\n let dirty = false\n let cancelFlush: (() => void) | null = null\n\n const write = (): void => {\n element.style.strokeDashoffset = String(length * (1 - clamp01(source.get())))\n }\n const scheduleFlush = (): void => {\n if (cancelFlush !== null) return\n cancelFlush = scheduler.subscribe(() => {\n cancelFlush?.()\n cancelFlush = null\n if (dirty) {\n dirty = false\n write()\n }\n }, 'render')\n }\n\n const unsubscribe = source.on('change', () => {\n dirty = true\n scheduleFlush()\n })\n write()\n\n return () => {\n unsubscribe()\n cancelFlush?.()\n cancelFlush = null\n element.style.strokeDasharray = previousDasharray\n element.style.strokeDashoffset = previousDashoffset\n }\n}\n\nexport interface Draw extends ScalarControls {\n /** The live 0..1 draw fraction (0 hidden, 1 drawn). Compose it anywhere. */\n readonly fraction: Animatable\n}\n\n/**\n * Draw a stroke on, physics-first. The fraction is a live Animatable - spring it\n * in (it can overshoot), interrupt it mid-draw, flick it, or scrub it from\n * scroll. Restores the original stroke-dash properties on revert().\n */\nexport function draw(path: DrawInput, options: DrawOptions = {}): Draw {\n const fraction = animatable(\n options.from ?? 0,\n options.scheduler !== undefined ? { scheduler: options.scheduler } : undefined,\n )\n const unbind = bindDraw(path, fraction, options)\n\n const revert = (): void => {\n fraction.stop()\n unbind()\n fraction.dispose()\n }\n\n if (options.to !== undefined) fraction.spring(options.to)\n\n return { fraction, ...scalarControls(fraction, revert) }\n}\n","import { animatable, getSharedScheduler, type Animatable, type Scheduler } from '@underlying/core'\nimport type { PathGeometry } from './geometry'\nimport { scalarControls, type ScalarControls } from './handle'\n\n/** A path element whose shape this package rewrites: geometry plus the `d` attribute. */\nexport interface MorphElement extends PathGeometry {\n getAttribute(name: string): string | null\n setAttribute(name: string, value: string): void\n}\n\n/** The shape to morph toward: raw path data (`\"M ...\"`) or any geometry/element. */\nexport type MorphTarget = string | PathGeometry\n\nexport interface MorphOptions {\n scheduler?: Scheduler\n /** Points sampled along each outline; more is smoother (and heavier). Default 64. */\n samples?: number\n /** Close the interpolated outline - for closed shapes (a star, a blob). */\n closed?: boolean\n /** Initial morph fraction, 0..1 (0 = original shape, 1 = target). Default 0. */\n from?: number\n /** Spring to this fraction on creation. */\n to?: number\n}\n\ninterface Point {\n x: number\n y: number\n}\n\nconst round = (n: number): number => Math.round(n * 100) / 100\n\n// Resample an outline into `count` points evenly spaced by arc length. This is\n// what lets ANY two shapes morph: both become the same-length point list, no\n// matching of bezier commands required.\nconst samplePoints = (geometry: PathGeometry, count: number): Point[] => {\n const length = geometry.getTotalLength()\n const divisor = count > 1 ? count - 1 : 1\n const points: Point[] = []\n for (let i = 0; i < count; i += 1) {\n const p = geometry.getPointAtLength((i / divisor) * length)\n points.push({ x: p.x, y: p.y })\n }\n return points\n}\n\nconst toPathData = (points: Point[], closed: boolean): string => {\n const first = points[0]\n if (first === undefined) return ''\n let d = `M ${round(first.x)} ${round(first.y)}`\n for (let i = 1; i < points.length; i += 1) {\n const p = points[i]\n if (p !== undefined) d += ` L ${round(p.x)} ${round(p.y)}`\n }\n return closed ? `${d} Z` : d\n}\n\nconst targetGeometry = (target: MorphTarget): PathGeometry => {\n if (typeof target !== 'string') return target\n if (typeof document === 'undefined') {\n throw new Error('@underlying/svg: morph target path data needs a DOM')\n }\n const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')\n path.setAttribute('d', target)\n return path as unknown as PathGeometry\n}\n\nexport interface Morph extends ScalarControls {\n /** The live 0..1 morph fraction (0 original, 1 target). Compose it anywhere. */\n readonly fraction: Animatable\n}\n\n/**\n * Morph one path into another, physics-first. Both outlines are resampled into\n * `samples` points along their length and interpolated, so any two shapes morph\n * (no matching command structure needed). The fraction is a live Animatable -\n * spring it, scrub it, or grab it mid-morph. `revert()` restores the original `d`.\n */\nexport function morph(element: MorphElement, target: MorphTarget, options: MorphOptions = {}): Morph {\n const count = Math.max(2, options.samples ?? 64)\n const closed = options.closed ?? false\n const fromPoints = samplePoints(element, count)\n const toPoints = samplePoints(targetGeometry(target), count)\n const originalD = element.getAttribute('d')\n\n const scheduler = options.scheduler ?? getSharedScheduler()\n const fraction = animatable(\n options.from ?? 0,\n options.scheduler !== undefined ? { scheduler: options.scheduler } : undefined,\n )\n\n let dirty = false\n let cancelFlush: (() => void) | null = null\n\n const write = (): void => {\n const f = fraction.get()\n const blended: Point[] = []\n for (let i = 0; i < count; i += 1) {\n const a = fromPoints[i]\n const b = toPoints[i]\n if (a === undefined || b === undefined) continue\n blended.push({ x: a.x + (b.x - a.x) * f, y: a.y + (b.y - a.y) * f })\n }\n element.setAttribute('d', toPathData(blended, closed))\n }\n const scheduleFlush = (): void => {\n if (cancelFlush !== null) return\n cancelFlush = scheduler.subscribe(() => {\n cancelFlush?.()\n cancelFlush = null\n if (dirty) {\n dirty = false\n write()\n }\n }, 'render')\n }\n\n const unsubscribe = fraction.on('change', () => {\n dirty = true\n scheduleFlush()\n })\n write()\n\n const revert = (): void => {\n fraction.stop()\n unsubscribe()\n cancelFlush?.()\n cancelFlush = null\n if (originalD !== null) element.setAttribute('d', originalD)\n fraction.dispose()\n }\n\n if (options.to !== undefined) fraction.spring(options.to)\n\n return { fraction, ...scalarControls(fraction, revert) }\n}\n"],"names":["clamp01","resolvePathGeometry","path","element","samplePath","options","geometry","length","eps","t","distance","point","behind","ahead","angle","scalarControls","value","revert","target","next","velocity","bindPath","source","scheduler","getSharedScheduler","sampler","autoRotate","rotates","offset","dirty","cancelFlush","write","move","scheduleFlush","unsubscribe","motionPath","animatable","previousTransform","unbind","v","resolveDrawElement","bindDraw","previousDasharray","previousDashoffset","draw","fraction","round","n","samplePoints","count","divisor","points","p","toPathData","closed","first","d","i","targetGeometry","morph","fromPoints","toPoints","originalD","f","blended","a","b"],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import { type Animatable, type Scheduler } from '@underlying/core';
2
+ import type { PathGeometry } from './geometry';
3
+ import { type ScalarControls } from './handle';
4
+ /** A path element whose shape this package rewrites: geometry plus the `d` attribute. */
5
+ export interface MorphElement extends PathGeometry {
6
+ getAttribute(name: string): string | null;
7
+ setAttribute(name: string, value: string): void;
8
+ }
9
+ /** The shape to morph toward: raw path data (`"M ..."`) or any geometry/element. */
10
+ export type MorphTarget = string | PathGeometry;
11
+ export interface MorphOptions {
12
+ scheduler?: Scheduler;
13
+ /** Points sampled along each outline; more is smoother (and heavier). Default 64. */
14
+ samples?: number;
15
+ /** Close the interpolated outline - for closed shapes (a star, a blob). */
16
+ closed?: boolean;
17
+ /** Initial morph fraction, 0..1 (0 = original shape, 1 = target). Default 0. */
18
+ from?: number;
19
+ /** Spring to this fraction on creation. */
20
+ to?: number;
21
+ }
22
+ export interface Morph extends ScalarControls {
23
+ /** The live 0..1 morph fraction (0 original, 1 target). Compose it anywhere. */
24
+ readonly fraction: Animatable;
25
+ }
26
+ /**
27
+ * Morph one path into another, physics-first. Both outlines are resampled into
28
+ * `samples` points along their length and interpolated, so any two shapes morph
29
+ * (no matching command structure needed). The fraction is a live Animatable -
30
+ * spring it, scrub it, or grab it mid-morph. `revert()` restores the original `d`.
31
+ */
32
+ export declare function morph(element: MorphElement, target: MorphTarget, options?: MorphOptions): Morph;
33
+ //# sourceMappingURL=morph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"morph.d.ts","sourceRoot":"","sources":["../src/morph.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAClG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAkB,KAAK,cAAc,EAAE,MAAM,UAAU,CAAA;AAE9D,yFAAyF;AACzF,MAAM,WAAW,YAAa,SAAQ,YAAY;IAChD,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IACzC,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAChD;AAED,oFAAoF;AACpF,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,YAAY,CAAA;AAE/C,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,qFAAqF;IACrF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,2CAA2C;IAC3C,EAAE,CAAC,EAAE,MAAM,CAAA;CACZ;AA4CD,MAAM,WAAW,KAAM,SAAQ,cAAc;IAC3C,gFAAgF;IAChF,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAA;CAC9B;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,GAAE,YAAiB,GAAG,KAAK,CAyDnG"}
@@ -0,0 +1,36 @@
1
+ import { type Animatable, type Scheduler } from '@underlying/core';
2
+ import { type PathInput, type SamplePathOptions } from './geometry';
3
+ import { type ScalarControls } from './handle';
4
+ export interface PathBindOptions extends SamplePathOptions {
5
+ /**
6
+ * Turn the element to face along the path. `true` aligns to the tangent;
7
+ * a number adds that many degrees of offset (e.g. `90` for a north-up icon).
8
+ */
9
+ autoRotate?: boolean | number;
10
+ /** Scheduler driving the value and the style flush. Defaults to the shared one. */
11
+ scheduler?: Scheduler;
12
+ }
13
+ export interface MotionPathOptions extends PathBindOptions {
14
+ /** Initial progress, 0..1. Default 0. */
15
+ from?: number;
16
+ /** Spring to this progress on creation - the GSAP-familiar one-call form. */
17
+ to?: number;
18
+ }
19
+ /**
20
+ * Low-level binder: map a driver Animatable (0..1) onto an element's transform
21
+ * along a path. You own the driver - spring, decay, or scrub it from scroll or a
22
+ * timeline. Writes the current point synchronously at bind; returns an unbind fn.
23
+ */
24
+ export declare function bindPath(element: HTMLElement | SVGElement, path: PathInput, source: Animatable, options?: PathBindOptions): () => void;
25
+ export interface MotionPath extends ScalarControls {
26
+ /** The live 0..1 driver. Compose it: scroll.scrub(mp.t), a timeline, a sequence. */
27
+ readonly t: Animatable;
28
+ }
29
+ /**
30
+ * Ride an element along an SVG path, physics-first. Progress `t` is a live
31
+ * Animatable, so you can spring it, flick it down the path and let it settle,
32
+ * retarget it mid-flight (velocity conserved), or hand `t` to scroll/timeline.
33
+ * `autoRotate` turns the element to face along the path.
34
+ */
35
+ export declare function motionPath(element: HTMLElement | SVGElement, path: PathInput, options?: MotionPathOptions): MotionPath;
36
+ //# sourceMappingURL=motion-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"motion-path.d.ts","sourceRoot":"","sources":["../src/motion-path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAClG,OAAO,EAAc,KAAK,SAAS,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC/E,OAAO,EAAkB,KAAK,cAAc,EAAE,MAAM,UAAU,CAAA;AAE9D,MAAM,WAAW,eAAgB,SAAQ,iBAAiB;IACxD;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;IAC7B,mFAAmF;IACnF,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB;AAED,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,6EAA6E;IAC7E,EAAE,CAAC,EAAE,MAAM,CAAA;CACZ;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CACtB,OAAO,EAAE,WAAW,GAAG,UAAU,EACjC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,UAAU,EAClB,OAAO,GAAE,eAAoB,GAC5B,MAAM,IAAI,CAyCZ;AAED,MAAM,WAAW,UAAW,SAAQ,cAAc;IAChD,oFAAoF;IACpF,QAAQ,CAAC,CAAC,EAAE,UAAU,CAAA;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,WAAW,GAAG,UAAU,EACjC,IAAI,EAAE,SAAS,EACf,OAAO,GAAE,iBAAsB,GAC9B,UAAU,CAkBZ"}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@underlying/svg",
3
+ "version": "0.1.0-beta.0",
4
+ "description": "SVG path animation for @underlying/core: ride a path, draw a stroke on, or morph one shape into another, physics-first - flick, interrupt, retarget, or scrub. MotionPath, DrawSVG and morph.",
5
+ "license": "MIT",
6
+ "author": "underlyi.ng <contact@underlyi.ng>",
7
+ "keywords": [
8
+ "animation",
9
+ "svg",
10
+ "motionpath",
11
+ "drawsvg",
12
+ "morph",
13
+ "path",
14
+ "stroke",
15
+ "physics",
16
+ "spring"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "provenance": false
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/underlyingjs/underlying.git",
25
+ "directory": "packages/svg"
26
+ },
27
+ "homepage": "https://github.com/underlyingjs/underlying#readme",
28
+ "bugs": "https://github.com/underlyingjs/underlying/issues",
29
+ "type": "module",
30
+ "sideEffects": false,
31
+ "main": "./dist/index.cjs",
32
+ "module": "./dist/index.js",
33
+ "types": "./dist/index.d.ts",
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.js",
38
+ "require": "./dist/index.cjs"
39
+ }
40
+ },
41
+ "files": [
42
+ "dist"
43
+ ],
44
+ "engines": {
45
+ "node": ">=20"
46
+ },
47
+ "dependencies": {
48
+ "@underlying/core": "0.1.0-beta.2"
49
+ },
50
+ "devDependencies": {
51
+ "esbuild": "^0.25.0",
52
+ "jsdom": "^26.1.0",
53
+ "typescript": "^5.8.3",
54
+ "vite": "^6.3.5",
55
+ "vitest": "^3.2.4"
56
+ },
57
+ "scripts": {
58
+ "test": "vitest run",
59
+ "test:watch": "vitest",
60
+ "build": "vite build && tsc -p tsconfig.build.json",
61
+ "typecheck": "tsc --noEmit",
62
+ "size": "node scripts/check-size.mjs"
63
+ }
64
+ }