@vib3code/sdk 2.0.1

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 (258) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/DOCS/BLUEPRINT_EXECUTION_PLAN_2026-01-07.md +34 -0
  3. package/DOCS/CI_TESTING.md +38 -0
  4. package/DOCS/CLI_ONBOARDING.md +75 -0
  5. package/DOCS/CONTROL_REFERENCE.md +64 -0
  6. package/DOCS/DEV_TRACK_ANALYSIS.md +77 -0
  7. package/DOCS/DEV_TRACK_PLAN_2026-01-07.md +42 -0
  8. package/DOCS/DEV_TRACK_SESSION_2026-01-31.md +220 -0
  9. package/DOCS/ENV_SETUP.md +189 -0
  10. package/DOCS/EXPORT_FORMATS.md +417 -0
  11. package/DOCS/GPU_DISPOSAL_GUIDE.md +21 -0
  12. package/DOCS/LICENSING_TIERS.md +275 -0
  13. package/DOCS/MASTER_PLAN_2026-01-31.md +570 -0
  14. package/DOCS/OBS_SETUP_GUIDE.md +98 -0
  15. package/DOCS/PROJECT_SETUP.md +66 -0
  16. package/DOCS/RENDERER_LIFECYCLE.md +40 -0
  17. package/DOCS/REPO_MANIFEST.md +121 -0
  18. package/DOCS/SESSION_014_PLAN.md +195 -0
  19. package/DOCS/SESSION_LOG_2026-01-07.md +56 -0
  20. package/DOCS/STRATEGIC_BLUEPRINT_2026-01-07.md +72 -0
  21. package/DOCS/SYSTEM_AUDIT_2026-01-30.md +738 -0
  22. package/DOCS/SYSTEM_INVENTORY.md +520 -0
  23. package/DOCS/TELEMETRY_EXPORTS.md +34 -0
  24. package/DOCS/WEBGPU_STATUS.md +38 -0
  25. package/DOCS/XR_BENCHMARKS.md +608 -0
  26. package/LICENSE +21 -0
  27. package/README.md +426 -0
  28. package/docs/.nojekyll +0 -0
  29. package/docs/01-dissolution_of_euclidean_hegemony.html +346 -0
  30. package/docs/02-hyperspatial_ego_death.html +346 -0
  31. package/docs/03-post_cartesian_sublime.html +346 -0
  32. package/docs/04-crystalline_void_meditation.html +346 -0
  33. package/docs/05-quantum_decoherence_ballet.html +346 -0
  34. package/docs/06-dissolution_of_euclidean_hegemony.html +346 -0
  35. package/docs/07-hyperspatial_ego_death.html +346 -0
  36. package/docs/08-post_cartesian_sublime.html +346 -0
  37. package/docs/09-crystalline_void_meditation.html +346 -0
  38. package/docs/10-quantum_decoherence_ballet.html +346 -0
  39. package/docs/11-dissolution_of_euclidean_hegemony.html +346 -0
  40. package/docs/12-hyperspatial_ego_death.html +346 -0
  41. package/docs/13-post_cartesian_sublime.html +346 -0
  42. package/docs/index.html +794 -0
  43. package/docs/test-hub.html +441 -0
  44. package/docs/url-state.js +102 -0
  45. package/docs/vib3-exports/01-quantum-quantum-tetrahedron-lattice.html +489 -0
  46. package/docs/vib3-exports/02-quantum-quantum-hypersphere-matrix.html +489 -0
  47. package/docs/vib3-exports/03-quantum-quantum-hypertetra-fractal.html +489 -0
  48. package/docs/vib3-exports/04-faceted-faceted-crystal-structure.html +407 -0
  49. package/docs/vib3-exports/05-faceted-faceted-klein-bottle.html +407 -0
  50. package/docs/vib3-exports/06-faceted-faceted-hypertetra-torus.html +407 -0
  51. package/docs/vib3-exports/07-holographic-holographic-wave-field.html +457 -0
  52. package/docs/vib3-exports/08-holographic-holographic-hypersphere-sphere.html +457 -0
  53. package/docs/vib3-exports/09-holographic-holographic-hypertetra-crystal.html +457 -0
  54. package/docs/vib3-exports/index.html +238 -0
  55. package/docs/webgpu-live.html +702 -0
  56. package/package.json +367 -0
  57. package/src/advanced/AIPresetGenerator.js +777 -0
  58. package/src/advanced/MIDIController.js +703 -0
  59. package/src/advanced/OffscreenWorker.js +1051 -0
  60. package/src/advanced/WebGPUCompute.js +1051 -0
  61. package/src/advanced/WebXRRenderer.js +680 -0
  62. package/src/agent/cli/AgentCLI.js +615 -0
  63. package/src/agent/cli/index.js +14 -0
  64. package/src/agent/index.js +73 -0
  65. package/src/agent/mcp/MCPServer.js +950 -0
  66. package/src/agent/mcp/index.js +9 -0
  67. package/src/agent/mcp/tools.js +548 -0
  68. package/src/agent/telemetry/EventStream.js +669 -0
  69. package/src/agent/telemetry/Instrumentation.js +618 -0
  70. package/src/agent/telemetry/TelemetryExporters.js +427 -0
  71. package/src/agent/telemetry/TelemetryService.js +464 -0
  72. package/src/agent/telemetry/index.js +52 -0
  73. package/src/benchmarks/BenchmarkRunner.js +381 -0
  74. package/src/benchmarks/MetricsCollector.js +299 -0
  75. package/src/benchmarks/index.js +9 -0
  76. package/src/benchmarks/scenes.js +259 -0
  77. package/src/cli/index.js +675 -0
  78. package/src/config/ApiConfig.js +88 -0
  79. package/src/core/CanvasManager.js +217 -0
  80. package/src/core/ErrorReporter.js +117 -0
  81. package/src/core/ParameterMapper.js +333 -0
  82. package/src/core/Parameters.js +396 -0
  83. package/src/core/RendererContracts.js +200 -0
  84. package/src/core/UnifiedResourceManager.js +370 -0
  85. package/src/core/VIB3Engine.js +636 -0
  86. package/src/core/renderers/FacetedRendererAdapter.js +32 -0
  87. package/src/core/renderers/HolographicRendererAdapter.js +29 -0
  88. package/src/core/renderers/QuantumRendererAdapter.js +29 -0
  89. package/src/core/renderers/RendererLifecycleManager.js +63 -0
  90. package/src/creative/ColorPresetsSystem.js +980 -0
  91. package/src/creative/ParameterTimeline.js +1061 -0
  92. package/src/creative/PostProcessingPipeline.js +1113 -0
  93. package/src/creative/TransitionAnimator.js +683 -0
  94. package/src/export/CSSExporter.js +226 -0
  95. package/src/export/CardGeneratorBase.js +279 -0
  96. package/src/export/ExportManager.js +580 -0
  97. package/src/export/FacetedCardGenerator.js +279 -0
  98. package/src/export/HolographicCardGenerator.js +543 -0
  99. package/src/export/LottieExporter.js +552 -0
  100. package/src/export/QuantumCardGenerator.js +315 -0
  101. package/src/export/SVGExporter.js +519 -0
  102. package/src/export/ShaderExporter.js +903 -0
  103. package/src/export/TradingCardGenerator.js +3055 -0
  104. package/src/export/TradingCardManager.js +181 -0
  105. package/src/export/VIB3PackageExporter.js +559 -0
  106. package/src/export/index.js +14 -0
  107. package/src/export/systems/TradingCardSystemFaceted.js +494 -0
  108. package/src/export/systems/TradingCardSystemHolographic.js +452 -0
  109. package/src/export/systems/TradingCardSystemQuantum.js +411 -0
  110. package/src/faceted/FacetedSystem.js +963 -0
  111. package/src/features/CollectionManager.js +433 -0
  112. package/src/gallery/CollectionManager.js +240 -0
  113. package/src/gallery/GallerySystem.js +485 -0
  114. package/src/geometry/GeometryFactory.js +314 -0
  115. package/src/geometry/GeometryLibrary.js +72 -0
  116. package/src/geometry/buffers/BufferBuilder.js +338 -0
  117. package/src/geometry/buffers/index.js +18 -0
  118. package/src/geometry/generators/Crystal.js +420 -0
  119. package/src/geometry/generators/Fractal.js +298 -0
  120. package/src/geometry/generators/KleinBottle.js +197 -0
  121. package/src/geometry/generators/Sphere.js +192 -0
  122. package/src/geometry/generators/Tesseract.js +160 -0
  123. package/src/geometry/generators/Tetrahedron.js +225 -0
  124. package/src/geometry/generators/Torus.js +304 -0
  125. package/src/geometry/generators/Wave.js +341 -0
  126. package/src/geometry/index.js +142 -0
  127. package/src/geometry/warp/HypersphereCore.js +211 -0
  128. package/src/geometry/warp/HypertetraCore.js +386 -0
  129. package/src/geometry/warp/index.js +57 -0
  130. package/src/holograms/HolographicVisualizer.js +1073 -0
  131. package/src/holograms/RealHolographicSystem.js +966 -0
  132. package/src/holograms/variantRegistry.js +69 -0
  133. package/src/integrations/FigmaPlugin.js +854 -0
  134. package/src/integrations/OBSMode.js +754 -0
  135. package/src/integrations/ThreeJsPackage.js +660 -0
  136. package/src/integrations/TouchDesignerExport.js +552 -0
  137. package/src/integrations/frameworks/Vib3React.js +591 -0
  138. package/src/integrations/frameworks/Vib3Svelte.js +654 -0
  139. package/src/integrations/frameworks/Vib3Vue.js +628 -0
  140. package/src/llm/LLMParameterInterface.js +240 -0
  141. package/src/llm/LLMParameterUI.js +577 -0
  142. package/src/math/Mat4x4.js +708 -0
  143. package/src/math/Projection.js +341 -0
  144. package/src/math/Rotor4D.js +637 -0
  145. package/src/math/Vec4.js +476 -0
  146. package/src/math/constants.js +164 -0
  147. package/src/math/index.js +68 -0
  148. package/src/math/projections.js +54 -0
  149. package/src/math/rotations.js +196 -0
  150. package/src/quantum/QuantumEngine.js +906 -0
  151. package/src/quantum/QuantumVisualizer.js +1103 -0
  152. package/src/reactivity/ReactivityConfig.js +499 -0
  153. package/src/reactivity/ReactivityManager.js +586 -0
  154. package/src/reactivity/SpatialInputSystem.js +1783 -0
  155. package/src/reactivity/index.js +93 -0
  156. package/src/render/CommandBuffer.js +465 -0
  157. package/src/render/MultiCanvasBridge.js +340 -0
  158. package/src/render/RenderCommand.js +514 -0
  159. package/src/render/RenderResourceRegistry.js +523 -0
  160. package/src/render/RenderState.js +552 -0
  161. package/src/render/RenderTarget.js +512 -0
  162. package/src/render/ShaderLoader.js +253 -0
  163. package/src/render/ShaderProgram.js +599 -0
  164. package/src/render/UnifiedRenderBridge.js +496 -0
  165. package/src/render/backends/WebGLBackend.js +1108 -0
  166. package/src/render/backends/WebGPUBackend.js +1409 -0
  167. package/src/render/commands/CommandBufferExecutor.js +607 -0
  168. package/src/render/commands/RenderCommandBuffer.js +661 -0
  169. package/src/render/commands/index.js +17 -0
  170. package/src/render/index.js +367 -0
  171. package/src/scene/Disposable.js +498 -0
  172. package/src/scene/MemoryPool.js +618 -0
  173. package/src/scene/Node4D.js +697 -0
  174. package/src/scene/ResourceManager.js +599 -0
  175. package/src/scene/Scene4D.js +540 -0
  176. package/src/scene/index.js +98 -0
  177. package/src/schemas/error.schema.json +84 -0
  178. package/src/schemas/extension.schema.json +88 -0
  179. package/src/schemas/index.js +214 -0
  180. package/src/schemas/parameters.schema.json +142 -0
  181. package/src/schemas/tool-pack.schema.json +44 -0
  182. package/src/schemas/tool-response.schema.json +127 -0
  183. package/src/shaders/common/fullscreen.vert.glsl +5 -0
  184. package/src/shaders/common/fullscreen.vert.wgsl +17 -0
  185. package/src/shaders/common/geometry24.glsl +65 -0
  186. package/src/shaders/common/geometry24.wgsl +54 -0
  187. package/src/shaders/common/rotation4d.glsl +85 -0
  188. package/src/shaders/common/rotation4d.wgsl +86 -0
  189. package/src/shaders/common/uniforms.glsl +44 -0
  190. package/src/shaders/common/uniforms.wgsl +48 -0
  191. package/src/shaders/faceted/faceted.frag.glsl +129 -0
  192. package/src/shaders/faceted/faceted.frag.wgsl +164 -0
  193. package/src/shaders/holographic/holographic.frag.glsl +406 -0
  194. package/src/shaders/holographic/holographic.frag.wgsl +185 -0
  195. package/src/shaders/quantum/quantum.frag.glsl +513 -0
  196. package/src/shaders/quantum/quantum.frag.wgsl +361 -0
  197. package/src/testing/ParallelTestFramework.js +519 -0
  198. package/src/testing/__snapshots__/exportFormats.test.js.snap +24 -0
  199. package/src/testing/exportFormats.test.js +8 -0
  200. package/src/testing/projections.test.js +14 -0
  201. package/src/testing/rotations.test.js +37 -0
  202. package/src/ui/InteractivityMenu.js +516 -0
  203. package/src/ui/StatusManager.js +96 -0
  204. package/src/ui/adaptive/renderers/webgpu/BufferLayout.ts +252 -0
  205. package/src/ui/adaptive/renderers/webgpu/PolytopeInstanceBuffer.ts +144 -0
  206. package/src/ui/adaptive/renderers/webgpu/TripleBufferedUniform.ts +170 -0
  207. package/src/ui/adaptive/renderers/webgpu/WebGPURenderer.ts +735 -0
  208. package/src/ui/adaptive/renderers/webgpu/index.ts +112 -0
  209. package/src/variations/VariationManager.js +431 -0
  210. package/src/viewer/AudioReactivity.js +505 -0
  211. package/src/viewer/CardBending.js +481 -0
  212. package/src/viewer/GalleryUI.js +832 -0
  213. package/src/viewer/ReactivityManager.js +590 -0
  214. package/src/viewer/TradingCardExporter.js +600 -0
  215. package/src/viewer/ViewerPortal.js +374 -0
  216. package/src/viewer/index.js +12 -0
  217. package/src/wasm/WasmLoader.js +296 -0
  218. package/src/wasm/index.js +132 -0
  219. package/tools/agentic/mcpTools.js +88 -0
  220. package/tools/cli/agent-cli.js +92 -0
  221. package/tools/export/formats.js +24 -0
  222. package/tools/math/rotation-baseline.mjs +64 -0
  223. package/tools/shader-sync-verify.js +937 -0
  224. package/tools/telemetry/manifestPipeline.js +141 -0
  225. package/tools/telemetry/telemetryEvents.js +35 -0
  226. package/types/adaptive-sdk.d.ts +185 -0
  227. package/types/advanced/AIPresetGenerator.d.ts +81 -0
  228. package/types/advanced/MIDIController.d.ts +100 -0
  229. package/types/advanced/OffscreenWorker.d.ts +82 -0
  230. package/types/advanced/WebGPUCompute.d.ts +52 -0
  231. package/types/advanced/WebXRRenderer.d.ts +77 -0
  232. package/types/advanced/index.d.ts +46 -0
  233. package/types/core/ErrorReporter.d.ts +50 -0
  234. package/types/core/VIB3Engine.d.ts +204 -0
  235. package/types/creative/ColorPresetsSystem.d.ts +91 -0
  236. package/types/creative/ParameterTimeline.d.ts +74 -0
  237. package/types/creative/PostProcessingPipeline.d.ts +109 -0
  238. package/types/creative/TransitionAnimator.d.ts +71 -0
  239. package/types/creative/index.d.ts +35 -0
  240. package/types/integrations/FigmaPlugin.d.ts +46 -0
  241. package/types/integrations/OBSMode.d.ts +74 -0
  242. package/types/integrations/ThreeJsPackage.d.ts +62 -0
  243. package/types/integrations/TouchDesignerExport.d.ts +36 -0
  244. package/types/integrations/Vib3React.d.ts +74 -0
  245. package/types/integrations/Vib3Svelte.d.ts +63 -0
  246. package/types/integrations/Vib3Vue.d.ts +55 -0
  247. package/types/integrations/index.d.ts +52 -0
  248. package/types/reactivity/SpatialInputSystem.d.ts +173 -0
  249. package/types/reactivity/index.d.ts +394 -0
  250. package/types/render/CommandBuffer.d.ts +169 -0
  251. package/types/render/RenderCommand.d.ts +312 -0
  252. package/types/render/RenderState.d.ts +279 -0
  253. package/types/render/RenderTarget.d.ts +254 -0
  254. package/types/render/ShaderProgram.d.ts +277 -0
  255. package/types/render/UnifiedRenderBridge.d.ts +143 -0
  256. package/types/render/WebGLBackend.d.ts +168 -0
  257. package/types/render/WebGPUBackend.d.ts +186 -0
  258. package/types/render/index.d.ts +141 -0
