@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.
- package/CHANGELOG.md +118 -0
- package/DOCS/BLUEPRINT_EXECUTION_PLAN_2026-01-07.md +34 -0
- package/DOCS/CI_TESTING.md +38 -0
- package/DOCS/CLI_ONBOARDING.md +75 -0
- package/DOCS/CONTROL_REFERENCE.md +64 -0
- package/DOCS/DEV_TRACK_ANALYSIS.md +77 -0
- package/DOCS/DEV_TRACK_PLAN_2026-01-07.md +42 -0
- package/DOCS/DEV_TRACK_SESSION_2026-01-31.md +220 -0
- package/DOCS/ENV_SETUP.md +189 -0
- package/DOCS/EXPORT_FORMATS.md +417 -0
- package/DOCS/GPU_DISPOSAL_GUIDE.md +21 -0
- package/DOCS/LICENSING_TIERS.md +275 -0
- package/DOCS/MASTER_PLAN_2026-01-31.md +570 -0
- package/DOCS/OBS_SETUP_GUIDE.md +98 -0
- package/DOCS/PROJECT_SETUP.md +66 -0
- package/DOCS/RENDERER_LIFECYCLE.md +40 -0
- package/DOCS/REPO_MANIFEST.md +121 -0
- package/DOCS/SESSION_014_PLAN.md +195 -0
- package/DOCS/SESSION_LOG_2026-01-07.md +56 -0
- package/DOCS/STRATEGIC_BLUEPRINT_2026-01-07.md +72 -0
- package/DOCS/SYSTEM_AUDIT_2026-01-30.md +738 -0
- package/DOCS/SYSTEM_INVENTORY.md +520 -0
- package/DOCS/TELEMETRY_EXPORTS.md +34 -0
- package/DOCS/WEBGPU_STATUS.md +38 -0
- package/DOCS/XR_BENCHMARKS.md +608 -0
- package/LICENSE +21 -0
- package/README.md +426 -0
- package/docs/.nojekyll +0 -0
- package/docs/01-dissolution_of_euclidean_hegemony.html +346 -0
- package/docs/02-hyperspatial_ego_death.html +346 -0
- package/docs/03-post_cartesian_sublime.html +346 -0
- package/docs/04-crystalline_void_meditation.html +346 -0
- package/docs/05-quantum_decoherence_ballet.html +346 -0
- package/docs/06-dissolution_of_euclidean_hegemony.html +346 -0
- package/docs/07-hyperspatial_ego_death.html +346 -0
- package/docs/08-post_cartesian_sublime.html +346 -0
- package/docs/09-crystalline_void_meditation.html +346 -0
- package/docs/10-quantum_decoherence_ballet.html +346 -0
- package/docs/11-dissolution_of_euclidean_hegemony.html +346 -0
- package/docs/12-hyperspatial_ego_death.html +346 -0
- package/docs/13-post_cartesian_sublime.html +346 -0
- package/docs/index.html +794 -0
- package/docs/test-hub.html +441 -0
- package/docs/url-state.js +102 -0
- package/docs/vib3-exports/01-quantum-quantum-tetrahedron-lattice.html +489 -0
- package/docs/vib3-exports/02-quantum-quantum-hypersphere-matrix.html +489 -0
- package/docs/vib3-exports/03-quantum-quantum-hypertetra-fractal.html +489 -0
- package/docs/vib3-exports/04-faceted-faceted-crystal-structure.html +407 -0
- package/docs/vib3-exports/05-faceted-faceted-klein-bottle.html +407 -0
- package/docs/vib3-exports/06-faceted-faceted-hypertetra-torus.html +407 -0
- package/docs/vib3-exports/07-holographic-holographic-wave-field.html +457 -0
- package/docs/vib3-exports/08-holographic-holographic-hypersphere-sphere.html +457 -0
- package/docs/vib3-exports/09-holographic-holographic-hypertetra-crystal.html +457 -0
- package/docs/vib3-exports/index.html +238 -0
- package/docs/webgpu-live.html +702 -0
- package/package.json +367 -0
- package/src/advanced/AIPresetGenerator.js +777 -0
- package/src/advanced/MIDIController.js +703 -0
- package/src/advanced/OffscreenWorker.js +1051 -0
- package/src/advanced/WebGPUCompute.js +1051 -0
- package/src/advanced/WebXRRenderer.js +680 -0
- package/src/agent/cli/AgentCLI.js +615 -0
- package/src/agent/cli/index.js +14 -0
- package/src/agent/index.js +73 -0
- package/src/agent/mcp/MCPServer.js +950 -0
- package/src/agent/mcp/index.js +9 -0
- package/src/agent/mcp/tools.js +548 -0
- package/src/agent/telemetry/EventStream.js +669 -0
- package/src/agent/telemetry/Instrumentation.js +618 -0
- package/src/agent/telemetry/TelemetryExporters.js +427 -0
- package/src/agent/telemetry/TelemetryService.js +464 -0
- package/src/agent/telemetry/index.js +52 -0
- package/src/benchmarks/BenchmarkRunner.js +381 -0
- package/src/benchmarks/MetricsCollector.js +299 -0
- package/src/benchmarks/index.js +9 -0
- package/src/benchmarks/scenes.js +259 -0
- package/src/cli/index.js +675 -0
- package/src/config/ApiConfig.js +88 -0
- package/src/core/CanvasManager.js +217 -0
- package/src/core/ErrorReporter.js +117 -0
- package/src/core/ParameterMapper.js +333 -0
- package/src/core/Parameters.js +396 -0
- package/src/core/RendererContracts.js +200 -0
- package/src/core/UnifiedResourceManager.js +370 -0
- package/src/core/VIB3Engine.js +636 -0
- package/src/core/renderers/FacetedRendererAdapter.js +32 -0
- package/src/core/renderers/HolographicRendererAdapter.js +29 -0
- package/src/core/renderers/QuantumRendererAdapter.js +29 -0
- package/src/core/renderers/RendererLifecycleManager.js +63 -0
- package/src/creative/ColorPresetsSystem.js +980 -0
- package/src/creative/ParameterTimeline.js +1061 -0
- package/src/creative/PostProcessingPipeline.js +1113 -0
- package/src/creative/TransitionAnimator.js +683 -0
- package/src/export/CSSExporter.js +226 -0
- package/src/export/CardGeneratorBase.js +279 -0
- package/src/export/ExportManager.js +580 -0
- package/src/export/FacetedCardGenerator.js +279 -0
- package/src/export/HolographicCardGenerator.js +543 -0
- package/src/export/LottieExporter.js +552 -0
- package/src/export/QuantumCardGenerator.js +315 -0
- package/src/export/SVGExporter.js +519 -0
- package/src/export/ShaderExporter.js +903 -0
- package/src/export/TradingCardGenerator.js +3055 -0
- package/src/export/TradingCardManager.js +181 -0
- package/src/export/VIB3PackageExporter.js +559 -0
- package/src/export/index.js +14 -0
- package/src/export/systems/TradingCardSystemFaceted.js +494 -0
- package/src/export/systems/TradingCardSystemHolographic.js +452 -0
- package/src/export/systems/TradingCardSystemQuantum.js +411 -0
- package/src/faceted/FacetedSystem.js +963 -0
- package/src/features/CollectionManager.js +433 -0
- package/src/gallery/CollectionManager.js +240 -0
- package/src/gallery/GallerySystem.js +485 -0
- package/src/geometry/GeometryFactory.js +314 -0
- package/src/geometry/GeometryLibrary.js +72 -0
- package/src/geometry/buffers/BufferBuilder.js +338 -0
- package/src/geometry/buffers/index.js +18 -0
- package/src/geometry/generators/Crystal.js +420 -0
- package/src/geometry/generators/Fractal.js +298 -0
- package/src/geometry/generators/KleinBottle.js +197 -0
- package/src/geometry/generators/Sphere.js +192 -0
- package/src/geometry/generators/Tesseract.js +160 -0
- package/src/geometry/generators/Tetrahedron.js +225 -0
- package/src/geometry/generators/Torus.js +304 -0
- package/src/geometry/generators/Wave.js +341 -0
- package/src/geometry/index.js +142 -0
- package/src/geometry/warp/HypersphereCore.js +211 -0
- package/src/geometry/warp/HypertetraCore.js +386 -0
- package/src/geometry/warp/index.js +57 -0
- package/src/holograms/HolographicVisualizer.js +1073 -0
- package/src/holograms/RealHolographicSystem.js +966 -0
- package/src/holograms/variantRegistry.js +69 -0
- package/src/integrations/FigmaPlugin.js +854 -0
- package/src/integrations/OBSMode.js +754 -0
- package/src/integrations/ThreeJsPackage.js +660 -0
- package/src/integrations/TouchDesignerExport.js +552 -0
- package/src/integrations/frameworks/Vib3React.js +591 -0
- package/src/integrations/frameworks/Vib3Svelte.js +654 -0
- package/src/integrations/frameworks/Vib3Vue.js +628 -0
- package/src/llm/LLMParameterInterface.js +240 -0
- package/src/llm/LLMParameterUI.js +577 -0
- package/src/math/Mat4x4.js +708 -0
- package/src/math/Projection.js +341 -0
- package/src/math/Rotor4D.js +637 -0
- package/src/math/Vec4.js +476 -0
- package/src/math/constants.js +164 -0
- package/src/math/index.js +68 -0
- package/src/math/projections.js +54 -0
- package/src/math/rotations.js +196 -0
- package/src/quantum/QuantumEngine.js +906 -0
- package/src/quantum/QuantumVisualizer.js +1103 -0
- package/src/reactivity/ReactivityConfig.js +499 -0
- package/src/reactivity/ReactivityManager.js +586 -0
- package/src/reactivity/SpatialInputSystem.js +1783 -0
- package/src/reactivity/index.js +93 -0
- package/src/render/CommandBuffer.js +465 -0
- package/src/render/MultiCanvasBridge.js +340 -0
- package/src/render/RenderCommand.js +514 -0
- package/src/render/RenderResourceRegistry.js +523 -0
- package/src/render/RenderState.js +552 -0
- package/src/render/RenderTarget.js +512 -0
- package/src/render/ShaderLoader.js +253 -0
- package/src/render/ShaderProgram.js +599 -0
- package/src/render/UnifiedRenderBridge.js +496 -0
- package/src/render/backends/WebGLBackend.js +1108 -0
- package/src/render/backends/WebGPUBackend.js +1409 -0
- package/src/render/commands/CommandBufferExecutor.js +607 -0
- package/src/render/commands/RenderCommandBuffer.js +661 -0
- package/src/render/commands/index.js +17 -0
- package/src/render/index.js +367 -0
- package/src/scene/Disposable.js +498 -0
- package/src/scene/MemoryPool.js +618 -0
- package/src/scene/Node4D.js +697 -0
- package/src/scene/ResourceManager.js +599 -0
- package/src/scene/Scene4D.js +540 -0
- package/src/scene/index.js +98 -0
- package/src/schemas/error.schema.json +84 -0
- package/src/schemas/extension.schema.json +88 -0
- package/src/schemas/index.js +214 -0
- package/src/schemas/parameters.schema.json +142 -0
- package/src/schemas/tool-pack.schema.json +44 -0
- package/src/schemas/tool-response.schema.json +127 -0
- package/src/shaders/common/fullscreen.vert.glsl +5 -0
- package/src/shaders/common/fullscreen.vert.wgsl +17 -0
- package/src/shaders/common/geometry24.glsl +65 -0
- package/src/shaders/common/geometry24.wgsl +54 -0
- package/src/shaders/common/rotation4d.glsl +85 -0
- package/src/shaders/common/rotation4d.wgsl +86 -0
- package/src/shaders/common/uniforms.glsl +44 -0
- package/src/shaders/common/uniforms.wgsl +48 -0
- package/src/shaders/faceted/faceted.frag.glsl +129 -0
- package/src/shaders/faceted/faceted.frag.wgsl +164 -0
- package/src/shaders/holographic/holographic.frag.glsl +406 -0
- package/src/shaders/holographic/holographic.frag.wgsl +185 -0
- package/src/shaders/quantum/quantum.frag.glsl +513 -0
- package/src/shaders/quantum/quantum.frag.wgsl +361 -0
- package/src/testing/ParallelTestFramework.js +519 -0
- package/src/testing/__snapshots__/exportFormats.test.js.snap +24 -0
- package/src/testing/exportFormats.test.js +8 -0
- package/src/testing/projections.test.js +14 -0
- package/src/testing/rotations.test.js +37 -0
- package/src/ui/InteractivityMenu.js +516 -0
- package/src/ui/StatusManager.js +96 -0
- package/src/ui/adaptive/renderers/webgpu/BufferLayout.ts +252 -0
- package/src/ui/adaptive/renderers/webgpu/PolytopeInstanceBuffer.ts +144 -0
- package/src/ui/adaptive/renderers/webgpu/TripleBufferedUniform.ts +170 -0
- package/src/ui/adaptive/renderers/webgpu/WebGPURenderer.ts +735 -0
- package/src/ui/adaptive/renderers/webgpu/index.ts +112 -0
- package/src/variations/VariationManager.js +431 -0
- package/src/viewer/AudioReactivity.js +505 -0
- package/src/viewer/CardBending.js +481 -0
- package/src/viewer/GalleryUI.js +832 -0
- package/src/viewer/ReactivityManager.js +590 -0
- package/src/viewer/TradingCardExporter.js +600 -0
- package/src/viewer/ViewerPortal.js +374 -0
- package/src/viewer/index.js +12 -0
- package/src/wasm/WasmLoader.js +296 -0
- package/src/wasm/index.js +132 -0
- package/tools/agentic/mcpTools.js +88 -0
- package/tools/cli/agent-cli.js +92 -0
- package/tools/export/formats.js +24 -0
- package/tools/math/rotation-baseline.mjs +64 -0
- package/tools/shader-sync-verify.js +937 -0
- package/tools/telemetry/manifestPipeline.js +141 -0
- package/tools/telemetry/telemetryEvents.js +35 -0
- package/types/adaptive-sdk.d.ts +185 -0
- package/types/advanced/AIPresetGenerator.d.ts +81 -0
- package/types/advanced/MIDIController.d.ts +100 -0
- package/types/advanced/OffscreenWorker.d.ts +82 -0
- package/types/advanced/WebGPUCompute.d.ts +52 -0
- package/types/advanced/WebXRRenderer.d.ts +77 -0
- package/types/advanced/index.d.ts +46 -0
- package/types/core/ErrorReporter.d.ts +50 -0
- package/types/core/VIB3Engine.d.ts +204 -0
- package/types/creative/ColorPresetsSystem.d.ts +91 -0
- package/types/creative/ParameterTimeline.d.ts +74 -0
- package/types/creative/PostProcessingPipeline.d.ts +109 -0
- package/types/creative/TransitionAnimator.d.ts +71 -0
- package/types/creative/index.d.ts +35 -0
- package/types/integrations/FigmaPlugin.d.ts +46 -0
- package/types/integrations/OBSMode.d.ts +74 -0
- package/types/integrations/ThreeJsPackage.d.ts +62 -0
- package/types/integrations/TouchDesignerExport.d.ts +36 -0
- package/types/integrations/Vib3React.d.ts +74 -0
- package/types/integrations/Vib3Svelte.d.ts +63 -0
- package/types/integrations/Vib3Vue.d.ts +55 -0
- package/types/integrations/index.d.ts +52 -0
- package/types/reactivity/SpatialInputSystem.d.ts +173 -0
- package/types/reactivity/index.d.ts +394 -0
- package/types/render/CommandBuffer.d.ts +169 -0
- package/types/render/RenderCommand.d.ts +312 -0
- package/types/render/RenderState.d.ts +279 -0
- package/types/render/RenderTarget.d.ts +254 -0
- package/types/render/ShaderProgram.d.ts +277 -0
- package/types/render/UnifiedRenderBridge.d.ts +143 -0
- package/types/render/WebGLBackend.d.ts +168 -0
- package/types/render/WebGPUBackend.d.ts +186 -0
- 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
|
+
}
|