@vielzeug/necromancer 26.11.5 → 26.11.7

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/README.md CHANGED
@@ -21,7 +21,10 @@ document.body.append(button);
21
21
 
22
22
  const handle = animate(
23
23
  button,
24
- [{ opacity: 0, transform: 'translateY(8px)' }, { opacity: 1, transform: 'translateY(0)' }],
24
+ [
25
+ { opacity: 0, transform: 'translateY(8px)' },
26
+ { opacity: 1, transform: 'translateY(0)' },
27
+ ],
25
28
  { duration: 180, easing: 'ease-out' },
26
29
  );
27
30
 
package/dist/_elements.js CHANGED
@@ -1,8 +1,7 @@
1
- //#region src/_elements.ts
1
+ // src/_elements.ts
2
2
  function uniqueElements(e) {
3
3
  return [...new Set(e)];
4
4
  }
5
- //#endregion
6
5
  export { uniqueElements };
7
6
 
8
7
  //# sourceMappingURL=_elements.js.map
package/dist/_handle.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/_handle.ts
1
+ // src/_handle.ts
2
2
  function attachAbortSignalListener(e, t) {
3
3
  let abort = () => t(e.reason);
4
4
  return e.addEventListener("abort", abort, { once: !0 }), e.aborted ? (e.removeEventListener("abort", abort), t(e.reason), () => void 0) : () => e.removeEventListener("abort", abort);
@@ -49,7 +49,6 @@ function createAnimationGroup(e, t) {
49
49
  }
50
50
  };
51
51
  }
52
- //#endregion
53
52
  export { attachAbortSignalListener, createAnimationGroup, createAnimationHandle };
54
53
 
55
54
  //# sourceMappingURL=_handle.js.map
