@vib3code/sdk 2.0.1 → 2.0.3-canary.75a3290

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 (136) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/DOCS/AGENT_HARNESS_ARCHITECTURE.md +243 -0
  3. package/DOCS/CLI_ONBOARDING.md +1 -1
  4. package/DOCS/CROSS_SITE_DESIGN_PATTERNS.md +117 -0
  5. package/DOCS/EPIC_SCROLL_EVENTS.md +773 -0
  6. package/DOCS/HANDOFF_LANDING_PAGE.md +154 -0
  7. package/DOCS/HANDOFF_SDK_DEVELOPMENT.md +493 -0
  8. package/DOCS/MULTIVIZ_CHOREOGRAPHY_PATTERNS.md +937 -0
  9. package/DOCS/PRODUCT_STRATEGY.md +63 -0
  10. package/DOCS/README.md +103 -0
  11. package/DOCS/REFERENCE_SCROLL_ANALYSIS.md +97 -0
  12. package/DOCS/ROADMAP.md +111 -0
  13. package/DOCS/SCROLL_TIMELINE_v3.md +269 -0
  14. package/DOCS/SITE_REFACTOR_PLAN.md +100 -0
  15. package/DOCS/STATUS.md +24 -0
  16. package/DOCS/SYSTEM_INVENTORY.md +33 -30
  17. package/DOCS/VISUAL_ANALYSIS_CLICKERSS.md +85 -0
  18. package/DOCS/VISUAL_ANALYSIS_FACETAD.md +133 -0
  19. package/DOCS/VISUAL_ANALYSIS_SIMONE.md +95 -0
  20. package/DOCS/VISUAL_ANALYSIS_TABLESIDE.md +86 -0
  21. package/DOCS/{BLUEPRINT_EXECUTION_PLAN_2026-01-07.md → archive/BLUEPRINT_EXECUTION_PLAN_2026-01-07.md} +1 -1
  22. package/DOCS/{DEV_TRACK_ANALYSIS.md → archive/DEV_TRACK_ANALYSIS.md} +3 -0
  23. package/DOCS/{SYSTEM_AUDIT_2026-01-30.md → archive/SYSTEM_AUDIT_2026-01-30.md} +3 -0
  24. package/DOCS/{DEV_TRACK_SESSION_2026-01-31.md → dev-tracks/DEV_TRACK_SESSION_2026-01-31.md} +1 -1
  25. package/DOCS/dev-tracks/DEV_TRACK_SESSION_2026-02-06.md +231 -0
  26. package/DOCS/dev-tracks/DEV_TRACK_SESSION_2026-02-13.md +127 -0
  27. package/DOCS/dev-tracks/DEV_TRACK_SESSION_2026-02-15.md +142 -0
  28. package/DOCS/dev-tracks/README.md +10 -0
  29. package/README.md +26 -13
  30. package/cpp/CMakeLists.txt +236 -0
  31. package/cpp/bindings/embind.cpp +269 -0
  32. package/cpp/build.sh +129 -0
  33. package/cpp/geometry/Crystal.cpp +103 -0
  34. package/cpp/geometry/Fractal.cpp +136 -0
  35. package/cpp/geometry/GeometryGenerator.cpp +262 -0
  36. package/cpp/geometry/KleinBottle.cpp +71 -0
  37. package/cpp/geometry/Sphere.cpp +134 -0
  38. package/cpp/geometry/Tesseract.cpp +94 -0
  39. package/cpp/geometry/Tetrahedron.cpp +83 -0
  40. package/cpp/geometry/Torus.cpp +65 -0
  41. package/cpp/geometry/WarpFunctions.cpp +238 -0
  42. package/cpp/geometry/Wave.cpp +85 -0
  43. package/cpp/include/vib3_ffi.h +238 -0
  44. package/cpp/math/Mat4x4.cpp +409 -0
  45. package/cpp/math/Mat4x4.hpp +209 -0
  46. package/cpp/math/Projection.cpp +142 -0
  47. package/cpp/math/Projection.hpp +148 -0
  48. package/cpp/math/Rotor4D.cpp +322 -0
  49. package/cpp/math/Rotor4D.hpp +204 -0
  50. package/cpp/math/Vec4.cpp +303 -0
  51. package/cpp/math/Vec4.hpp +225 -0
  52. package/cpp/src/vib3_ffi.cpp +607 -0
  53. package/cpp/tests/Geometry_test.cpp +213 -0
  54. package/cpp/tests/Mat4x4_test.cpp +494 -0
  55. package/cpp/tests/Projection_test.cpp +298 -0
  56. package/cpp/tests/Rotor4D_test.cpp +423 -0
  57. package/cpp/tests/Vec4_test.cpp +489 -0
  58. package/package.json +40 -27
  59. package/src/agent/index.js +1 -3
  60. package/src/agent/mcp/MCPServer.js +918 -0
  61. package/src/agent/mcp/index.js +1 -1
  62. package/src/agent/mcp/stdio-server.js +264 -0
  63. package/src/agent/mcp/tools.js +454 -0
  64. package/src/cli/index.js +374 -44
  65. package/src/core/CanvasManager.js +97 -204
  66. package/src/core/ErrorReporter.js +1 -1
  67. package/src/core/Parameters.js +1 -1
  68. package/src/core/VIB3Engine.js +93 -4
  69. package/src/core/VitalitySystem.js +53 -0
  70. package/src/core/index.js +18 -0
  71. package/src/core/renderers/FacetedRendererAdapter.js +10 -9
  72. package/src/core/renderers/HolographicRendererAdapter.js +13 -9
  73. package/src/core/renderers/QuantumRendererAdapter.js +11 -7
  74. package/src/creative/AestheticMapper.js +628 -0
  75. package/src/creative/ChoreographyPlayer.js +481 -0
  76. package/src/creative/index.js +11 -0
  77. package/src/export/TradingCardManager.js +3 -4
  78. package/src/export/index.js +11 -1
  79. package/src/faceted/FacetedSystem.js +241 -388
  80. package/src/holograms/HolographicVisualizer.js +29 -12
  81. package/src/holograms/RealHolographicSystem.js +194 -43
  82. package/src/math/index.js +7 -7
  83. package/src/polychora/PolychoraSystem.js +77 -0
  84. package/src/quantum/QuantumEngine.js +103 -66
  85. package/src/quantum/QuantumVisualizer.js +7 -2
  86. package/src/reactivity/index.js +3 -5
  87. package/src/render/LayerPresetManager.js +372 -0
  88. package/src/render/LayerReactivityBridge.js +344 -0
  89. package/src/render/LayerRelationshipGraph.js +610 -0
  90. package/src/render/MultiCanvasBridge.js +148 -25
  91. package/src/render/UnifiedRenderBridge.js +3 -0
  92. package/src/render/index.js +27 -2
  93. package/src/scene/index.js +4 -4
  94. package/src/shaders/faceted/faceted.frag.glsl +220 -80
  95. package/src/shaders/faceted/faceted.frag.wgsl +138 -97
  96. package/src/shaders/holographic/holographic.frag.glsl +28 -9
  97. package/src/shaders/holographic/holographic.frag.wgsl +107 -38
  98. package/src/shaders/quantum/quantum.frag.glsl +1 -0
  99. package/src/shaders/quantum/quantum.frag.wgsl +1 -1
  100. package/src/testing/ParallelTestFramework.js +2 -2
  101. package/src/viewer/GalleryUI.js +17 -0
  102. package/src/viewer/ViewerPortal.js +2 -2
  103. package/src/viewer/index.js +1 -1
  104. package/tools/headless-renderer.js +258 -0
  105. package/tools/shader-sync-verify.js +8 -4
  106. package/tools/site-analysis/all-reports.json +32 -0
  107. package/tools/site-analysis/combined-analysis.md +50 -0
  108. package/tools/site-analyzer.mjs +779 -0
  109. package/tools/visual-catalog/capture.js +276 -0
  110. package/tools/visual-catalog/composite.js +138 -0
  111. package/types/adaptive-sdk.d.ts +204 -5
  112. package/types/agent/cli.d.ts +78 -0
  113. package/types/agent/index.d.ts +18 -0
  114. package/types/agent/mcp.d.ts +87 -0
  115. package/types/agent/telemetry.d.ts +190 -0
  116. package/types/core/VIB3Engine.d.ts +26 -0
  117. package/types/core/index.d.ts +261 -0
  118. package/types/creative/AestheticMapper.d.ts +72 -0
  119. package/types/creative/ChoreographyPlayer.d.ts +96 -0
  120. package/types/creative/index.d.ts +17 -0
  121. package/types/export/index.d.ts +243 -0
  122. package/types/geometry/index.d.ts +164 -0
  123. package/types/math/index.d.ts +214 -0
  124. package/types/render/LayerPresetManager.d.ts +78 -0
  125. package/types/render/LayerReactivityBridge.d.ts +85 -0
  126. package/types/render/LayerRelationshipGraph.d.ts +174 -0
  127. package/types/render/index.d.ts +3 -0
  128. package/types/scene/index.d.ts +204 -0
  129. package/types/systems/index.d.ts +244 -0
  130. package/types/variations/index.d.ts +62 -0
  131. package/types/viewer/index.d.ts +225 -0
  132. /package/DOCS/{DEV_TRACK_PLAN_2026-01-07.md → archive/DEV_TRACK_PLAN_2026-01-07.md} +0 -0
  133. /package/DOCS/{SESSION_014_PLAN.md → archive/SESSION_014_PLAN.md} +0 -0
  134. /package/DOCS/{SESSION_LOG_2026-01-07.md → archive/SESSION_LOG_2026-01-07.md} +0 -0
  135. /package/DOCS/{STRATEGIC_BLUEPRINT_2026-01-07.md → archive/STRATEGIC_BLUEPRINT_2026-01-07.md} +0 -0
  136. /package/src/viewer/{ReactivityManager.js → ViewerInputHandler.js} +0 -0