@@ -0,0 +1,1061 @@
1
+ /**
2
+ * ParameterTimeline.js - VIB3+ Parameter Animation Keyframe/Timeline System
3
+ *
4
+ * Provides a multi-track keyframe timeline for animating any set of VIB3+
5
+ * parameters over time. Supports configurable easing per keyframe, multiple
6
+ * playback modes (loop, bounce, once), play/pause/seek/speed controls,
7
+ * BPM quantization for music sync, and full serialization.
8
+ *
9
+ * @module creative/ParameterTimeline
10
+ * @version 1.0.0
11
+ * @author VIB3+ Creative Tooling - Phase B
12
+ */
13
+
14
+ /**
15
+ * @typedef {Object} Keyframe
16
+ * @property {number} time - Timestamp in milliseconds from timeline start
17
+ * @property {number} value - Parameter value at this keyframe
18
+ * @property {string} easing - Easing function name for interpolation TO this keyframe
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} Track
23
+ * @property {string} param - Parameter name this track controls
24
+ * @property {Keyframe[]} keyframes - Sorted array of keyframes
25
+ * @property {boolean} enabled - Whether this track is active
26
+ */
27
+
28
+ /**
29
+ * @typedef {'loop'|'bounce'|'once'} LoopMode
30
+ * - 'loop': restart from beginning when reaching the end
31
+ * - 'bounce': reverse direction at each end (ping-pong)
32
+ * - 'once': stop at the end
33
+ */
34
+
35
+ /**
36
+ * Parameter animation timeline with keyframe interpolation.
37
+ *
38
+ * Each parameter gets its own track containing ordered keyframes. During
39
+ * playback, values are interpolated between keyframes using configurable
40
+ * easing functions. Supports loop/bounce/once playback modes, variable
41
+ * speed, BPM quantization, and full import/export.
42
+ *
43
+ * @example
44
+ * const timeline = new ParameterTimeline((name, value) => {
45
+ * engine.setParameter(name, value);
46
+ * });
47
+ *
48
+ * timeline.setDuration(8000); // 8 seconds
49
+ *
50
+ * // Add keyframes for hue parameter
51
+ * timeline.addTrack('hue');
52
+ * timeline.addKeyframe('hue', 0, 0, 'linear');
53
+ * timeline.addKeyframe('hue', 2000, 120, 'easeInOut');
54
+ * timeline.addKeyframe('hue', 4000, 240, 'easeInOut');
55
+ * timeline.addKeyframe('hue', 8000, 360, 'easeOut');
56
+ *
57
+ * // Add keyframes for chaos
58
+ * timeline.addTrack('chaos');
59
+ * timeline.addKeyframe('chaos', 0, 0, 'linear');
60
+ * timeline.addKeyframe('chaos', 4000, 1.0, 'bounce');
61
+ * timeline.addKeyframe('chaos', 8000, 0, 'easeInOut');
62
+ *
63
+ * timeline.play();
64
+ */
65
+ export class ParameterTimeline {
66
+ /**
67
+ * Create a new ParameterTimeline.
68
+ *
69
+ * @param {Function} parameterUpdateFn - Callback invoked as (paramName, value)
70
+ * whenever a VIB3+ parameter should be updated.
71
+ */
72
+ constructor(parameterUpdateFn) {
73
+ if (typeof parameterUpdateFn !== 'function') {
74
+ throw new Error('ParameterTimeline requires a parameterUpdateFn callback');
75
+ }
76
+
77
+ /** @type {Function} */
78
+ this.updateParameter = parameterUpdateFn;
79
+
80
+ /** @type {Map<string, Track>} Parameter tracks keyed by parameter name */
81
+ this.tracks = new Map();
82
+
83
+ /** @type {number} Total timeline duration in milliseconds */
84
+ this.duration = 10000;
85
+
86
+ /** @type {number} Current playback position in milliseconds */
87
+ this.currentTime = 0;
88
+
89
+ /** @type {boolean} Whether the timeline is currently playing */
90
+ this.playing = false;
91
+
92
+ /** @type {LoopMode} Playback loop mode */
93
+ this.loopMode = 'loop';
94
+
95
+ /** @type {number} Playback speed multiplier (0.1-10) */
96
+ this.playbackSpeed = 1.0;
97
+
98
+ /** @type {number} Beats per minute for quantization */
99
+ this.bpm = 120;
100
+
101
+ /** @type {number|null} requestAnimationFrame ID */
102
+ this._frameId = null;
103
+
104
+ /** @type {number} Timestamp of last animation frame */
105
+ this._lastFrameTime = 0;
106
+
107
+ /** @type {number} Direction for bounce mode: 1 = forward, -1 = reverse */
108
+ this._bounceDirection = 1;
109
+
110
+ /** @type {Function|null} Callback when playback reaches the end (once mode) */
111
+ this._onComplete = null;
112
+
113
+ /** @type {Function|null} Callback on each frame with current time */
114
+ this._onTick = null;
115
+
116
+ /** @type {Object<string, Function>} Easing function library */
117
+ this._easingFunctions = this._buildEasingFunctions();
118
+ }
119
+
120
+ // -------------------------------------------------------------------------
121
+ // Public API - Track Management
122
+ // -------------------------------------------------------------------------
123
+
124
+ /**
125
+ * Add a new parameter track to the timeline.
126
+ *
127
+ * @param {string} param - VIB3+ parameter name (e.g., 'hue', 'chaos', 'speed')
128
+ * @returns {boolean} true if the track was created, false if it already exists
129
+ */
130
+ addTrack(param) {
131
+ if (typeof param !== 'string' || param.trim().length === 0) {
132
+ console.warn('ParameterTimeline: Track param must be a non-empty string');
133
+ return false;
134
+ }
135
+
136
+ if (this.tracks.has(param)) {
137
+ return false; // Track already exists
138
+ }
139
+
140
+ this.tracks.set(param, {
141
+ param,
142
+ keyframes: [],
143
+ enabled: true
144
+ });
145
+
146
+ return true;
147
+ }
148
+
149
+ /**
150
+ * Remove a parameter track and all its keyframes.
151
+ *
152
+ * @param {string} param - Parameter name
153
+ * @returns {boolean} true if the track was removed
154
+ */
155
+ removeTrack(param) {
156
+ return this.tracks.delete(param);
157
+ }
158
+
159
+ /**
160
+ * Enable or disable a track without removing it.
161
+ *
162
+ * @param {string} param - Parameter name
163
+ * @param {boolean} enabled - New enabled state
164
+ * @returns {boolean} true if the track was found
165
+ */
166
+ setTrackEnabled(param, enabled) {
167
+ const track = this.tracks.get(param);
168
+ if (!track) return false;
169
+ track.enabled = Boolean(enabled);
170
+ return true;
171
+ }
172
+
173
+ /**
174
+ * Get all track names.
175
+ *
176
+ * @returns {string[]}
177
+ */
178
+ getTrackNames() {
179
+ return Array.from(this.tracks.keys());
180
+ }
181
+
182
+ /**
183
+ * Get all keyframes for a specific track.
184
+ *
185
+ * @param {string} param - Parameter name
186
+ * @returns {Keyframe[]|null} Copy of the keyframes array, or null if track not found
187
+ */
188
+ getTrackKeyframes(param) {
189
+ const track = this.tracks.get(param);
190
+ if (!track) return null;
191
+ return track.keyframes.map(kf => ({ ...kf }));
192
+ }
193
+
194
+ // -------------------------------------------------------------------------
195
+ // Public API - Keyframe Management
196
+ // -------------------------------------------------------------------------
197
+
198
+ /**
199
+ * Add a keyframe to a track. If no track exists for the parameter, one is
200
+ * created automatically. Keyframes are kept sorted by time.
201
+ *
202
+ * @param {string} param - Parameter name
203
+ * @param {number} time - Timestamp in milliseconds (0 to duration)
204
+ * @param {number} value - Parameter value at this keyframe
205
+ * @param {string} [easing='linear'] - Easing function name for interpolation TO this keyframe
206
+ * @returns {number} Index of the inserted keyframe, or -1 on failure
207
+ */
208
+ addKeyframe(param, time, value, easing = 'linear') {
209
+ // Auto-create track if needed
210
+ if (!this.tracks.has(param)) {
211
+ this.addTrack(param);
212
+ }
213
+
214
+ const track = this.tracks.get(param);
215
+ if (!track) return -1;
216
+
217
+ // Validate
218
+ time = this._clamp(Number(time), 0, this.duration);
219
+ value = Number(value);
220
+ if (!Number.isFinite(value)) {
221
+ console.warn(`ParameterTimeline: Non-finite value for keyframe on "${param}"`);
222
+ return -1;
223
+ }
224
+
225
+ const easingName = this._easingFunctions[easing] ? easing : 'linear';
226
+
227
+ /** @type {Keyframe} */
228
+ const keyframe = { time, value, easing: easingName };
229
+
230
+ // Insert in sorted order (by time)
231
+ const insertIdx = this._findInsertIndex(track.keyframes, time);
232
+
233
+ // If a keyframe at the exact same time already exists, replace it
234
+ if (insertIdx < track.keyframes.length && track.keyframes[insertIdx].time === time) {
235
+ track.keyframes[insertIdx] = keyframe;
236
+ return insertIdx;
237
+ }
238
+
239
+ track.keyframes.splice(insertIdx, 0, keyframe);
240
+ return insertIdx;
241
+ }
242
+
243
+ /**
244
+ * Remove a keyframe by index from a track.
245
+ *
246
+ * @param {string} param - Parameter name
247
+ * @param {number} index - Keyframe index
248
+ * @returns {boolean} true if removed
249
+ */
250
+ removeKeyframe(param, index) {
251
+ const track = this.tracks.get(param);
252
+ if (!track) return false;
253
+
254
+ if (index < 0 || index >= track.keyframes.length) {
255
+ return false;
256
+ }
257
+
258
+ track.keyframes.splice(index, 1);
259
+ return true;
260
+ }
261
+
262
+ /**
263
+ * Remove a keyframe at a specific time from a track.
264
+ *
265
+ * @param {string} param - Parameter name
266
+ * @param {number} time - Keyframe time in milliseconds
267
+ * @param {number} [tolerance=10] - Time tolerance for matching (ms)
268
+ * @returns {boolean} true if a keyframe was removed
269
+ */
270
+ removeKeyframeAtTime(param, time, tolerance = 10) {
271
+ const track = this.tracks.get(param);
272
+ if (!track) return false;
273
+
274
+ const idx = track.keyframes.findIndex(kf => Math.abs(kf.time - time) <= tolerance);
275
+ if (idx >= 0) {
276
+ track.keyframes.splice(idx, 1);
277
+ return true;
278
+ }
279
+ return false;
280
+ }
281
+
282
+ /**
283
+ * Update an existing keyframe's value and/or easing.
284
+ *
285
+ * @param {string} param - Parameter name
286
+ * @param {number} index - Keyframe index
287
+ * @param {Object} updates - Properties to update: { value?, easing?, time? }
288
+ * @returns {boolean} true if updated
289
+ */
290
+ updateKeyframe(param, index, updates) {
291
+ const track = this.tracks.get(param);
292
+ if (!track || index < 0 || index >= track.keyframes.length) {
293
+ return false;
294
+ }
295
+
296
+ const kf = track.keyframes[index];
297
+
298
+ if (typeof updates.value === 'number' && Number.isFinite(updates.value)) {
299
+ kf.value = updates.value;
300
+ }
301
+ if (typeof updates.easing === 'string' && this._easingFunctions[updates.easing]) {
302
+ kf.easing = updates.easing;
303
+ }
304
+ if (typeof updates.time === 'number' && Number.isFinite(updates.time)) {
305
+ kf.time = this._clamp(updates.time, 0, this.duration);
306
+ // Re-sort after time change
307
+ track.keyframes.sort((a, b) => a.time - b.time);
308
+ }
309
+
310
+ return true;
311
+ }
312
+
313
+ /**
314
+ * Clear all keyframes from a specific track.
315
+ *
316
+ * @param {string} param - Parameter name
317
+ * @returns {boolean} true if the track was found and cleared
318
+ */
319
+ clearTrack(param) {
320
+ const track = this.tracks.get(param);
321
+ if (!track) return false;
322
+ track.keyframes = [];
323
+ return true;
324
+ }
325
+
326
+ /**
327
+ * Clear all keyframes from all tracks.
328
+ */
329
+ clearAllKeyframes() {
330
+ for (const [, track] of this.tracks) {
331
+ track.keyframes = [];
332
+ }
333
+ }
334
+
335
+ // -------------------------------------------------------------------------
336
+ // Public API - Playback Controls
337
+ // -------------------------------------------------------------------------
338
+
339
+ /**
340
+ * Start or resume playback.
341
+ *
342
+ * @param {Function} [onComplete] - Callback when playback completes (once mode only)
343
+ */
344
+ play(onComplete = null) {
345
+ if (this.playing) return;
346
+
347
+ this.playing = true;
348
+ this._onComplete = typeof onComplete === 'function' ? onComplete : null;
349
+ this._lastFrameTime = performance.now();
350
+
351
+ // In 'once' mode, if we are at the end, reset to start
352
+ if (this.loopMode === 'once' && this.currentTime >= this.duration) {
353
+ this.currentTime = 0;
354
+ }
355
+
356
+ this._tick();
357
+ }
358
+
359
+ /**
360
+ * Pause playback at the current position.
361
+ */
362
+ pause() {
363
+ this.playing = false;
364
+ if (this._frameId !== null) {
365
+ cancelAnimationFrame(this._frameId);
366
+ this._frameId = null;
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Stop playback and reset to the beginning.
372
+ */
373
+ stop() {
374
+ this.pause();
375
+ this.currentTime = 0;
376
+ this._bounceDirection = 1;
377
+ this._applyCurrentValues();
378
+ }
379
+
380
+ /**
381
+ * Seek to a specific time position.
382
+ *
383
+ * @param {number} time - Time in milliseconds (clamped to 0-duration)
384
+ */
385
+ seek(time) {
386
+ this.currentTime = this._clamp(Number(time), 0, this.duration);
387
+ this._applyCurrentValues();
388
+ }
389
+
390
+ /**
391
+ * Seek to a specific normalized position.
392
+ *
393
+ * @param {number} position - Position from 0 (start) to 1 (end)
394
+ */
395
+ seekNormalized(position) {
396
+ this.seek(this._clamp(Number(position), 0, 1) * this.duration);
397
+ }
398
+
399
+ /**
400
+ * Set the playback speed multiplier.
401
+ *
402
+ * @param {number} speed - Speed multiplier (0.1-10)
403
+ */
404
+ setSpeed(speed) {
405
+ this.playbackSpeed = this._clamp(Number(speed), 0.1, 10);
406
+ }
407
+
408
+ /**
409
+ * Set the loop mode.
410
+ *
411
+ * @param {LoopMode} mode - 'loop', 'bounce', or 'once'
412
+ */
413
+ setLoopMode(mode) {
414
+ if (['loop', 'bounce', 'once'].includes(mode)) {
415
+ this.loopMode = mode;
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Set the total timeline duration.
421
+ *
422
+ * @param {number} ms - Duration in milliseconds (minimum 100ms)
423
+ */
424
+ setDuration(ms) {
425
+ this.duration = Math.max(100, Number(ms) || 10000);
426
+ // Clamp current time
427
+ if (this.currentTime > this.duration) {
428
+ this.currentTime = this.duration;
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Set an on-tick callback that fires each animation frame with the current time.
434
+ *
435
+ * @param {Function|null} callback - (currentTime: number, normalizedTime: number) => void
436
+ */
437
+ onTick(callback) {
438
+ this._onTick = typeof callback === 'function' ? callback : null;
439
+ }
440
+
441
+ // -------------------------------------------------------------------------
442
+ // Public API - BPM / Music Sync
443
+ // -------------------------------------------------------------------------
444
+
445
+ /**
446
+ * Set the BPM (beats per minute) for quantization.
447
+ *
448
+ * @param {number} bpm - Beats per minute (20-300)
449
+ */
450
+ setBPM(bpm) {
451
+ this.bpm = this._clamp(Number(bpm), 20, 300);
452
+ }
453
+
454
+ /**
455
+ * Get the duration of one beat in milliseconds at the current BPM.
456
+ *
457
+ * @returns {number} Beat duration in ms
458
+ */
459
+ getBeatDuration() {
460
+ return 60000 / this.bpm;
461
+ }
462
+
463
+ /**
464
+ * Get the duration of one bar (4 beats) in milliseconds.
465
+ *
466
+ * @returns {number} Bar duration in ms
467
+ */
468
+ getBarDuration() {
469
+ return this.getBeatDuration() * 4;
470
+ }
471
+
472
+ /**
473
+ * Convert a beat number to a timestamp.
474
+ *
475
+ * @param {number} beat - Beat number (0-based)
476
+ * @returns {number} Timestamp in milliseconds
477
+ */
478
+ beatToTime(beat) {
479
+ return beat * this.getBeatDuration();
480
+ }
481
+
482
+ /**
483
+ * Convert a timestamp to a beat number.
484
+ *
485
+ * @param {number} time - Timestamp in milliseconds
486
+ * @returns {number} Beat number (fractional)
487
+ */
488
+ timeToBeat(time) {
489
+ return time / this.getBeatDuration();
490
+ }
491
+
492
+ /**
493
+ * Quantize all keyframe times to the nearest beat subdivision.
494
+ *
495
+ * @param {number} [subdivision=1] - Beat subdivision (1 = whole beat,
496
+ * 0.5 = half beat, 0.25 = quarter beat, etc.)
497
+ */
498
+ quantizeToBeats(subdivision = 1) {
499
+ const beatMs = this.getBeatDuration() * subdivision;
500
+ if (beatMs <= 0) return;
501
+
502
+ for (const [, track] of this.tracks) {
503
+ for (const kf of track.keyframes) {
504
+ kf.time = Math.round(kf.time / beatMs) * beatMs;
505
+ kf.time = this._clamp(kf.time, 0, this.duration);
506
+ }
507
+ // Re-sort after quantization
508
+ track.keyframes.sort((a, b) => a.time - b.time);
509
+ // Remove duplicates at same time (keep last)
510
+ this._deduplicateKeyframes(track);
511
+ }
512
+ }
513
+
514
+ /**
515
+ * Snap the timeline duration to the nearest number of bars.
516
+ *
517
+ * @param {number} [bars] - Number of bars. If not specified, rounds the
518
+ * current duration to the nearest whole bar count.
519
+ */
520
+ snapDurationToBars(bars) {
521
+ const barMs = this.getBarDuration();
522
+ if (typeof bars === 'number' && bars > 0) {
523
+ this.duration = Math.round(bars) * barMs;
524
+ } else {
525
+ this.duration = Math.max(barMs, Math.round(this.duration / barMs) * barMs);
526
+ }
527
+ }
528
+
529
+ // -------------------------------------------------------------------------
530
+ // Public API - Value Interpolation
531
+ // -------------------------------------------------------------------------
532
+
533
+ /**
534
+ * Get the interpolated value of a parameter at a specific time.
535
+ *
536
+ * @param {string} param - Parameter name
537
+ * @param {number} time - Time in milliseconds
538
+ * @returns {number|null} Interpolated value, or null if no keyframes exist
539
+ */
540
+ getValueAtTime(param, time) {
541
+ const track = this.tracks.get(param);
542
+ if (!track || track.keyframes.length === 0) {
543
+ return null;
544
+ }
545
+
546
+ const keyframes = track.keyframes;
547
+
548
+ // Before first keyframe
549
+ if (time <= keyframes[0].time) {
550
+ return keyframes[0].value;
551
+ }
552
+
553
+ // After last keyframe
554
+ if (time >= keyframes[keyframes.length - 1].time) {
555
+ return keyframes[keyframes.length - 1].value;
556
+ }
557
+
558
+ // Find surrounding keyframes
559
+ for (let i = 0; i < keyframes.length - 1; i++) {
560
+ const kf0 = keyframes[i];
561
+ const kf1 = keyframes[i + 1];
562
+
563
+ if (time >= kf0.time && time <= kf1.time) {
564
+ const segment = kf1.time - kf0.time;
565
+ if (segment === 0) return kf1.value;
566
+
567
+ const localT = (time - kf0.time) / segment;
568
+ const easeFn = this._easingFunctions[kf1.easing] || this._easingFunctions.linear;
569
+ const eased = easeFn(localT);
570
+
571
+ // Special circular interpolation for hue
572
+ if (param === 'hue') {
573
+ return this._lerpHue(kf0.value, kf1.value, eased);
574
+ }
575
+
576
+ return kf0.value + (kf1.value - kf0.value) * eased;
577
+ }
578
+ }
579
+
580
+ return keyframes[keyframes.length - 1].value;
581
+ }
582
+
583
+ /**
584
+ * Get all parameter values at a specific time.
585
+ *
586
+ * @param {number} time - Time in milliseconds
587
+ * @returns {Object<string, number>} Map of parameter names to interpolated values
588
+ */
589
+ getAllValuesAtTime(time) {
590
+ const values = {};
591
+ for (const [param, track] of this.tracks) {
592
+ if (track.enabled && track.keyframes.length > 0) {
593
+ const value = this.getValueAtTime(param, time);
594
+ if (value !== null) {
595
+ values[param] = value;
596
+ }
597
+ }
598
+ }
599
+ return values;
600
+ }
601
+
602
+ // -------------------------------------------------------------------------
603
+ // Public API - Serialization
604
+ // -------------------------------------------------------------------------
605
+
606
+ /**
607
+ * Export the entire timeline as a serializable object.
608
+ *
609
+ * @returns {Object} Timeline data
610
+ */
611
+ exportTimeline() {
612
+ const tracks = {};
613
+ for (const [param, track] of this.tracks) {
614
+ tracks[param] = {
615
+ enabled: track.enabled,
616
+ keyframes: track.keyframes.map(kf => ({
617
+ time: kf.time,
618
+ value: kf.value,
619
+ easing: kf.easing
620
+ }))
621
+ };
622
+ }
623
+
624
+ return {
625
+ type: 'vib3-parameter-timeline',
626
+ version: '1.0.0',
627
+ timestamp: new Date().toISOString(),
628
+ duration: this.duration,
629
+ loopMode: this.loopMode,
630
+ playbackSpeed: this.playbackSpeed,
631
+ bpm: this.bpm,
632
+ tracks
633
+ };
634
+ }
635
+
636
+ /**
637
+ * Import a timeline from serialized data.
638
+ *
639
+ * @param {Object} data - Timeline data as returned by exportTimeline()
640
+ * @returns {boolean} true if imported successfully
641
+ */
642
+ importTimeline(data) {
643
+ if (!data || data.type !== 'vib3-parameter-timeline') {
644
+ console.warn('ParameterTimeline: Invalid import data');
645
+ return false;
646
+ }
647
+
648
+ // Stop playback during import
649
+ this.pause();
650
+
651
+ // Restore settings
652
+ if (typeof data.duration === 'number' && data.duration > 0) {
653
+ this.duration = data.duration;
654
+ }
655
+ if (typeof data.loopMode === 'string') {
656
+ this.setLoopMode(data.loopMode);
657
+ }
658
+ if (typeof data.playbackSpeed === 'number') {
659
+ this.playbackSpeed = this._clamp(data.playbackSpeed, 0.1, 10);
660
+ }
661
+ if (typeof data.bpm === 'number') {
662
+ this.bpm = this._clamp(data.bpm, 20, 300);
663
+ }
664
+
665
+ // Import tracks
666
+ this.tracks.clear();
667
+ this.currentTime = 0;
668
+
669
+ if (data.tracks && typeof data.tracks === 'object') {
670
+ for (const [param, trackData] of Object.entries(data.tracks)) {
671
+ this.addTrack(param);
672
+ const track = this.tracks.get(param);
673
+ if (!track) continue;
674
+
675
+ track.enabled = trackData.enabled !== false;
676
+
677
+ if (Array.isArray(trackData.keyframes)) {
678
+ for (const kf of trackData.keyframes) {
679
+ if (typeof kf.time === 'number' && typeof kf.value === 'number') {
680
+ track.keyframes.push({
681
+ time: this._clamp(kf.time, 0, this.duration),
682
+ value: kf.value,
683
+ easing: this._easingFunctions[kf.easing] ? kf.easing : 'linear'
684
+ });
685
+ }
686
+ }
687
+ track.keyframes.sort((a, b) => a.time - b.time);
688
+ }
689
+ }
690
+ }
691
+
692
+ return true;
693
+ }
694
+
695
+ // -------------------------------------------------------------------------
696
+ // Public API - Queries
697
+ // -------------------------------------------------------------------------
698
+
699
+ /**
700
+ * Get the current normalized playback position (0-1).
701
+ *
702
+ * @returns {number}
703
+ */
704
+ getNormalizedTime() {
705
+ return this.duration > 0 ? this.currentTime / this.duration : 0;
706
+ }
707
+
708
+ /**
709
+ * Get the current beat position.
710
+ *
711
+ * @returns {number} Current beat number (fractional)
712
+ */
713
+ getCurrentBeat() {
714
+ return this.timeToBeat(this.currentTime);
715
+ }
716
+
717
+ /**
718
+ * Get the total number of beats in the timeline.
719
+ *
720
+ * @returns {number}
721
+ */
722
+ getTotalBeats() {
723
+ return this.timeToBeat(this.duration);
724
+ }
725
+
726
+ /**
727
+ * Get available easing function names.
728
+ *
729
+ * @returns {string[]}
730
+ */
731
+ getEasingNames() {
732
+ return Object.keys(this._easingFunctions);
733
+ }
734
+
735
+ // -------------------------------------------------------------------------
736
+ // Public API - Lifecycle
737
+ // -------------------------------------------------------------------------
738
+
739
+ /**
740
+ * Clean up resources and stop playback.
741
+ */
742
+ dispose() {
743
+ this.pause();
744
+ this.tracks.clear();
745
+ this._onComplete = null;
746
+ this._onTick = null;
747
+ }
748
+
749
+ // -------------------------------------------------------------------------
750
+ // Private - Animation Loop
751
+ // -------------------------------------------------------------------------
752
+
753
+ /**
754
+ * Main animation frame tick.
755
+ * @private
756
+ */
757
+ _tick() {
758
+ if (!this.playing) return;
759
+
760
+ const now = performance.now();
761
+ const delta = (now - this._lastFrameTime) * this.playbackSpeed;
762
+ this._lastFrameTime = now;
763
+
764
+ // Advance time based on mode
765
+ if (this.loopMode === 'bounce') {
766
+ this.currentTime += delta * this._bounceDirection;
767
+
768
+ if (this.currentTime >= this.duration) {
769
+ this.currentTime = this.duration;
770
+ this._bounceDirection = -1;
771
+ } else if (this.currentTime <= 0) {
772
+ this.currentTime = 0;
773
+ this._bounceDirection = 1;
774
+ }
775
+ } else {
776
+ this.currentTime += delta;
777
+
778
+ if (this.currentTime >= this.duration) {
779
+ if (this.loopMode === 'loop') {
780
+ this.currentTime = this.currentTime % this.duration;
781
+ } else {
782
+ // 'once' mode
783
+ this.currentTime = this.duration;
784
+ this._applyCurrentValues();
785
+ this.playing = false;
786
+ this._frameId = null;
787
+ if (this._onComplete) {
788
+ this._onComplete();
789
+ }
790
+ return;
791
+ }
792
+ }
793
+ }
794
+
795
+ // Apply interpolated values
796
+ this._applyCurrentValues();
797
+
798
+ // Notify tick callback
799
+ if (this._onTick) {
800
+ this._onTick(this.currentTime, this.getNormalizedTime());
801
+ }
802
+
803
+ // Schedule next frame
804
+ this._frameId = requestAnimationFrame(() => this._tick());
805
+ }
806
+
807
+ /**
808
+ * Evaluate and apply all track values at the current time.
809
+ * @private
810
+ */
811
+ _applyCurrentValues() {
812
+ for (const [param, track] of this.tracks) {
813
+ if (!track.enabled || track.keyframes.length === 0) continue;
814
+
815
+ const value = this.getValueAtTime(param, this.currentTime);
816
+ if (value !== null) {
817
+ this.updateParameter(param, value);
818
+ }
819
+ }
820
+ }
821
+
822
+ // -------------------------------------------------------------------------
823
+ // Private - Keyframe Utilities
824
+ // -------------------------------------------------------------------------
825
+
826
+ /**
827
+ * Find the insertion index for a keyframe at a given time (binary search).
828
+ *
829
+ * @param {Keyframe[]} keyframes - Sorted keyframes array
830
+ * @param {number} time - Target time
831
+ * @returns {number} Insertion index
832
+ * @private
833
+ */
834
+ _findInsertIndex(keyframes, time) {
835
+ let low = 0;
836
+ let high = keyframes.length;
837
+
838
+ while (low < high) {
839
+ const mid = (low + high) >>> 1;
840
+ if (keyframes[mid].time < time) {
841
+ low = mid + 1;
842
+ } else {
843
+ high = mid;
844
+ }
845
+ }
846
+
847
+ return low;
848
+ }
849
+
850
+ /**
851
+ * Remove duplicate keyframes at the same time, keeping the last one.
852
+ *
853
+ * @param {Track} track
854
+ * @private
855
+ */
856
+ _deduplicateKeyframes(track) {
857
+ const seen = new Map();
858
+ for (let i = track.keyframes.length - 1; i >= 0; i--) {
859
+ const time = track.keyframes[i].time;
860
+ if (seen.has(time)) {
861
+ track.keyframes.splice(i, 1);
862
+ } else {
863
+ seen.set(time, true);
864
+ }
865
+ }
866
+ }
867
+
868
+ // -------------------------------------------------------------------------
869
+ // Private - Math Utilities
870
+ // -------------------------------------------------------------------------
871
+
872
+ /**
873
+ * Clamp a value to a range.
874
+ *
875
+ * @param {number} value
876
+ * @param {number} min
877
+ * @param {number} max
878
+ * @returns {number}
879
+ * @private
880
+ */
881
+ _clamp(value, min, max) {
882
+ if (!Number.isFinite(value)) return min;
883
+ return Math.max(min, Math.min(max, value));
884
+ }
885
+
886
+ /**
887
+ * Circular hue interpolation (shortest path around 360-degree wheel).
888
+ *
889
+ * @param {number} a - Start hue (0-360)
890
+ * @param {number} b - End hue (0-360)
891
+ * @param {number} t - Progress (0-1)
892
+ * @returns {number}
893
+ * @private
894
+ */
895
+ _lerpHue(a, b, t) {
896
+ let diff = b - a;
897
+ if (diff > 180) diff -= 360;
898
+ if (diff < -180) diff += 360;
899
+ let result = a + diff * t;
900
+ if (result < 0) result += 360;
901
+ if (result >= 360) result -= 360;
902
+ return result;
903
+ }
904
+
905
+ /**
906
+ * Build the complete set of easing functions.
907
+ *
908
+ * @returns {Object<string, Function>}
909
+ * @private
910
+ */
911
+ _buildEasingFunctions() {
912
+ return {
913
+ /**
914
+ * No easing, constant velocity.
915
+ * @param {number} t
916
+ * @returns {number}
917
+ */
918
+ linear(t) {
919
+ return t;
920
+ },
921
+
922
+ /**
923
+ * Cubic acceleration from zero velocity.
924
+ * @param {number} t
925
+ * @returns {number}
926
+ */
927
+ easeIn(t) {
928
+ return t * t * t;
929
+ },
930
+
931
+ /**
932
+ * Cubic deceleration to zero velocity.
933
+ * @param {number} t
934
+ * @returns {number}
935
+ */
936
+ easeOut(t) {
937
+ return 1 - Math.pow(1 - t, 3);
938
+ },
939
+
940
+ /**
941
+ * Cubic acceleration then deceleration.
942
+ * @param {number} t
943
+ * @returns {number}
944
+ */
945
+ easeInOut(t) {
946
+ return t < 0.5
947
+ ? 4 * t * t * t
948
+ : 1 - Math.pow(-2 * t + 2, 3) / 2;
949
+ },
950
+
951
+ /**
952
+ * Quadratic ease-in.
953
+ * @param {number} t
954
+ * @returns {number}
955
+ */
956
+ easeInQuad(t) {
957
+ return t * t;
958
+ },
959
+
960
+ /**
961
+ * Quadratic ease-out.
962
+ * @param {number} t
963
+ * @returns {number}
964
+ */
965
+ easeOutQuad(t) {
966
+ return 1 - (1 - t) * (1 - t);
967
+ },
968
+
969
+ /**
970
+ * Bounce effect that simulates a ball bouncing to rest.
971
+ * @param {number} t
972
+ * @returns {number}
973
+ */
974
+ bounce(t) {
975
+ const n1 = 7.5625;
976
+ const d1 = 2.75;
977
+
978
+ if (t < 1 / d1) {
979
+ return n1 * t * t;
980
+ } else if (t < 2 / d1) {
981
+ return n1 * (t -= 1.5 / d1) * t + 0.75;
982
+ } else if (t < 2.5 / d1) {
983
+ return n1 * (t -= 2.25 / d1) * t + 0.9375;
984
+ } else {
985
+ return n1 * (t -= 2.625 / d1) * t + 0.984375;
986
+ }
987
+ },
988
+
989
+ /**
990
+ * Elastic oscillation that overshoots then settles.
991
+ * @param {number} t
992
+ * @returns {number}
993
+ */
994
+ elastic(t) {
995
+ if (t === 0 || t === 1) return t;
996
+ const c4 = (2 * Math.PI) / 3;
997
+ return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
998
+ },
999
+
1000
+ /**
1001
+ * Smooth cubic Bezier-style ease (alias for easeInOut).
1002
+ * @param {number} t
1003
+ * @returns {number}
1004
+ */
1005
+ cubic(t) {
1006
+ return t < 0.5
1007
+ ? 4 * t * t * t
1008
+ : 1 - Math.pow(-2 * t + 2, 3) / 2;
1009
+ },
1010
+
1011
+ /**
1012
+ * Back ease-out: slightly overshoots then returns.
1013
+ * @param {number} t
1014
+ * @returns {number}
1015
+ */
1016
+ backOut(t) {
1017
+ const c1 = 1.70158;
1018
+ const c3 = c1 + 1;
1019
+ return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
1020
+ },
1021
+
1022
+ /**
1023
+ * Back ease-in: pulls back before accelerating.
1024
+ * @param {number} t
1025
+ * @returns {number}
1026
+ */
1027
+ backIn(t) {
1028
+ const c1 = 1.70158;
1029
+ const c3 = c1 + 1;
1030
+ return c3 * t * t * t - c1 * t * t;
1031
+ },
1032
+
1033
+ /**
1034
+ * Stepped/staircase easing (4 steps).
1035
+ * @param {number} t
1036
+ * @returns {number}
1037
+ */
1038
+ steps(t) {
1039
+ return Math.floor(t * 4) / 4;
1040
+ },
1041
+
1042
+ /**
1043
+ * Sinusoidal ease-in-out.
1044
+ * @param {number} t
1045
+ * @returns {number}
1046
+ */
1047
+ sineInOut(t) {
1048
+ return -(Math.cos(Math.PI * t) - 1) / 2;
1049
+ },
1050
+
1051
+ /**
1052
+ * Exponential ease-out.
1053
+ * @param {number} t
1054
+ * @returns {number}
1055
+ */
1056
+ expoOut(t) {
1057
+ return t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
1058
+ }
1059
+ };
1060
+ }
1061
+ }