@underlying/flip 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,55 @@
1
+ <div align="center">
2
+
3
+ # @underlying/flip
4
+
5
+ **Layout and shared-element transitions, physics-first.**
6
+
7
+ </div>
8
+
9
+ Animate an element from its old place to its new one - both position and size - by measuring its box before and after a DOM change and springing the difference away. The play is a real spring, so it is interruptible: change the layout again mid-flight and each element retargets from its live position and velocity, bending into the new layout instead of restarting. Built on [`@underlying/core`](https://github.com/underlyingjs/underlying/tree/main/packages/core); about 0.9 kB gzip on top of it.
10
+
11
+ ```bash
12
+ npm install @underlying/core @underlying/flip
13
+ ```
14
+
15
+ ## Layout: `flip(targets, mutate)`
16
+
17
+ Wrap any DOM change. `flip` measures First, runs your mutation, measures Last, applies the inverse transform so nothing jumps, then springs to identity.
18
+
19
+ ```ts
20
+ import { flip } from '@underlying/flip'
21
+
22
+ // reorder, filter, resize - the tiles spring to their new places
23
+ flip(tiles, () => grid.append(...reordered), { stiffness: 320, damping: 26 })
24
+
25
+ // position AND size: a grid <-> list toggle resizes every tile smoothly
26
+ flip(tiles, () => grid.classList.toggle('list'))
27
+ ```
28
+
29
+ By default it inverts position and size (translate + scale, pinned to the top-left). Pass `{ scale: false }` for position only.
30
+
31
+ ## Shared element: `snapshot()` + `play()`
32
+
33
+ When the old and new elements are different DOM nodes (a thumbnail expanding into a detail view, a route change), capture the old set, change the DOM, then play the new set from the captured boxes - matched by `data-flip-id`.
34
+
35
+ ```ts
36
+ import { snapshot, play } from '@underlying/flip'
37
+
38
+ const state = snapshot(thumbnails) // data-flip-id on each
39
+ // ... navigate / re-render: the detail view mounts ...
40
+ play(state, { targets: detailEls, stiffness: 260, damping: 24 })
41
+ ```
42
+
43
+ A target with no matching key in the snapshot is left alone.
44
+
45
+ ## Interruption
46
+
47
+ Every `flip()` / `play()` retargets from the live spring, velocity conserved - press the button again while the tiles are still moving and the motion redirects, never restarts. That is the whole point.
48
+
49
+ ## Options
50
+
51
+ `FlipOptions` extends the core `SpringOptions` (`stiffness`, `damping`, `mass`, ...) plus `scale?: boolean` and `scheduler?`.
52
+
53
+ ## License
54
+
55
+ MIT (c) underlyi.ng
package/dist/flip.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { type Scheduler, type SpringOptions } from '@underlying/core';
2
+ export type FlipTargets = HTMLElement | Iterable<HTMLElement>;
3
+ export interface FlipOptions extends SpringOptions {
4
+ scheduler?: Scheduler;
5
+ /** Invert and animate size changes too (scale), not only position. Default true. */
6
+ scale?: boolean;
7
+ }
8
+ export interface FlipPlayOptions extends FlipOptions {
9
+ /** The elements to animate from the snapshot boxes, matched by `data-flip-id`. */
10
+ targets: FlipTargets;
11
+ }
12
+ interface Box {
13
+ readonly left: number;
14
+ readonly top: number;
15
+ readonly width: number;
16
+ readonly height: number;
17
+ }
18
+ /** A captured set of element boxes, keyed by `data-flip-id` (or the element itself). */
19
+ export interface FlipSnapshot {
20
+ readonly boxes: ReadonlyMap<string | HTMLElement, Box>;
21
+ }
22
+ /**
23
+ * Physics-first FLIP. Measures each element's box (First), runs `mutate` to
24
+ * change the DOM, measures again (Last), applies the inverse transform - both
25
+ * position AND size - so nothing visibly jumps, then springs every element to
26
+ * its new place. The play is a spring, not a baked tween: call flip() again
27
+ * mid-flight and each element retargets from its live position and velocity, so
28
+ * the motion bends into the new layout instead of restarting. That
29
+ * interruptibility is the whole point.
30
+ */
31
+ export declare function flip(targets: FlipTargets, mutate: () => void, options?: FlipOptions): void;
32
+ /**
33
+ * Capture each target's box, keyed by its `data-flip-id` (or the element
34
+ * itself). Pair with `play()` for shared-element / route transitions, where the
35
+ * old elements and the new ones are different DOM nodes: snapshot the old set,
36
+ * change the DOM, then play the new set from the captured boxes.
37
+ */
38
+ export declare function snapshot(targets: FlipTargets): FlipSnapshot;
39
+ /**
40
+ * Animate each target from its matching box in the snapshot to its current
41
+ * place - matched by `data-flip-id`. A target with no match in the snapshot is
42
+ * left alone. Interruptible like `flip()`.
43
+ */
44
+ export declare function play(snap: FlipSnapshot, options: FlipPlayOptions): void;
45
+ export {};
46
+ //# sourceMappingURL=flip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flip.d.ts","sourceRoot":"","sources":["../src/flip.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+B,KAAK,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAElG,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAA;AAE7D,MAAM,WAAW,WAAY,SAAQ,aAAa;IAChD,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,oFAAoF;IACpF,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,MAAM,WAAW,eAAgB,SAAQ,WAAW;IAClD,kFAAkF;IAClF,OAAO,EAAE,WAAW,CAAA;CACrB;AAED,UAAU,GAAG;IACX,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED,wFAAwF;AACxF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,GAAG,WAAW,EAAE,GAAG,CAAC,CAAA;CACvD;AA6GD;;;;;;;;GAQG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,IAAI,EAAE,OAAO,GAAE,WAAgB,GAAG,IAAI,CAsB9F;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,WAAW,GAAG,YAAY,CAI3D;AAED;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAQvE"}
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const y=require("@underlying/core"),x={x:0,y:0,sx:0,sy:0},g=new WeakMap,u=s=>s instanceof HTMLElement?[s]:Array.from(s),d=s=>{const t=s.getBoundingClientRect();return{left:t.left,top:t.top,width:t.width,height:t.height}},p=s=>s.dataset.flipId??s,v=(s,t,e,n,c)=>{if(t===0&&e===0&&n===1&&c===1){s.style.transform="";return}const i=n===1&&c===1?"":` scale(${n}, ${c})`;s.style.transform=`translate3d(${t}px, ${e}px, 0)${i}`},b=(s,t)=>{let e=g.get(s);if(e===void 0){const n=t.scheduler!==void 0?{scheduler:t.scheduler}:{},c=y.animatable(0,n),i=y.animatable(0,n),o=y.animatable(1,n),r=y.animatable(1,n),a=()=>v(s,c.get(),i.get(),o.get(),r.get());c.on("change",a),i.on("change",a),o.on("change",a),r.on("change",a),s.style.transformOrigin="0 0",e={x:c,y:i,sx:o,sy:r},g.set(s,e)}return e},m=s=>{const t=g.get(s);if(t===void 0)return x;const e={x:t.x.velocity(),y:t.y.velocity(),sx:t.sx.velocity(),sy:t.sy.velocity()};return t.x.stop(),t.y.stop(),t.sx.stop(),t.sy.stop(),e},w=(s,t,e,n)=>{const c=n.scale!==!1,i=d(s),o=t.left-i.left,r=t.top-i.top,a=c&&i.width>0?t.width/i.width:1,h=c&&i.height>0?t.height/i.height:1;if(o===0&&r===0&&a===1&&h===1){const f=g.get(s);f!==void 0&&(f.x.set(0),f.y.set(0),f.sx.set(1),f.sy.set(1)),s.style.transform="";return}const l=b(s,n);v(s,o,r,a,h),l.x.set(o,{velocity:e.x}),l.y.set(r,{velocity:e.y}),l.sx.set(a,{velocity:e.sx}),l.sy.set(h,{velocity:e.sy}),l.x.spring(0,n),l.y.spring(0,n),l.sx.spring(1,n),l.sy.spring(1,n)};function M(s,t,e={}){const n=u(s),c=new Map,i=new Map;for(const o of n)c.set(o,d(o)),i.set(o,m(o));for(const o of n)o.style.transform="";t();for(const o of n){const r=c.get(o);r!==void 0&&w(o,r,i.get(o)??x,e)}}function O(s){const t=new Map;for(const e of u(s))t.set(p(e),d(e));return{boxes:t}}function S(s,t){for(const e of u(t.targets)){const n=s.boxes.get(p(e));if(n===void 0)continue;const c=m(e);e.style.transform="",w(e,n,c,t)}}exports.flip=M;exports.play=S;exports.snapshot=O;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../src/flip.ts"],"sourcesContent":["import { animatable, type Animatable, type Scheduler, type SpringOptions } from '@underlying/core'\n\nexport type FlipTargets = HTMLElement | Iterable<HTMLElement>\n\nexport interface FlipOptions extends SpringOptions {\n scheduler?: Scheduler\n /** Invert and animate size changes too (scale), not only position. Default true. */\n scale?: boolean\n}\n\nexport interface FlipPlayOptions extends FlipOptions {\n /** The elements to animate from the snapshot boxes, matched by `data-flip-id`. */\n targets: FlipTargets\n}\n\ninterface Box {\n readonly left: number\n readonly top: number\n readonly width: number\n readonly height: number\n}\n\n/** A captured set of element boxes, keyed by `data-flip-id` (or the element itself). */\nexport interface FlipSnapshot {\n readonly boxes: ReadonlyMap<string | HTMLElement, Box>\n}\n\ninterface FlipState {\n readonly x: Animatable\n readonly y: Animatable\n readonly sx: Animatable\n readonly sy: Animatable\n}\n\ninterface Velocity {\n readonly x: number\n readonly y: number\n readonly sx: number\n readonly sy: number\n}\n\nconst ZERO_VELOCITY: Velocity = { x: 0, y: 0, sx: 0, sy: 0 }\n\n// FLIP owns the element's transform directly: the writes must be SYNCHRONOUS so\n// the inverted box paints before the browser shows the new layout (no flash) -\n// which is why this drives style.transform itself instead of bindStyle (whose\n// flush is deferred to the render phase). One spring set per element, reused.\nconst states = new WeakMap<HTMLElement, FlipState>()\n\nconst toElements = (targets: FlipTargets): HTMLElement[] =>\n targets instanceof HTMLElement ? [targets] : Array.from(targets)\n\nconst measure = (element: HTMLElement): Box => {\n const r = element.getBoundingClientRect()\n return { left: r.left, top: r.top, width: r.width, height: r.height }\n}\n\nconst keyOf = (element: HTMLElement): string | HTMLElement => element.dataset.flipId ?? element\n\nconst writeTransform = (element: HTMLElement, x: number, y: number, sx: number, sy: number): void => {\n if (x === 0 && y === 0 && sx === 1 && sy === 1) {\n element.style.transform = ''\n return\n }\n const scale = sx === 1 && sy === 1 ? '' : ` scale(${sx}, ${sy})`\n element.style.transform = `translate3d(${x}px, ${y}px, 0)${scale}`\n}\n\nconst ensureState = (element: HTMLElement, options: FlipOptions): FlipState => {\n let state = states.get(element)\n if (state === undefined) {\n const valueOptions = options.scheduler !== undefined ? { scheduler: options.scheduler } : {}\n const x = animatable(0, valueOptions)\n const y = animatable(0, valueOptions)\n const sx = animatable(1, valueOptions)\n const sy = animatable(1, valueOptions)\n const write = (): void => writeTransform(element, x.get(), y.get(), sx.get(), sy.get())\n x.on('change', write)\n y.on('change', write)\n sx.on('change', write)\n sy.on('change', write)\n element.style.transformOrigin = '0 0' // pin the top-left so scale and translate align\n state = { x, y, sx, sy }\n states.set(element, state)\n }\n return state\n}\n\n// Stop any in-flight spring and read its live velocity, so the next play carries\n// the momentum instead of restarting (the interruptible handoff).\nconst seizeVelocity = (element: HTMLElement): Velocity => {\n const state = states.get(element)\n if (state === undefined) return ZERO_VELOCITY\n const velocity = { x: state.x.velocity(), y: state.y.velocity(), sx: state.sx.velocity(), sy: state.sy.velocity() }\n state.x.stop()\n state.y.stop()\n state.sx.stop()\n state.sy.stop()\n return velocity\n}\n\n// Invert (First minus the element's current natural box) and spring to identity.\nconst invertAndSpring = (element: HTMLElement, first: Box, velocity: Velocity, options: FlipOptions): void => {\n const useScale = options.scale !== false\n const last = measure(element)\n const dx = first.left - last.left\n const dy = first.top - last.top\n const sx = useScale && last.width > 0 ? first.width / last.width : 1\n const sy = useScale && last.height > 0 ? first.height / last.height : 1\n\n if (dx === 0 && dy === 0 && sx === 1 && sy === 1) {\n const existing = states.get(element)\n if (existing !== undefined) {\n existing.x.set(0)\n existing.y.set(0)\n existing.sx.set(1)\n existing.sy.set(1)\n }\n element.style.transform = ''\n return\n }\n\n const state = ensureState(element, options)\n writeTransform(element, dx, dy, sx, sy) // appear at First synchronously - no flash\n state.x.set(dx, { velocity: velocity.x })\n state.y.set(dy, { velocity: velocity.y })\n state.sx.set(sx, { velocity: velocity.sx })\n state.sy.set(sy, { velocity: velocity.sy })\n state.x.spring(0, options)\n state.y.spring(0, options)\n state.sx.spring(1, options)\n state.sy.spring(1, options)\n}\n\n/**\n * Physics-first FLIP. Measures each element's box (First), runs `mutate` to\n * change the DOM, measures again (Last), applies the inverse transform - both\n * position AND size - so nothing visibly jumps, then springs every element to\n * its new place. The play is a spring, not a baked tween: call flip() again\n * mid-flight and each element retargets from its live position and velocity, so\n * the motion bends into the new layout instead of restarting. That\n * interruptibility is the whole point.\n */\nexport function flip(targets: FlipTargets, mutate: () => void, options: FlipOptions = {}): void {\n const elements = toElements(targets)\n\n // First: current visual box (with any in-flight transform), plus the live\n // velocity of a running spring.\n const first = new Map<HTMLElement, Box>()\n const velocity = new Map<HTMLElement, Velocity>()\n for (const element of elements) {\n first.set(element, measure(element))\n velocity.set(element, seizeVelocity(element))\n }\n\n // Strip transforms so the Last measurement is the natural layout box.\n for (const element of elements) element.style.transform = ''\n\n mutate()\n\n for (const element of elements) {\n const box = first.get(element)\n if (box === undefined) continue\n invertAndSpring(element, box, velocity.get(element) ?? ZERO_VELOCITY, options)\n }\n}\n\n/**\n * Capture each target's box, keyed by its `data-flip-id` (or the element\n * itself). Pair with `play()` for shared-element / route transitions, where the\n * old elements and the new ones are different DOM nodes: snapshot the old set,\n * change the DOM, then play the new set from the captured boxes.\n */\nexport function snapshot(targets: FlipTargets): FlipSnapshot {\n const boxes = new Map<string | HTMLElement, Box>()\n for (const element of toElements(targets)) boxes.set(keyOf(element), measure(element))\n return { boxes }\n}\n\n/**\n * Animate each target from its matching box in the snapshot to its current\n * place - matched by `data-flip-id`. A target with no match in the snapshot is\n * left alone. Interruptible like `flip()`.\n */\nexport function play(snap: FlipSnapshot, options: FlipPlayOptions): void {\n for (const element of toElements(options.targets)) {\n const first = snap.boxes.get(keyOf(element))\n if (first === undefined) continue\n const velocity = seizeVelocity(element)\n element.style.transform = '' // ensure Last is the natural box\n invertAndSpring(element, first, velocity, options)\n }\n}\n"],"names":["ZERO_VELOCITY","states","toElements","targets","measure","element","r","keyOf","writeTransform","x","y","sx","sy","scale","ensureState","options","state","valueOptions","animatable","write","seizeVelocity","velocity","invertAndSpring","first","useScale","last","dx","dy","existing","flip","mutate","elements","box","snapshot","boxes","play","snap"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { flip, play, snapshot } from './flip';
2
+ export type { FlipOptions, FlipPlayOptions, FlipSnapshot, FlipTargets } from './flip';
3
+ //# 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,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAA;AAC7C,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,63 @@
1
+ import { animatable as y } from "@underlying/core";
2
+ const u = { x: 0, y: 0, sx: 0, sy: 0 }, g = /* @__PURE__ */ new WeakMap(), h = (s) => s instanceof HTMLElement ? [s] : Array.from(s), d = (s) => {
3
+ const t = s.getBoundingClientRect();
4
+ return { left: t.left, top: t.top, width: t.width, height: t.height };
5
+ }, p = (s) => s.dataset.flipId ?? s, v = (s, t, e, n, c) => {
6
+ if (t === 0 && e === 0 && n === 1 && c === 1) {
7
+ s.style.transform = "";
8
+ return;
9
+ }
10
+ const r = n === 1 && c === 1 ? "" : ` scale(${n}, ${c})`;
11
+ s.style.transform = `translate3d(${t}px, ${e}px, 0)${r}`;
12
+ }, M = (s, t) => {
13
+ let e = g.get(s);
14
+ if (e === void 0) {
15
+ const n = t.scheduler !== void 0 ? { scheduler: t.scheduler } : {}, c = y(0, n), r = y(0, n), o = y(1, n), i = y(1, n), a = () => v(s, c.get(), r.get(), o.get(), i.get());
16
+ c.on("change", a), r.on("change", a), o.on("change", a), i.on("change", a), s.style.transformOrigin = "0 0", e = { x: c, y: r, sx: o, sy: i }, g.set(s, e);
17
+ }
18
+ return e;
19
+ }, m = (s) => {
20
+ const t = g.get(s);
21
+ if (t === void 0) return u;
22
+ const e = { x: t.x.velocity(), y: t.y.velocity(), sx: t.sx.velocity(), sy: t.sy.velocity() };
23
+ return t.x.stop(), t.y.stop(), t.sx.stop(), t.sy.stop(), e;
24
+ }, w = (s, t, e, n) => {
25
+ const c = n.scale !== !1, r = d(s), o = t.left - r.left, i = t.top - r.top, a = c && r.width > 0 ? t.width / r.width : 1, x = c && r.height > 0 ? t.height / r.height : 1;
26
+ if (o === 0 && i === 0 && a === 1 && x === 1) {
27
+ const l = g.get(s);
28
+ l !== void 0 && (l.x.set(0), l.y.set(0), l.sx.set(1), l.sy.set(1)), s.style.transform = "";
29
+ return;
30
+ }
31
+ const f = M(s, n);
32
+ v(s, o, i, a, x), f.x.set(o, { velocity: e.x }), f.y.set(i, { velocity: e.y }), f.sx.set(a, { velocity: e.sx }), f.sy.set(x, { velocity: e.sy }), f.x.spring(0, n), f.y.spring(0, n), f.sx.spring(1, n), f.sy.spring(1, n);
33
+ };
34
+ function $(s, t, e = {}) {
35
+ const n = h(s), c = /* @__PURE__ */ new Map(), r = /* @__PURE__ */ new Map();
36
+ for (const o of n)
37
+ c.set(o, d(o)), r.set(o, m(o));
38
+ for (const o of n) o.style.transform = "";
39
+ t();
40
+ for (const o of n) {
41
+ const i = c.get(o);
42
+ i !== void 0 && w(o, i, r.get(o) ?? u, e);
43
+ }
44
+ }
45
+ function b(s) {
46
+ const t = /* @__PURE__ */ new Map();
47
+ for (const e of h(s)) t.set(p(e), d(e));
48
+ return { boxes: t };
49
+ }
50
+ function E(s, t) {
51
+ for (const e of h(t.targets)) {
52
+ const n = s.boxes.get(p(e));
53
+ if (n === void 0) continue;
54
+ const c = m(e);
55
+ e.style.transform = "", w(e, n, c, t);
56
+ }
57
+ }
58
+ export {
59
+ $ as flip,
60
+ E as play,
61
+ b as snapshot
62
+ };
63
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/flip.ts"],"sourcesContent":["import { animatable, type Animatable, type Scheduler, type SpringOptions } from '@underlying/core'\n\nexport type FlipTargets = HTMLElement | Iterable<HTMLElement>\n\nexport interface FlipOptions extends SpringOptions {\n scheduler?: Scheduler\n /** Invert and animate size changes too (scale), not only position. Default true. */\n scale?: boolean\n}\n\nexport interface FlipPlayOptions extends FlipOptions {\n /** The elements to animate from the snapshot boxes, matched by `data-flip-id`. */\n targets: FlipTargets\n}\n\ninterface Box {\n readonly left: number\n readonly top: number\n readonly width: number\n readonly height: number\n}\n\n/** A captured set of element boxes, keyed by `data-flip-id` (or the element itself). */\nexport interface FlipSnapshot {\n readonly boxes: ReadonlyMap<string | HTMLElement, Box>\n}\n\ninterface FlipState {\n readonly x: Animatable\n readonly y: Animatable\n readonly sx: Animatable\n readonly sy: Animatable\n}\n\ninterface Velocity {\n readonly x: number\n readonly y: number\n readonly sx: number\n readonly sy: number\n}\n\nconst ZERO_VELOCITY: Velocity = { x: 0, y: 0, sx: 0, sy: 0 }\n\n// FLIP owns the element's transform directly: the writes must be SYNCHRONOUS so\n// the inverted box paints before the browser shows the new layout (no flash) -\n// which is why this drives style.transform itself instead of bindStyle (whose\n// flush is deferred to the render phase). One spring set per element, reused.\nconst states = new WeakMap<HTMLElement, FlipState>()\n\nconst toElements = (targets: FlipTargets): HTMLElement[] =>\n targets instanceof HTMLElement ? [targets] : Array.from(targets)\n\nconst measure = (element: HTMLElement): Box => {\n const r = element.getBoundingClientRect()\n return { left: r.left, top: r.top, width: r.width, height: r.height }\n}\n\nconst keyOf = (element: HTMLElement): string | HTMLElement => element.dataset.flipId ?? element\n\nconst writeTransform = (element: HTMLElement, x: number, y: number, sx: number, sy: number): void => {\n if (x === 0 && y === 0 && sx === 1 && sy === 1) {\n element.style.transform = ''\n return\n }\n const scale = sx === 1 && sy === 1 ? '' : ` scale(${sx}, ${sy})`\n element.style.transform = `translate3d(${x}px, ${y}px, 0)${scale}`\n}\n\nconst ensureState = (element: HTMLElement, options: FlipOptions): FlipState => {\n let state = states.get(element)\n if (state === undefined) {\n const valueOptions = options.scheduler !== undefined ? { scheduler: options.scheduler } : {}\n const x = animatable(0, valueOptions)\n const y = animatable(0, valueOptions)\n const sx = animatable(1, valueOptions)\n const sy = animatable(1, valueOptions)\n const write = (): void => writeTransform(element, x.get(), y.get(), sx.get(), sy.get())\n x.on('change', write)\n y.on('change', write)\n sx.on('change', write)\n sy.on('change', write)\n element.style.transformOrigin = '0 0' // pin the top-left so scale and translate align\n state = { x, y, sx, sy }\n states.set(element, state)\n }\n return state\n}\n\n// Stop any in-flight spring and read its live velocity, so the next play carries\n// the momentum instead of restarting (the interruptible handoff).\nconst seizeVelocity = (element: HTMLElement): Velocity => {\n const state = states.get(element)\n if (state === undefined) return ZERO_VELOCITY\n const velocity = { x: state.x.velocity(), y: state.y.velocity(), sx: state.sx.velocity(), sy: state.sy.velocity() }\n state.x.stop()\n state.y.stop()\n state.sx.stop()\n state.sy.stop()\n return velocity\n}\n\n// Invert (First minus the element's current natural box) and spring to identity.\nconst invertAndSpring = (element: HTMLElement, first: Box, velocity: Velocity, options: FlipOptions): void => {\n const useScale = options.scale !== false\n const last = measure(element)\n const dx = first.left - last.left\n const dy = first.top - last.top\n const sx = useScale && last.width > 0 ? first.width / last.width : 1\n const sy = useScale && last.height > 0 ? first.height / last.height : 1\n\n if (dx === 0 && dy === 0 && sx === 1 && sy === 1) {\n const existing = states.get(element)\n if (existing !== undefined) {\n existing.x.set(0)\n existing.y.set(0)\n existing.sx.set(1)\n existing.sy.set(1)\n }\n element.style.transform = ''\n return\n }\n\n const state = ensureState(element, options)\n writeTransform(element, dx, dy, sx, sy) // appear at First synchronously - no flash\n state.x.set(dx, { velocity: velocity.x })\n state.y.set(dy, { velocity: velocity.y })\n state.sx.set(sx, { velocity: velocity.sx })\n state.sy.set(sy, { velocity: velocity.sy })\n state.x.spring(0, options)\n state.y.spring(0, options)\n state.sx.spring(1, options)\n state.sy.spring(1, options)\n}\n\n/**\n * Physics-first FLIP. Measures each element's box (First), runs `mutate` to\n * change the DOM, measures again (Last), applies the inverse transform - both\n * position AND size - so nothing visibly jumps, then springs every element to\n * its new place. The play is a spring, not a baked tween: call flip() again\n * mid-flight and each element retargets from its live position and velocity, so\n * the motion bends into the new layout instead of restarting. That\n * interruptibility is the whole point.\n */\nexport function flip(targets: FlipTargets, mutate: () => void, options: FlipOptions = {}): void {\n const elements = toElements(targets)\n\n // First: current visual box (with any in-flight transform), plus the live\n // velocity of a running spring.\n const first = new Map<HTMLElement, Box>()\n const velocity = new Map<HTMLElement, Velocity>()\n for (const element of elements) {\n first.set(element, measure(element))\n velocity.set(element, seizeVelocity(element))\n }\n\n // Strip transforms so the Last measurement is the natural layout box.\n for (const element of elements) element.style.transform = ''\n\n mutate()\n\n for (const element of elements) {\n const box = first.get(element)\n if (box === undefined) continue\n invertAndSpring(element, box, velocity.get(element) ?? ZERO_VELOCITY, options)\n }\n}\n\n/**\n * Capture each target's box, keyed by its `data-flip-id` (or the element\n * itself). Pair with `play()` for shared-element / route transitions, where the\n * old elements and the new ones are different DOM nodes: snapshot the old set,\n * change the DOM, then play the new set from the captured boxes.\n */\nexport function snapshot(targets: FlipTargets): FlipSnapshot {\n const boxes = new Map<string | HTMLElement, Box>()\n for (const element of toElements(targets)) boxes.set(keyOf(element), measure(element))\n return { boxes }\n}\n\n/**\n * Animate each target from its matching box in the snapshot to its current\n * place - matched by `data-flip-id`. A target with no match in the snapshot is\n * left alone. Interruptible like `flip()`.\n */\nexport function play(snap: FlipSnapshot, options: FlipPlayOptions): void {\n for (const element of toElements(options.targets)) {\n const first = snap.boxes.get(keyOf(element))\n if (first === undefined) continue\n const velocity = seizeVelocity(element)\n element.style.transform = '' // ensure Last is the natural box\n invertAndSpring(element, first, velocity, options)\n }\n}\n"],"names":["ZERO_VELOCITY","states","toElements","targets","measure","element","r","keyOf","writeTransform","x","y","sx","sy","scale","ensureState","options","state","valueOptions","animatable","write","seizeVelocity","velocity","invertAndSpring","first","useScale","last","dx","dy","existing","flip","mutate","elements","box","snapshot","boxes","play","snap"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@underlying/flip",
3
+ "version": "0.1.0-beta.0",
4
+ "description": "Physics-first FLIP for @underlying/core: animate layout and shared-element transitions from old place to new, position and size - interruptible, retargeting mid-flight from live velocity.",
5
+ "license": "MIT",
6
+ "author": "underlyi.ng <contact@underlyi.ng>",
7
+ "keywords": [
8
+ "animation",
9
+ "flip",
10
+ "layout",
11
+ "transition",
12
+ "shared-element",
13
+ "physics",
14
+ "spring"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "provenance": false
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/underlyingjs/underlying.git",
23
+ "directory": "packages/flip"
24
+ },
25
+ "homepage": "https://github.com/underlyingjs/underlying#readme",
26
+ "bugs": "https://github.com/underlyingjs/underlying/issues",
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "main": "./dist/index.cjs",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "import": "./dist/index.js",
36
+ "require": "./dist/index.cjs"
37
+ }
38
+ },
39
+ "files": [
40
+ "dist"
41
+ ],
42
+ "engines": {
43
+ "node": ">=20"
44
+ },
45
+ "dependencies": {
46
+ "@underlying/core": "0.1.0-beta.6"
47
+ },
48
+ "devDependencies": {
49
+ "esbuild": "^0.25.0",
50
+ "jsdom": "^26.1.0",
51
+ "typescript": "^5.8.3",
52
+ "vite": "^6.3.5",
53
+ "vitest": "^3.2.4"
54
+ },
55
+ "scripts": {
56
+ "test": "vitest run",
57
+ "test:watch": "vitest",
58
+ "build": "vite build && tsc -p tsconfig.build.json",
59
+ "typecheck": "tsc --noEmit",
60
+ "size": "node scripts/check-size.mjs"
61
+ }
62
+ }