@@ -0,0 +1,610 @@
1
+ /**
2
+ * LayerRelationshipGraph — Keystone-driven inter-layer parameter system for VIB3+
3
+ *
4
+ * Instead of blasting identical parameters to all 5 layers with static multipliers,
5
+ * this system designates one layer as the **keystone** (the driver) and derives
6
+ * every other layer's parameters through configurable **relationship functions**.
7
+ *
8
+ * Each relationship function receives the keystone's resolved parameters and returns
9
+ * the dependent layer's parameters. This allows layers to mirror, complement, chase,
10
+ * harmonize with, or react to the keystone — producing actual inter-layer dynamics
11
+ * instead of "same shader, different opacity."
12
+ *
13
+ * Supports per-layer shader assignment so layers aren't forced to run the same program.
14
+ *
15
+ * Usage:
16
+ * const graph = new LayerRelationshipGraph({ keystone: 'content' });
17
+ * graph.setRelationship('background', 'echo'); // attenuated follower
18
+ * graph.setRelationship('shadow', 'complement'); // color complement, density inverse
19
+ * graph.setRelationship('highlight', 'harmonic'); // 2x density, shifted hue
20
+ * graph.setRelationship('accent', 'reactive'); // amplifies delta from keystone
21
+ *
22
+ * // Custom relationship
23
+ * graph.setRelationship('accent', (keystoneParams, time) => ({
24
+ * ...keystoneParams,
25
+ * hue: (keystoneParams.hue + 180) % 360,
26
+ * density: keystoneParams.density * 2.5,
27
+ * speed: keystoneParams.speed * 0.4,
28
+ * }));
29
+ *
30
+ * // Per-layer shader
31
+ * graph.setLayerShader('accent', 'holographic-glitch');
32
+ *
33
+ * // Resolve all layers for a frame
34
+ * const resolved = graph.resolveAll(keystoneParams, time);
35
+ * // => { background: {...}, shadow: {...}, content: keystoneParams, highlight: {...}, accent: {...} }
36
+ */
37
+
38
+ /** @type {string[]} Standard layer order (back to front) */
39
+ const LAYER_ORDER = ['background', 'shadow', 'content', 'highlight', 'accent'];
40
+
41
+ // ============================================================================
42
+ // Preset Relationship Functions
43
+ // ============================================================================
44
+
45
+ /**
46
+ * @typedef {function(Object, number): Object} RelationshipFn
47
+ * Takes (keystoneParams, time) and returns derived layer params.
48
+ */
49
+
50
+ /**
51
+ * Echo — attenuated follower.
52
+ * Same parameters as keystone but scaled down. The most basic relationship.
53
+ * Background/shadow layers typically use this.
54
+ *
55
+ * @param {Object} config - Attenuation config
56
+ * @param {number} [config.opacity=0.3] - Opacity multiplier
57
+ * @param {number} [config.densityScale=0.5] - Density multiplier
58
+ * @param {number} [config.speedScale=0.3] - Speed multiplier
59
+ * @param {number} [config.intensityScale=0.4] - Intensity multiplier
60
+ * @returns {RelationshipFn}
61
+ */
62
+ function echo(config = {}) {
63
+ const {
64
+ opacity = 0.3,
65
+ densityScale = 0.5,
66
+ speedScale = 0.3,
67
+ intensityScale = 0.4
68
+ } = config;
69
+
70
+ return (kp, _time) => ({
71
+ ...kp,
72
+ layerOpacity: (kp.layerOpacity || 1.0) * opacity,
73
+ density: (kp.density || 1.0) * densityScale,
74
+ speed: (kp.speed || 1.0) * speedScale,
75
+ intensity: (kp.intensity || 0.5) * intensityScale
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Mirror — inverts spatial parameters relative to keystone.
81
+ * Rotation planes get negated, hue shifts 180°. Creates visual counterpoint.
82
+ *
83
+ * @param {Object} config
84
+ * @param {number} [config.opacity=0.5]
85
+ * @param {boolean} [config.invertRotation=true] - Negate 4D rotation angles
86
+ * @param {number} [config.hueShift=180] - Degrees to shift hue
87
+ * @returns {RelationshipFn}
88
+ */
89
+ function mirror(config = {}) {
90
+ const {
91
+ opacity = 0.5,
92
+ invertRotation = true,
93
+ hueShift = 180
94
+ } = config;
95
+
96
+ return (kp, _time) => {
97
+ const result = { ...kp, layerOpacity: (kp.layerOpacity || 1.0) * opacity };
98
+
99
+ if (kp.hue !== undefined) {
100
+ result.hue = (kp.hue + hueShift) % 360;
101
+ }
102
+
103
+ if (invertRotation) {
104
+ if (kp.rot4dXW !== undefined) result.rot4dXW = -kp.rot4dXW;
105
+ if (kp.rot4dYW !== undefined) result.rot4dYW = -kp.rot4dYW;
106
+ if (kp.rot4dZW !== undefined) result.rot4dZW = -kp.rot4dZW;
107
+ }
108
+
109
+ return result;
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Complement — color complement with inverted density curve.
115
+ * Hue opposite, density inverts around midpoint, chaos complementary.
116
+ * Good for shadow/background to create depth contrast.
117
+ *
118
+ * @param {Object} config
119
+ * @param {number} [config.opacity=0.4]
120
+ * @param {number} [config.densityPivot=1.0] - Density value to invert around
121
+ * @param {number} [config.chaosInvert=true] - Invert chaos (high → low, low → high)
122
+ * @returns {RelationshipFn}
123
+ */
124
+ function complement(config = {}) {
125
+ const {
126
+ opacity = 0.4,
127
+ densityPivot = 1.0,
128
+ chaosInvert = true
129
+ } = config;
130
+
131
+ return (kp, _time) => {
132
+ const result = { ...kp, layerOpacity: (kp.layerOpacity || 1.0) * opacity };
133
+
134
+ if (kp.hue !== undefined) {
135
+ result.hue = (kp.hue + 180) % 360;
136
+ }
137
+
138
+ if (kp.density !== undefined) {
139
+ // Invert around pivot: if keystone density is 0.3, complement is 1.7
140
+ result.density = Math.max(0.1, densityPivot * 2 - kp.density);
141
+ }
142
+
143
+ if (chaosInvert && kp.chaos !== undefined) {
144
+ result.chaos = Math.max(0, 1.0 - kp.chaos);
145
+ }
146
+
147
+ return result;
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Harmonic — musical harmonic relationships.
153
+ * Density at integer multiples (2x, 3x), hue shifted by golden angle (~137.5°).
154
+ * Speed at fractional ratios. Creates visual "chords."
155
+ *
156
+ * @param {Object} config
157
+ * @param {number} [config.opacity=0.5]
158
+ * @param {number} [config.densityHarmonic=2] - Integer multiple for density
159
+ * @param {number} [config.speedRatio=0.5] - Fractional speed ratio
160
+ * @param {number} [config.hueAngle=137.508] - Golden angle for hue shift
161
+ * @returns {RelationshipFn}
162
+ */
163
+ function harmonic(config = {}) {
164
+ const {
165
+ opacity = 0.5,
166
+ densityHarmonic = 2,
167
+ speedRatio = 0.5,
168
+ hueAngle = 137.508
169
+ } = config;
170
+
171
+ return (kp, _time) => {
172
+ const result = { ...kp, layerOpacity: (kp.layerOpacity || 1.0) * opacity };
173
+
174
+ if (kp.density !== undefined) {
175
+ result.density = kp.density * densityHarmonic;
176
+ }
177
+
178
+ if (kp.speed !== undefined) {
179
+ result.speed = kp.speed * speedRatio;
180
+ }
181
+
182
+ if (kp.hue !== undefined) {
183
+ result.hue = (kp.hue + hueAngle) % 360;
184
+ }
185
+
186
+ return result;
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Reactive — amplifies the delta from a baseline.
192
+ * Tracks parameter changes over time and exaggerates them.
193
+ * The more the keystone changes, the more reactive layers diverge.
194
+ *
195
+ * @param {Object} config
196
+ * @param {number} [config.opacity=0.6]
197
+ * @param {number} [config.gain=2.0] - How much to amplify deltas
198
+ * @param {number} [config.decay=0.95] - How fast accumulated delta decays per frame
199
+ * @returns {RelationshipFn}
200
+ */
201
+ function reactive(config = {}) {
202
+ const {
203
+ opacity = 0.6,
204
+ gain = 2.0,
205
+ decay = 0.95
206
+ } = config;
207
+
208
+ let prevParams = null;
209
+ let accumulatedDelta = {};
210
+
211
+ return (kp, _time) => {
212
+ const result = { ...kp, layerOpacity: (kp.layerOpacity || 1.0) * opacity };
213
+
214
+ if (prevParams) {
215
+ for (const key of Object.keys(kp)) {
216
+ if (typeof kp[key] !== 'number') continue;
217
+ const delta = kp[key] - (prevParams[key] || 0);
218
+ accumulatedDelta[key] = ((accumulatedDelta[key] || 0) + delta * gain) * decay;
219
+ result[key] = kp[key] + (accumulatedDelta[key] || 0);
220
+ }
221
+ }
222
+
223
+ prevParams = { ...kp };
224
+ return result;
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Chase — follows the keystone with a time delay (lerp toward keystone each frame).
230
+ * Creates a trailing/ghosting effect. Layers feel like they're "catching up."
231
+ *
232
+ * @param {Object} config
233
+ * @param {number} [config.opacity=0.5]
234
+ * @param {number} [config.lerpRate=0.1] - How fast to chase (0 = frozen, 1 = instant)
235
+ * @returns {RelationshipFn}
236
+ */
237
+ function chase(config = {}) {
238
+ const {
239
+ opacity = 0.5,
240
+ lerpRate = 0.1
241
+ } = config;
242
+
243
+ let currentParams = null;
244
+
245
+ return (kp, _time) => {
246
+ if (!currentParams) {
247
+ currentParams = { ...kp };
248
+ }
249
+
250
+ // Lerp each numeric parameter toward keystone
251
+ for (const key of Object.keys(kp)) {
252
+ if (typeof kp[key] !== 'number') continue;
253
+ const current = currentParams[key] !== undefined ? currentParams[key] : kp[key];
254
+ currentParams[key] = current + (kp[key] - current) * lerpRate;
255
+ }
256
+
257
+ return {
258
+ ...currentParams,
259
+ layerOpacity: (kp.layerOpacity || 1.0) * opacity
260
+ };
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Registry of preset relationship factories.
266
+ * Each entry is a function that takes optional config and returns a RelationshipFn.
267
+ */
268
+ const PRESET_REGISTRY = {
269
+ echo,
270
+ mirror,
271
+ complement,
272
+ harmonic,
273
+ reactive,
274
+ chase
275
+ };
276
+
277
+ // ============================================================================
278
+ // Default Layer Relationship Profiles
279
+ // ============================================================================
280
+
281
+ /**
282
+ * Named profiles that configure the full 5-layer graph at once.
283
+ * Each profile sets the keystone and relationships for all layers.
284
+ */
285
+ const PROFILES = {
286
+ /** Classic holographic — content drives, others echo/complement */
287
+ holographic: {
288
+ keystone: 'content',
289
+ relationships: {
290
+ background: { preset: 'echo', config: { opacity: 0.2, densityScale: 0.4, speedScale: 0.2, intensityScale: 0.3 } },
291
+ shadow: { preset: 'complement', config: { opacity: 0.4, densityPivot: 1.0 } },
292
+ highlight: { preset: 'harmonic', config: { opacity: 0.6, densityHarmonic: 1.5, speedRatio: 0.8, hueAngle: 60 } },
293
+ accent: { preset: 'reactive', config: { opacity: 0.3, gain: 2.5, decay: 0.92 } }
294
+ }
295
+ },
296
+
297
+ /** Mirror mode — highlight mirrors keystone, accent chases */
298
+ symmetry: {
299
+ keystone: 'content',
300
+ relationships: {
301
+ background: { preset: 'echo', config: { opacity: 0.15, densityScale: 0.3, speedScale: 0.15 } },
302
+ shadow: { preset: 'mirror', config: { opacity: 0.35 } },
303
+ highlight: { preset: 'mirror', config: { opacity: 0.5, hueShift: 120 } },
304
+ accent: { preset: 'chase', config: { opacity: 0.4, lerpRate: 0.05 } }
305
+ }
306
+ },
307
+
308
+ /** Harmonic chord — layers at musical intervals */
309
+ chord: {
310
+ keystone: 'content',
311
+ relationships: {
312
+ background: { preset: 'harmonic', config: { opacity: 0.2, densityHarmonic: 0.5, speedRatio: 0.25, hueAngle: 0 } },
313
+ shadow: { preset: 'harmonic', config: { opacity: 0.35, densityHarmonic: 1.5, speedRatio: 0.667, hueAngle: 137.508 } },
314
+ highlight: { preset: 'harmonic', config: { opacity: 0.55, densityHarmonic: 2, speedRatio: 0.5, hueAngle: 275.016 } },
315
+ accent: { preset: 'harmonic', config: { opacity: 0.3, densityHarmonic: 3, speedRatio: 0.333, hueAngle: 52.524 } }
316
+ }
317
+ },
318
+
319
+ /** Reactive storm — all layers react to keystone changes */
320
+ storm: {
321
+ keystone: 'content',
322
+ relationships: {
323
+ background: { preset: 'reactive', config: { opacity: 0.25, gain: 1.5, decay: 0.98 } },
324
+ shadow: { preset: 'reactive', config: { opacity: 0.4, gain: 2.0, decay: 0.95 } },
325
+ highlight: { preset: 'reactive', config: { opacity: 0.6, gain: 3.0, decay: 0.90 } },
326
+ accent: { preset: 'reactive', config: { opacity: 0.35, gain: 4.0, decay: 0.88 } }
327
+ }
328
+ },
329
+
330
+ /** Legacy — replicates the old static multiplier behavior exactly */
331
+ legacy: {
332
+ keystone: 'content',
333
+ relationships: {
334
+ background: { preset: 'echo', config: { opacity: 0.2, densityScale: 0.4, speedScale: 0.2, intensityScale: 0.4 } },
335
+ shadow: { preset: 'echo', config: { opacity: 0.4, densityScale: 0.8, speedScale: 0.3, intensityScale: 0.6 } },
336
+ highlight: { preset: 'echo', config: { opacity: 0.6, densityScale: 1.5, speedScale: 0.8, intensityScale: 0.8 } },
337
+ accent: { preset: 'echo', config: { opacity: 0.3, densityScale: 2.5, speedScale: 0.4, intensityScale: 0.5 } }
338
+ }
339
+ }
340
+ };
341
+
342
+ // ============================================================================
343
+ // LayerRelationshipGraph
344
+ // ============================================================================
345
+
346
+ export class LayerRelationshipGraph {
347
+ /**
348
+ * @param {Object} [config]
349
+ * @param {string} [config.keystone='content'] - The driver layer
350
+ * @param {string} [config.profile] - Named profile to load (holographic, symmetry, chord, storm, legacy)
351
+ */
352
+ constructor(config = {}) {
353
+ /** @type {string} The keystone (driver) layer name */
354
+ this._keystone = config.keystone || 'content';
355
+
356
+ /** @type {Map<string, RelationshipFn>} layer name → resolved relationship function */
357
+ this._relationships = new Map();
358
+
359
+ /** @type {Map<string, string>} layer name → shader program name */
360
+ this._layerShaders = new Map();
361
+
362
+ /** @type {Map<string, {preset: string, config: Object}|null>} serializable config per layer */
363
+ this._relationshipConfigs = new Map();
364
+
365
+ /** @type {string|null} Currently active profile name */
366
+ this._activeProfile = null;
367
+
368
+ // Load profile if specified
369
+ if (config.profile && PROFILES[config.profile]) {
370
+ this.loadProfile(config.profile);
371
+ }
372
+ }
373
+
374
+ // ========================================================================
375
+ // Configuration
376
+ // ========================================================================
377
+
378
+ /**
379
+ * Set the keystone (driver) layer.
380
+ * @param {string} layerName - One of LAYER_ORDER
381
+ */
382
+ setKeystone(layerName) {
383
+ if (!LAYER_ORDER.includes(layerName)) {
384
+ throw new Error(`Invalid layer name: "${layerName}". Must be one of: ${LAYER_ORDER.join(', ')}`);
385
+ }
386
+ this._keystone = layerName;
387
+ this._activeProfile = null;
388
+ }
389
+
390
+ /** @returns {string} Current keystone layer name */
391
+ get keystone() {
392
+ return this._keystone;
393
+ }
394
+
395
+ /**
396
+ * Set the relationship for a dependent layer.
397
+ *
398
+ * @param {string} layerName - The dependent layer
399
+ * @param {string|RelationshipFn|{preset: string, config?: Object}} relationship
400
+ * - String: preset name ('echo', 'mirror', 'complement', 'harmonic', 'reactive', 'chase')
401
+ * - Function: custom (keystoneParams, time) => layerParams
402
+ * - Object: { preset: 'name', config: {...} }
403
+ */
404
+ setRelationship(layerName, relationship) {
405
+ if (!LAYER_ORDER.includes(layerName)) {
406
+ throw new Error(`Invalid layer name: "${layerName}". Must be one of: ${LAYER_ORDER.join(', ')}`);
407
+ }
408
+
409
+ if (layerName === this._keystone) {
410
+ throw new Error(`Cannot set relationship on keystone layer "${layerName}". Change the keystone first.`);
411
+ }
412
+
413
+ if (typeof relationship === 'function') {
414
+ this._relationships.set(layerName, relationship);
415
+ this._relationshipConfigs.set(layerName, null); // custom fn, not serializable
416
+ } else if (typeof relationship === 'string') {
417
+ const factory = PRESET_REGISTRY[relationship];
418
+ if (!factory) {
419
+ throw new Error(`Unknown relationship preset: "${relationship}". Available: ${Object.keys(PRESET_REGISTRY).join(', ')}`);
420
+ }
421
+ this._relationships.set(layerName, factory());
422
+ this._relationshipConfigs.set(layerName, { preset: relationship, config: {} });
423
+ } else if (relationship && typeof relationship === 'object' && relationship.preset) {
424
+ const factory = PRESET_REGISTRY[relationship.preset];
425
+ if (!factory) {
426
+ throw new Error(`Unknown relationship preset: "${relationship.preset}". Available: ${Object.keys(PRESET_REGISTRY).join(', ')}`);
427
+ }
428
+ this._relationships.set(layerName, factory(relationship.config || {}));
429
+ this._relationshipConfigs.set(layerName, { preset: relationship.preset, config: relationship.config || {} });
430
+ } else {
431
+ throw new Error('Relationship must be a preset name (string), a function, or { preset, config }.');
432
+ }
433
+
434
+ this._activeProfile = null;
435
+ }
436
+
437
+ /**
438
+ * Set the shader program for a specific layer.
439
+ * Layers without an explicit shader use the default (same as keystone).
440
+ *
441
+ * @param {string} layerName
442
+ * @param {string} shaderName - Shader program name to use for this layer
443
+ */
444
+ setLayerShader(layerName, shaderName) {
445
+ if (!LAYER_ORDER.includes(layerName)) {
446
+ throw new Error(`Invalid layer name: "${layerName}".`);
447
+ }
448
+ this._layerShaders.set(layerName, shaderName);
449
+ }
450
+
451
+ /**
452
+ * Get the shader name for a layer (null means use default).
453
+ * @param {string} layerName
454
+ * @returns {string|null}
455
+ */
456
+ getLayerShader(layerName) {
457
+ return this._layerShaders.get(layerName) || null;
458
+ }
459
+
460
+ /**
461
+ * Load a named profile, configuring the full graph at once.
462
+ * @param {string} profileName - One of: holographic, symmetry, chord, storm, legacy
463
+ */
464
+ loadProfile(profileName) {
465
+ const profile = PROFILES[profileName];
466
+ if (!profile) {
467
+ throw new Error(`Unknown profile: "${profileName}". Available: ${Object.keys(PROFILES).join(', ')}`);
468
+ }
469
+
470
+ this._keystone = profile.keystone || 'content';
471
+ this._relationships.clear();
472
+ this._relationshipConfigs.clear();
473
+
474
+ for (const [layerName, rel] of Object.entries(profile.relationships)) {
475
+ const factory = PRESET_REGISTRY[rel.preset];
476
+ if (factory) {
477
+ this._relationships.set(layerName, factory(rel.config || {}));
478
+ this._relationshipConfigs.set(layerName, { preset: rel.preset, config: rel.config || {} });
479
+ }
480
+ }
481
+
482
+ this._activeProfile = profileName;
483
+ }
484
+
485
+ /** @returns {string|null} Active profile name, or null if custom */
486
+ get activeProfile() {
487
+ return this._activeProfile;
488
+ }
489
+
490
+ /** @returns {string[]} Available profile names */
491
+ static get profileNames() {
492
+ return Object.keys(PROFILES);
493
+ }
494
+
495
+ /** @returns {string[]} Available preset relationship names */
496
+ static get presetNames() {
497
+ return Object.keys(PRESET_REGISTRY);
498
+ }
499
+
500
+ // ========================================================================
501
+ // Resolution
502
+ // ========================================================================
503
+
504
+ /**
505
+ * Resolve parameters for a single dependent layer.
506
+ *
507
+ * @param {Object} keystoneParams - The keystone layer's parameters
508
+ * @param {string} layerName - The layer to resolve
509
+ * @param {number} time - Current time (ms or frame count)
510
+ * @returns {Object} Resolved parameters for the layer
511
+ */
512
+ resolve(keystoneParams, layerName, time) {
513
+ if (layerName === this._keystone) {
514
+ return { ...keystoneParams };
515
+ }
516
+
517
+ const fn = this._relationships.get(layerName);
518
+ if (!fn) {
519
+ // No relationship defined — passthrough keystone params unchanged
520
+ return { ...keystoneParams };
521
+ }
522
+
523
+ return fn(keystoneParams, time);
524
+ }
525
+
526
+ /**
527
+ * Resolve parameters for all layers at once.
528
+ *
529
+ * @param {Object} keystoneParams - The keystone layer's parameters
530
+ * @param {number} time - Current time (ms or frame count)
531
+ * @returns {Object<string, Object>} Map of layerName → resolved params
532
+ */
533
+ resolveAll(keystoneParams, time) {
534
+ const resolved = {};
535
+ for (const layerName of LAYER_ORDER) {
536
+ resolved[layerName] = this.resolve(keystoneParams, layerName, time);
537
+ }
538
+ return resolved;
539
+ }
540
+
541
+ // ========================================================================
542
+ // Serialization
543
+ // ========================================================================
544
+
545
+ /**
546
+ * Export the graph configuration for saving (gallery, presets).
547
+ * Custom functions are exported as null (not serializable).
548
+ *
549
+ * @returns {Object} Serializable graph configuration
550
+ */
551
+ exportConfig() {
552
+ const relationships = {};
553
+ for (const [layerName, config] of this._relationshipConfigs) {
554
+ relationships[layerName] = config; // null for custom fns
555
+ }
556
+
557
+ const shaders = {};
558
+ for (const [layerName, shaderName] of this._layerShaders) {
559
+ shaders[layerName] = shaderName;
560
+ }
561
+
562
+ return {
563
+ keystone: this._keystone,
564
+ profile: this._activeProfile,
565
+ relationships,
566
+ shaders
567
+ };
568
+ }
569
+
570
+ /**
571
+ * Import a graph configuration (from gallery, presets).
572
+ *
573
+ * @param {Object} config - Previously exported config
574
+ */
575
+ importConfig(config) {
576
+ if (!config) return;
577
+
578
+ if (config.profile && PROFILES[config.profile]) {
579
+ this.loadProfile(config.profile);
580
+ } else {
581
+ this._keystone = config.keystone || 'content';
582
+ this._relationships.clear();
583
+ this._relationshipConfigs.clear();
584
+
585
+ if (config.relationships) {
586
+ for (const [layerName, rel] of Object.entries(config.relationships)) {
587
+ if (rel && rel.preset) {
588
+ this.setRelationship(layerName, rel);
589
+ }
590
+ }
591
+ }
592
+ }
593
+
594
+ // Restore shader assignments
595
+ this._layerShaders.clear();
596
+ if (config.shaders) {
597
+ for (const [layerName, shaderName] of Object.entries(config.shaders)) {
598
+ this._layerShaders.set(layerName, shaderName);
599
+ }
600
+ }
601
+ }
602
+ }
603
+
604
+ // ============================================================================
605
+ // Exports
606
+ // ============================================================================
607
+
608
+ export { LAYER_ORDER, PROFILES, PRESET_REGISTRY };
609
+ export { echo, mirror, complement, harmonic, reactive, chase };
610
+ export default LayerRelationshipGraph;