egaki 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/dist/cli/cache-utils.d.ts +33 -0
  3. package/dist/cli/cache-utils.d.ts.map +1 -0
  4. package/dist/cli/cache-utils.js +157 -0
  5. package/dist/cli/cached-generate.d.ts +105 -0
  6. package/dist/cli/cached-generate.d.ts.map +1 -0
  7. package/dist/cli/cached-generate.js +208 -0
  8. package/dist/cli/cached-generate.test.d.ts +11 -0
  9. package/dist/cli/cached-generate.test.d.ts.map +1 -0
  10. package/dist/cli/cached-generate.test.js +431 -0
  11. package/dist/cli/cartesia-provider.d.ts +16 -0
  12. package/dist/cli/cartesia-provider.d.ts.map +1 -0
  13. package/dist/cli/cartesia-provider.js +184 -0
  14. package/dist/cli/chatgpt-auth.d.ts.map +1 -0
  15. package/dist/cli/cli.d.ts +4 -0
  16. package/dist/cli/cli.d.ts.map +1 -0
  17. package/dist/cli/cli.js +1329 -0
  18. package/dist/cli/clone-generate.d.ts +26 -0
  19. package/dist/cli/clone-generate.d.ts.map +1 -0
  20. package/dist/cli/clone-generate.js +148 -0
  21. package/dist/{credentials.d.ts → cli/credentials.d.ts} +11 -2
  22. package/dist/cli/credentials.d.ts.map +1 -0
  23. package/dist/{credentials.js → cli/credentials.js} +88 -5
  24. package/dist/cli/elevenlabs-speech-provider.d.ts +26 -0
  25. package/dist/cli/elevenlabs-speech-provider.d.ts.map +1 -0
  26. package/dist/cli/elevenlabs-speech-provider.js +199 -0
  27. package/dist/cli/generate.d.ts +140 -0
  28. package/dist/cli/generate.d.ts.map +1 -0
  29. package/dist/cli/generate.js +604 -0
  30. package/dist/cli/generate.test.d.ts +2 -0
  31. package/dist/cli/generate.test.d.ts.map +1 -0
  32. package/dist/cli/generate.test.js +131 -0
  33. package/dist/cli/login.d.ts.map +1 -0
  34. package/dist/{login.js → cli/login.js} +45 -4
  35. package/dist/cli/main.d.ts +3 -0
  36. package/dist/cli/main.d.ts.map +1 -0
  37. package/dist/cli/main.js +7 -0
  38. package/dist/cli/model-catalog.d.ts +141 -0
  39. package/dist/cli/model-catalog.d.ts.map +1 -0
  40. package/dist/cli/model-catalog.js +2135 -0
  41. package/dist/{models.d.ts → cli/models.d.ts} +16 -9
  42. package/dist/cli/models.d.ts.map +1 -0
  43. package/dist/cli/models.js +313 -0
  44. package/dist/cli/open-browser.d.ts.map +1 -0
  45. package/dist/cli/openai-speech-provider.d.ts +3 -0
  46. package/dist/cli/openai-speech-provider.d.ts.map +1 -0
  47. package/dist/cli/openai-speech-provider.js +28 -0
  48. package/dist/cli/separation-generate.d.ts +34 -0
  49. package/dist/cli/separation-generate.d.ts.map +1 -0
  50. package/dist/cli/separation-generate.js +117 -0
  51. package/dist/cli/speech-catalog.d.ts +35 -0
  52. package/dist/cli/speech-catalog.d.ts.map +1 -0
  53. package/dist/cli/speech-catalog.js +195 -0
  54. package/dist/cli/speech-generate.d.ts +86 -0
  55. package/dist/cli/speech-generate.d.ts.map +1 -0
  56. package/dist/cli/speech-generate.js +156 -0
  57. package/dist/cli/speech-models.d.ts +8 -0
  58. package/dist/cli/speech-models.d.ts.map +1 -0
  59. package/dist/cli/speech-models.js +109 -0
  60. package/dist/cli/speech-provider.test.d.ts +2 -0
  61. package/dist/cli/speech-provider.test.d.ts.map +1 -0
  62. package/dist/cli/speech-provider.test.js +197 -0
  63. package/dist/cli/subscription.d.ts.map +1 -0
  64. package/dist/cli/transcription-catalog.d.ts +35 -0
  65. package/dist/cli/transcription-catalog.d.ts.map +1 -0
  66. package/dist/cli/transcription-catalog.js +192 -0
  67. package/dist/cli/transcription-generate.d.ts +62 -0
  68. package/dist/cli/transcription-generate.d.ts.map +1 -0
  69. package/dist/cli/transcription-generate.js +144 -0
  70. package/dist/cli/transcription-models.d.ts +9 -0
  71. package/dist/cli/transcription-models.d.ts.map +1 -0
  72. package/dist/cli/transcription-models.js +100 -0
  73. package/dist/cli/transcription-providers.d.ts +22 -0
  74. package/dist/cli/transcription-providers.d.ts.map +1 -0
  75. package/dist/cli/transcription-providers.js +290 -0
  76. package/dist/cli/video-model-catalog.d.ts +2 -0
  77. package/dist/cli/video-model-catalog.d.ts.map +1 -0
  78. package/dist/cli/video-model-catalog.js +5 -0
  79. package/dist/cli/xai-auth.d.ts +29 -0
  80. package/dist/cli/xai-auth.d.ts.map +1 -0
  81. package/dist/cli/xai-auth.js +325 -0
  82. package/dist/vite/angled-screen.d.ts +57 -0
  83. package/dist/vite/angled-screen.d.ts.map +1 -0
  84. package/dist/vite/angled-screen.js +115 -0
  85. package/dist/vite/app.d.ts +51 -0
  86. package/dist/vite/app.d.ts.map +1 -0
  87. package/dist/vite/app.js +150 -0
  88. package/dist/vite/bands-shader.d.ts +47 -0
  89. package/dist/vite/bands-shader.d.ts.map +1 -0
  90. package/dist/vite/bands-shader.js +267 -0
  91. package/dist/vite/code-block.d.ts +107 -0
  92. package/dist/vite/code-block.d.ts.map +1 -0
  93. package/dist/vite/code-block.js +641 -0
  94. package/dist/vite/components.d.ts +36 -0
  95. package/dist/vite/components.d.ts.map +1 -0
  96. package/dist/vite/components.js +113 -0
  97. package/dist/vite/dispersion-rings-shader.d.ts +44 -0
  98. package/dist/vite/dispersion-rings-shader.d.ts.map +1 -0
  99. package/dist/vite/dispersion-rings-shader.js +282 -0
  100. package/dist/vite/easing-curves.d.ts +129 -0
  101. package/dist/vite/easing-curves.d.ts.map +1 -0
  102. package/dist/vite/easing-curves.js +584 -0
  103. package/dist/vite/easing-curves.test.d.ts +2 -0
  104. package/dist/vite/easing-curves.test.d.ts.map +1 -0
  105. package/dist/vite/easing-curves.test.js +200 -0
  106. package/dist/vite/generated-media-client.d.ts +35 -0
  107. package/dist/vite/generated-media-client.d.ts.map +1 -0
  108. package/dist/vite/generated-media-client.js +76 -0
  109. package/dist/vite/keyframes.d.ts +129 -0
  110. package/dist/vite/keyframes.d.ts.map +1 -0
  111. package/dist/vite/keyframes.js +194 -0
  112. package/dist/vite/layout-editor.d.ts +45 -0
  113. package/dist/vite/layout-editor.d.ts.map +1 -0
  114. package/dist/vite/layout-editor.js +587 -0
  115. package/dist/vite/layout-transition.d.ts +136 -0
  116. package/dist/vite/layout-transition.d.ts.map +1 -0
  117. package/dist/vite/layout-transition.js +581 -0
  118. package/dist/vite/liquid-gradient-shader.d.ts +62 -0
  119. package/dist/vite/liquid-gradient-shader.d.ts.map +1 -0
  120. package/dist/vite/liquid-gradient-shader.js +413 -0
  121. package/dist/vite/mdx-client.d.ts +15 -0
  122. package/dist/vite/mdx-client.d.ts.map +1 -0
  123. package/dist/vite/mdx-client.js +265 -0
  124. package/dist/vite/mdx-parse.d.ts +102 -0
  125. package/dist/vite/mdx-parse.d.ts.map +1 -0
  126. package/dist/vite/mdx-parse.js +251 -0
  127. package/dist/vite/mdx-provided-components.d.ts +18 -0
  128. package/dist/vite/mdx-provided-components.d.ts.map +1 -0
  129. package/dist/vite/mdx-provided-components.js +14 -0
  130. package/dist/vite/mdx-video.d.ts +327 -0
  131. package/dist/vite/mdx-video.d.ts.map +1 -0
  132. package/dist/vite/mdx-video.js +596 -0
  133. package/dist/vite/mdx-video.test.d.ts +9 -0
  134. package/dist/vite/mdx-video.test.d.ts.map +1 -0
  135. package/dist/vite/mdx-video.test.js +1945 -0
  136. package/dist/vite/media-components.d.ts +45 -0
  137. package/dist/vite/media-components.d.ts.map +1 -0
  138. package/dist/vite/media-components.js +355 -0
  139. package/dist/vite/media-duration-store.d.ts +66 -0
  140. package/dist/vite/media-duration-store.d.ts.map +1 -0
  141. package/dist/vite/media-duration-store.js +204 -0
  142. package/dist/vite/player-page.d.ts +35 -0
  143. package/dist/vite/player-page.d.ts.map +1 -0
  144. package/dist/vite/player-page.js +720 -0
  145. package/dist/vite/render-client.d.ts +18 -0
  146. package/dist/vite/render-client.d.ts.map +1 -0
  147. package/dist/vite/render-client.js +67 -0
  148. package/dist/vite/sdk.d.ts +221 -0
  149. package/dist/vite/sdk.d.ts.map +1 -0
  150. package/dist/vite/sdk.js +355 -0
  151. package/dist/vite/server-components.d.ts +55 -0
  152. package/dist/vite/server-components.d.ts.map +1 -0
  153. package/dist/vite/server-components.js +156 -0
  154. package/dist/vite/server-mdx.d.ts +74 -0
  155. package/dist/vite/server-mdx.d.ts.map +1 -0
  156. package/dist/vite/server-mdx.js +259 -0
  157. package/dist/vite/shader-renderer.d.ts +48 -0
  158. package/dist/vite/shader-renderer.d.ts.map +1 -0
  159. package/dist/vite/shader-renderer.js +411 -0
  160. package/dist/vite/store-hooks.d.ts +12 -0
  161. package/dist/vite/store-hooks.d.ts.map +1 -0
  162. package/dist/vite/store-hooks.js +93 -0
  163. package/dist/vite/store.d.ts +75 -0
  164. package/dist/vite/store.d.ts.map +1 -0
  165. package/dist/vite/store.js +52 -0
  166. package/dist/vite/store.test.d.ts +6 -0
  167. package/dist/vite/store.test.d.ts.map +1 -0
  168. package/dist/vite/store.test.js +172 -0
  169. package/dist/vite/tweakpane-hook.d.ts +94 -0
  170. package/dist/vite/tweakpane-hook.d.ts.map +1 -0
  171. package/dist/vite/tweakpane-hook.js +450 -0
  172. package/dist/vite/vite-plugin.d.ts +28 -0
  173. package/dist/vite/vite-plugin.d.ts.map +1 -0
  174. package/dist/vite/vite-plugin.js +538 -0
  175. package/dist/vite/wave-gradient-shader.d.ts +41 -0
  176. package/dist/vite/wave-gradient-shader.d.ts.map +1 -0
  177. package/dist/vite/wave-gradient-shader.js +207 -0
  178. package/package.json +103 -25
  179. package/skills/egaki/SKILL.md +162 -1
  180. package/src/cli/cache-utils.ts +169 -0
  181. package/src/cli/cached-generate.test.ts +531 -0
  182. package/src/cli/cached-generate.ts +348 -0
  183. package/src/cli/cartesia-provider.ts +249 -0
  184. package/src/cli/cli.ts +1818 -0
  185. package/src/cli/clone-generate.ts +202 -0
  186. package/src/{credentials.ts → cli/credentials.ts} +90 -7
  187. package/src/cli/elevenlabs-speech-provider.ts +243 -0
  188. package/src/cli/generate.test.ts +166 -0
  189. package/src/cli/generate.ts +912 -0
  190. package/src/{login.ts → cli/login.ts} +48 -3
  191. package/src/cli/main.ts +8 -0
  192. package/src/cli/model-catalog.ts +2336 -0
  193. package/src/cli/models.ts +408 -0
  194. package/src/cli/openai-speech-provider.ts +34 -0
  195. package/src/cli/separation-generate.ts +187 -0
  196. package/src/cli/speech-catalog.ts +253 -0
  197. package/src/cli/speech-generate.ts +275 -0
  198. package/src/cli/speech-models.ts +146 -0
  199. package/src/cli/speech-provider.test.ts +226 -0
  200. package/src/cli/transcription-catalog.ts +250 -0
  201. package/src/cli/transcription-generate.ts +214 -0
  202. package/src/cli/transcription-models.ts +136 -0
  203. package/src/cli/transcription-providers.ts +427 -0
  204. package/src/cli/video-model-catalog.ts +14 -0
  205. package/src/cli/xai-auth.ts +396 -0
  206. package/src/vite/angled-screen.tsx +214 -0
  207. package/src/vite/app.tsx +197 -0
  208. package/src/vite/bands-shader.tsx +319 -0
  209. package/src/vite/code-block.tsx +939 -0
  210. package/src/vite/components.tsx +278 -0
  211. package/src/vite/dispersion-rings-shader.tsx +331 -0
  212. package/src/vite/easing-curves.test.ts +232 -0
  213. package/src/vite/easing-curves.ts +743 -0
  214. package/src/vite/generated-media-client.tsx +121 -0
  215. package/src/vite/keyframes.tsx +347 -0
  216. package/src/vite/layout-editor.tsx +709 -0
  217. package/src/vite/layout-transition.tsx +706 -0
  218. package/src/vite/liquid-gradient-shader.tsx +474 -0
  219. package/src/vite/mdx-client.tsx +420 -0
  220. package/src/vite/mdx-parse.ts +343 -0
  221. package/src/vite/mdx-provided-components.ts +19 -0
  222. package/src/vite/mdx-video.test.tsx +2219 -0
  223. package/src/vite/mdx-video.tsx +976 -0
  224. package/src/vite/media-components.tsx +473 -0
  225. package/src/vite/media-duration-store.ts +225 -0
  226. package/src/vite/motion-timing.ts +194 -0
  227. package/src/vite/player-page.tsx +1107 -0
  228. package/src/vite/render-client.ts +84 -0
  229. package/src/vite/sdk.ts +598 -0
  230. package/src/vite/server-components.tsx +241 -0
  231. package/src/vite/server-mdx.ts +285 -0
  232. package/src/vite/shader-renderer.tsx +535 -0
  233. package/src/vite/store-hooks.ts +117 -0
  234. package/src/vite/store.test.ts +202 -0
  235. package/src/vite/store.ts +115 -0
  236. package/src/vite/styles.css +21 -0
  237. package/src/vite/tweakpane-hook.tsx +593 -0
  238. package/src/vite/virtual-modules.d.ts +30 -0
  239. package/src/vite/vite-plugin.ts +584 -0
  240. package/src/vite/wave-gradient-shader.tsx +253 -0
  241. package/README.md +0 -266
  242. package/dist/antigravity-auth.d.ts +0 -15
  243. package/dist/antigravity-auth.d.ts.map +0 -1
  244. package/dist/antigravity-auth.js +0 -544
  245. package/dist/antigravity-image.d.ts +0 -11
  246. package/dist/antigravity-image.d.ts.map +0 -1
  247. package/dist/antigravity-image.js +0 -285
  248. package/dist/chatgpt-auth.d.ts.map +0 -1
  249. package/dist/cli.d.ts +0 -3
  250. package/dist/cli.d.ts.map +0 -1
  251. package/dist/cli.js +0 -968
  252. package/dist/credentials.d.ts.map +0 -1
  253. package/dist/gateway-model-candidates.generated.d.ts +0 -11
  254. package/dist/gateway-model-candidates.generated.d.ts.map +0 -1
  255. package/dist/gateway-model-candidates.generated.js +0 -64
  256. package/dist/login.d.ts.map +0 -1
  257. package/dist/model-catalog.d.ts +0 -42
  258. package/dist/model-catalog.d.ts.map +0 -1
  259. package/dist/model-catalog.js +0 -1001
  260. package/dist/models.d.ts.map +0 -1
  261. package/dist/models.js +0 -246
  262. package/dist/open-browser.d.ts.map +0 -1
  263. package/dist/subscription.d.ts.map +0 -1
  264. package/dist/video-model-catalog.d.ts +0 -52
  265. package/dist/video-model-catalog.d.ts.map +0 -1
  266. package/dist/video-model-catalog.js +0 -306
  267. package/src/cli.ts +0 -1378
  268. package/src/model-catalog.ts +0 -1077
  269. package/src/models.ts +0 -309
  270. package/src/video-model-catalog.ts +0 -363
  271. /package/dist/{chatgpt-auth.d.ts → cli/chatgpt-auth.d.ts} +0 -0
  272. /package/dist/{chatgpt-auth.js → cli/chatgpt-auth.js} +0 -0
  273. /package/dist/{login.d.ts → cli/login.d.ts} +0 -0
  274. /package/dist/{open-browser.d.ts → cli/open-browser.d.ts} +0 -0
  275. /package/dist/{open-browser.js → cli/open-browser.js} +0 -0
  276. /package/dist/{subscription.d.ts → cli/subscription.d.ts} +0 -0
  277. /package/dist/{subscription.js → cli/subscription.js} +0 -0
  278. /package/src/{chatgpt-auth.ts → cli/chatgpt-auth.ts} +0 -0
  279. /package/src/{open-browser.ts → cli/open-browser.ts} +0 -0
  280. /package/src/{subscription.ts → cli/subscription.ts} +0 -0