package/dist/_options.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/_options.ts
1
+ // src/_options.ts
2
2
  function resolveAnimationOptions(e = {}, t = !1) {
3
3
  let { motion: n, signal: r, ...i } = e;
4
4
  return t ? {
@@ -17,7 +17,6 @@ function withStaggeredDelay(e, t) {
17
17
  delay: n + t
18
18
  };
19
19
  }
20
- //#endregion
21
20
  export { resolveAnimationOptions, withStaggeredDelay };
22
21
 
23
22
  //# sourceMappingURL=_options.js.map
@@ -1,2 +1,2 @@
1
- const e=require("./_handle.cjs"),t=require("./_options.cjs"),n=require("./errors.cjs"),r=require("./animate.cjs"),i=require("./_elements.cjs");function validateStagger(e,t){if(!Number.isFinite(e)||e<0)throw new n.NecromancerConfigError(`stagger must be a finite, non-negative number.`);if(e>0&&t!==void 0&&typeof t!=`number`)throw new n.NecromancerConfigError(`A non-zero stagger requires a numeric delay; received ${typeof t}.`)}function animateEach(n,a,o={}){let{signal:s,stagger:c=0,...l}=o;validateStagger(c,l.delay),s?.throwIfAborted();let u=i.uniqueElements(n),d=u.map((e,t)=>typeof a==`function`?a(e,t,u.length):a);s?.throwIfAborted();let f=[];try{for(let[e,n]of u.entries()){let i=d[e];f.push(r.animate(n,i,t.withStaggeredDelay(l,e*c)))}}catch(e){for(let t of f)t.dispose(e);throw e}return e.createAnimationGroup(f,s)}exports.animateEach=animateEach;
1
+ const e=require("./_elements.cjs"),t=require("./_handle.cjs"),n=require("./_options.cjs"),r=require("./animate.cjs"),i=require("./errors.cjs");function validateStagger(e,t){if(!Number.isFinite(e)||e<0)throw new i.NecromancerConfigError(`stagger must be a finite, non-negative number.`);if(e>0&&t!==void 0&&typeof t!=`number`)throw new i.NecromancerConfigError(`A non-zero stagger requires a numeric delay; received ${typeof t}.`)}function animateEach(i,a,o={}){let{signal:s,stagger:c=0,...l}=o;validateStagger(c,l.delay),s?.throwIfAborted();let u=e.uniqueElements(i),d=u.map((e,t)=>typeof a==`function`?a(e,t,u.length):a);s?.throwIfAborted();let f=[];try{for(let[e,t]of u.entries()){let i=d[e];f.push(r.animate(t,i,n.withStaggeredDelay(l,e*c)))}}catch(e){for(let t of f)t.dispose(e);throw e}return t.createAnimationGroup(f,s)}exports.animateEach=animateEach;
2
2
  //# sourceMappingURL=animate-each.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"animate-each.cjs","names":["NecromancerConfigError","uniqueElements","animate","withStaggeredDelay","createAnimationGroup","require__handle","require__options","require_errors","require_animate","require__elements","stagger","delay","animationOptions","options","signal","unique","elements","frames","keyframes","element","index","handles","currentFrames","error","handle"],"sources":["../src/animate-each.ts"],"sourcesContent":["import { uniqueElements } from './_elements';\nimport { createAnimationGroup } from './_handle';\nimport { withStaggeredDelay } from './_options';\nimport { animate } from './animate';\nimport { NecromancerConfigError } from './errors';\nimport type { AnimateEachOptions, AnimationGroup, AnimationHandle, KeyframeFactory, Keyframes } from './types';\n\nfunction validateStagger(stagger: number, delay: number | string | null | undefined): void {\n if (!Number.isFinite(stagger) || stagger < 0) {\n throw new NecromancerConfigError('stagger must be a finite, non-negative number.');\n }\n\n // A non-zero stagger adds to the delay, so it needs a numeric one. Checking here,\n // before any animation starts, keeps a pure config error free of side effects.\n if (stagger > 0 && delay !== undefined && typeof delay !== 'number') {\n throw new NecromancerConfigError(`A non-zero stagger requires a numeric delay; received ${typeof delay}.`);\n }\n}\n\n/**\n * Starts lifecycle-owned animations for unique elements in iteration order.\n *\n * Each subsequent element starts after `stagger` milliseconds. A keyframe factory\n * receives the stable unique-element index and total before any animation starts.\n */\nexport function animateEach<ElementType extends Element>(\n elements: Iterable<ElementType>,\n keyframes: Keyframes | KeyframeFactory<ElementType>,\n options: AnimateEachOptions = {},\n): AnimationGroup {\n const { signal, stagger = 0, ...animationOptions } = options;\n\n validateStagger(stagger, animationOptions.delay);\n\n signal?.throwIfAborted();\n\n const unique = uniqueElements(elements);\n const frames = unique.map((element, index) =>\n typeof keyframes === 'function' ? keyframes(element, index, unique.length) : keyframes,\n );\n\n signal?.throwIfAborted();\n\n const handles: AnimationHandle[] = [];\n\n try {\n for (const [index, element] of unique.entries()) {\n // `frames` was built above from `unique`, one entry per element, so this index is always in range.\n const currentFrames = frames[index]!;\n\n handles.push(animate(element, currentFrames, withStaggeredDelay(animationOptions, index * stagger)));\n }\n } catch (error) {\n for (const handle of handles) handle.dispose(error);\n\n throw error;\n }\n\n return createAnimationGroup(handles, signal);\n}\n"],"mappings":"A,+IAOA,SAAS,gBAAgB,EAAiB,EAAiD,CACzF,GAAI,CAAC,OAAO,SAASU,CAAO,GAAKA,EAAU,EACzC,MAAM,IAAIV,EAAAA,uBAAuB,gDAAgD,EAKnF,GAAIU,EAAU,GAAKC,IAAU,IAAA,IAAa,OAAOA,GAAU,SACzD,MAAM,IAAIX,EAAAA,uBAAuB,yDAAyD,OAAOW,EAAM,EAAE,CAE7G,CAQA,SAAgB,YACd,EACA,EACA,EAA8B,CAAC,EACf,CAChB,GAAM,CAAE,SAAQ,UAAU,EAAG,GAAGC,GAAqBC,EAErD,gBAAgBH,EAASE,EAAiB,KAAK,EAE/CE,GAAQ,eAAe,EAEvB,IAAMC,EAASd,EAAAA,eAAee,CAAQ,EAChCC,EAASF,EAAO,KAAK,EAAS,IAClC,OAAOG,GAAc,WAAaA,EAAUC,EAASC,EAAOL,EAAO,MAAM,EAAIG,CAC/E,EAEAJ,GAAQ,eAAe,EAEvB,IAAMO,EAA6B,CAAC,EAEpC,GAAI,CACF,IAAK,GAAM,CAACD,EAAOD,KAAYJ,EAAO,QAAQ,EAAG,CAE/C,IAAMO,EAAgBL,EAAOG,GAE7BC,EAAQ,KAAKnB,EAAAA,QAAQiB,EAASG,EAAenB,EAAAA,mBAAmBS,EAAkBQ,EAAQV,CAAO,CAAC,CAAC,CACrG,CACF,OAASa,EAAO,CACd,IAAK,IAAMC,KAAUH,EAASG,EAAO,QAAQD,CAAK,EAElD,MAAMA,CACR,CAEA,OAAOnB,EAAAA,qBAAqBiB,EAASP,CAAM,CAC7C,C"}
1
+ {"version":3,"file":"animate-each.cjs","names":["NecromancerConfigError","uniqueElements","animate","withStaggeredDelay","createAnimationGroup","require__elements","require__handle","require__options","require_animate","require_errors","stagger","delay","animationOptions","options","signal","unique","elements","frames","keyframes","element","index","handles","currentFrames","error","handle"],"sources":["../src/animate-each.ts"],"sourcesContent":["import { uniqueElements } from './_elements';\nimport { createAnimationGroup } from './_handle';\nimport { withStaggeredDelay } from './_options';\nimport { animate } from './animate';\nimport { NecromancerConfigError } from './errors';\nimport type { AnimateEachOptions, AnimationGroup, AnimationHandle, KeyframeFactory, Keyframes } from './types';\n\nfunction validateStagger(stagger: number, delay: number | string | null | undefined): void {\n if (!Number.isFinite(stagger) || stagger < 0) {\n throw new NecromancerConfigError('stagger must be a finite, non-negative number.');\n }\n\n // A non-zero stagger adds to the delay, so it needs a numeric one. Checking here,\n // before any animation starts, keeps a pure config error free of side effects.\n if (stagger > 0 && delay !== undefined && typeof delay !== 'number') {\n throw new NecromancerConfigError(`A non-zero stagger requires a numeric delay; received ${typeof delay}.`);\n }\n}\n\n/**\n * Starts lifecycle-owned animations for unique elements in iteration order.\n *\n * Each subsequent element starts after `stagger` milliseconds. A keyframe factory\n * receives the stable unique-element index and total before any animation starts.\n */\nexport function animateEach<ElementType extends Element>(\n elements: Iterable<ElementType>,\n keyframes: Keyframes | KeyframeFactory<ElementType>,\n options: AnimateEachOptions = {},\n): AnimationGroup {\n const { signal, stagger = 0, ...animationOptions } = options;\n\n validateStagger(stagger, animationOptions.delay);\n\n signal?.throwIfAborted();\n\n const unique = uniqueElements(elements);\n const frames = unique.map((element, index) =>\n typeof keyframes === 'function' ? keyframes(element, index, unique.length) : keyframes,\n );\n\n signal?.throwIfAborted();\n\n const handles: AnimationHandle[] = [];\n\n try {\n for (const [index, element] of unique.entries()) {\n // `frames` was built above from `unique`, one entry per element, so this index is always in range.\n const currentFrames = frames[index]!;\n\n handles.push(animate(element, currentFrames, withStaggeredDelay(animationOptions, index * stagger)));\n }\n } catch (error) {\n for (const handle of handles) handle.dispose(error);\n\n throw error;\n }\n\n return createAnimationGroup(handles, signal);\n}\n"],"mappings":"A,+IAOA,SAAS,gBAAgB,EAAiB,EAAiD,CACzF,GAAI,CAAC,OAAO,SAASU,CAAO,GAAKA,EAAU,EACzC,MAAM,IAAIV,EAAAA,uBAAuB,gDAAgD,EAKnF,GAAIU,EAAU,GAAKC,IAAU,IAAA,IAAa,OAAOA,GAAU,SACzD,MAAM,IAAIX,EAAAA,uBAAuB,yDAAyD,OAAOW,EAAM,EAAE,CAE7G,CAQA,SAAgB,YACd,EACA,EACA,EAA8B,CAAC,EACf,CAChB,GAAM,CAAE,SAAQ,UAAU,EAAG,GAAGC,GAAqBC,EAErD,gBAAgBH,EAASE,EAAiB,KAAK,EAE/CE,GAAQ,eAAe,EAEvB,IAAMC,EAASd,EAAAA,eAAee,CAAQ,EAChCC,EAASF,EAAO,KAAK,EAAS,IAClC,OAAOG,GAAc,WAAaA,EAAUC,EAASC,EAAOL,EAAO,MAAM,EAAIG,CAC/E,EAEAJ,GAAQ,eAAe,EAEvB,IAAMO,EAA6B,CAAC,EAEpC,GAAI,CACF,IAAK,GAAM,CAACD,EAAOD,KAAYJ,EAAO,QAAQ,EAAG,CAE/C,IAAMO,EAAgBL,EAAOG,GAE7BC,EAAQ,KAAKnB,EAAAA,QAAQiB,EAASG,EAAenB,EAAAA,mBAAmBS,EAAkBQ,EAAQV,CAAO,CAAC,CAAC,CACrG,CACF,OAASa,EAAO,CACd,IAAK,IAAMC,KAAUH,EAASG,EAAO,QAAQD,CAAK,EAElD,MAAMA,CACR,CAEA,OAAOnB,EAAAA,qBAAqBiB,EAASP,CAAM,CAC7C,C"}
@@ -1,31 +1,30 @@
1
- import { createAnimationGroup as e } from "./_handle.js";
2
- import { withStaggeredDelay as t } from "./_options.js";
3
- import { NecromancerConfigError as n } from "./errors.js";
1
+ import { uniqueElements as e } from "./_elements.js";
2
+ import { createAnimationGroup as t } from "./_handle.js";
3
+ import { withStaggeredDelay as n } from "./_options.js";
4
4
  import { animate as r } from "./animate.js";
5
- import { uniqueElements as i } from "./_elements.js";
6
- //#region src/animate-each.ts
5
+ import { NecromancerConfigError as i } from "./errors.js";
6
+ // src/animate-each.ts
7
7
  function validateStagger(e, t) {
8
- if (!Number.isFinite(e) || e < 0) throw new n("stagger must be a finite, non-negative number.");
9
- if (e > 0 && t !== void 0 && typeof t != "number") throw new n(`A non-zero stagger requires a numeric delay; received ${typeof t}.`);
8
+ if (!Number.isFinite(e) || e < 0) throw new i("stagger must be a finite, non-negative number.");
9
+ if (e > 0 && t !== void 0 && typeof t != "number") throw new i(`A non-zero stagger requires a numeric delay; received ${typeof t}.`);
10
10
  }
11
- function animateEach(n, a, o = {}) {
11
+ function animateEach(i, a, o = {}) {
12
12
  let { signal: s, stagger: c = 0, ...l } = o;
13
13
  validateStagger(c, l.delay), s?.throwIfAborted();
14
- let u = i(n), d = u.map((e, t) => typeof a == "function" ? a(e, t, u.length) : a);
14
+ let u = e(i), d = u.map((e, t) => typeof a == "function" ? a(e, t, u.length) : a);
15
15
  s?.throwIfAborted();
16
16
  let f = [];
17
17
  try {
18
- for (let [e, n] of u.entries()) {
18
+ for (let [e, t] of u.entries()) {
19
19
  let i = d[e];
20
- f.push(r(n, i, t(l, e * c)));
20
+ f.push(r(t, i, n(l, e * c)));
21
21
  }
22
22
  } catch (e) {
23
23
  for (let t of f) t.dispose(e);
24
24
  throw e;
25
25
  }
26
- return e(f, s);
26
+ return t(f, s);
27
27
  }
28
- //#endregion
29
28
  export { animateEach };
30
29
 
31
30
  //# sourceMappingURL=animate-each.js.map
package/dist/animate.js CHANGED
@@ -2,13 +2,12 @@ import { createAnimationHandle as e } from "./_handle.js";
2
2
  import { resolveAnimationOptions as t } from "./_options.js";
3
3
  import { NecromancerUnsupportedError as n } from "./errors.js";
4
4
  import { shouldReduceMotion as r } from "./motion.js";
5
- //#region src/animate.ts
5
+ // src/animate.ts
6
6
  function animate(i, a, o = {}) {
7
7
  if (o.signal?.throwIfAborted(), typeof i.animate != "function") throw new n("The Web Animations API is not available in this environment.");
8
8
  let s = i.animate(a, t(o, r(o.motion ?? "system")));
9
9
  return e(s, o.signal);
10
10
  }
11
- //#endregion
12
11
  export { animate };
13
12
 
14
13
  //# sourceMappingURL=animate.js.map
package/dist/errors.js CHANGED
@@ -1,10 +1,9 @@
1
- //#region src/errors.ts
1
+ // src/errors.ts
2
2
  var NecromancerError = class extends Error {
3
3
  constructor(e, t) {
4
4
  super(e, t), this.name = new.target.name, Object.setPrototypeOf(this, new.target.prototype);
5
5
  }
6
6
  }, NecromancerConfigError = class extends NecromancerError {}, NecromancerUnsupportedError = class extends NecromancerError {};
7
- //#endregion
8
7
  export { NecromancerConfigError, NecromancerError, NecromancerUnsupportedError };
9
8
 
10
9
  //# sourceMappingURL=errors.js.map
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./errors.cjs"),t=require("./motion.cjs"),n=require("./animate.cjs"),r=require("./animate-each.cjs"),i=require("./layout.cjs"),a=require("./spring.cjs");exports.NecromancerConfigError=e.NecromancerConfigError,exports.NecromancerError=e.NecromancerError,exports.NecromancerUnsupportedError=e.NecromancerUnsupportedError,exports.animate=n.animate,exports.animateEach=r.animateEach,exports.captureLayout=i.captureLayout,exports.shouldReduceMotion=t.shouldReduceMotion,exports.spring=a.spring;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./animate.cjs"),t=require("./animate-each.cjs"),n=require("./errors.cjs"),r=require("./layout.cjs"),i=require("./motion.cjs"),a=require("./spring.cjs");exports.NecromancerConfigError=n.NecromancerConfigError,exports.NecromancerError=n.NecromancerError,exports.NecromancerUnsupportedError=n.NecromancerUnsupportedError,exports.animate=e.animate,exports.animateEach=t.animateEach,exports.captureLayout=r.captureLayout,exports.shouldReduceMotion=i.shouldReduceMotion,exports.spring=a.spring;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { NecromancerConfigError as e, NecromancerError as t, NecromancerUnsupportedError as n } from "./errors.js";
2
- import { shouldReduceMotion as r } from "./motion.js";
3
- import { animate as i } from "./animate.js";
4
- import { animateEach as a } from "./animate-each.js";
5
- import { captureLayout as o } from "./layout.js";
1
+ import { animate as e } from "./animate.js";
2
+ import { animateEach as t } from "./animate-each.js";
3
+ import { NecromancerConfigError as n, NecromancerError as r, NecromancerUnsupportedError as i } from "./errors.js";
4
+ import { captureLayout as a } from "./layout.js";
5
+ import { shouldReduceMotion as o } from "./motion.js";
6
6
  import { spring as s } from "./spring.js";
7
- export { e as NecromancerConfigError, t as NecromancerError, n as NecromancerUnsupportedError, i as animate, a as animateEach, o as captureLayout, r as shouldReduceMotion, s as spring };
7
+ export { n as NecromancerConfigError, r as NecromancerError, i as NecromancerUnsupportedError, e as animate, t as animateEach, a as captureLayout, o as shouldReduceMotion, s as spring };
package/dist/layout.cjs CHANGED
@@ -1,2 +1,2 @@
1
- const e=require("./errors.cjs"),t=require("./_elements.cjs"),n=require("./animate-each.cjs");function round(e,t){let n=10**t;return Math.round(e*n)/n}function measureSize(e){if(`offsetWidth`in e&&`offsetHeight`in e)return{height:e.offsetHeight,width:e.offsetWidth};let t=e.getBoundingClientRect();return{height:t.height,width:t.width}}function measureLayout(e){let t=e.getBoundingClientRect(),n=measureSize(e);return{height:n.height,width:n.width,x:t.x,y:t.y}}function computeScale(e,t){return t===0?1:e/t}function readKey(t,n,r){let i=n(t);if(typeof i!=`string`||i.length===0)throw new e.NecromancerConfigError(`getKey must return a non-empty string for every ${r} element.`);return i}function createKeyMap(t,n,r){let i=new Map;for(let a of t){let t=readKey(a.element,n,r);if(i.has(t))throw new e.NecromancerConfigError(`getKey returned duplicate keys for the ${r} elements.`);i.set(t,a)}return i}function changedElement(e,t){if(!e.isConnected)return;let n=measureLayout(e),r=t.x-n.x,i=t.y-n.y,a=computeScale(t.width,n.width),o=computeScale(t.height,n.height);return r===0&&i===0&&a===1&&o===1?void 0:{element:e,scaleX:a,scaleY:o,x:r,y:i}}function matchByIdentity(e,t){let n=new Map(e.map(e=>[e.element,e]));return t.flatMap(e=>{let t=n.get(e);return t?[changedElement(e,t.rect)].filter(e=>!!e):[]})}function matchByKey(t,n,r){let i=new Set;return n.flatMap(n=>{let a=readKey(n,r,`committed`),o=t.get(a);if(i.has(a))throw new e.NecromancerConfigError(`getKey returned duplicate keys for the committed elements.`);return i.add(a),o?[changedElement(n,o.rect)].filter(e=>!!e):[]})}function captureLayout(r,i={}){let a=t.uniqueElements(r).map(e=>({element:e,rect:measureLayout(e)})),o=i.getKey,s=o?createKeyMap(a,o,`captured`):void 0,c=!1;return{animate(r={}){if(c)throw new e.NecromancerConfigError(`This layout transition has already been animated.`);let{elements:i,...l}=r,u=t.uniqueElements(i??a.map(({element:e})=>e)),d=o&&s?matchByKey(s,u,o):matchByIdentity(a,u);return c=!0,n.animateEach(d.map(({element:e})=>e),(e,t)=>{let n=d[t],r=`${round(n.x,2)}px ${round(n.y,2)}px`;return[{composite:`add`,scale:`${round(n.scaleX,4)} ${round(n.scaleY,4)}`,translate:r},{composite:`add`,scale:`1 1`,translate:`0px 0px`}]},l)}}}exports.captureLayout=captureLayout;
1
+ const e=require("./_elements.cjs"),t=require("./animate-each.cjs"),n=require("./errors.cjs");function round(e,t){let n=10**t;return Math.round(e*n)/n}function measureSize(e){if(`offsetWidth`in e&&`offsetHeight`in e)return{height:e.offsetHeight,width:e.offsetWidth};let t=e.getBoundingClientRect();return{height:t.height,width:t.width}}function measureLayout(e){let t=e.getBoundingClientRect(),n=measureSize(e);return{height:n.height,width:n.width,x:t.x,y:t.y}}function computeScale(e,t){return t===0?1:e/t}function readKey(e,t,r){let i=t(e);if(typeof i!=`string`||i.length===0)throw new n.NecromancerConfigError(`getKey must return a non-empty string for every ${r} element.`);return i}function createKeyMap(e,t,r){let i=new Map;for(let a of e){let e=readKey(a.element,t,r);if(i.has(e))throw new n.NecromancerConfigError(`getKey returned duplicate keys for the ${r} elements.`);i.set(e,a)}return i}function changedElement(e,t){if(!e.isConnected)return;let n=measureLayout(e),r=t.x-n.x,i=t.y-n.y,a=computeScale(t.width,n.width),o=computeScale(t.height,n.height);return r===0&&i===0&&a===1&&o===1?void 0:{element:e,scaleX:a,scaleY:o,x:r,y:i}}function matchByIdentity(e,t){let n=new Map(e.map(e=>[e.element,e]));return t.flatMap(e=>{let t=n.get(e);return t?[changedElement(e,t.rect)].filter(e=>!!e):[]})}function matchByKey(e,t,r){let i=new Set;return t.flatMap(t=>{let a=readKey(t,r,`committed`),o=e.get(a);if(i.has(a))throw new n.NecromancerConfigError(`getKey returned duplicate keys for the committed elements.`);return i.add(a),o?[changedElement(t,o.rect)].filter(e=>!!e):[]})}function captureLayout(r,i={}){let a=e.uniqueElements(r).map(e=>({element:e,rect:measureLayout(e)})),o=i.getKey,s=o?createKeyMap(a,o,`captured`):void 0,c=!1;return{animate(r={}){if(c)throw new n.NecromancerConfigError(`This layout transition has already been animated.`);let{elements:i,...l}=r,u=e.uniqueElements(i??a.map(({element:e})=>e)),d=o&&s?matchByKey(s,u,o):matchByIdentity(a,u);return c=!0,t.animateEach(d.map(({element:e})=>e),(e,t)=>{let n=d[t],r=`${round(n.x,2)}px ${round(n.y,2)}px`;return[{composite:`add`,scale:`${round(n.scaleX,4)} ${round(n.scaleY,4)}`,translate:r},{composite:`add`,scale:`1 1`,translate:`0px 0px`}]},l)}}}exports.captureLayout=captureLayout;
2
2
  //# sourceMappingURL=layout.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"layout.cjs","names":["NecromancerConfigError","uniqueElements","animateEach","require_errors","require__elements","require_animate_each","factor","precision","value","element","rect","size","next","captured","key","getKey","phase","keys","entry","entries","x","y","scaleX","scaleY","elements","change","committed","options","consumed","committedElements","animationOptions","changed","index","translate"],"sources":["../src/layout.ts"],"sourcesContent":["import { uniqueElements } from './_elements';\nimport { animateEach } from './animate-each';\nimport { NecromancerConfigError } from './errors';\nimport type { LayoutAnimationOptions, LayoutCaptureOptions, LayoutTransition } from './types';\n\ntype LayoutRect = Readonly<Pick<DOMRectReadOnly, 'height' | 'width' | 'x' | 'y'>>;\ntype LayoutEntry<ElementType extends Element> = Readonly<{ element: ElementType; rect: LayoutRect }>;\ntype LayoutChange<ElementType extends Element> = Readonly<{\n element: ElementType;\n scaleX: number;\n scaleY: number;\n x: number;\n y: number;\n}>;\n\n// Rounds before templating into a keyframe string purely for DevTools readability: a `getBoundingClientRect()`\n// subtraction routinely produces values like `12.340000000000002`, which is noise once rendered as CSS.\n// Pixels only need whole-number precision; scale factors (close to 1) need finer precision to stay visually exact.\nfunction round(value: number, precision: number): number {\n const factor = 10 ** precision;\n\n return Math.round(value * factor) / factor;\n}\n\n// `getBoundingClientRect()`'s width/height is the rotated, axis-aligned bounding box: using it\n// for size would distort the scale ratio under any rotation. `offsetWidth`/`offsetHeight` report\n// the box's own untransformed layout size instead. Elements without an offset box (e.g. SVG)\n// fall back to the bounding rect, accepting that same distortion as the only option available.\nfunction measureSize(element: Element): Readonly<{ height: number; width: number }> {\n if ('offsetWidth' in element && 'offsetHeight' in element) {\n return { height: (element as HTMLElement).offsetHeight, width: (element as HTMLElement).offsetWidth };\n }\n\n const rect = element.getBoundingClientRect();\n\n return { height: rect.height, width: rect.width };\n}\n\nfunction measureLayout(element: Element): LayoutRect {\n const rect = element.getBoundingClientRect();\n const size = measureSize(element);\n\n return { height: size.height, width: size.width, x: rect.x, y: rect.y };\n}\n\n// `next` is 0 exactly when the element has collapsed (e.g. `display: none` mid-transition);\n// there's no meaningful ratio to compute then, so treat it as unscaled rather than producing Infinity/NaN.\nfunction computeScale(captured: number, next: number): number {\n return next === 0 ? 1 : captured / next;\n}\n\nfunction readKey<ElementType extends Element>(\n element: ElementType,\n getKey: (element: ElementType) => string,\n phase: string,\n): string {\n const key = getKey(element);\n\n if (typeof key !== 'string' || key.length === 0) {\n throw new NecromancerConfigError(`getKey must return a non-empty string for every ${phase} element.`);\n }\n\n return key;\n}\n\nfunction createKeyMap<ElementType extends Element>(\n entries: readonly LayoutEntry<ElementType>[],\n getKey: (element: ElementType) => string,\n phase: string,\n) {\n const keys = new Map<string, LayoutEntry<ElementType>>();\n\n for (const entry of entries) {\n const key = readKey(entry.element, getKey, phase);\n\n if (keys.has(key)) {\n throw new NecromancerConfigError(`getKey returned duplicate keys for the ${phase} elements.`);\n }\n\n keys.set(key, entry);\n }\n\n return keys;\n}\n\nfunction changedElement<ElementType extends Element>(\n element: ElementType,\n rect: LayoutRect,\n): LayoutChange<ElementType> | undefined {\n if (!element.isConnected) return undefined;\n\n const next = measureLayout(element);\n const x = rect.x - next.x;\n const y = rect.y - next.y;\n const scaleX = computeScale(rect.width, next.width);\n const scaleY = computeScale(rect.height, next.height);\n\n return x === 0 && y === 0 && scaleX === 1 && scaleY === 1 ? undefined : { element, scaleX, scaleY, x, y };\n}\n\nfunction matchByIdentity<ElementType extends Element>(\n entries: readonly LayoutEntry<ElementType>[],\n elements: readonly ElementType[],\n): LayoutChange<ElementType>[] {\n const captured = new Map(entries.map((entry) => [entry.element, entry]));\n\n return elements.flatMap((element) => {\n const entry = captured.get(element);\n\n return entry\n ? [changedElement(element, entry.rect)].filter((change): change is LayoutChange<ElementType> => Boolean(change))\n : [];\n });\n}\n\nfunction matchByKey<ElementType extends Element>(\n captured: ReadonlyMap<string, LayoutEntry<ElementType>>,\n elements: readonly ElementType[],\n getKey: (element: ElementType) => string,\n): LayoutChange<ElementType>[] {\n const committed = new Set<string>();\n\n return elements.flatMap((element) => {\n const key = readKey(element, getKey, 'committed');\n const entry = captured.get(key);\n\n if (committed.has(key)) {\n throw new NecromancerConfigError('getKey returned duplicate keys for the committed elements.');\n }\n\n committed.add(key);\n\n return entry\n ? [changedElement(element, entry.rect)].filter((change): change is LayoutChange<ElementType> => Boolean(change))\n : [];\n });\n}\n\n/**\n * Captures the current position and size of unique elements for a later FLIP animation.\n *\n * Each element's `x`/`y` position and `width`/`height` are captured. Rotation and other\n * transforms are not captured or compensated. Capture before mutating the DOM, then call\n * the returned transition's `animate()` method after the browser has applied the resulting\n * layout: position changes animate via translate, and size changes via scale, both\n * additively composed on top of any authored transform.\n */\nexport function captureLayout<ElementType extends Element>(\n elements: Iterable<ElementType>,\n options: LayoutCaptureOptions<ElementType> = {},\n): LayoutTransition<ElementType> {\n const entries: LayoutEntry<ElementType>[] = uniqueElements(elements).map((element) => ({\n element,\n rect: measureLayout(element),\n }));\n const getKey = options.getKey;\n const captured = getKey ? createKeyMap(entries, getKey, 'captured') : undefined;\n\n let consumed = false;\n\n return {\n animate(options: LayoutAnimationOptions<ElementType> = {}) {\n if (consumed) throw new NecromancerConfigError('This layout transition has already been animated.');\n\n const { elements: committedElements, ...animationOptions } = options;\n const elements = uniqueElements(committedElements ?? entries.map(({ element }) => element));\n const changed = getKey && captured ? matchByKey(captured, elements, getKey) : matchByIdentity(entries, elements);\n\n consumed = true;\n\n return animateEach(\n changed.map(({ element }) => element),\n (_, index) => {\n // `changed` was mapped directly into the elements array above, so this index is always in range.\n const change = changed[index]!;\n const translate = `${round(change.x, 2)}px ${round(change.y, 2)}px`;\n const scale = `${round(change.scaleX, 4)} ${round(change.scaleY, 4)}`;\n\n return [\n { composite: 'add', scale, translate },\n { composite: 'add', scale: '1 1', translate: '0px 0px' },\n ];\n },\n animationOptions,\n );\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"layout.cjs","names":["NecromancerConfigError","uniqueElements","animateEach","require__elements","require_animate_each","require_errors","factor","precision","value","element","rect","size","next","captured","key","getKey","phase","keys","entry","entries","x","y","scaleX","scaleY","elements","change","committed","options","consumed","committedElements","animationOptions","changed","index","translate"],"sources":["../src/layout.ts"],"sourcesContent":["import { uniqueElements } from './_elements';\nimport { animateEach } from './animate-each';\nimport { NecromancerConfigError } from './errors';\nimport type { LayoutAnimationOptions, LayoutCaptureOptions, LayoutTransition } from './types';\n\ntype LayoutRect = Readonly<Pick<DOMRectReadOnly, 'height' | 'width' | 'x' | 'y'>>;\ntype LayoutEntry<ElementType extends Element> = Readonly<{ element: ElementType; rect: LayoutRect }>;\ntype LayoutChange<ElementType extends Element> = Readonly<{\n element: ElementType;\n scaleX: number;\n scaleY: number;\n x: number;\n y: number;\n}>;\n\n// Rounds before templating into a keyframe string purely for DevTools readability: a `getBoundingClientRect()`\n// subtraction routinely produces values like `12.340000000000002`, which is noise once rendered as CSS.\n// Pixels only need whole-number precision; scale factors (close to 1) need finer precision to stay visually exact.\nfunction round(value: number, precision: number): number {\n const factor = 10 ** precision;\n\n return Math.round(value * factor) / factor;\n}\n\n// `getBoundingClientRect()`'s width/height is the rotated, axis-aligned bounding box: using it\n// for size would distort the scale ratio under any rotation. `offsetWidth`/`offsetHeight` report\n// the box's own untransformed layout size instead. Elements without an offset box (e.g. SVG)\n// fall back to the bounding rect, accepting that same distortion as the only option available.\nfunction measureSize(element: Element): Readonly<{ height: number; width: number }> {\n if ('offsetWidth' in element && 'offsetHeight' in element) {\n return { height: (element as HTMLElement).offsetHeight, width: (element as HTMLElement).offsetWidth };\n }\n\n const rect = element.getBoundingClientRect();\n\n return { height: rect.height, width: rect.width };\n}\n\nfunction measureLayout(element: Element): LayoutRect {\n const rect = element.getBoundingClientRect();\n const size = measureSize(element);\n\n return { height: size.height, width: size.width, x: rect.x, y: rect.y };\n}\n\n// `next` is 0 exactly when the element has collapsed (e.g. `display: none` mid-transition);\n// there's no meaningful ratio to compute then, so treat it as unscaled rather than producing Infinity/NaN.\nfunction computeScale(captured: number, next: number): number {\n return next === 0 ? 1 : captured / next;\n}\n\nfunction readKey<ElementType extends Element>(\n element: ElementType,\n getKey: (element: ElementType) => string,\n phase: string,\n): string {\n const key = getKey(element);\n\n if (typeof key !== 'string' || key.length === 0) {\n throw new NecromancerConfigError(`getKey must return a non-empty string for every ${phase} element.`);\n }\n\n return key;\n}\n\nfunction createKeyMap<ElementType extends Element>(\n entries: readonly LayoutEntry<ElementType>[],\n getKey: (element: ElementType) => string,\n phase: string,\n) {\n const keys = new Map<string, LayoutEntry<ElementType>>();\n\n for (const entry of entries) {\n const key = readKey(entry.element, getKey, phase);\n\n if (keys.has(key)) {\n throw new NecromancerConfigError(`getKey returned duplicate keys for the ${phase} elements.`);\n }\n\n keys.set(key, entry);\n }\n\n return keys;\n}\n\nfunction changedElement<ElementType extends Element>(\n element: ElementType,\n rect: LayoutRect,\n): LayoutChange<ElementType> | undefined {\n if (!element.isConnected) return undefined;\n\n const next = measureLayout(element);\n const x = rect.x - next.x;\n const y = rect.y - next.y;\n const scaleX = computeScale(rect.width, next.width);\n const scaleY = computeScale(rect.height, next.height);\n\n return x === 0 && y === 0 && scaleX === 1 && scaleY === 1 ? undefined : { element, scaleX, scaleY, x, y };\n}\n\nfunction matchByIdentity<ElementType extends Element>(\n entries: readonly LayoutEntry<ElementType>[],\n elements: readonly ElementType[],\n): LayoutChange<ElementType>[] {\n const captured = new Map(entries.map((entry) => [entry.element, entry]));\n\n return elements.flatMap((element) => {\n const entry = captured.get(element);\n\n return entry\n ? [changedElement(element, entry.rect)].filter((change): change is LayoutChange<ElementType> => Boolean(change))\n : [];\n });\n}\n\nfunction matchByKey<ElementType extends Element>(\n captured: ReadonlyMap<string, LayoutEntry<ElementType>>,\n elements: readonly ElementType[],\n getKey: (element: ElementType) => string,\n): LayoutChange<ElementType>[] {\n const committed = new Set<string>();\n\n return elements.flatMap((element) => {\n const key = readKey(element, getKey, 'committed');\n const entry = captured.get(key);\n\n if (committed.has(key)) {\n throw new NecromancerConfigError('getKey returned duplicate keys for the committed elements.');\n }\n\n committed.add(key);\n\n return entry\n ? [changedElement(element, entry.rect)].filter((change): change is LayoutChange<ElementType> => Boolean(change))\n : [];\n });\n}\n\n/**\n * Captures the current position and size of unique elements for a later FLIP animation.\n *\n * Each element's `x`/`y` position and `width`/`height` are captured. Rotation and other\n * transforms are not captured or compensated. Capture before mutating the DOM, then call\n * the returned transition's `animate()` method after the browser has applied the resulting\n * layout: position changes animate via translate, and size changes via scale, both\n * additively composed on top of any authored transform.\n */\nexport function captureLayout<ElementType extends Element>(\n elements: Iterable<ElementType>,\n options: LayoutCaptureOptions<ElementType> = {},\n): LayoutTransition<ElementType> {\n const entries: LayoutEntry<ElementType>[] = uniqueElements(elements).map((element) => ({\n element,\n rect: measureLayout(element),\n }));\n const getKey = options.getKey;\n const captured = getKey ? createKeyMap(entries, getKey, 'captured') : undefined;\n\n let consumed = false;\n\n return {\n animate(options: LayoutAnimationOptions<ElementType> = {}) {\n if (consumed) throw new NecromancerConfigError('This layout transition has already been animated.');\n\n const { elements: committedElements, ...animationOptions } = options;\n const elements = uniqueElements(committedElements ?? entries.map(({ element }) => element));\n const changed = getKey && captured ? matchByKey(captured, elements, getKey) : matchByIdentity(entries, elements);\n\n consumed = true;\n\n return animateEach(\n changed.map(({ element }) => element),\n (_, index) => {\n // `changed` was mapped directly into the elements array above, so this index is always in range.\n const change = changed[index]!;\n const translate = `${round(change.x, 2)}px ${round(change.y, 2)}px`;\n const scale = `${round(change.scaleX, 4)} ${round(change.scaleY, 4)}`;\n\n return [\n { composite: 'add', scale, translate },\n { composite: 'add', scale: '1 1', translate: '0px 0px' },\n ];\n },\n animationOptions,\n );\n },\n };\n}\n"],"mappings":"A,6FAkBA,SAAS,MAAM,EAAe,EAA2B,CACvD,IAAMM,EAAS,IAAMC,EAErB,OAAO,KAAK,MAAMC,EAAQF,CAAM,EAAIA,CACtC,CAMA,SAAS,YAAY,EAA+D,CAClF,GAAI,gBAAiBG,GAAW,iBAAkBA,EAChD,MAAO,CAAE,OAASA,EAAwB,aAAc,MAAQA,EAAwB,WAAY,EAGtG,IAAMC,EAAOD,EAAQ,sBAAsB,EAE3C,MAAO,CAAE,OAAQC,EAAK,OAAQ,MAAOA,EAAK,KAAM,CAClD,CAEA,SAAS,cAAc,EAA8B,CACnD,IAAMA,EAAOD,EAAQ,sBAAsB,EACrCE,EAAO,YAAYF,CAAO,EAEhC,MAAO,CAAE,OAAQE,EAAK,OAAQ,MAAOA,EAAK,MAAO,EAAGD,EAAK,EAAG,EAAGA,EAAK,CAAE,CACxE,CAIA,SAAS,aAAa,EAAkB,EAAsB,CAC5D,OAAOE,IAAS,EAAI,EAAIC,EAAWD,CACrC,CAEA,SAAS,QACP,EACA,EACA,EACQ,CACR,IAAME,EAAMC,EAAON,CAAO,EAE1B,GAAI,OAAOK,GAAQ,UAAYA,EAAI,SAAW,EAC5C,MAAM,IAAId,EAAAA,uBAAuB,mDAAmDgB,EAAM,UAAU,EAGtG,OAAOF,CACT,CAEA,SAAS,aACP,EACA,EACA,EACA,CACA,IAAMG,EAAO,IAAI,IAEjB,IAAK,IAAMC,KAASC,EAAS,CAC3B,IAAML,EAAM,QAAQI,EAAM,QAASH,EAAQC,CAAK,EAEhD,GAAIC,EAAK,IAAIH,CAAG,EACd,MAAM,IAAId,EAAAA,uBAAuB,0CAA0CgB,EAAM,WAAW,EAG9FC,EAAK,IAAIH,EAAKI,CAAK,CACrB,CAEA,OAAOD,CACT,CAEA,SAAS,eACP,EACA,EACuC,CACvC,GAAI,CAACR,EAAQ,YAAa,OAE1B,IAAMG,EAAO,cAAcH,CAAO,EAC5BW,EAAIV,EAAK,EAAIE,EAAK,EAClBS,EAAIX,EAAK,EAAIE,EAAK,EAClBU,EAAS,aAAaZ,EAAK,MAAOE,EAAK,KAAK,EAC5CW,EAAS,aAAab,EAAK,OAAQE,EAAK,MAAM,EAEpD,OAAOQ,IAAM,GAAKC,IAAM,GAAKC,IAAW,GAAKC,IAAW,EAAI,IAAA,GAAY,CAAE,UAAS,SAAQ,SAAQ,IAAG,GAAE,CAC1G,CAEA,SAAS,gBACP,EACA,EAC6B,CAC7B,IAAMV,EAAW,IAAI,IAAIM,EAAQ,IAAK,GAAU,CAACD,EAAM,QAASA,CAAK,CAAC,CAAC,EAEvE,OAAOM,EAAS,QAAS,GAAY,CACnC,IAAMN,EAAQL,EAAS,IAAIJ,CAAO,EAElC,OAAOS,EACH,CAAC,eAAeT,EAASS,EAAM,IAAI,CAAC,CAAC,CAAC,OAAQ,GAAgD,EAAQO,CAAO,EAC7G,CAAC,CACP,CAAC,CACH,CAEA,SAAS,WACP,EACA,EACA,EAC6B,CAC7B,IAAMC,EAAY,IAAI,IAEtB,OAAOF,EAAS,QAAS,GAAY,CACnC,IAAMV,EAAM,QAAQL,EAASM,EAAQ,WAAW,EAC1CG,EAAQL,EAAS,IAAIC,CAAG,EAE9B,GAAIY,EAAU,IAAIZ,CAAG,EACnB,MAAM,IAAId,EAAAA,uBAAuB,4DAA4D,EAK/F,OAFA0B,EAAU,IAAIZ,CAAG,EAEVI,EACH,CAAC,eAAeT,EAASS,EAAM,IAAI,CAAC,CAAC,CAAC,OAAQ,GAAgD,EAAQO,CAAO,EAC7G,CAAC,CACP,CAAC,CACH,CAWA,SAAgB,cACd,EACA,EAA6C,CAAC,EACf,CAC/B,IAAMN,EAAsClB,EAAAA,eAAeuB,CAAQ,CAAC,CAAC,IAAK,IAAa,CACrF,UACA,KAAM,cAAcf,CAAO,CAC7B,EAAE,EACIM,EAASY,EAAQ,OACjBd,EAAWE,EAAS,aAAaI,EAASJ,EAAQ,UAAU,EAAI,IAAA,GAElEa,EAAW,GAEf,MAAO,CACL,QAAQ,EAA+C,CAAC,EAAG,CACzD,GAAIA,EAAU,MAAM,IAAI5B,EAAAA,uBAAuB,mDAAmD,EAElG,GAAM,CAAE,SAAU6B,EAAmB,GAAGC,GAAqBH,EACvDH,EAAWvB,EAAAA,eAAe4B,GAAqBV,EAAQ,KAAK,CAAE,aAAcV,CAAO,CAAC,EACpFsB,EAAUhB,GAAUF,EAAW,WAAWA,EAAUW,EAAUT,CAAM,EAAI,gBAAgBI,EAASK,CAAQ,EAI/G,MAFA,GAAW,GAEJtB,EAAAA,YACL6B,EAAQ,KAAK,CAAE,aAActB,CAAO,GACnC,EAAG,IAAU,CAEZ,IAAMgB,EAASM,EAAQC,GACjBC,EAAY,GAAG,MAAMR,EAAO,EAAG,CAAC,EAAE,KAAK,MAAMA,EAAO,EAAG,CAAC,EAAE,IAGhE,MAAO,CACL,CAAE,UAAW,MAAO,MAAA,GAHL,MAAMA,EAAO,OAAQ,CAAC,EAAE,GAAG,MAAMA,EAAO,OAAQ,CAAC,IAGrC,WAAU,EACrC,CAAE,UAAW,MAAO,MAAO,MAAO,UAAW,SAAU,CACzD,CACF,EACAK,CACF,CACF,CACF,CACF,C"}
package/dist/layout.js CHANGED
@@ -1,7 +1,7 @@
1
- import { NecromancerConfigError as e } from "./errors.js";
2
- import { uniqueElements as t } from "./_elements.js";
3
- import { animateEach as n } from "./animate-each.js";
4
- //#region src/layout.ts
1
+ import { uniqueElements as e } from "./_elements.js";
2
+ import { animateEach as t } from "./animate-each.js";
3
+ import { NecromancerConfigError as n } from "./errors.js";
4
+ // src/layout.ts
5
5
  function round(e, t) {
6
6
  let n = 10 ** t;
7
7
  return Math.round(e * n) / n;
@@ -29,17 +29,17 @@ function measureLayout(e) {
29
29
  function computeScale(e, t) {
30
30
  return t === 0 ? 1 : e / t;
31
31
  }
32
- function readKey(t, n, r) {
33
- let i = n(t);
34
- if (typeof i != "string" || i.length === 0) throw new e(`getKey must return a non-empty string for every ${r} element.`);
32
+ function readKey(e, t, r) {
33
+ let i = t(e);
34
+ if (typeof i != "string" || i.length === 0) throw new n(`getKey must return a non-empty string for every ${r} element.`);
35
35
  return i;
36
36
  }
37
- function createKeyMap(t, n, r) {
37
+ function createKeyMap(e, t, r) {
38
38
  let i = /* @__PURE__ */ new Map();
39
- for (let a of t) {
40
- let t = readKey(a.element, n, r);
41
- if (i.has(t)) throw new e(`getKey returned duplicate keys for the ${r} elements.`);
42
- i.set(t, a);
39
+ for (let a of e) {
40
+ let e = readKey(a.element, t, r);
41
+ if (i.has(e)) throw new n(`getKey returned duplicate keys for the ${r} elements.`);
42
+ i.set(e, a);
43
43
  }
44
44
  return i;
45
45
  }
@@ -61,23 +61,23 @@ function matchByIdentity(e, t) {
61
61
  return t ? [changedElement(e, t.rect)].filter((e) => !!e) : [];
62
62
  });
63
63
  }
64
- function matchByKey(t, n, r) {
64
+ function matchByKey(e, t, r) {
65
65
  let i = /* @__PURE__ */ new Set();
66
- return n.flatMap((n) => {
67
- let a = readKey(n, r, "committed"), o = t.get(a);
68
- if (i.has(a)) throw new e("getKey returned duplicate keys for the committed elements.");
69
- return i.add(a), o ? [changedElement(n, o.rect)].filter((e) => !!e) : [];
66
+ return t.flatMap((t) => {
67
+ let a = readKey(t, r, "committed"), o = e.get(a);
68
+ if (i.has(a)) throw new n("getKey returned duplicate keys for the committed elements.");
69
+ return i.add(a), o ? [changedElement(t, o.rect)].filter((e) => !!e) : [];
70
70
  });
71
71
  }
72
72
  function captureLayout(r, i = {}) {
73
- let a = t(r).map((e) => ({
73
+ let a = e(r).map((e) => ({
74
74
  element: e,
75
75
  rect: measureLayout(e)
76
76
  })), o = i.getKey, s = o ? createKeyMap(a, o, "captured") : void 0, c = !1;
77
77
  return { animate(r = {}) {
78
- if (c) throw new e("This layout transition has already been animated.");
79
- let { elements: i, ...l } = r, u = t(i ?? a.map(({ element: e }) => e)), d = o && s ? matchByKey(s, u, o) : matchByIdentity(a, u);
80
- return c = !0, n(d.map(({ element: e }) => e), (e, t) => {
78
+ if (c) throw new n("This layout transition has already been animated.");
79
+ let { elements: i, ...l } = r, u = e(i ?? a.map(({ element: e }) => e)), d = o && s ? matchByKey(s, u, o) : matchByIdentity(a, u);
80
+ return c = !0, t(d.map(({ element: e }) => e), (e, t) => {
81
81
  let n = d[t], r = `${round(n.x, 2)}px ${round(n.y, 2)}px`;
82
82
  return [{
83
83
  composite: "add",
@@ -91,7 +91,6 @@ function captureLayout(r, i = {}) {
91
91
  }, l);
92
92
  } };
93
93
  }
94
- //#endregion
95
94
  export { captureLayout };
96
95
 
97
96
  //# sourceMappingURL=layout.js.map
package/dist/motion.js CHANGED
@@ -1,8 +1,7 @@
1
- //#region src/motion.ts
1
+ // src/motion.ts
2
2
  function shouldReduceMotion(e) {
3
3
  return e === "full" ? !1 : e === "reduced" || typeof matchMedia == "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
4
4
  }
5
- //#endregion
6
5
  export { shouldReduceMotion };
7
6
 
8
7
  //# sourceMappingURL=motion.js.map
@@ -1,4 +1,4 @@
1
- //#region src/_handle.ts
1
+ // src/_handle.ts
2
2
  function attachAbortSignalListener(e, t) {
3
3
  let abort = () => t(e.reason);
4
4
  return e.addEventListener("abort", abort, { once: !0 }), e.aborted ? (e.removeEventListener("abort", abort), t(e.reason), () => void 0) : () => e.removeEventListener("abort", abort);
@@ -49,8 +49,7 @@ function createAnimationGroup(e, t) {
49
49
  }
50
50
  };
51
51
  }
52
- //#endregion
53
- //#region src/_options.ts
52
+ // src/_options.ts
54
53
  function resolveAnimationOptions(e = {}, t = !1) {
55
54
  let { motion: n, signal: r, ...i } = e;
56
55
  return t ? {
@@ -69,31 +68,26 @@ function withStaggeredDelay(e, t) {
69
68
  delay: n + t
70
69
  };
71
70
  }
72
- //#endregion
73
- //#region src/errors.ts
71
+ // src/errors.ts
74
72
  var NecromancerError = class extends Error {
75
73
  constructor(e, t) {
76
74
  super(e, t), this.name = new.target.name, Object.setPrototypeOf(this, new.target.prototype);
77
75
  }
78
76
  }, NecromancerConfigError = class extends NecromancerError {}, NecromancerUnsupportedError = class extends NecromancerError {};
79
- //#endregion
80
- //#region src/motion.ts
77
+ // src/motion.ts
81
78
  function shouldReduceMotion(e) {
82
79
  return e === "full" ? !1 : e === "reduced" || typeof matchMedia == "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
83
80
  }
84
- //#endregion
85
- //#region src/animate.ts
81
+ // src/animate.ts
86
82
  function animate(e, t, n = {}) {
87
83
  if (n.signal?.throwIfAborted(), typeof e.animate != "function") throw new NecromancerUnsupportedError("The Web Animations API is not available in this environment.");
88
84
  return createAnimationHandle(e.animate(t, resolveAnimationOptions(n, shouldReduceMotion(n.motion ?? "system"))), n.signal);
89
85
  }
90
- //#endregion
91
- //#region src/_elements.ts
86
+ // src/_elements.ts
92
87
  function uniqueElements(e) {
93
88
  return [...new Set(e)];
94
89
  }
95
- //#endregion
96
- //#region src/animate-each.ts
90
+ // src/animate-each.ts
97
91
  function validateStagger(e, t) {
98
92
  if (!Number.isFinite(e) || e < 0) throw new NecromancerConfigError("stagger must be a finite, non-negative number.");
99
93
  if (e > 0 && t !== void 0 && typeof t != "number") throw new NecromancerConfigError(`A non-zero stagger requires a numeric delay; received ${typeof t}.`);
@@ -115,8 +109,7 @@ function animateEach(e, t, n = {}) {
115
109
  }
116
110
  return createAnimationGroup(c, r);
117
111
  }
118
- //#endregion
119
- //#region src/layout.ts
112
+ // src/layout.ts
120
113
  function round(e, t) {
121
114
  let n = 10 ** t;
122
115
  return Math.round(e * n) / n;
@@ -206,8 +199,7 @@ function captureLayout(e, t = {}) {
206
199
  }, o);
207
200
  } };
208
201
  }
209
- //#endregion
210
- //#region src/spring.ts
202
+ // src/spring.ts
211
203
  var e = {
212
204
  damping: 26,
213
205
  mass: 1,
@@ -265,7 +257,6 @@ function spring(r, i, a, o = {}) {
265
257
  }
266
258
  };
267
259
  }
268
- //#endregion
269
260
  export { NecromancerConfigError, NecromancerError, NecromancerUnsupportedError, animate, animateEach, captureLayout, shouldReduceMotion, spring };
270
261
 
271
262
  //# sourceMappingURL=necromancer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"necromancer.js","names":["dispose","signal","animation","onSettled","disposed","disposeReason","removeAbortListener","result","outcome","reason","results","handles","handle","childResults","_motion","_signal","nativeOptions","options","reduced","offset","delay","message","opts","mode","element","keyframes","elements","stagger","animationOptions","unique","frames","index","currentFrames","error","factor","precision","value","rect","size","next","captured","key","getKey","phase","keys","entry","entries","x","y","scaleX","scaleY","change","committed","consumed","committedElements","changed","translate","DEFAULTS","MAX_FRAME_SECONDS","SUBSTEP_SECONDS","name","from","to","stiffness","damping","mass","velocity0","settled","frame","resolve","animationResult","onUpdate","velocity","lastTime","elapsed","time","remaining","dt","acceleration"],"sources":["../src/_handle.ts","../src/_options.ts","../src/errors.ts","../src/motion.ts","../src/animate.ts","../src/_elements.ts","../src/animate-each.ts","../src/layout.ts","../src/spring.ts"],"sourcesContent":["import type { AnimationGroup, AnimationHandle, AnimationResult } from './types';\n\n/**\n * Attaches an abort listener to a signal and returns a function that removes it.\n * When the signal aborts, calls the dispose function with the signal's reason.\n */\nexport function attachAbortSignalListener(signal: AbortSignal, dispose: (reason?: unknown) => void): () => void {\n const abort = () => dispose(signal.reason);\n\n signal.addEventListener('abort', abort, { once: true });\n\n if (signal.aborted) {\n signal.removeEventListener('abort', abort);\n dispose(signal.reason);\n return () => undefined;\n }\n\n return () => signal.removeEventListener('abort', abort);\n}\n\nfunction createResultPromise(animation: Animation, onSettled: () => void): Promise<AnimationResult> {\n return animation.finished.then(\n () => {\n onSettled();\n\n return { status: 'finished' };\n },\n (reason: unknown) => {\n onSettled();\n\n return { reason, status: 'cancelled' };\n },\n );\n}\n\n/** Wraps a native Animation with Necromancer's lifecycle contract. */\nexport function createAnimationHandle(animation: Animation, signal?: AbortSignal): AnimationHandle {\n let disposed = false;\n let disposeReason: unknown;\n let removeAbortListener: (() => void) | undefined;\n\n const result: Promise<AnimationResult> = createResultPromise(animation, () => {\n removeAbortListener?.();\n removeAbortListener = undefined;\n }).then((outcome): AnimationResult => {\n // An owner-driven cancellation reports the owner's dispose reason (none for a\n // bare dispose()), never the native cancel's DOMException: only an external\n // cancellation of the native animation surfaces that reason here.\n if (disposed && outcome.status === 'cancelled') {\n return disposeReason === undefined ? { status: 'cancelled' } : { reason: disposeReason, status: 'cancelled' };\n }\n\n return outcome;\n });\n\n const dispose = (reason?: unknown): void => {\n if (disposed) return;\n\n disposed = true;\n disposeReason = reason;\n removeAbortListener?.();\n removeAbortListener = undefined;\n animation.cancel();\n };\n\n if (signal) {\n removeAbortListener = attachAbortSignalListener(signal, dispose);\n }\n\n return {\n animation,\n dispose,\n get disposed() {\n return disposed;\n },\n result,\n [Symbol.dispose]() {\n this.dispose();\n },\n };\n}\n\n/** Combines child animation handles under one disposal owner. */\nexport function createAnimationGroup(handles: readonly AnimationHandle[], signal?: AbortSignal): AnimationGroup {\n let disposed = false;\n let removeAbortListener: (() => void) | undefined;\n\n const results: Promise<readonly AnimationResult[]> = Promise.all(handles.map((handle) => handle.result)).then(\n (childResults): readonly AnimationResult[] => {\n removeAbortListener?.();\n removeAbortListener = undefined;\n\n return childResults;\n },\n );\n\n const dispose = (reason?: unknown): void => {\n if (disposed) return;\n\n disposed = true;\n removeAbortListener?.();\n removeAbortListener = undefined;\n for (const handle of handles) handle.dispose(reason);\n };\n\n if (signal) {\n removeAbortListener = attachAbortSignalListener(signal, dispose);\n }\n\n return {\n dispose,\n get disposed() {\n return disposed;\n },\n handles,\n results,\n [Symbol.dispose]() {\n this.dispose();\n },\n };\n}\n","import type { AnimateOptions } from './types';\n\n/** Converts ownership options to native timing, collapsing reduced motion to an instant transition. */\nexport function resolveAnimationOptions(options: AnimateOptions = {}, reduced = false): KeyframeAnimationOptions {\n const { motion: _motion, signal: _signal, ...nativeOptions } = options;\n\n return reduced ? { ...nativeOptions, delay: 0, duration: 0, endDelay: 0, iterations: 1 } : nativeOptions;\n}\n\n/** Adds an element's stagger offset to a validated numeric delay. */\nexport function withStaggeredDelay(options: AnimateOptions, offset: number): AnimateOptions {\n if (offset === 0) return options;\n\n // animateEach() validates up front that a non-zero stagger carries a numeric\n // (or absent) delay, so a non-zero offset here can only ever meet a number.\n const delay = options.delay ?? 0;\n\n return { ...options, delay: (delay as number) + offset };\n}\n","/** Base class for errors raised by `@vielzeug/necromancer`. */\nexport class NecromancerError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown when a Necromancer-owned option is invalid. */\nexport class NecromancerConfigError extends NecromancerError {}\n\n/** Thrown when the runtime does not implement the Web Animations API. */\nexport class NecromancerUnsupportedError extends NecromancerError {}\n","import type { MotionMode } from './types';\n\n/**\n * Reports whether the requested motion preference reduces visible movement.\n *\n * This is the same check `animate()` applies internally, exposed so hand-rolled\n * effects (scroll behavior, tweens) stay consistent with it.\n */\nexport function shouldReduceMotion(mode: MotionMode): boolean {\n if (mode === 'full') return false;\n\n if (mode === 'reduced') return true;\n\n return typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;\n}\n","import { createAnimationHandle } from './_handle';\nimport { resolveAnimationOptions } from './_options';\nimport { NecromancerUnsupportedError } from './errors';\nimport { shouldReduceMotion } from './motion';\nimport type { AnimateOptions, AnimationHandle, Keyframes } from './types';\n\n/**\n * Starts a lifecycle-owned native Web Animation for one element.\n *\n * The returned handle cancels its native animation when disposed. By default, the\n * operating system's reduced-motion preference suppresses movement.\n */\nexport function animate(element: Element, keyframes: Keyframes, options: AnimateOptions = {}): AnimationHandle {\n options.signal?.throwIfAborted();\n\n if (typeof element.animate !== 'function') {\n throw new NecromancerUnsupportedError('The Web Animations API is not available in this environment.');\n }\n\n const nativeAnimation = element.animate(\n keyframes as Keyframe[] | PropertyIndexedKeyframes,\n resolveAnimationOptions(options, shouldReduceMotion(options.motion ?? 'system')),\n );\n return createAnimationHandle(nativeAnimation, options.signal);\n}\n","/** Deduplicates elements while preserving first-seen order. Shared by `animateEach()` and `captureLayout()`. */\nexport function uniqueElements<ElementType extends Element>(elements: Iterable<ElementType>): ElementType[] {\n return [...new Set(elements)];\n}\n","import { uniqueElements } from './_elements';\nimport { createAnimationGroup } from './_handle';\nimport { withStaggeredDelay } from './_options';\nimport { animate } from './animate';\nimport { NecromancerConfigError } from './errors';\nimport type { AnimateEachOptions, AnimationGroup, AnimationHandle, KeyframeFactory, Keyframes } from './types';\n\nfunction validateStagger(stagger: number, delay: number | string | null | undefined): void {\n if (!Number.isFinite(stagger) || stagger < 0) {\n throw new NecromancerConfigError('stagger must be a finite, non-negative number.');\n }\n\n // A non-zero stagger adds to the delay, so it needs a numeric one. Checking here,\n // before any animation starts, keeps a pure config error free of side effects.\n if (stagger > 0 && delay !== undefined && typeof delay !== 'number') {\n throw new NecromancerConfigError(`A non-zero stagger requires a numeric delay; received ${typeof delay}.`);\n }\n}\n\n/**\n * Starts lifecycle-owned animations for unique elements in iteration order.\n *\n * Each subsequent element starts after `stagger` milliseconds. A keyframe factory\n * receives the stable unique-element index and total before any animation starts.\n */\nexport function animateEach<ElementType extends Element>(\n elements: Iterable<ElementType>,\n keyframes: Keyframes | KeyframeFactory<ElementType>,\n options: AnimateEachOptions = {},\n): AnimationGroup {\n const { signal, stagger = 0, ...animationOptions } = options;\n\n validateStagger(stagger, animationOptions.delay);\n\n signal?.throwIfAborted();\n\n const unique = uniqueElements(elements);\n const frames = unique.map((element, index) =>\n typeof keyframes === 'function' ? keyframes(element, index, unique.length) : keyframes,\n );\n\n signal?.throwIfAborted();\n\n const handles: AnimationHandle[] = [];\n\n try {\n for (const [index, element] of unique.entries()) {\n // `frames` was built above from `unique`, one entry per element, so this index is always in range.\n const currentFrames = frames[index]!;\n\n handles.push(animate(element, currentFrames, withStaggeredDelay(animationOptions, index * stagger)));\n }\n } catch (error) {\n for (const handle of handles) handle.dispose(error);\n\n throw error;\n }\n\n return createAnimationGroup(handles, signal);\n}\n","import { uniqueElements } from './_elements';\nimport { animateEach } from './animate-each';\nimport { NecromancerConfigError } from './errors';\nimport type { LayoutAnimationOptions, LayoutCaptureOptions, LayoutTransition } from './types';\n\ntype LayoutRect = Readonly<Pick<DOMRectReadOnly, 'height' | 'width' | 'x' | 'y'>>;\ntype LayoutEntry<ElementType extends Element> = Readonly<{ element: ElementType; rect: LayoutRect }>;\ntype LayoutChange<ElementType extends Element> = Readonly<{\n element: ElementType;\n scaleX: number;\n scaleY: number;\n x: number;\n y: number;\n}>;\n\n// Rounds before templating into a keyframe string purely for DevTools readability: a `getBoundingClientRect()`\n// subtraction routinely produces values like `12.340000000000002`, which is noise once rendered as CSS.\n// Pixels only need whole-number precision; scale factors (close to 1) need finer precision to stay visually exact.\nfunction round(value: number, precision: number): number {\n const factor = 10 ** precision;\n\n return Math.round(value * factor) / factor;\n}\n\n// `getBoundingClientRect()`'s width/height is the rotated, axis-aligned bounding box: using it\n// for size would distort the scale ratio under any rotation. `offsetWidth`/`offsetHeight` report\n// the box's own untransformed layout size instead. Elements without an offset box (e.g. SVG)\n// fall back to the bounding rect, accepting that same distortion as the only option available.\nfunction measureSize(element: Element): Readonly<{ height: number; width: number }> {\n if ('offsetWidth' in element && 'offsetHeight' in element) {\n return { height: (element as HTMLElement).offsetHeight, width: (element as HTMLElement).offsetWidth };\n }\n\n const rect = element.getBoundingClientRect();\n\n return { height: rect.height, width: rect.width };\n}\n\nfunction measureLayout(element: Element): LayoutRect {\n const rect = element.getBoundingClientRect();\n const size = measureSize(element);\n\n return { height: size.height, width: size.width, x: rect.x, y: rect.y };\n}\n\n// `next` is 0 exactly when the element has collapsed (e.g. `display: none` mid-transition);\n// there's no meaningful ratio to compute then, so treat it as unscaled rather than producing Infinity/NaN.\nfunction computeScale(captured: number, next: number): number {\n return next === 0 ? 1 : captured / next;\n}\n\nfunction readKey<ElementType extends Element>(\n element: ElementType,\n getKey: (element: ElementType) => string,\n phase: string,\n): string {\n const key = getKey(element);\n\n if (typeof key !== 'string' || key.length === 0) {\n throw new NecromancerConfigError(`getKey must return a non-empty string for every ${phase} element.`);\n }\n\n return key;\n}\n\nfunction createKeyMap<ElementType extends Element>(\n entries: readonly LayoutEntry<ElementType>[],\n getKey: (element: ElementType) => string,\n phase: string,\n) {\n const keys = new Map<string, LayoutEntry<ElementType>>();\n\n for (const entry of entries) {\n const key = readKey(entry.element, getKey, phase);\n\n if (keys.has(key)) {\n throw new NecromancerConfigError(`getKey returned duplicate keys for the ${phase} elements.`);\n }\n\n keys.set(key, entry);\n }\n\n return keys;\n}\n\nfunction changedElement<ElementType extends Element>(\n element: ElementType,\n rect: LayoutRect,\n): LayoutChange<ElementType> | undefined {\n if (!element.isConnected) return undefined;\n\n const next = measureLayout(element);\n const x = rect.x - next.x;\n const y = rect.y - next.y;\n const scaleX = computeScale(rect.width, next.width);\n const scaleY = computeScale(rect.height, next.height);\n\n return x === 0 && y === 0 && scaleX === 1 && scaleY === 1 ? undefined : { element, scaleX, scaleY, x, y };\n}\n\nfunction matchByIdentity<ElementType extends Element>(\n entries: readonly LayoutEntry<ElementType>[],\n elements: readonly ElementType[],\n): LayoutChange<ElementType>[] {\n const captured = new Map(entries.map((entry) => [entry.element, entry]));\n\n return elements.flatMap((element) => {\n const entry = captured.get(element);\n\n return entry\n ? [changedElement(element, entry.rect)].filter((change): change is LayoutChange<ElementType> => Boolean(change))\n : [];\n });\n}\n\nfunction matchByKey<ElementType extends Element>(\n captured: ReadonlyMap<string, LayoutEntry<ElementType>>,\n elements: readonly ElementType[],\n getKey: (element: ElementType) => string,\n): LayoutChange<ElementType>[] {\n const committed = new Set<string>();\n\n return elements.flatMap((element) => {\n const key = readKey(element, getKey, 'committed');\n const entry = captured.get(key);\n\n if (committed.has(key)) {\n throw new NecromancerConfigError('getKey returned duplicate keys for the committed elements.');\n }\n\n committed.add(key);\n\n return entry\n ? [changedElement(element, entry.rect)].filter((change): change is LayoutChange<ElementType> => Boolean(change))\n : [];\n });\n}\n\n/**\n * Captures the current position and size of unique elements for a later FLIP animation.\n *\n * Each element's `x`/`y` position and `width`/`height` are captured. Rotation and other\n * transforms are not captured or compensated. Capture before mutating the DOM, then call\n * the returned transition's `animate()` method after the browser has applied the resulting\n * layout: position changes animate via translate, and size changes via scale, both\n * additively composed on top of any authored transform.\n */\nexport function captureLayout<ElementType extends Element>(\n elements: Iterable<ElementType>,\n options: LayoutCaptureOptions<ElementType> = {},\n): LayoutTransition<ElementType> {\n const entries: LayoutEntry<ElementType>[] = uniqueElements(elements).map((element) => ({\n element,\n rect: measureLayout(element),\n }));\n const getKey = options.getKey;\n const captured = getKey ? createKeyMap(entries, getKey, 'captured') : undefined;\n\n let consumed = false;\n\n return {\n animate(options: LayoutAnimationOptions<ElementType> = {}) {\n if (consumed) throw new NecromancerConfigError('This layout transition has already been animated.');\n\n const { elements: committedElements, ...animationOptions } = options;\n const elements = uniqueElements(committedElements ?? entries.map(({ element }) => element));\n const changed = getKey && captured ? matchByKey(captured, elements, getKey) : matchByIdentity(entries, elements);\n\n consumed = true;\n\n return animateEach(\n changed.map(({ element }) => element),\n (_, index) => {\n // `changed` was mapped directly into the elements array above, so this index is always in range.\n const change = changed[index]!;\n const translate = `${round(change.x, 2)}px ${round(change.y, 2)}px`;\n const scale = `${round(change.scaleX, 4)} ${round(change.scaleY, 4)}`;\n\n return [\n { composite: 'add', scale, translate },\n { composite: 'add', scale: '1 1', translate: '0px 0px' },\n ];\n },\n animationOptions,\n );\n },\n };\n}\n","import { attachAbortSignalListener } from './_handle';\nimport { NecromancerConfigError } from './errors';\nimport { shouldReduceMotion } from './motion';\nimport type { AnimationResult, MotionMode } from './types';\n\n/**\n * Physical spring parameters, in the damped-harmonic-oscillator model:\n * `mass * x'' = -stiffness * (x - to) - damping * x'`.\n */\nexport type SpringOptions = Readonly<{\n /** Spring stiffness k. Higher settles faster. Defaults to `170`. */\n readonly stiffness?: number;\n /** Viscous damping c. Lower overshoots more; `2 * sqrt(k * mass)` is critically damped. Defaults to `26`. */\n readonly damping?: number;\n /** Mass m. Defaults to `1`. */\n readonly mass?: number;\n /** Initial velocity in units/s, typically the finger velocity at release. Defaults to `0`. */\n readonly velocity?: number;\n /** Both |x - to| and |velocity| must fall below this to rest. Defaults to `0.1`. */\n readonly precision?: number;\n /** Reduced motion jumps straight to the target. Defaults to `'system'`. */\n readonly motion?: MotionMode;\n /** Disposes the returned handle when aborted. */\n readonly signal?: AbortSignal;\n}>;\n\n/** Called once per frame with the current spring value and velocity (units/s). */\nexport type SpringUpdate = (value: number, velocity: number) => void;\n\n/** A lifecycle-owned spring animation. */\nexport interface SpringHandle {\n /** Stops the spring mid-flight and releases owned listeners. */\n dispose(reason?: unknown): void;\n /** Whether this handle has been disposed. */\n readonly disposed: boolean;\n /** Resolves once the spring rests or is disposed. */\n readonly result: Promise<AnimationResult>;\n /** Calls {@link dispose}. */\n [Symbol.dispose](): void;\n}\n\nconst DEFAULTS = { damping: 26, mass: 1, precision: 0.1, stiffness: 170 } as const;\n\n/** Longest frame integrated in one step; a backgrounded tab resumes without a physics jump. */\nconst MAX_FRAME_SECONDS = 1 / 30;\n\n/** Fixed physics substep; keeps the explicit integrator stable at stiff settings. */\nconst SUBSTEP_SECONDS = 1 / 240;\n\nfunction validate(name: string, value: number): number {\n if (!Number.isFinite(value) || value <= 0) {\n throw new NecromancerConfigError(`${name} must be a positive finite number, got: ${value}`);\n }\n return value;\n}\n\n/**\n * Endpoint validation. Unlike the config knobs, a spring boundary may legitimately be zero or\n * negative (a height animating from `0`, a panel sliding to a negative offset), so only finiteness\n * is enforced. It still has to be checked: the rest test compares against `to`, so a non-finite\n * endpoint can never be satisfied and the spring would request frames forever while emitting NaN.\n */\nfunction validateBound(name: string, value: number): number {\n if (!Number.isFinite(value)) {\n throw new NecromancerConfigError(`${name} must be a finite number, got: ${value}`);\n }\n return value;\n}\n\n/**\n * Animates a scalar value on a physical spring, calling `onUpdate` once per frame.\n *\n * Unlike a fixed-duration easing curve, a spring takes its **initial velocity**\n * from the caller — typically the pointer velocity a gesture reported at release\n * — so a fling continues from the finger's momentum and a slow drag settles\n * without overshoot. The motion is interruptible by design: dispose the handle\n * and start a new spring from the current value and velocity.\n *\n * Under reduced motion the value jumps to the target in a single update.\n */\nexport function spring(from: number, to: number, onUpdate: SpringUpdate, options: SpringOptions = {}): SpringHandle {\n validateBound('from', from);\n validateBound('to', to);\n const stiffness = validate('stiffness', options.stiffness ?? DEFAULTS.stiffness);\n const damping = validate('damping', options.damping ?? DEFAULTS.damping);\n const mass = validate('mass', options.mass ?? DEFAULTS.mass);\n const precision = validate('precision', options.precision ?? DEFAULTS.precision);\n const velocity0 = options.velocity ?? 0;\n if (!Number.isFinite(velocity0)) {\n throw new NecromancerConfigError(`velocity must be a finite number, got: ${velocity0}`);\n }\n options.signal?.throwIfAborted();\n\n let disposed = false;\n let settled = false;\n let frame = 0;\n let removeAbortListener: (() => void) | undefined;\n let settle: (result: AnimationResult) => void = () => undefined;\n\n const result = new Promise<AnimationResult>((resolve) => {\n settle = (animationResult) => {\n settled = true;\n removeAbortListener?.();\n removeAbortListener = undefined;\n resolve(animationResult);\n };\n\n if (shouldReduceMotion(options.motion ?? 'system')) {\n onUpdate(to, 0);\n settle({ status: 'finished' });\n return;\n }\n\n let value = from;\n let velocity = velocity0;\n let lastTime: number | undefined;\n\n const step = (time: number): void => {\n if (disposed) return;\n\n const elapsed = lastTime === undefined ? 0 : Math.min((time - lastTime) / 1000, MAX_FRAME_SECONDS);\n lastTime = time;\n\n let remaining = elapsed;\n while (remaining > 0) {\n const dt = Math.min(remaining, SUBSTEP_SECONDS);\n remaining -= dt;\n // Semi-implicit Euler: update velocity first, then position from it.\n const acceleration = (-stiffness * (value - to) - damping * velocity) / mass;\n velocity += acceleration * dt;\n value += velocity * dt;\n }\n\n if (Math.abs(value - to) < precision && Math.abs(velocity) < precision) {\n onUpdate(to, 0);\n settle({ status: 'finished' });\n return;\n }\n\n onUpdate(value, velocity);\n frame = requestAnimationFrame(step);\n };\n\n frame = requestAnimationFrame(step);\n });\n\n const dispose = (reason?: unknown): void => {\n if (disposed || settled) return;\n\n disposed = true;\n cancelAnimationFrame(frame);\n settle({ reason, status: 'cancelled' });\n };\n\n if (options.signal && !settled) {\n removeAbortListener = attachAbortSignalListener(options.signal, dispose);\n }\n\n return {\n dispose,\n get disposed() {\n return disposed;\n },\n result,\n [Symbol.dispose]() {\n this.dispose();\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"necromancer.js","names":["dispose","signal","animation","onSettled","disposed","disposeReason","removeAbortListener","result","outcome","reason","results","handles","handle","childResults","_motion","_signal","nativeOptions","options","reduced","offset","delay","message","opts","mode","element","keyframes","elements","stagger","animationOptions","unique","frames","index","currentFrames","error","factor","precision","value","rect","size","next","captured","key","getKey","phase","keys","entry","entries","x","y","scaleX","scaleY","change","committed","consumed","committedElements","changed","translate","DEFAULTS","MAX_FRAME_SECONDS","SUBSTEP_SECONDS","name","from","to","stiffness","damping","mass","velocity0","settled","frame","resolve","animationResult","onUpdate","velocity","lastTime","elapsed","time","remaining","dt","acceleration"],"sources":["../src/_handle.ts","../src/_options.ts","../src/errors.ts","../src/motion.ts","../src/animate.ts","../src/_elements.ts","../src/animate-each.ts","../src/layout.ts","../src/spring.ts"],"sourcesContent":["import type { AnimationGroup, AnimationHandle, AnimationResult } from './types';\n\n/**\n * Attaches an abort listener to a signal and returns a function that removes it.\n * When the signal aborts, calls the dispose function with the signal's reason.\n */\nexport function attachAbortSignalListener(signal: AbortSignal, dispose: (reason?: unknown) => void): () => void {\n const abort = () => dispose(signal.reason);\n\n signal.addEventListener('abort', abort, { once: true });\n\n if (signal.aborted) {\n signal.removeEventListener('abort', abort);\n dispose(signal.reason);\n return () => undefined;\n }\n\n return () => signal.removeEventListener('abort', abort);\n}\n\nfunction createResultPromise(animation: Animation, onSettled: () => void): Promise<AnimationResult> {\n return animation.finished.then(\n () => {\n onSettled();\n\n return { status: 'finished' };\n },\n (reason: unknown) => {\n onSettled();\n\n return { reason, status: 'cancelled' };\n },\n );\n}\n\n/** Wraps a native Animation with Necromancer's lifecycle contract. */\nexport function createAnimationHandle(animation: Animation, signal?: AbortSignal): AnimationHandle {\n let disposed = false;\n let disposeReason: unknown;\n let removeAbortListener: (() => void) | undefined;\n\n const result: Promise<AnimationResult> = createResultPromise(animation, () => {\n removeAbortListener?.();\n removeAbortListener = undefined;\n }).then((outcome): AnimationResult => {\n // An owner-driven cancellation reports the owner's dispose reason (none for a\n // bare dispose()), never the native cancel's DOMException: only an external\n // cancellation of the native animation surfaces that reason here.\n if (disposed && outcome.status === 'cancelled') {\n return disposeReason === undefined ? { status: 'cancelled' } : { reason: disposeReason, status: 'cancelled' };\n }\n\n return outcome;\n });\n\n const dispose = (reason?: unknown): void => {\n if (disposed) return;\n\n disposed = true;\n disposeReason = reason;\n removeAbortListener?.();\n removeAbortListener = undefined;\n animation.cancel();\n };\n\n if (signal) {\n removeAbortListener = attachAbortSignalListener(signal, dispose);\n }\n\n return {\n animation,\n dispose,\n get disposed() {\n return disposed;\n },\n result,\n [Symbol.dispose]() {\n this.dispose();\n },\n };\n}\n\n/** Combines child animation handles under one disposal owner. */\nexport function createAnimationGroup(handles: readonly AnimationHandle[], signal?: AbortSignal): AnimationGroup {\n let disposed = false;\n let removeAbortListener: (() => void) | undefined;\n\n const results: Promise<readonly AnimationResult[]> = Promise.all(handles.map((handle) => handle.result)).then(\n (childResults): readonly AnimationResult[] => {\n removeAbortListener?.();\n removeAbortListener = undefined;\n\n return childResults;\n },\n );\n\n const dispose = (reason?: unknown): void => {\n if (disposed) return;\n\n disposed = true;\n removeAbortListener?.();\n removeAbortListener = undefined;\n for (const handle of handles) handle.dispose(reason);\n };\n\n if (signal) {\n removeAbortListener = attachAbortSignalListener(signal, dispose);\n }\n\n return {\n dispose,\n get disposed() {\n return disposed;\n },\n handles,\n results,\n [Symbol.dispose]() {\n this.dispose();\n },\n };\n}\n","import type { AnimateOptions } from './types';\n\n/** Converts ownership options to native timing, collapsing reduced motion to an instant transition. */\nexport function resolveAnimationOptions(options: AnimateOptions = {}, reduced = false): KeyframeAnimationOptions {\n const { motion: _motion, signal: _signal, ...nativeOptions } = options;\n\n return reduced ? { ...nativeOptions, delay: 0, duration: 0, endDelay: 0, iterations: 1 } : nativeOptions;\n}\n\n/** Adds an element's stagger offset to a validated numeric delay. */\nexport function withStaggeredDelay(options: AnimateOptions, offset: number): AnimateOptions {\n if (offset === 0) return options;\n\n // animateEach() validates up front that a non-zero stagger carries a numeric\n // (or absent) delay, so a non-zero offset here can only ever meet a number.\n const delay = options.delay ?? 0;\n\n return { ...options, delay: (delay as number) + offset };\n}\n","/** Base class for errors raised by `@vielzeug/necromancer`. */\nexport class NecromancerError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown when a Necromancer-owned option is invalid. */\nexport class NecromancerConfigError extends NecromancerError {}\n\n/** Thrown when the runtime does not implement the Web Animations API. */\nexport class NecromancerUnsupportedError extends NecromancerError {}\n","import type { MotionMode } from './types';\n\n/**\n * Reports whether the requested motion preference reduces visible movement.\n *\n * This is the same check `animate()` applies internally, exposed so hand-rolled\n * effects (scroll behavior, tweens) stay consistent with it.\n */\nexport function shouldReduceMotion(mode: MotionMode): boolean {\n if (mode === 'full') return false;\n\n if (mode === 'reduced') return true;\n\n return typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;\n}\n","import { createAnimationHandle } from './_handle';\nimport { resolveAnimationOptions } from './_options';\nimport { NecromancerUnsupportedError } from './errors';\nimport { shouldReduceMotion } from './motion';\nimport type { AnimateOptions, AnimationHandle, Keyframes } from './types';\n\n/**\n * Starts a lifecycle-owned native Web Animation for one element.\n *\n * The returned handle cancels its native animation when disposed. By default, the\n * operating system's reduced-motion preference suppresses movement.\n */\nexport function animate(element: Element, keyframes: Keyframes, options: AnimateOptions = {}): AnimationHandle {\n options.signal?.throwIfAborted();\n\n if (typeof element.animate !== 'function') {\n throw new NecromancerUnsupportedError('The Web Animations API is not available in this environment.');\n }\n\n const nativeAnimation = element.animate(\n keyframes as Keyframe[] | PropertyIndexedKeyframes,\n resolveAnimationOptions(options, shouldReduceMotion(options.motion ?? 'system')),\n );\n return createAnimationHandle(nativeAnimation, options.signal);\n}\n","/** Deduplicates elements while preserving first-seen order. Shared by `animateEach()` and `captureLayout()`. */\nexport function uniqueElements<ElementType extends Element>(elements: Iterable<ElementType>): ElementType[] {\n return [...new Set(elements)];\n}\n","import { uniqueElements } from './_elements';\nimport { createAnimationGroup } from './_handle';\nimport { withStaggeredDelay } from './_options';\nimport { animate } from './animate';\nimport { NecromancerConfigError } from './errors';\nimport type { AnimateEachOptions, AnimationGroup, AnimationHandle, KeyframeFactory, Keyframes } from './types';\n\nfunction validateStagger(stagger: number, delay: number | string | null | undefined): void {\n if (!Number.isFinite(stagger) || stagger < 0) {\n throw new NecromancerConfigError('stagger must be a finite, non-negative number.');\n }\n\n // A non-zero stagger adds to the delay, so it needs a numeric one. Checking here,\n // before any animation starts, keeps a pure config error free of side effects.\n if (stagger > 0 && delay !== undefined && typeof delay !== 'number') {\n throw new NecromancerConfigError(`A non-zero stagger requires a numeric delay; received ${typeof delay}.`);\n }\n}\n\n/**\n * Starts lifecycle-owned animations for unique elements in iteration order.\n *\n * Each subsequent element starts after `stagger` milliseconds. A keyframe factory\n * receives the stable unique-element index and total before any animation starts.\n */\nexport function animateEach<ElementType extends Element>(\n elements: Iterable<ElementType>,\n keyframes: Keyframes | KeyframeFactory<ElementType>,\n options: AnimateEachOptions = {},\n): AnimationGroup {\n const { signal, stagger = 0, ...animationOptions } = options;\n\n validateStagger(stagger, animationOptions.delay);\n\n signal?.throwIfAborted();\n\n const unique = uniqueElements(elements);\n const frames = unique.map((element, index) =>\n typeof keyframes === 'function' ? keyframes(element, index, unique.length) : keyframes,\n );\n\n signal?.throwIfAborted();\n\n const handles: AnimationHandle[] = [];\n\n try {\n for (const [index, element] of unique.entries()) {\n // `frames` was built above from `unique`, one entry per element, so this index is always in range.\n const currentFrames = frames[index]!;\n\n handles.push(animate(element, currentFrames, withStaggeredDelay(animationOptions, index * stagger)));\n }\n } catch (error) {\n for (const handle of handles) handle.dispose(error);\n\n throw error;\n }\n\n return createAnimationGroup(handles, signal);\n}\n","import { uniqueElements } from './_elements';\nimport { animateEach } from './animate-each';\nimport { NecromancerConfigError } from './errors';\nimport type { LayoutAnimationOptions, LayoutCaptureOptions, LayoutTransition } from './types';\n\ntype LayoutRect = Readonly<Pick<DOMRectReadOnly, 'height' | 'width' | 'x' | 'y'>>;\ntype LayoutEntry<ElementType extends Element> = Readonly<{ element: ElementType; rect: LayoutRect }>;\ntype LayoutChange<ElementType extends Element> = Readonly<{\n element: ElementType;\n scaleX: number;\n scaleY: number;\n x: number;\n y: number;\n}>;\n\n// Rounds before templating into a keyframe string purely for DevTools readability: a `getBoundingClientRect()`\n// subtraction routinely produces values like `12.340000000000002`, which is noise once rendered as CSS.\n// Pixels only need whole-number precision; scale factors (close to 1) need finer precision to stay visually exact.\nfunction round(value: number, precision: number): number {\n const factor = 10 ** precision;\n\n return Math.round(value * factor) / factor;\n}\n\n// `getBoundingClientRect()`'s width/height is the rotated, axis-aligned bounding box: using it\n// for size would distort the scale ratio under any rotation. `offsetWidth`/`offsetHeight` report\n// the box's own untransformed layout size instead. Elements without an offset box (e.g. SVG)\n// fall back to the bounding rect, accepting that same distortion as the only option available.\nfunction measureSize(element: Element): Readonly<{ height: number; width: number }> {\n if ('offsetWidth' in element && 'offsetHeight' in element) {\n return { height: (element as HTMLElement).offsetHeight, width: (element as HTMLElement).offsetWidth };\n }\n\n const rect = element.getBoundingClientRect();\n\n return { height: rect.height, width: rect.width };\n}\n\nfunction measureLayout(element: Element): LayoutRect {\n const rect = element.getBoundingClientRect();\n const size = measureSize(element);\n\n return { height: size.height, width: size.width, x: rect.x, y: rect.y };\n}\n\n// `next` is 0 exactly when the element has collapsed (e.g. `display: none` mid-transition);\n// there's no meaningful ratio to compute then, so treat it as unscaled rather than producing Infinity/NaN.\nfunction computeScale(captured: number, next: number): number {\n return next === 0 ? 1 : captured / next;\n}\n\nfunction readKey<ElementType extends Element>(\n element: ElementType,\n getKey: (element: ElementType) => string,\n phase: string,\n): string {\n const key = getKey(element);\n\n if (typeof key !== 'string' || key.length === 0) {\n throw new NecromancerConfigError(`getKey must return a non-empty string for every ${phase} element.`);\n }\n\n return key;\n}\n\nfunction createKeyMap<ElementType extends Element>(\n entries: readonly LayoutEntry<ElementType>[],\n getKey: (element: ElementType) => string,\n phase: string,\n) {\n const keys = new Map<string, LayoutEntry<ElementType>>();\n\n for (const entry of entries) {\n const key = readKey(entry.element, getKey, phase);\n\n if (keys.has(key)) {\n throw new NecromancerConfigError(`getKey returned duplicate keys for the ${phase} elements.`);\n }\n\n keys.set(key, entry);\n }\n\n return keys;\n}\n\nfunction changedElement<ElementType extends Element>(\n element: ElementType,\n rect: LayoutRect,\n): LayoutChange<ElementType> | undefined {\n if (!element.isConnected) return undefined;\n\n const next = measureLayout(element);\n const x = rect.x - next.x;\n const y = rect.y - next.y;\n const scaleX = computeScale(rect.width, next.width);\n const scaleY = computeScale(rect.height, next.height);\n\n return x === 0 && y === 0 && scaleX === 1 && scaleY === 1 ? undefined : { element, scaleX, scaleY, x, y };\n}\n\nfunction matchByIdentity<ElementType extends Element>(\n entries: readonly LayoutEntry<ElementType>[],\n elements: readonly ElementType[],\n): LayoutChange<ElementType>[] {\n const captured = new Map(entries.map((entry) => [entry.element, entry]));\n\n return elements.flatMap((element) => {\n const entry = captured.get(element);\n\n return entry\n ? [changedElement(element, entry.rect)].filter((change): change is LayoutChange<ElementType> => Boolean(change))\n : [];\n });\n}\n\nfunction matchByKey<ElementType extends Element>(\n captured: ReadonlyMap<string, LayoutEntry<ElementType>>,\n elements: readonly ElementType[],\n getKey: (element: ElementType) => string,\n): LayoutChange<ElementType>[] {\n const committed = new Set<string>();\n\n return elements.flatMap((element) => {\n const key = readKey(element, getKey, 'committed');\n const entry = captured.get(key);\n\n if (committed.has(key)) {\n throw new NecromancerConfigError('getKey returned duplicate keys for the committed elements.');\n }\n\n committed.add(key);\n\n return entry\n ? [changedElement(element, entry.rect)].filter((change): change is LayoutChange<ElementType> => Boolean(change))\n : [];\n });\n}\n\n/**\n * Captures the current position and size of unique elements for a later FLIP animation.\n *\n * Each element's `x`/`y` position and `width`/`height` are captured. Rotation and other\n * transforms are not captured or compensated. Capture before mutating the DOM, then call\n * the returned transition's `animate()` method after the browser has applied the resulting\n * layout: position changes animate via translate, and size changes via scale, both\n * additively composed on top of any authored transform.\n */\nexport function captureLayout<ElementType extends Element>(\n elements: Iterable<ElementType>,\n options: LayoutCaptureOptions<ElementType> = {},\n): LayoutTransition<ElementType> {\n const entries: LayoutEntry<ElementType>[] = uniqueElements(elements).map((element) => ({\n element,\n rect: measureLayout(element),\n }));\n const getKey = options.getKey;\n const captured = getKey ? createKeyMap(entries, getKey, 'captured') : undefined;\n\n let consumed = false;\n\n return {\n animate(options: LayoutAnimationOptions<ElementType> = {}) {\n if (consumed) throw new NecromancerConfigError('This layout transition has already been animated.');\n\n const { elements: committedElements, ...animationOptions } = options;\n const elements = uniqueElements(committedElements ?? entries.map(({ element }) => element));\n const changed = getKey && captured ? matchByKey(captured, elements, getKey) : matchByIdentity(entries, elements);\n\n consumed = true;\n\n return animateEach(\n changed.map(({ element }) => element),\n (_, index) => {\n // `changed` was mapped directly into the elements array above, so this index is always in range.\n const change = changed[index]!;\n const translate = `${round(change.x, 2)}px ${round(change.y, 2)}px`;\n const scale = `${round(change.scaleX, 4)} ${round(change.scaleY, 4)}`;\n\n return [\n { composite: 'add', scale, translate },\n { composite: 'add', scale: '1 1', translate: '0px 0px' },\n ];\n },\n animationOptions,\n );\n },\n };\n}\n","import { attachAbortSignalListener } from './_handle';\nimport { NecromancerConfigError } from './errors';\nimport { shouldReduceMotion } from './motion';\nimport type { AnimationResult, MotionMode } from './types';\n\n/**\n * Physical spring parameters, in the damped-harmonic-oscillator model:\n * `mass * x'' = -stiffness * (x - to) - damping * x'`.\n */\nexport type SpringOptions = Readonly<{\n /** Spring stiffness k. Higher settles faster. Defaults to `170`. */\n readonly stiffness?: number;\n /** Viscous damping c. Lower overshoots more; `2 * sqrt(k * mass)` is critically damped. Defaults to `26`. */\n readonly damping?: number;\n /** Mass m. Defaults to `1`. */\n readonly mass?: number;\n /** Initial velocity in units/s, typically the finger velocity at release. Defaults to `0`. */\n readonly velocity?: number;\n /** Both |x - to| and |velocity| must fall below this to rest. Defaults to `0.1`. */\n readonly precision?: number;\n /** Reduced motion jumps straight to the target. Defaults to `'system'`. */\n readonly motion?: MotionMode;\n /** Disposes the returned handle when aborted. */\n readonly signal?: AbortSignal;\n}>;\n\n/** Called once per frame with the current spring value and velocity (units/s). */\nexport type SpringUpdate = (value: number, velocity: number) => void;\n\n/** A lifecycle-owned spring animation. */\nexport interface SpringHandle {\n /** Stops the spring mid-flight and releases owned listeners. */\n dispose(reason?: unknown): void;\n /** Whether this handle has been disposed. */\n readonly disposed: boolean;\n /** Resolves once the spring rests or is disposed. */\n readonly result: Promise<AnimationResult>;\n /** Calls {@link dispose}. */\n [Symbol.dispose](): void;\n}\n\nconst DEFAULTS = { damping: 26, mass: 1, precision: 0.1, stiffness: 170 } as const;\n\n/** Longest frame integrated in one step; a backgrounded tab resumes without a physics jump. */\nconst MAX_FRAME_SECONDS = 1 / 30;\n\n/** Fixed physics substep; keeps the explicit integrator stable at stiff settings. */\nconst SUBSTEP_SECONDS = 1 / 240;\n\nfunction validate(name: string, value: number): number {\n if (!Number.isFinite(value) || value <= 0) {\n throw new NecromancerConfigError(`${name} must be a positive finite number, got: ${value}`);\n }\n return value;\n}\n\n/**\n * Endpoint validation. Unlike the config knobs, a spring boundary may legitimately be zero or\n * negative (a height animating from `0`, a panel sliding to a negative offset), so only finiteness\n * is enforced. It still has to be checked: the rest test compares against `to`, so a non-finite\n * endpoint can never be satisfied and the spring would request frames forever while emitting NaN.\n */\nfunction validateBound(name: string, value: number): number {\n if (!Number.isFinite(value)) {\n throw new NecromancerConfigError(`${name} must be a finite number, got: ${value}`);\n }\n return value;\n}\n\n/**\n * Animates a scalar value on a physical spring, calling `onUpdate` once per frame.\n *\n * Unlike a fixed-duration easing curve, a spring takes its **initial velocity**\n * from the caller — typically the pointer velocity a gesture reported at release\n * — so a fling continues from the finger's momentum and a slow drag settles\n * without overshoot. The motion is interruptible by design: dispose the handle\n * and start a new spring from the current value and velocity.\n *\n * Under reduced motion the value jumps to the target in a single update.\n */\nexport function spring(from: number, to: number, onUpdate: SpringUpdate, options: SpringOptions = {}): SpringHandle {\n validateBound('from', from);\n validateBound('to', to);\n const stiffness = validate('stiffness', options.stiffness ?? DEFAULTS.stiffness);\n const damping = validate('damping', options.damping ?? DEFAULTS.damping);\n const mass = validate('mass', options.mass ?? DEFAULTS.mass);\n const precision = validate('precision', options.precision ?? DEFAULTS.precision);\n const velocity0 = options.velocity ?? 0;\n if (!Number.isFinite(velocity0)) {\n throw new NecromancerConfigError(`velocity must be a finite number, got: ${velocity0}`);\n }\n options.signal?.throwIfAborted();\n\n let disposed = false;\n let settled = false;\n let frame = 0;\n let removeAbortListener: (() => void) | undefined;\n let settle: (result: AnimationResult) => void = () => undefined;\n\n const result = new Promise<AnimationResult>((resolve) => {\n settle = (animationResult) => {\n settled = true;\n removeAbortListener?.();\n removeAbortListener = undefined;\n resolve(animationResult);\n };\n\n if (shouldReduceMotion(options.motion ?? 'system')) {\n onUpdate(to, 0);\n settle({ status: 'finished' });\n return;\n }\n\n let value = from;\n let velocity = velocity0;\n let lastTime: number | undefined;\n\n const step = (time: number): void => {\n if (disposed) return;\n\n const elapsed = lastTime === undefined ? 0 : Math.min((time - lastTime) / 1000, MAX_FRAME_SECONDS);\n lastTime = time;\n\n let remaining = elapsed;\n while (remaining > 0) {\n const dt = Math.min(remaining, SUBSTEP_SECONDS);\n remaining -= dt;\n // Semi-implicit Euler: update velocity first, then position from it.\n const acceleration = (-stiffness * (value - to) - damping * velocity) / mass;\n velocity += acceleration * dt;\n value += velocity * dt;\n }\n\n if (Math.abs(value - to) < precision && Math.abs(velocity) < precision) {\n onUpdate(to, 0);\n settle({ status: 'finished' });\n return;\n }\n\n onUpdate(value, velocity);\n frame = requestAnimationFrame(step);\n };\n\n frame = requestAnimationFrame(step);\n });\n\n const dispose = (reason?: unknown): void => {\n if (disposed || settled) return;\n\n disposed = true;\n cancelAnimationFrame(frame);\n settle({ reason, status: 'cancelled' });\n };\n\n if (options.signal && !settled) {\n removeAbortListener = attachAbortSignalListener(options.signal, dispose);\n }\n\n return {\n dispose,\n get disposed() {\n return disposed;\n },\n result,\n [Symbol.dispose]() {\n this.dispose();\n },\n };\n}\n"],"mappings":"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"}
package/dist/spring.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { attachAbortSignalListener as e } from "./_handle.js";
2
2
  import { NecromancerConfigError as t } from "./errors.js";
3
3
  import { shouldReduceMotion as n } from "./motion.js";
4
- //#region src/spring.ts
4
+ // src/spring.ts
5
5
  var r = {
6
6
  damping: 26,
7
7
  mass: 1,
@@ -59,7 +59,6 @@ function spring(o, s, c, l = {}) {
59
59
  }
60
60
  };
61
61
  }
62
- //#endregion
63
62
  export { spring };
64
63
 
65
64
  //# sourceMappingURL=spring.js.map
package/dist/testing.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/testing/index.ts
1
+ // src/testing/index.ts
2
2
  var FakeAnimation = class {
3
3
  #e;
4
4
  #t;
@@ -43,7 +43,6 @@ function installFakeAnimations() {
43
43
  function createRect(e, t, n = 20, r = 20) {
44
44
  return new DOMRect(e, t, n, r);
45
45
  }
46
- //#endregion
47
46
  export { FakeAnimation, createRect, installFakeAnimations };
48
47
 
49
48
  //# sourceMappingURL=testing.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vielzeug/necromancer",
3
- "version": "26.11.5",
3
+ "version": "26.11.7",
4
4
  "description": "Lifecycle-owned Web Animations API primitives with native access, per-handle groups, and additive FLIP",
5
5
  "repository": {
6
6
  "type": "git",
@@ -43,22 +43,22 @@
43
43
  "registry": "https://registry.npmjs.org/"
44
44
  },
45
45
  "scripts": {
46
- "build": "vite build && pnpm --silent run build:bundle && pnpm --silent run build:types && pnpm --silent run test:types",
46
+ "build": "vite build && bun run --silent build:bundle && bun run --silent build:types && bun run --silent test:types",
47
47
  "build:bundle": "vite build --config vite.bundle.config.ts",
48
48
  "build:types": "tsc -p tsconfig.declarations.json",
49
49
  "fix": "biome check --write src",
50
50
  "lint": "biome ci src",
51
- "prepublishOnly": "pnpm --silent run build",
51
+ "prepublishOnly": "bun run --silent build",
52
52
  "test": "vitest",
53
- "test:e2e": "pnpm --silent run build && playwright test",
53
+ "test:e2e": "bun run --silent build && playwright test",
54
54
  "test:types": "tsc -p tsconfig.type-tests.json"
55
55
  },
56
56
  "devDependencies": {
57
- "@playwright/test": "^1.64.1",
58
- "@types/node": "^26.6.5",
59
- "jsdom": "^30.1.3",
57
+ "@playwright/test": "^1.65.0",
58
+ "@types/node": "^26.6.6",
59
+ "jsdom": "^31.0.0",
60
60
  "typescript": "^6.0.3",
61
- "vite": "^8.3.4",
62
- "vitest": "^5.0.5"
61
+ "vite": "^8.3.6",
62
+ "vitest": "^5.1.0"
63
63
  }
64
64
  }