@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,980 @@
1
+ /**
2
+ * ColorPresetsSystem.js - VIB3+ Color Presets & Themes System
3
+ *
4
+ * Provides 20+ built-in color presets organized by mood/theme, with support
5
+ * for smooth transitions, custom presets, and serialization. Integrates with
6
+ * the VIB3+ parameter system via the parameterUpdateFn callback pattern.
7
+ *
8
+ * @module creative/ColorPresetsSystem
9
+ * @version 1.0.0
10
+ * @author VIB3+ Creative Tooling - Phase B
11
+ */
12
+
13
+ /**
14
+ * @typedef {Object} ColorPresetConfig
15
+ * @property {number} hue - Primary hue value (0-360)
16
+ * @property {number} saturation - Color saturation (0-1)
17
+ * @property {number} intensity - Visual intensity/brightness (0-1)
18
+ * @property {number} [hueShift=0] - Secondary hue offset applied over time (degrees)
19
+ * @property {number} [saturationRange=0] - Range of saturation oscillation (0-1)
20
+ * @property {number} [intensityRange=0] - Range of intensity oscillation (0-1)
21
+ * @property {number} [speed] - Optional speed override (0.1-3)
22
+ * @property {number} [chaos] - Optional chaos override (0-1)
23
+ * @property {number} [morphFactor] - Optional morph factor override (0-2)
24
+ * @property {string} [category] - Category grouping for UI display
25
+ * @property {string} [description] - Human-readable description
26
+ */
27
+
28
+ /**
29
+ * @typedef {Object} TransitionState
30
+ * @property {ColorPresetConfig} from - Starting preset configuration
31
+ * @property {ColorPresetConfig} to - Target preset configuration
32
+ * @property {number} startTime - Transition start timestamp (ms)
33
+ * @property {number} duration - Transition duration (ms)
34
+ * @property {number} progress - Current progress (0-1)
35
+ */
36
+
37
+ /**
38
+ * Color presets and themes system for VIB3+ visualization engine.
39
+ *
40
+ * Manages a library of built-in and custom color presets, each defining
41
+ * hue, saturation, intensity and optional secondary color shift parameters.
42
+ * Supports smooth animated transitions between presets.
43
+ *
44
+ * @example
45
+ * const colors = new ColorPresetsSystem((name, value) => {
46
+ * engine.setParameter(name, value);
47
+ * });
48
+ *
49
+ * // Apply a preset with smooth transition
50
+ * colors.applyPreset('Cyberpunk Neon');
51
+ *
52
+ * // List all presets
53
+ * const presets = colors.getPresets();
54
+ *
55
+ * // Create and apply a custom preset
56
+ * colors.createCustomPreset('My Theme', { hue: 42, saturation: 0.9, intensity: 0.7 });
57
+ * colors.applyPreset('My Theme');
58
+ */
59
+ export class ColorPresetsSystem {
60
+ /**
61
+ * Create a new ColorPresetsSystem.
62
+ *
63
+ * @param {Function} parameterUpdateFn - Callback invoked as (paramName, value)
64
+ * whenever a VIB3+ parameter should be updated. This follows the standard
65
+ * VIB3+ parameter callback pattern.
66
+ */
67
+ constructor(parameterUpdateFn) {
68
+ if (typeof parameterUpdateFn !== 'function') {
69
+ throw new Error('ColorPresetsSystem requires a parameterUpdateFn callback');
70
+ }
71
+
72
+ /** @type {Function} */
73
+ this.updateParameter = parameterUpdateFn;
74
+
75
+ /** @type {Map<string, ColorPresetConfig>} Built-in presets */
76
+ this.presets = new Map();
77
+
78
+ /** @type {Map<string, ColorPresetConfig>} User-created presets */
79
+ this.customPresets = new Map();
80
+
81
+ /** @type {string|null} Name of the currently active preset */
82
+ this.currentPreset = null;
83
+
84
+ /** @type {TransitionState|null} Active transition state */
85
+ this.transitionState = null;
86
+
87
+ /** @type {number|null} requestAnimationFrame ID for transitions */
88
+ this._frameId = null;
89
+
90
+ /** @type {Function|null} Callback invoked when a transition completes */
91
+ this._onTransitionComplete = null;
92
+
93
+ this._initBuiltInPresets();
94
+ }
95
+
96
+ // -------------------------------------------------------------------------
97
+ // Initialization
98
+ // -------------------------------------------------------------------------
99
+
100
+ /**
101
+ * Initialize all built-in color presets.
102
+ * @private
103
+ */
104
+ _initBuiltInPresets() {
105
+ const builtIn = [
106
+ {
107
+ name: 'Cyberpunk Neon',
108
+ config: {
109
+ hue: 290,
110
+ saturation: 0.95,
111
+ intensity: 0.85,
112
+ hueShift: 30,
113
+ saturationRange: 0.1,
114
+ intensityRange: 0.15,
115
+ speed: 1.4,
116
+ chaos: 0.35,
117
+ category: 'Futuristic',
118
+ description: 'Hot magenta and electric blue neon glow'
119
+ }
120
+ },
121
+ {
122
+ name: 'Ocean Deep',
123
+ config: {
124
+ hue: 200,
125
+ saturation: 0.75,
126
+ intensity: 0.55,
127
+ hueShift: 15,
128
+ saturationRange: 0.1,
129
+ intensityRange: 0.1,
130
+ speed: 0.6,
131
+ chaos: 0.1,
132
+ category: 'Nature',
133
+ description: 'Deep ocean blues and teals with gentle motion'
134
+ }
135
+ },
136
+ {
137
+ name: 'Solar Flare',
138
+ config: {
139
+ hue: 30,
140
+ saturation: 0.9,
141
+ intensity: 0.9,
142
+ hueShift: 25,
143
+ saturationRange: 0.1,
144
+ intensityRange: 0.2,
145
+ speed: 2.0,
146
+ chaos: 0.5,
147
+ category: 'Cosmic',
148
+ description: 'Explosive orange and yellow solar eruption'
149
+ }
150
+ },
151
+ {
152
+ name: 'Midnight Purple',
153
+ config: {
154
+ hue: 270,
155
+ saturation: 0.7,
156
+ intensity: 0.4,
157
+ hueShift: 10,
158
+ saturationRange: 0.05,
159
+ intensityRange: 0.08,
160
+ speed: 0.5,
161
+ chaos: 0.05,
162
+ category: 'Dark',
163
+ description: 'Subdued deep purple with subtle shimmer'
164
+ }
165
+ },
166
+ {
167
+ name: 'Arctic Aurora',
168
+ config: {
169
+ hue: 160,
170
+ saturation: 0.8,
171
+ intensity: 0.65,
172
+ hueShift: 50,
173
+ saturationRange: 0.15,
174
+ intensityRange: 0.2,
175
+ speed: 0.8,
176
+ chaos: 0.2,
177
+ category: 'Nature',
178
+ description: 'Northern lights green-to-violet sweeps'
179
+ }
180
+ },
181
+ {
182
+ name: 'Volcanic',
183
+ config: {
184
+ hue: 10,
185
+ saturation: 0.85,
186
+ intensity: 0.75,
187
+ hueShift: 15,
188
+ saturationRange: 0.1,
189
+ intensityRange: 0.25,
190
+ speed: 1.2,
191
+ chaos: 0.45,
192
+ category: 'Elemental',
193
+ description: 'Molten reds and oranges with dark undertones'
194
+ }
195
+ },
196
+ {
197
+ name: 'Forest Mystic',
198
+ config: {
199
+ hue: 130,
200
+ saturation: 0.6,
201
+ intensity: 0.5,
202
+ hueShift: 20,
203
+ saturationRange: 0.1,
204
+ intensityRange: 0.1,
205
+ speed: 0.7,
206
+ chaos: 0.15,
207
+ category: 'Nature',
208
+ description: 'Enchanted forest greens with mossy accents'
209
+ }
210
+ },
211
+ {
212
+ name: 'Retro Wave',
213
+ config: {
214
+ hue: 310,
215
+ saturation: 0.9,
216
+ intensity: 0.8,
217
+ hueShift: 40,
218
+ saturationRange: 0.1,
219
+ intensityRange: 0.15,
220
+ speed: 1.5,
221
+ chaos: 0.3,
222
+ category: 'Retro',
223
+ description: 'Synthwave pink and cyan retrowave palette'
224
+ }
225
+ },
226
+ {
227
+ name: 'Quantum Void',
228
+ config: {
229
+ hue: 240,
230
+ saturation: 0.5,
231
+ intensity: 0.3,
232
+ hueShift: 5,
233
+ saturationRange: 0.05,
234
+ intensityRange: 0.15,
235
+ speed: 0.4,
236
+ chaos: 0.6,
237
+ category: 'Cosmic',
238
+ description: 'Near-black indigo with sporadic quantum flickers'
239
+ }
240
+ },
241
+ {
242
+ name: 'Golden Hour',
243
+ config: {
244
+ hue: 45,
245
+ saturation: 0.8,
246
+ intensity: 0.7,
247
+ hueShift: 10,
248
+ saturationRange: 0.05,
249
+ intensityRange: 0.1,
250
+ speed: 0.5,
251
+ chaos: 0.05,
252
+ category: 'Warm',
253
+ description: 'Warm golden light of late afternoon'
254
+ }
255
+ },
256
+ {
257
+ name: 'Ice Crystal',
258
+ config: {
259
+ hue: 190,
260
+ saturation: 0.55,
261
+ intensity: 0.8,
262
+ hueShift: 8,
263
+ saturationRange: 0.1,
264
+ intensityRange: 0.1,
265
+ speed: 0.6,
266
+ chaos: 0.08,
267
+ category: 'Elemental',
268
+ description: 'Crisp icy blue-white crystalline shimmer'
269
+ }
270
+ },
271
+ {
272
+ name: 'Blood Moon',
273
+ config: {
274
+ hue: 0,
275
+ saturation: 0.85,
276
+ intensity: 0.5,
277
+ hueShift: 10,
278
+ saturationRange: 0.1,
279
+ intensityRange: 0.15,
280
+ speed: 0.7,
281
+ chaos: 0.25,
282
+ category: 'Dark',
283
+ description: 'Deep crimson with dark blood-red pulsation'
284
+ }
285
+ },
286
+ {
287
+ name: 'Digital Rain',
288
+ config: {
289
+ hue: 120,
290
+ saturation: 0.9,
291
+ intensity: 0.7,
292
+ hueShift: 5,
293
+ saturationRange: 0.05,
294
+ intensityRange: 0.2,
295
+ speed: 1.8,
296
+ chaos: 0.4,
297
+ category: 'Futuristic',
298
+ description: 'Matrix-green cascading digital streams'
299
+ }
300
+ },
301
+ {
302
+ name: 'Sunset Gradient',
303
+ config: {
304
+ hue: 20,
305
+ saturation: 0.85,
306
+ intensity: 0.75,
307
+ hueShift: 35,
308
+ saturationRange: 0.1,
309
+ intensityRange: 0.1,
310
+ speed: 0.5,
311
+ chaos: 0.1,
312
+ category: 'Warm',
313
+ description: 'Orange-to-purple sunset sweep across the sky'
314
+ }
315
+ },
316
+ {
317
+ name: 'Deep Space',
318
+ config: {
319
+ hue: 250,
320
+ saturation: 0.6,
321
+ intensity: 0.35,
322
+ hueShift: 20,
323
+ saturationRange: 0.1,
324
+ intensityRange: 0.2,
325
+ speed: 0.3,
326
+ chaos: 0.15,
327
+ category: 'Cosmic',
328
+ description: 'Dark nebula blues with distant starlight'
329
+ }
330
+ },
331
+ {
332
+ name: 'Toxic Green',
333
+ config: {
334
+ hue: 100,
335
+ saturation: 0.95,
336
+ intensity: 0.8,
337
+ hueShift: 10,
338
+ saturationRange: 0.05,
339
+ intensityRange: 0.15,
340
+ speed: 1.6,
341
+ chaos: 0.5,
342
+ category: 'Futuristic',
343
+ description: 'Radioactive green glow with toxic highlights'
344
+ }
345
+ },
346
+ {
347
+ name: 'Royal Purple',
348
+ config: {
349
+ hue: 280,
350
+ saturation: 0.75,
351
+ intensity: 0.6,
352
+ hueShift: 12,
353
+ saturationRange: 0.08,
354
+ intensityRange: 0.1,
355
+ speed: 0.8,
356
+ chaos: 0.12,
357
+ category: 'Elegant',
358
+ description: 'Rich regal purple with velvet depth'
359
+ }
360
+ },
361
+ {
362
+ name: 'Coral Reef',
363
+ config: {
364
+ hue: 350,
365
+ saturation: 0.7,
366
+ intensity: 0.65,
367
+ hueShift: 25,
368
+ saturationRange: 0.1,
369
+ intensityRange: 0.1,
370
+ speed: 0.7,
371
+ chaos: 0.2,
372
+ category: 'Nature',
373
+ description: 'Coral pink and aqua reef ecosystem'
374
+ }
375
+ },
376
+ {
377
+ name: 'Thunderstorm',
378
+ config: {
379
+ hue: 220,
380
+ saturation: 0.6,
381
+ intensity: 0.5,
382
+ hueShift: 15,
383
+ saturationRange: 0.15,
384
+ intensityRange: 0.35,
385
+ speed: 1.3,
386
+ chaos: 0.65,
387
+ category: 'Elemental',
388
+ description: 'Electric blues and greys with lightning flashes'
389
+ }
390
+ },
391
+ {
392
+ name: 'Holographic Rainbow',
393
+ config: {
394
+ hue: 0,
395
+ saturation: 0.85,
396
+ intensity: 0.75,
397
+ hueShift: 120,
398
+ saturationRange: 0.15,
399
+ intensityRange: 0.2,
400
+ speed: 1.0,
401
+ chaos: 0.2,
402
+ category: 'Special',
403
+ description: 'Full-spectrum holographic rainbow sweep'
404
+ }
405
+ },
406
+ {
407
+ name: 'Lavender Dream',
408
+ config: {
409
+ hue: 260,
410
+ saturation: 0.5,
411
+ intensity: 0.6,
412
+ hueShift: 8,
413
+ saturationRange: 0.05,
414
+ intensityRange: 0.05,
415
+ speed: 0.4,
416
+ chaos: 0.03,
417
+ category: 'Elegant',
418
+ description: 'Soft pastel lavender with dreamlike calm'
419
+ }
420
+ },
421
+ {
422
+ name: 'Amber Glow',
423
+ config: {
424
+ hue: 38,
425
+ saturation: 0.85,
426
+ intensity: 0.65,
427
+ hueShift: 8,
428
+ saturationRange: 0.05,
429
+ intensityRange: 0.1,
430
+ speed: 0.6,
431
+ chaos: 0.08,
432
+ category: 'Warm',
433
+ description: 'Warm amber candlelight radiance'
434
+ }
435
+ }
436
+ ];
437
+
438
+ for (const { name, config } of builtIn) {
439
+ this.presets.set(name, { ...config });
440
+ }
441
+ }
442
+
443
+ // -------------------------------------------------------------------------
444
+ // Public API - Querying
445
+ // -------------------------------------------------------------------------
446
+
447
+ /**
448
+ * Get a flat list of all available presets (built-in + custom).
449
+ *
450
+ * @returns {Array<{name: string, config: ColorPresetConfig, isCustom: boolean}>}
451
+ */
452
+ getPresets() {
453
+ const list = [];
454
+
455
+ for (const [name, config] of this.presets) {
456
+ list.push({ name, config: { ...config }, isCustom: false });
457
+ }
458
+ for (const [name, config] of this.customPresets) {
459
+ list.push({ name, config: { ...config }, isCustom: true });
460
+ }
461
+
462
+ return list;
463
+ }
464
+
465
+ /**
466
+ * Get presets grouped by their category.
467
+ *
468
+ * @returns {Object<string, Array<{name: string, config: ColorPresetConfig, isCustom: boolean}>>}
469
+ */
470
+ getPresetsByCategory() {
471
+ const categories = {};
472
+ const all = this.getPresets();
473
+
474
+ for (const preset of all) {
475
+ const cat = preset.config.category || 'Uncategorized';
476
+ if (!categories[cat]) {
477
+ categories[cat] = [];
478
+ }
479
+ categories[cat].push(preset);
480
+ }
481
+
482
+ return categories;
483
+ }
484
+
485
+ /**
486
+ * Get a single preset configuration by name.
487
+ *
488
+ * @param {string} name - Preset name
489
+ * @returns {ColorPresetConfig|null} The preset config, or null if not found
490
+ */
491
+ getPreset(name) {
492
+ const config = this.customPresets.get(name) || this.presets.get(name);
493
+ return config ? { ...config } : null;
494
+ }
495
+
496
+ /**
497
+ * Get the name of the currently active preset.
498
+ *
499
+ * @returns {string|null}
500
+ */
501
+ getCurrentPreset() {
502
+ return this.currentPreset;
503
+ }
504
+
505
+ /**
506
+ * Check whether a transition animation is currently active.
507
+ *
508
+ * @returns {boolean}
509
+ */
510
+ isTransitioning() {
511
+ return this.transitionState !== null;
512
+ }
513
+
514
+ // -------------------------------------------------------------------------
515
+ // Public API - Applying Presets
516
+ // -------------------------------------------------------------------------
517
+
518
+ /**
519
+ * Apply a color preset by name.
520
+ *
521
+ * @param {string} name - Name of the preset to apply
522
+ * @param {boolean} [transition=true] - Whether to smoothly animate to the preset
523
+ * @param {number} [duration=800] - Transition duration in milliseconds
524
+ * @param {Function} [onComplete] - Optional callback when transition finishes
525
+ * @returns {boolean} true if the preset was found and applied
526
+ */
527
+ applyPreset(name, transition = true, duration = 800, onComplete = null) {
528
+ const config = this.customPresets.get(name) || this.presets.get(name);
529
+ if (!config) {
530
+ console.warn(`ColorPresetsSystem: Unknown preset "${name}"`);
531
+ return false;
532
+ }
533
+
534
+ if (transition && duration > 0) {
535
+ this._startTransition(config, duration, onComplete);
536
+ } else {
537
+ this._applyConfigImmediate(config);
538
+ if (typeof onComplete === 'function') {
539
+ onComplete();
540
+ }
541
+ }
542
+
543
+ this.currentPreset = name;
544
+ return true;
545
+ }
546
+
547
+ /**
548
+ * Apply a raw preset configuration object directly (without looking up by name).
549
+ *
550
+ * @param {ColorPresetConfig} config - The configuration to apply
551
+ * @param {boolean} [transition=true] - Whether to animate
552
+ * @param {number} [duration=800] - Transition duration in ms
553
+ */
554
+ applyConfig(config, transition = true, duration = 800) {
555
+ if (!config || typeof config.hue !== 'number') {
556
+ console.warn('ColorPresetsSystem: Invalid config object');
557
+ return;
558
+ }
559
+
560
+ if (transition && duration > 0) {
561
+ this._startTransition(config, duration);
562
+ } else {
563
+ this._applyConfigImmediate(config);
564
+ }
565
+
566
+ this.currentPreset = null;
567
+ }
568
+
569
+ // -------------------------------------------------------------------------
570
+ // Public API - Custom Presets
571
+ // -------------------------------------------------------------------------
572
+
573
+ /**
574
+ * Create a new custom preset.
575
+ *
576
+ * @param {string} name - Name for the custom preset
577
+ * @param {ColorPresetConfig} config - Preset configuration
578
+ * @returns {boolean} true if created, false if name conflicts with built-in
579
+ */
580
+ createCustomPreset(name, config) {
581
+ if (typeof name !== 'string' || name.trim().length === 0) {
582
+ console.warn('ColorPresetsSystem: Preset name must be a non-empty string');
583
+ return false;
584
+ }
585
+
586
+ if (this.presets.has(name)) {
587
+ console.warn(`ColorPresetsSystem: Cannot overwrite built-in preset "${name}"`);
588
+ return false;
589
+ }
590
+
591
+ const validated = this._validateConfig(config);
592
+ this.customPresets.set(name.trim(), validated);
593
+ return true;
594
+ }
595
+
596
+ /**
597
+ * Update an existing custom preset.
598
+ *
599
+ * @param {string} name - Name of the custom preset to update
600
+ * @param {ColorPresetConfig} config - New configuration
601
+ * @returns {boolean} true if updated
602
+ */
603
+ updateCustomPreset(name, config) {
604
+ if (!this.customPresets.has(name)) {
605
+ console.warn(`ColorPresetsSystem: Custom preset "${name}" not found`);
606
+ return false;
607
+ }
608
+
609
+ const validated = this._validateConfig(config);
610
+ this.customPresets.set(name, validated);
611
+ return true;
612
+ }
613
+
614
+ /**
615
+ * Delete a custom preset.
616
+ *
617
+ * @param {string} name - Name of the custom preset to remove
618
+ * @returns {boolean} true if deleted
619
+ */
620
+ deleteCustomPreset(name) {
621
+ return this.customPresets.delete(name);
622
+ }
623
+
624
+ // -------------------------------------------------------------------------
625
+ // Public API - Serialization
626
+ // -------------------------------------------------------------------------
627
+
628
+ /**
629
+ * Export a preset for external storage or sharing.
630
+ *
631
+ * @param {string} [name] - Preset name to export. If omitted, exports current state.
632
+ * @returns {Object} Serializable preset data
633
+ */
634
+ exportPreset(name) {
635
+ let config;
636
+ let presetName;
637
+
638
+ if (name) {
639
+ config = this.getPreset(name);
640
+ presetName = name;
641
+ } else if (this.currentPreset) {
642
+ config = this.getPreset(this.currentPreset);
643
+ presetName = this.currentPreset;
644
+ } else {
645
+ console.warn('ColorPresetsSystem: No preset specified or active for export');
646
+ return null;
647
+ }
648
+
649
+ if (!config) {
650
+ console.warn(`ColorPresetsSystem: Preset "${name}" not found for export`);
651
+ return null;
652
+ }
653
+
654
+ return {
655
+ type: 'vib3-color-preset',
656
+ version: '1.0.0',
657
+ name: presetName,
658
+ timestamp: new Date().toISOString(),
659
+ config: { ...config }
660
+ };
661
+ }
662
+
663
+ /**
664
+ * Export all custom presets for backup.
665
+ *
666
+ * @returns {Object} Serializable object containing all custom presets
667
+ */
668
+ exportAllCustomPresets() {
669
+ const presets = {};
670
+ for (const [name, config] of this.customPresets) {
671
+ presets[name] = { ...config };
672
+ }
673
+
674
+ return {
675
+ type: 'vib3-color-preset-collection',
676
+ version: '1.0.0',
677
+ timestamp: new Date().toISOString(),
678
+ presets
679
+ };
680
+ }
681
+
682
+ /**
683
+ * Import a preset from serialized data.
684
+ *
685
+ * @param {Object} data - Preset data as returned by exportPreset()
686
+ * @returns {boolean} true if imported successfully
687
+ */
688
+ importPreset(data) {
689
+ if (!data || typeof data !== 'object') {
690
+ console.warn('ColorPresetsSystem: Invalid import data');
691
+ return false;
692
+ }
693
+
694
+ if (data.type === 'vib3-color-preset' && data.config) {
695
+ const name = data.name || `Imported ${Date.now()}`;
696
+ return this.createCustomPreset(name, data.config);
697
+ }
698
+
699
+ if (data.type === 'vib3-color-preset-collection' && data.presets) {
700
+ let count = 0;
701
+ for (const [name, config] of Object.entries(data.presets)) {
702
+ if (this.createCustomPreset(name, config)) {
703
+ count++;
704
+ }
705
+ }
706
+ return count > 0;
707
+ }
708
+
709
+ console.warn('ColorPresetsSystem: Unrecognized import format');
710
+ return false;
711
+ }
712
+
713
+ // -------------------------------------------------------------------------
714
+ // Public API - Transition Control
715
+ // -------------------------------------------------------------------------
716
+
717
+ /**
718
+ * Cancel any in-progress transition and keep current values.
719
+ */
720
+ cancelTransition() {
721
+ if (this._frameId !== null) {
722
+ cancelAnimationFrame(this._frameId);
723
+ this._frameId = null;
724
+ }
725
+ this.transitionState = null;
726
+ this._onTransitionComplete = null;
727
+ }
728
+
729
+ // -------------------------------------------------------------------------
730
+ // Public API - Lifecycle
731
+ // -------------------------------------------------------------------------
732
+
733
+ /**
734
+ * Clean up resources and cancel any active animation.
735
+ */
736
+ dispose() {
737
+ this.cancelTransition();
738
+ this.presets.clear();
739
+ this.customPresets.clear();
740
+ this.currentPreset = null;
741
+ }
742
+
743
+ // -------------------------------------------------------------------------
744
+ // Private - Transition Engine
745
+ // -------------------------------------------------------------------------
746
+
747
+ /**
748
+ * Begin a smooth transition to the target configuration.
749
+ *
750
+ * @param {ColorPresetConfig} targetConfig - Target color configuration
751
+ * @param {number} duration - Duration in milliseconds
752
+ * @param {Function} [onComplete] - Completion callback
753
+ * @private
754
+ */
755
+ _startTransition(targetConfig, duration, onComplete = null) {
756
+ // Cancel any existing transition
757
+ this.cancelTransition();
758
+
759
+ // Capture the current parameter values as the starting point
760
+ const fromConfig = this._captureCurrentState();
761
+
762
+ this.transitionState = {
763
+ from: fromConfig,
764
+ to: { ...targetConfig },
765
+ startTime: performance.now(),
766
+ duration,
767
+ progress: 0
768
+ };
769
+
770
+ this._onTransitionComplete = onComplete;
771
+ this._tickTransition();
772
+ }
773
+
774
+ /**
775
+ * Animation frame tick for the transition.
776
+ * @private
777
+ */
778
+ _tickTransition() {
779
+ if (!this.transitionState) return;
780
+
781
+ const now = performance.now();
782
+ const elapsed = now - this.transitionState.startTime;
783
+ const progress = Math.min(elapsed / this.transitionState.duration, 1);
784
+
785
+ // Smooth ease-in-out curve
786
+ const eased = this._easeInOutCubic(progress);
787
+
788
+ this.transitionState.progress = progress;
789
+
790
+ // Interpolate and apply each animatable parameter
791
+ const from = this.transitionState.from;
792
+ const to = this.transitionState.to;
793
+
794
+ // Hue needs special circular interpolation
795
+ const hue = this._lerpHue(from.hue, to.hue, eased);
796
+ this.updateParameter('hue', Math.round(hue));
797
+
798
+ // Linear interpolation for saturation and intensity
799
+ if (typeof to.saturation === 'number') {
800
+ this.updateParameter('saturation', this._lerp(from.saturation, to.saturation, eased));
801
+ }
802
+ if (typeof to.intensity === 'number') {
803
+ this.updateParameter('intensity', this._lerp(from.intensity, to.intensity, eased));
804
+ }
805
+
806
+ // Optional parameter overrides
807
+ if (typeof to.speed === 'number' && typeof from.speed === 'number') {
808
+ this.updateParameter('speed', this._lerp(from.speed, to.speed, eased));
809
+ }
810
+ if (typeof to.chaos === 'number' && typeof from.chaos === 'number') {
811
+ this.updateParameter('chaos', this._lerp(from.chaos, to.chaos, eased));
812
+ }
813
+ if (typeof to.morphFactor === 'number' && typeof from.morphFactor === 'number') {
814
+ this.updateParameter('morphFactor', this._lerp(from.morphFactor, to.morphFactor, eased));
815
+ }
816
+
817
+ // Continue or finish
818
+ if (progress < 1) {
819
+ this._frameId = requestAnimationFrame(() => this._tickTransition());
820
+ } else {
821
+ this.transitionState = null;
822
+ this._frameId = null;
823
+ if (typeof this._onTransitionComplete === 'function') {
824
+ const cb = this._onTransitionComplete;
825
+ this._onTransitionComplete = null;
826
+ cb();
827
+ }
828
+ }
829
+ }
830
+
831
+ /**
832
+ * Apply a configuration immediately without transition.
833
+ *
834
+ * @param {ColorPresetConfig} config
835
+ * @private
836
+ */
837
+ _applyConfigImmediate(config) {
838
+ if (typeof config.hue === 'number') {
839
+ this.updateParameter('hue', Math.round(config.hue));
840
+ }
841
+ if (typeof config.saturation === 'number') {
842
+ this.updateParameter('saturation', config.saturation);
843
+ }
844
+ if (typeof config.intensity === 'number') {
845
+ this.updateParameter('intensity', config.intensity);
846
+ }
847
+ if (typeof config.speed === 'number') {
848
+ this.updateParameter('speed', config.speed);
849
+ }
850
+ if (typeof config.chaos === 'number') {
851
+ this.updateParameter('chaos', config.chaos);
852
+ }
853
+ if (typeof config.morphFactor === 'number') {
854
+ this.updateParameter('morphFactor', config.morphFactor);
855
+ }
856
+ }
857
+
858
+ /**
859
+ * Capture the current state of animatable parameters as a snapshot.
860
+ * Uses sensible defaults for any parameter the callback system cannot
861
+ * report back (since the callback is write-only).
862
+ *
863
+ * @returns {ColorPresetConfig}
864
+ * @private
865
+ */
866
+ _captureCurrentState() {
867
+ // If we have a current preset, use its values as the starting state
868
+ if (this.currentPreset) {
869
+ const preset = this.customPresets.get(this.currentPreset) || this.presets.get(this.currentPreset);
870
+ if (preset) {
871
+ return { ...preset };
872
+ }
873
+ }
874
+
875
+ // Fallback: use VIB3+ defaults from Parameters.js
876
+ return {
877
+ hue: 200,
878
+ saturation: 0.8,
879
+ intensity: 0.5,
880
+ speed: 1.0,
881
+ chaos: 0.2,
882
+ morphFactor: 1.0
883
+ };
884
+ }
885
+
886
+ // -------------------------------------------------------------------------
887
+ // Private - Validation
888
+ // -------------------------------------------------------------------------
889
+
890
+ /**
891
+ * Validate and sanitize a preset config, filling defaults for missing fields.
892
+ *
893
+ * @param {Object} config - Raw config object
894
+ * @returns {ColorPresetConfig} Sanitized config
895
+ * @private
896
+ */
897
+ _validateConfig(config) {
898
+ return {
899
+ hue: this._clamp(Number(config.hue) || 0, 0, 360),
900
+ saturation: this._clamp(Number(config.saturation) ?? 0.8, 0, 1),
901
+ intensity: this._clamp(Number(config.intensity) ?? 0.5, 0, 1),
902
+ hueShift: Number(config.hueShift) || 0,
903
+ saturationRange: this._clamp(Number(config.saturationRange) || 0, 0, 1),
904
+ intensityRange: this._clamp(Number(config.intensityRange) || 0, 0, 1),
905
+ speed: config.speed != null ? this._clamp(Number(config.speed), 0.1, 3) : undefined,
906
+ chaos: config.chaos != null ? this._clamp(Number(config.chaos), 0, 1) : undefined,
907
+ morphFactor: config.morphFactor != null ? this._clamp(Number(config.morphFactor), 0, 2) : undefined,
908
+ category: typeof config.category === 'string' ? config.category : 'Custom',
909
+ description: typeof config.description === 'string' ? config.description : ''
910
+ };
911
+ }
912
+
913
+ // -------------------------------------------------------------------------
914
+ // Private - Math Utilities
915
+ // -------------------------------------------------------------------------
916
+
917
+ /**
918
+ * Linear interpolation.
919
+ *
920
+ * @param {number} a - Start value
921
+ * @param {number} b - End value
922
+ * @param {number} t - Progress (0-1)
923
+ * @returns {number}
924
+ * @private
925
+ */
926
+ _lerp(a, b, t) {
927
+ return a + (b - a) * t;
928
+ }
929
+
930
+ /**
931
+ * Circular hue interpolation that takes the shortest path around the
932
+ * 360-degree hue wheel.
933
+ *
934
+ * @param {number} a - Start hue (0-360)
935
+ * @param {number} b - End hue (0-360)
936
+ * @param {number} t - Progress (0-1)
937
+ * @returns {number} Interpolated hue (0-360)
938
+ * @private
939
+ */
940
+ _lerpHue(a, b, t) {
941
+ let diff = b - a;
942
+
943
+ // Take the shortest arc around the wheel
944
+ if (diff > 180) diff -= 360;
945
+ if (diff < -180) diff += 360;
946
+
947
+ let result = a + diff * t;
948
+ if (result < 0) result += 360;
949
+ if (result >= 360) result -= 360;
950
+
951
+ return result;
952
+ }
953
+
954
+ /**
955
+ * Clamp a value to a range.
956
+ *
957
+ * @param {number} value
958
+ * @param {number} min
959
+ * @param {number} max
960
+ * @returns {number}
961
+ * @private
962
+ */
963
+ _clamp(value, min, max) {
964
+ if (!Number.isFinite(value)) return min;
965
+ return Math.max(min, Math.min(max, value));
966
+ }
967
+
968
+ /**
969
+ * Cubic ease-in-out for smooth transitions.
970
+ *
971
+ * @param {number} t - Progress (0-1)
972
+ * @returns {number} Eased value
973
+ * @private
974
+ */
975
+ _easeInOutCubic(t) {
976
+ return t < 0.5
977
+ ? 4 * t * t * t
978
+ : 1 - Math.pow(-2 * t + 2, 3) / 2;
979
+ }
980
+ }