@@ -0,0 +1,743 @@
1
+ /**
2
+ * Easing curve engine for spring/bounce/overshoot presets.
3
+ *
4
+ * Ported from Jitter's easing engine (jitter.video webpack bundle,
5
+ * module 78102). The presets were previously stored as hardcoded sample
6
+ * arrays extracted from Jitter; they are now generated from the same
7
+ * three primitives Jitter uses:
8
+ *
9
+ * 1. Polybezier paths: control points with handles, linearly interpolated
10
+ * between intensity configs (d3-interpolate semantics), each segment
11
+ * evaluated with an analytic (Cardano) cubic-bezier y(x) solver.
12
+ * 2. Spring physics (elasticSnap): semi-implicit Euler, step 0.005,
13
+ * settle epsilon 1e-10, timeline truncated at 34% of settle time.
14
+ * 3. Bounce physics: explicit Euler, step 1, gravity 9.8e-6, velocity
15
+ * flipped and scaled by bounceFactor when crossing position 100,
16
+ * capped at 16 bounces.
17
+ *
18
+ * Each `*Samples` export contains 51 evenly-spaced points (t=0 to t=1)
19
+ * per intensity level (0, 25, 50, 75, 100), rounded to 4 decimals exactly
20
+ * like Jitter does. The generated values are verified against the original
21
+ * extracted arrays in easing-curves.test.ts. Values can exceed the 0-1
22
+ * range for overshoot and bounce effects.
23
+ *
24
+ * Porting lessons (do not "simplify" these or the outputs change):
25
+ * - Lerp must be `a * (1 - t) + b * t` (d3-interpolate form), not
26
+ * `a + (b - a) * t`, to stay bit-identical with Jitter's output.
27
+ * - The intensity pair search keeps the LOWER key on distance ties
28
+ * (strict `<` in the reduce), so intensity 25 lerps configs 0..50.
29
+ * - Spring acceleration must keep Jitter's exact expression order:
30
+ * `(-tension * (pos - 100) + -friction * v) / mass`.
31
+ * - Rounding is `Math.round((x + Number.EPSILON) * 1e4) / 1e4`.
32
+ */
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Cubic bezier y(x) solver — analytic Cardano roots
36
+ // ---------------------------------------------------------------------------
37
+
38
+ const identity = (e: number) => e
39
+
40
+ /** Solve the cubic for the bezier parameter via Cardano's formula. */
41
+ function cardano(e: number, t: number, n: number, r: number, i: number): number {
42
+ const { cbrt, sqrt, PI } = Math
43
+ const o = t + n * e
44
+ const a = o ** 2 + r
45
+ if (a > 0) {
46
+ const root = sqrt(a)
47
+ return cbrt(o + root) + cbrt(o - root) - i
48
+ }
49
+ const s = cbrt(sqrt(o * o - a))
50
+ const c = o ? Math.atan(sqrt(-a) / o) : -PI / 2
51
+ const u =
52
+ n < 0
53
+ ? (o > 0 ? 2 * PI : PI) - c
54
+ : i < 0
55
+ ? (o > 0 ? 2 * PI : -3 * PI) + c
56
+ : (o > 0 ? 0 : PI) + c
57
+ return 2 * s * Math.cos(u / 3) - i
58
+ }
59
+
60
+ /** Evaluate the cubic bezier polynomial at parameter e. */
61
+ const evalCubic = (e: number, t: number, n: number, r: number) =>
62
+ ((t * e + 3 * n) * e + r) * e
63
+
64
+ /** Symbol key for attaching bezier control points to easing functions. */
65
+ export const BEZIER_POINTS = Symbol.for('egaki:bezierPoints')
66
+
67
+ /**
68
+ * cubic-bezier(x1, y1, x2, y2) as a y(x) function. x1/x2 must be in [0, 1];
69
+ * y values may exceed [0, 1] for overshoot.
70
+ *
71
+ * The returned function has a `[BEZIER_POINTS]` property with the original
72
+ * `[x1, y1, x2, y2]` tuple so the tweakpane bezier blade can show and edit
73
+ * the exact curve.
74
+ */
75
+ export function cubicBezier(
76
+ x1: number,
77
+ y1: number,
78
+ x2: number,
79
+ y2: number,
80
+ ): (x: number) => number {
81
+ if (!(0 <= x1 && x1 <= 1 && 0 <= x2 && x2 <= 1)) {
82
+ throw new Error('Bezier x values must be in [0, 1] range')
83
+ }
84
+ if (x1 === y1 && x2 === y2) {
85
+ const fn = identity as any
86
+ fn[BEZIER_POINTS] = [x1, y1, x2, y2] as [number, number, number, number]
87
+ return fn
88
+ }
89
+ const i = 6 * (3 * x1 - 3 * x2 + 1)
90
+ const o = 6 * (x2 - 2 * x1)
91
+ const a = 3 * x1
92
+ const s = i * i
93
+ const c = o * o
94
+ const u = o / i
95
+ const l = (3 * o * a) / s - (c * o) / (s * i)
96
+ const d = (2 * a) / i - c / s
97
+ const f = d * d * d
98
+ const p = 3 / i
99
+ const h = 3 * y1 - 3 * y2 + 1
100
+ const m = y2 - 2 * y1
101
+ const y = 3 * y1
102
+ const solve = i ? cardano : identity
103
+ const fn = ((e: number) =>
104
+ 0 === e || 1 === e ? e : evalCubic(solve(e, l, p, f, u), h, m, y)) as any
105
+ fn[BEZIER_POINTS] = [x1, y1, x2, y2] as [number, number, number, number]
106
+ return fn
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Polybezier path: control points with handles
111
+ // ---------------------------------------------------------------------------
112
+
113
+ /** Handle: x is a fraction toward the neighbor anchor, y is in value units. */
114
+ export interface HandleXY {
115
+ x: number
116
+ y: number
117
+ }
118
+
119
+ /** A bare number is shorthand for `{ x: number, y: 0 }`. */
120
+ export type Handle = number | HandleXY
121
+
122
+ export interface ControlPoint {
123
+ x: number
124
+ y: number
125
+ /** Handle toward the previous point (incoming). */
126
+ lower?: Handle
127
+ /** Handle toward the next point (outgoing). */
128
+ upper?: Handle
129
+ }
130
+
131
+ function normalizeHandle(h: Handle | undefined): HandleXY | undefined {
132
+ if (h === undefined) return undefined
133
+ return typeof h === 'number' ? { x: h, y: 0 } : h
134
+ }
135
+
136
+ /** Scale a handle fraction by the x-distance to the neighbor anchor. */
137
+ function scaleHandle(dx: number, h: Handle | undefined): [number, number] | undefined {
138
+ const n = normalizeHandle(h)
139
+ if (n === undefined) return undefined
140
+ // `+ 0` normalizes -0 (e.g. handle x of 0 scaled by a negative dx), like Jitter.
141
+ return [n.x * dx + 0, n.y * dx + 0]
142
+ }
143
+
144
+ interface PolyPoint {
145
+ p: [number, number]
146
+ lower?: [number, number]
147
+ upper?: [number, number]
148
+ }
149
+
150
+ /** Build a y(x) easing function from polybezier control points. */
151
+ export function polybezier(points: ControlPoint[]): (x: number) => number {
152
+ const pts: PolyPoint[] = points.map((pt, n) => {
153
+ const out: PolyPoint = { p: [pt.x, pt.y] }
154
+ const next = points[n + 1]
155
+ const prev = points[n - 1]
156
+ if (n !== 0 && pt.lower !== undefined) {
157
+ out.lower = scaleHandle(prev!.x - pt.x, pt.lower)
158
+ }
159
+ if (n !== points.length - 1 && pt.upper !== undefined) {
160
+ out.upper = scaleHandle(next!.x - pt.x, pt.upper)
161
+ }
162
+ return out
163
+ })
164
+
165
+ // One bezier per segment; null when the segment is degenerate (dx or dy 0).
166
+ const segments: (((x: number) => number) | null)[] = []
167
+ {
168
+ let prev = pts[0]!
169
+ for (let o = 1; o < pts.length; o++) {
170
+ const cur = pts[o]!
171
+ const dx = cur.p[0] - prev.p[0]
172
+ const dy = cur.p[1] - prev.p[1]
173
+ const out = prev.upper || [0, 0]
174
+ const inc = cur.lower || [0, 0]
175
+ if (dx && dy) {
176
+ segments.push(cubicBezier(out[0] / dx, out[1] / dy, 1 + inc[0] / dx, 1 + inc[1] / dy))
177
+ } else {
178
+ segments.push(null)
179
+ }
180
+ prev = cur
181
+ }
182
+ }
183
+
184
+ return function (x) {
185
+ let before: PolyPoint | undefined
186
+ let point = pts[0]!
187
+ let o = 0
188
+ for (o = 0; o < pts.length; o++) {
189
+ point = pts[o]!
190
+ if (x < point.p[0]) break
191
+ if (x === point.p[0]) return point.p[1]
192
+ before = point
193
+ }
194
+ const anchorX = point.p[0]
195
+ if (!before || x > anchorX) {
196
+ // Extrapolate beyond the endpoints along the boundary handle.
197
+ const handle = (x > anchorX && point.upper) || (x < anchorX && point.lower)
198
+ return (
199
+ point.p[1] +
200
+ (handle && 0 !== handle[1] ? ((x - anchorX) * handle[1]) / handle[0] : 0)
201
+ )
202
+ }
203
+ const segment = segments[o - 1]
204
+ if (!segment) return before.p[1]
205
+ const dx = point.p[0] - before.p[0]
206
+ const dy = point.p[1] - before.p[1]
207
+ return before.p[1] + dy * segment((x - before.p[0]) / dx)
208
+ }
209
+ }
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // Intensity interpolation between configs
213
+ // ---------------------------------------------------------------------------
214
+
215
+ interface IntensityPair<T> {
216
+ adjustedIntensity: number
217
+ pair: [T, T]
218
+ }
219
+
220
+ /** Pick the two configs bracketing `t` and the local interpolation factor. */
221
+ function findIntensityPair<T>(configs: Record<number, T>, t: number): IntensityPair<T> {
222
+ const keys = Object.keys(configs)
223
+ .map(Number)
224
+ .sort((a, b) => a - b)
225
+ if (keys.length < 2) throw new Error('Too few items to find a pair.')
226
+ // On distance ties the reduce keeps the LOWER key (strict <), matching Jitter.
227
+ const nearest = keys.reduce((e, n) => (Math.abs(n - t) < Math.abs(e - t) ? n : e))
228
+ const o = keys.indexOf(nearest)
229
+ let pair: [number, number]
230
+ if (nearest < t) pair = [nearest, keys[o + 1]!]
231
+ else if (nearest > t) pair = [keys[o - 1]!, nearest]
232
+ else pair = 0 === o ? [nearest, keys[o + 1]!] : [keys[o - 1]!, nearest]
233
+ return {
234
+ adjustedIntensity: (t - pair[0]) / (pair[1] - pair[0]),
235
+ pair: [configs[pair[0]]!, configs[pair[1]]!],
236
+ }
237
+ }
238
+
239
+ /** d3-interpolate number form: a*(1-t) + b*t. Order matters for FP exactness. */
240
+ const lerpNumber = (a: number, b: number, t: number) => a * (1 - t) + b * t
241
+
242
+ function lerpHandle(
243
+ a: HandleXY | undefined,
244
+ b: HandleXY | undefined,
245
+ t: number,
246
+ ): HandleXY | undefined {
247
+ if (a === undefined || b === undefined) return b
248
+ return { x: lerpNumber(a.x, b.x, t), y: lerpNumber(a.y, b.y, t) }
249
+ }
250
+
251
+ function lerpControlPoints(
252
+ a: ControlPoint[],
253
+ b: ControlPoint[],
254
+ t: number,
255
+ ): ControlPoint[] {
256
+ return b.map((pb, i) => {
257
+ const pa = a[i]!
258
+ return {
259
+ x: lerpNumber(pa.x, pb.x, t),
260
+ y: lerpNumber(pa.y, pb.y, t),
261
+ lower: lerpHandle(normalizeHandle(pa.lower), normalizeHandle(pb.lower), t),
262
+ upper: lerpHandle(normalizeHandle(pa.upper), normalizeHandle(pb.upper), t),
263
+ }
264
+ })
265
+ }
266
+
267
+ /** An easing preset parameterized by intensity (0-100, continuous). */
268
+ export type EasingPreset = (intensity: number) => (t: number) => number
269
+
270
+ /** Polybezier preset parameterized by intensity. */
271
+ export function pathPreset(configs: Record<number, ControlPoint[]>): EasingPreset {
272
+ return (intensity) => {
273
+ const { adjustedIntensity, pair } = findIntensityPair(configs, intensity)
274
+ return polybezier(lerpControlPoints(pair[0], pair[1], adjustedIntensity))
275
+ }
276
+ }
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // Physics integration
280
+ // ---------------------------------------------------------------------------
281
+
282
+ interface PhysicsState {
283
+ position: number
284
+ velocity: number
285
+ }
286
+
287
+ interface PhysicsConfig {
288
+ integrationType?: 'semi-implicit' | 'explicit'
289
+ getAcceleration: (position: number, velocity: number) => number
290
+ isFinished: (previous: PhysicsState, current: PhysicsState) => boolean
291
+ applySideEffects?: (state: PhysicsState, step: number) => PhysicsState
292
+ v0: number
293
+ step?: number
294
+ /** Fraction of the simulated timeline mapped onto t in [0, 1]. */
295
+ sliceIndexRatio?: number
296
+ finalPosition?: number
297
+ }
298
+
299
+ /**
300
+ * Integrate a 1D physics simulation and return an easing function that maps
301
+ * t in [0, 1] onto the (possibly truncated) recorded timeline, normalized by
302
+ * the final position.
303
+ */
304
+ function physicsEasing(config: PhysicsConfig): (t: number) => number {
305
+ const {
306
+ integrationType = 'semi-implicit',
307
+ getAcceleration,
308
+ isFinished,
309
+ applySideEffects,
310
+ v0,
311
+ step = 1,
312
+ sliceIndexRatio = 1,
313
+ } = config
314
+ const state: PhysicsState = { position: 0, velocity: v0 }
315
+ const timeline: number[] = [0]
316
+ const advance = (dt: number) => {
317
+ const a = getAcceleration(state.position, state.velocity)
318
+ if ('semi-implicit' === integrationType) {
319
+ state.velocity = state.velocity + a * dt
320
+ state.position = state.position + state.velocity * dt
321
+ } else {
322
+ state.position = state.position + state.velocity * dt
323
+ state.velocity = state.velocity + a * dt
324
+ }
325
+ if (applySideEffects !== undefined) {
326
+ const next = applySideEffects(state, dt)
327
+ state.position = next.position
328
+ state.velocity = next.velocity
329
+ }
330
+ timeline.push(state.position)
331
+ }
332
+ let previous: PhysicsState = { ...state }
333
+ for (advance(step); !isFinished(previous, state); ) {
334
+ previous = { ...state }
335
+ advance(step)
336
+ }
337
+ const lastIndex = Math.floor((timeline.length - 1) * sliceIndexRatio)
338
+ const finalPosition = config.finalPosition ?? timeline[timeline.length - 1]!
339
+ return (t) => {
340
+ const idx = t * lastIndex
341
+ const lo = Math.floor(idx)
342
+ let position: number
343
+ if (lo === timeline.length - 1) position = timeline[lo]!
344
+ else {
345
+ const a = timeline[lo]!
346
+ position = a + (timeline[lo + 1]! - a) * (idx - lo)
347
+ }
348
+ return position / finalPosition
349
+ }
350
+ }
351
+
352
+ // ---------------------------------------------------------------------------
353
+ // Spring — used by elasticSnap
354
+ // ---------------------------------------------------------------------------
355
+
356
+ export interface SpringPhysicsConfig {
357
+ mass: number
358
+ tension: number
359
+ friction: number
360
+ v0: number
361
+ }
362
+
363
+ const SPRING_TARGET = 100
364
+ const SPRING_EPSILON = 1e-10
365
+
366
+ function springEasing(config: SpringPhysicsConfig): (t: number) => number {
367
+ const { mass, tension, friction, v0 } = config
368
+ return physicsEasing({
369
+ v0,
370
+ getAcceleration: (position, velocity) =>
371
+ (-tension * (position - SPRING_TARGET) + -friction * velocity) / mass,
372
+ isFinished: (previous, current) =>
373
+ Math.abs(current.velocity) < SPRING_EPSILON &&
374
+ Math.abs(current.position - SPRING_TARGET) < SPRING_EPSILON,
375
+ finalPosition: SPRING_TARGET,
376
+ step: 0.005,
377
+ sliceIndexRatio: 0.34,
378
+ })
379
+ }
380
+
381
+ function lerpSpringConfig(
382
+ a: SpringPhysicsConfig,
383
+ b: SpringPhysicsConfig,
384
+ t: number,
385
+ ): SpringPhysicsConfig {
386
+ return {
387
+ mass: lerpNumber(a.mass, b.mass, t),
388
+ tension: lerpNumber(a.tension, b.tension, t),
389
+ friction: lerpNumber(a.friction, b.friction, t),
390
+ v0: lerpNumber(a.v0, b.v0, t),
391
+ }
392
+ }
393
+
394
+ /** Spring preset parameterized by intensity. */
395
+ export function springPreset(configs: Record<number, SpringPhysicsConfig>): EasingPreset {
396
+ return (intensity) => {
397
+ const { adjustedIntensity, pair } = findIntensityPair(configs, intensity)
398
+ return springEasing(lerpSpringConfig(pair[0], pair[1], adjustedIntensity))
399
+ }
400
+ }
401
+
402
+ // ---------------------------------------------------------------------------
403
+ // Bounce
404
+ // ---------------------------------------------------------------------------
405
+
406
+ export interface BouncePhysicsConfig {
407
+ mass: number
408
+ /** Fraction of velocity kept (and flipped) at each bounce. */
409
+ bounceFactor: number
410
+ friction: number
411
+ v0: number
412
+ }
413
+
414
+ const BOUNCE_TARGET = 100
415
+ const BOUNCE_EPSILON = 1e-4
416
+ const GRAVITY = 9.8 / 1e6
417
+
418
+ function bounceEasing(config: BouncePhysicsConfig): (t: number) => number {
419
+ const { mass, bounceFactor, friction, v0 } = config
420
+ let bounces = 0
421
+ return physicsEasing({
422
+ v0,
423
+ step: 1,
424
+ integrationType: 'explicit',
425
+ finalPosition: BOUNCE_TARGET,
426
+ getAcceleration: (position, velocity) =>
427
+ (GRAVITY * mass + -friction * velocity) / mass,
428
+ isFinished: (previous, current) =>
429
+ (Math.abs(current.velocity) < BOUNCE_EPSILON &&
430
+ Math.abs(current.position - BOUNCE_TARGET) < BOUNCE_EPSILON) ||
431
+ bounces > 15,
432
+ applySideEffects: (state, dt) => {
433
+ const { position, velocity } = state
434
+ if (position < BOUNCE_TARGET) return state
435
+ // Time elapsed since crossing the floor within this step.
436
+ const overshootTime = dt - Math.abs(position - BOUNCE_TARGET) / Math.abs(velocity)
437
+ const bounceVelocity = -velocity * bounceFactor
438
+ bounces += 1
439
+ return {
440
+ position: BOUNCE_TARGET + overshootTime * bounceVelocity,
441
+ velocity: bounceVelocity,
442
+ }
443
+ },
444
+ })
445
+ }
446
+
447
+ function lerpBounceConfig(
448
+ a: BouncePhysicsConfig,
449
+ b: BouncePhysicsConfig,
450
+ t: number,
451
+ ): BouncePhysicsConfig {
452
+ return {
453
+ mass: lerpNumber(a.mass, b.mass, t),
454
+ bounceFactor: lerpNumber(a.bounceFactor, b.bounceFactor, t),
455
+ friction: lerpNumber(a.friction, b.friction, t),
456
+ v0: lerpNumber(a.v0, b.v0, t),
457
+ }
458
+ }
459
+
460
+ /** Bounce preset parameterized by intensity. */
461
+ export function bouncePreset(configs: Record<number, BouncePhysicsConfig>): EasingPreset {
462
+ return (intensity) => {
463
+ const { adjustedIntensity, pair } = findIntensityPair(configs, intensity)
464
+ return bounceEasing(lerpBounceConfig(pair[0], pair[1], adjustedIntensity))
465
+ }
466
+ }
467
+
468
+ // ---------------------------------------------------------------------------
469
+ // Sampling
470
+ // ---------------------------------------------------------------------------
471
+
472
+ export type Intensity = 0 | 25 | 50 | 75 | 100
473
+
474
+ /** Round to 4 decimals like Jitter; normalizes -0 to 0. */
475
+ function round4(value: number): number {
476
+ const rounded = Math.round((value + Number.EPSILON) * 1e4) / 1e4
477
+ return rounded === 0 ? 0 : rounded
478
+ }
479
+
480
+ /** Sample a preset at 51 evenly spaced points for all 5 intensity levels. */
481
+ export function samplePreset(preset: EasingPreset): Record<Intensity, number[]> {
482
+ const out = {} as Record<Intensity, number[]>
483
+ for (const intensity of [0, 25, 50, 75, 100] as const) {
484
+ const easing = preset(intensity)
485
+ out[intensity] = Array.from({ length: 51 }, (_, i) => round4(easing(i / 50)))
486
+ }
487
+ return out
488
+ }
489
+
490
+ // ---------------------------------------------------------------------------
491
+ // Jitter preset configs (verbatim from the bundle)
492
+ // ---------------------------------------------------------------------------
493
+
494
+ /** natural:throw:v1 — anticipate backward, fly past, settle */
495
+ export const naturalThrow = pathPreset({
496
+ 0: [
497
+ { x: 0, y: 0, upper: 0.6 },
498
+ { x: 0.33, y: -0.1, lower: 0.5, upper: 0.5 },
499
+ { x: 0.67, y: 1.2, lower: 0.2, upper: 0.2 },
500
+ { x: 1, y: 1, lower: 0.7 },
501
+ ],
502
+ 50: [
503
+ { x: 0, y: 0, upper: 0.7 },
504
+ { x: 0.33, y: -0.2, lower: 0.8, upper: 0.8 },
505
+ { x: 0.67, y: 1.3, lower: 0.1, upper: 0.1 },
506
+ { x: 1, y: 1, lower: 0.8 },
507
+ ],
508
+ 100: [
509
+ { x: 0, y: 0, upper: 0.8 },
510
+ { x: 0.33, y: -0.4, lower: 0.9, upper: 0.9 },
511
+ { x: 0.67, y: 1.5, lower: 0.05, upper: 0.05 },
512
+ { x: 1, y: 1, lower: 0.9 },
513
+ ],
514
+ })
515
+
516
+ /** slowdown:overshoot:v1 — decelerate with a single overshoot */
517
+ export const decelerateOvershoot = pathPreset({
518
+ 0: [
519
+ { x: 0, y: 0, upper: 0 },
520
+ { x: 0.35, y: 1.1, lower: 0.8, upper: 0.2 },
521
+ { x: 1, y: 1, lower: 0.7 },
522
+ ],
523
+ 50: [
524
+ { x: 0, y: 0, upper: 0 },
525
+ { x: 0.3, y: 1.3, lower: 0.8, upper: 0.2 },
526
+ { x: 1, y: 1, lower: 0.7 },
527
+ ],
528
+ 100: [
529
+ { x: 0, y: 0, upper: 0 },
530
+ { x: 0.22, y: 1.5, lower: 1, upper: 0.2 },
531
+ { x: 1, y: 1, lower: 0.7 },
532
+ ],
533
+ })
534
+
535
+ /** slowdown:elasticOvershoot:v1 — decelerate with elastic overshoot */
536
+ export const decelerateElastic = pathPreset({
537
+ 0: [
538
+ { x: 0, y: 0, upper: 0 },
539
+ { x: 0.5, y: 1.1, lower: 0.9, upper: 0.3 },
540
+ { x: 1, y: 1, lower: 0.5 },
541
+ ],
542
+ 50: [
543
+ { x: 0, y: 0, upper: 0 },
544
+ { x: 0.5, y: 1.2, lower: 1, upper: 0.4 },
545
+ { x: 1, y: 1, lower: 0.7 },
546
+ ],
547
+ 100: [
548
+ { x: 0, y: 0, upper: { x: 0, y: 0.5 } },
549
+ { x: 0.5, y: 1.5, lower: 1, upper: 0.7 },
550
+ { x: 1, y: 1, lower: 0.8 },
551
+ ],
552
+ })
553
+
554
+ /** accelerate:impulse:v1 — dip backward then accelerate away */
555
+ export const accelerateImpulse = pathPreset({
556
+ 0: [
557
+ { x: 0, y: 0, upper: 0.4 },
558
+ { x: 0.4, y: -0.1, lower: 0.4, upper: 0.7 },
559
+ { x: 1, y: 1, lower: 0 },
560
+ ],
561
+ 50: [
562
+ { x: 0, y: 0, upper: 0.4 },
563
+ { x: 0.5, y: -0.2, lower: 0.4, upper: 0.8 },
564
+ { x: 1, y: 1, lower: 0 },
565
+ ],
566
+ 100: [
567
+ { x: 0, y: 0, upper: 0.4 },
568
+ { x: 0.63, y: -0.5, lower: 0.4, upper: 1 },
569
+ { x: 1, y: 1, lower: 0 },
570
+ ],
571
+ })
572
+
573
+ /** accelerate:elastic:v1 — elastic windup then accelerate away */
574
+ export const accelerateElastic = pathPreset({
575
+ 0: [
576
+ { x: 0, y: 0, upper: 0.6 },
577
+ { x: 0.5, y: -0.1, lower: 0.5, upper: 1 },
578
+ { x: 1, y: 1, lower: 0 },
579
+ ],
580
+ 50: [
581
+ { x: 0, y: 0, upper: 0.7 },
582
+ { x: 0.5, y: -0.2, lower: 0.6, upper: 1 },
583
+ { x: 1, y: 1, lower: 0 },
584
+ ],
585
+ 100: [
586
+ { x: 0, y: 0, upper: 0.8 },
587
+ { x: 0.5, y: -0.5, lower: 0.9, upper: 1 },
588
+ { x: 1, y: 1, lower: { x: 0, y: 0.5 } },
589
+ ],
590
+ })
591
+
592
+ /** elastic:standard:v1 — spring snap with ringing oscillation */
593
+ export const elasticSnap = springPreset({
594
+ 0: { mass: 1, tension: 80, friction: 12.522, v0: 0 },
595
+ 50: { mass: 1, tension: 80, friction: 8.944, v0: 0 },
596
+ 100: { mass: 1, tension: 80, friction: 4.472, v0: 0 },
597
+ })
598
+
599
+ /** bounce:standard:v1 — like a ball dropping */
600
+ export const bounce = bouncePreset({
601
+ 0: { mass: 1, bounceFactor: 0.2, friction: 0, v0: 0 },
602
+ 50: { mass: 1, bounceFactor: 0.5, friction: 0, v0: 0 },
603
+ 100: { mass: 1, bounceFactor: 0.7, friction: 0, v0: 0 },
604
+ })
605
+
606
+ /** bounce:anticipate:v1 — pull back first, then bounce into place */
607
+ export const bounceAnticipate = bouncePreset({
608
+ 0: { mass: 20, bounceFactor: 0.7, friction: 0.001, v0: -0.05 },
609
+ 100: { mass: 2, bounceFactor: 0.5, friction: 0, v0: -0.05 },
610
+ })
611
+
612
+ /** bounce:throw:v1 — thrown with initial velocity, bounces on landing */
613
+ export const bounceThrow = bouncePreset({
614
+ 0: { mass: 1, bounceFactor: 0.7, friction: 0, v0: 0.02 },
615
+ 100: { mass: 1, bounceFactor: 0.3, friction: 0, v0: 0.1 },
616
+ })
617
+
618
+ /** impulse:standard:v1 — slow dip then gradual release */
619
+ export const impulseSlow = pathPreset({
620
+ 0: [
621
+ { x: 0, y: 0, upper: 0.3 },
622
+ { x: 0.17, y: -0.05, lower: 0.25, upper: 0.25 },
623
+ { x: 1, y: 1, lower: 0.7 },
624
+ ],
625
+ 50: [
626
+ { x: 0, y: 0, upper: 0.35 },
627
+ { x: 0.3, y: -0.2, lower: 0.25, upper: 0.25 },
628
+ { x: 1, y: 1, lower: 0.8 },
629
+ ],
630
+ 100: [
631
+ { x: 0, y: 0, upper: 0.5 },
632
+ { x: 0.47, y: -1, lower: 0.25, upper: 0.25 },
633
+ { x: 1, y: 1, lower: 1 },
634
+ ],
635
+ })
636
+
637
+ /** impulseAndOvershoot:standard:v1 — dip backward, overshoot, settle */
638
+ export const impulseOvershoot = pathPreset({
639
+ 0: [
640
+ { x: 0, y: 0, upper: 0.35 },
641
+ { x: 0.17, y: -0.1, lower: 0.35, upper: 0.4 },
642
+ { x: 0.67, y: 1.05, lower: 0.65, upper: 0.25 },
643
+ { x: 1, y: 1, lower: 0.6 },
644
+ ],
645
+ 50: [
646
+ { x: 0, y: 0, upper: 0.35 },
647
+ { x: 0.2, y: -0.2, lower: 0.35, upper: 0.45 },
648
+ { x: 0.6, y: 1.1, lower: 0.75, upper: 0.25 },
649
+ { x: 1, y: 1, lower: 0.6 },
650
+ ],
651
+ 100: [
652
+ { x: 0, y: 0, upper: 0.35 },
653
+ { x: 0.27, y: -1, lower: 0.3, upper: 0.6 },
654
+ { x: 0.47, y: 1.5, lower: 0.9, upper: 0.2 },
655
+ { x: 1, y: 1, lower: 0.6 },
656
+ ],
657
+ })
658
+
659
+ /** overshoot:standard:v1 — overshoot then settle back */
660
+ export const overshoot = pathPreset({
661
+ 0: [
662
+ { x: 0, y: 0, upper: 0 },
663
+ { x: 0.46, y: 1.05, lower: 0.8, upper: 0.2 },
664
+ { x: 1, y: 1, lower: 0.7 },
665
+ ],
666
+ 50: [
667
+ { x: 0, y: 0, upper: 0 },
668
+ { x: 0.25, y: 1.2, lower: 0.8, upper: 0.2 },
669
+ { x: 1, y: 1, lower: 0.75 },
670
+ ],
671
+ 100: [
672
+ { x: 0, y: 0, upper: 0 },
673
+ { x: 0.15, y: 2, lower: 0.8, upper: 0.1 },
674
+ { x: 1, y: 1, lower: 0.8 },
675
+ ],
676
+ })
677
+
678
+ /** overshoot:elastic:v1 — overshoot with elastic ringing */
679
+ export const overshootElastic = pathPreset({
680
+ 0: [
681
+ { x: 0, y: 0, upper: 0.5 },
682
+ { x: 0.5, y: 1.1, lower: 0.6, upper: 0.4 },
683
+ { x: 1, y: 1, lower: 0.5 },
684
+ ],
685
+ 50: [
686
+ { x: 0, y: 0, upper: 0.7 },
687
+ { x: 0.5, y: 1.2, lower: 0.8, upper: 0.6 },
688
+ { x: 1, y: 1, lower: 0.7 },
689
+ ],
690
+ 100: [
691
+ { x: 0, y: 0, upper: 0.8 },
692
+ { x: 0.5, y: 1.5, lower: 0.9, upper: 0.7 },
693
+ { x: 1, y: 1, lower: 0.8 },
694
+ ],
695
+ })
696
+
697
+ /** overshoot:bouncy:v1 — overshoot with a bouncy settle */
698
+ export const overshootBouncy = pathPreset({
699
+ 0: [
700
+ { x: 0, y: 0, upper: 0.8 },
701
+ { x: 0.5, y: 1.2, lower: 0.4, upper: 0.2 },
702
+ { x: 1, y: 1, lower: 0.6 },
703
+ ],
704
+ 50: [
705
+ { x: 0, y: 0, upper: 0.9 },
706
+ { x: 0.5, y: 1.5, lower: 0.2, upper: 0.1 },
707
+ { x: 1, y: 1, lower: 0.7 },
708
+ ],
709
+ 100: [
710
+ { x: 0, y: 0, upper: 1 },
711
+ { x: 0.5, y: 1.9, lower: 0.1, upper: 0.05 },
712
+ { x: 1, y: 1, lower: 0.9 },
713
+ ],
714
+ })
715
+
716
+ // ---------------------------------------------------------------------------
717
+ // Sampled curves — same data as the previously hardcoded arrays
718
+ // ---------------------------------------------------------------------------
719
+
720
+ export const naturalThrowSamples = samplePreset(naturalThrow)
721
+ export const decelerateOvershootSamples = samplePreset(decelerateOvershoot)
722
+ export const decelerateElasticSamples = samplePreset(decelerateElastic)
723
+ export const accelerateImpulseSamples = samplePreset(accelerateImpulse)
724
+ export const accelerateElasticSamples = samplePreset(accelerateElastic)
725
+ export const elasticSnapSamples = samplePreset(elasticSnap)
726
+ export const bounceSamples = samplePreset(bounce)
727
+ export const bounceAnticipateSamples = samplePreset(bounceAnticipate)
728
+ export const bounceThrowSamples = samplePreset(bounceThrow)
729
+ export const impulseSlowSamples = samplePreset(impulseSlow)
730
+ export const impulseOvershootSamples = samplePreset(impulseOvershoot)
731
+ export const overshootSamples = samplePreset(overshoot)
732
+ export const overshootElasticSamples = samplePreset(overshootElastic)
733
+ export const overshootBouncySamples = samplePreset(overshootBouncy)
734
+
735
+ /** Linearly interpolate a sampled curve at time t (0-1). */
736
+ export function lerpSamples(samples: readonly number[], t: number): number {
737
+ const clamped = Math.max(0, Math.min(1, t))
738
+ const idx = clamped * (samples.length - 1)
739
+ const lo = Math.floor(idx)
740
+ const hi = Math.min(lo + 1, samples.length - 1)
741
+ const frac = idx - lo
742
+ return samples[lo]! * (1 - frac) + samples[hi]! * frac
743
+ }