@vib3code/sdk 2.0.3-canary.0e9a1ac → 2.0.3-canary.60bc0f0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vib3code/sdk",
3
- "version": "2.0.3-canary.0e9a1ac",
3
+ "version": "2.0.3-canary.60bc0f0",
4
4
  "description": "VIB3+ 4D Visualization SDK - Unified engine with 6D rotation, MCP agentic integration, and cross-platform support",
5
5
  "type": "module",
6
6
  "main": "src/core/VIB3Engine.js",
@@ -9,6 +9,8 @@ import { telemetry, EventType, withTelemetry } from '../telemetry/index.js';
9
9
  import { AestheticMapper } from '../../creative/AestheticMapper.js';
10
10
  import { ChoreographyPlayer } from '../../creative/ChoreographyPlayer.js';
11
11
  import { ParameterTimeline } from '../../creative/ParameterTimeline.js';
12
+ import { ColorPresetsSystem } from '../../creative/ColorPresetsSystem.js';
13
+ import { TransitionAnimator } from '../../creative/TransitionAnimator.js';
12
14
  import { PRESET_REGISTRY } from '../../render/LayerRelationshipGraph.js';
13
15
 
14
16
  /**
@@ -42,6 +44,36 @@ export class MCPServer {
42
44
  this.engine = engine;
43
45
  this.sceneId = null;
44
46
  this.initialized = false;
47
+ this._gallerySlots = new Map();
48
+ }
49
+
50
+ /**
51
+ * Get or lazily create ColorPresetsSystem instance.
52
+ * Requires engine for the parameter update callback.
53
+ * @returns {ColorPresetsSystem|null}
54
+ */
55
+ _getColorPresets() {
56
+ if (this._colorPresets) return this._colorPresets;
57
+ if (!this.engine) return null;
58
+ this._colorPresets = new ColorPresetsSystem(
59
+ (name, value) => this.engine.setParameter(name, value)
60
+ );
61
+ return this._colorPresets;
62
+ }
63
+
64
+ /**
65
+ * Get or lazily create TransitionAnimator instance.
66
+ * Requires engine for the parameter update/get callbacks.
67
+ * @returns {TransitionAnimator|null}
68
+ */
69
+ _getTransitionAnimator() {
70
+ if (this._transitionAnimator) return this._transitionAnimator;
71
+ if (!this.engine) return null;
72
+ this._transitionAnimator = new TransitionAnimator(
73
+ (name, value) => this.engine.setParameter(name, value),
74
+ (name) => this.engine.getParameter(name)
75
+ );
76
+ return this._transitionAnimator;
45
77
  }
46
78
 
47
79
  buildResponse(operation, data, options = {}) {
@@ -484,32 +516,68 @@ export class MCPServer {
484
516
 
485
517
  telemetry.recordEvent(EventType.GALLERY_SAVE, { slot });
486
518
 
519
+ // Persist actual engine state if available
520
+ if (this.engine) {
521
+ const state = this.engine.exportState();
522
+ this._gallerySlots.set(slot, {
523
+ name: name || `Variation ${slot}`,
524
+ saved_at: new Date().toISOString(),
525
+ state
526
+ });
527
+ }
528
+
487
529
  return {
488
530
  slot,
489
531
  name: name || `Variation ${slot}`,
490
532
  saved_at: new Date().toISOString(),
533
+ persisted: !!this.engine,
534
+ gallery_size: this._gallerySlots.size,
491
535
  suggested_next_actions: ['load_from_gallery', 'randomize_parameters']
492
536
  };
493
537
  }
494
538
 
495
539
  /**
496
- * Load from gallery
540
+ * Load from gallery — restores previously saved state
497
541
  */
498
542
  loadFromGallery(args) {
499
543
  const { slot } = args;
500
544
 
501
- if (this.engine) {
502
- // Apply variation
503
- const params = this.engine.parameters?.generateVariationParameters?.(slot) || {};
504
- this.engine.setParameters(params);
545
+ telemetry.recordEvent(EventType.GALLERY_LOAD, { slot });
546
+
547
+ const saved = this._gallerySlots.get(slot);
548
+ if (saved && this.engine) {
549
+ // Restore saved state
550
+ this.engine.importState(saved.state);
551
+ return {
552
+ slot,
553
+ name: saved.name,
554
+ saved_at: saved.saved_at,
555
+ loaded_at: new Date().toISOString(),
556
+ restored: true,
557
+ ...this.getState()
558
+ };
505
559
  }
506
560
 
507
- telemetry.recordEvent(EventType.GALLERY_LOAD, { slot });
561
+ if (!saved) {
562
+ // No saved state — fall back to random variation
563
+ if (this.engine) {
564
+ const params = this.engine.parameters?.generateVariationParameters?.(slot) || {};
565
+ this.engine.setParameters(params);
566
+ }
567
+ return {
568
+ slot,
569
+ loaded_at: new Date().toISOString(),
570
+ restored: false,
571
+ note: 'No saved state in this slot — generated random variation',
572
+ ...this.getState()
573
+ };
574
+ }
508
575
 
509
576
  return {
510
577
  slot,
511
578
  loaded_at: new Date().toISOString(),
512
- ...this.getState()
579
+ restored: false,
580
+ note: 'Engine not initialized — cannot apply state'
513
581
  };
514
582
  }
515
583
 
@@ -1217,8 +1285,17 @@ export class MCPServer {
1217
1285
  (sum, step) => sum + step.duration + step.delay, 0
1218
1286
  );
1219
1287
 
1288
+ // Execute live if engine available
1289
+ let executing = false;
1290
+ const animator = this._getTransitionAnimator();
1291
+ if (animator) {
1292
+ const seqId = animator.sequence(normalizedSequence);
1293
+ executing = !!seqId;
1294
+ }
1295
+
1220
1296
  return {
1221
1297
  transition_id: transitionId,
1298
+ executing,
1222
1299
  step_count: normalizedSequence.length,
1223
1300
  total_duration_ms: totalDuration,
1224
1301
  steps: normalizedSequence.map((step, i) => ({
@@ -1228,7 +1305,7 @@ export class MCPServer {
1228
1305
  easing: step.easing,
1229
1306
  delay: step.delay
1230
1307
  })),
1231
- load_code: `const animator = new TransitionAnimator(\n (n, v) => engine.setParameter(n, v),\n (n) => engine.getParameter(n)\n);\nanimator.sequence(${JSON.stringify(normalizedSequence)});`,
1308
+ load_code: executing ? null : `const animator = new TransitionAnimator(\n (n, v) => engine.setParameter(n, v),\n (n) => engine.getParameter(n)\n);\nanimator.sequence(${JSON.stringify(normalizedSequence)});`,
1232
1309
  suggested_next_actions: ['describe_visual_state', 'create_timeline', 'save_to_gallery']
1233
1310
  };
1234
1311
  }
@@ -1237,55 +1314,41 @@ export class MCPServer {
1237
1314
  * Apply a named color preset
1238
1315
  */
1239
1316
  applyColorPreset(args) {
1240
- const { preset } = args;
1317
+ const { preset, transition = true, duration = 800 } = args;
1241
1318
 
1242
- // Color preset hue/saturation mappings (subset — full list in ColorPresetsSystem)
1243
- const COLOR_PRESETS = {
1244
- Ocean: { hue: 200, saturation: 0.8, intensity: 0.6 },
1245
- Lava: { hue: 15, saturation: 0.9, intensity: 0.8 },
1246
- Neon: { hue: 300, saturation: 1.0, intensity: 0.9 },
1247
- Monochrome: { hue: 0, saturation: 0.0, intensity: 0.6 },
1248
- Sunset: { hue: 30, saturation: 0.85, intensity: 0.7 },
1249
- Aurora: { hue: 140, saturation: 0.7, intensity: 0.6 },
1250
- Cyberpunk: { hue: 280, saturation: 0.9, intensity: 0.8 },
1251
- Forest: { hue: 120, saturation: 0.6, intensity: 0.5 },
1252
- Desert: { hue: 40, saturation: 0.5, intensity: 0.7 },
1253
- Galaxy: { hue: 260, saturation: 0.8, intensity: 0.4 },
1254
- Ice: { hue: 190, saturation: 0.5, intensity: 0.8 },
1255
- Fire: { hue: 10, saturation: 1.0, intensity: 0.9 },
1256
- Toxic: { hue: 100, saturation: 0.9, intensity: 0.7 },
1257
- Royal: { hue: 270, saturation: 0.7, intensity: 0.5 },
1258
- Pastel: { hue: 330, saturation: 0.3, intensity: 0.8 },
1259
- Retro: { hue: 50, saturation: 0.7, intensity: 0.6 },
1260
- Midnight: { hue: 240, saturation: 0.6, intensity: 0.3 },
1261
- Tropical: { hue: 160, saturation: 0.8, intensity: 0.7 },
1262
- Ethereal: { hue: 220, saturation: 0.4, intensity: 0.7 },
1263
- Volcanic: { hue: 5, saturation: 0.95, intensity: 0.6 },
1264
- Holographic: { hue: 180, saturation: 0.6, intensity: 0.8 },
1265
- Vaporwave: { hue: 310, saturation: 0.7, intensity: 0.7 }
1266
- };
1319
+ const colorSystem = this._getColorPresets();
1320
+
1321
+ if (colorSystem) {
1322
+ // Use real ColorPresetsSystem full preset library with transitions
1323
+ const config = colorSystem.getPreset(preset);
1324
+ if (!config) {
1325
+ const allPresets = colorSystem.getPresets().map(p => p.name);
1326
+ return {
1327
+ error: {
1328
+ type: 'ValidationError',
1329
+ code: 'INVALID_COLOR_PRESET',
1330
+ message: `Unknown color preset: ${preset}`,
1331
+ valid_options: allPresets
1332
+ }
1333
+ };
1334
+ }
1335
+
1336
+ colorSystem.applyPreset(preset, transition, duration);
1267
1337
 
1268
- const presetData = COLOR_PRESETS[preset];
1269
- if (!presetData) {
1270
1338
  return {
1271
- error: {
1272
- type: 'ValidationError',
1273
- code: 'INVALID_COLOR_PRESET',
1274
- message: `Unknown color preset: ${preset}`,
1275
- valid_options: Object.keys(COLOR_PRESETS)
1276
- }
1339
+ preset,
1340
+ applied: { hue: config.hue, saturation: config.saturation, intensity: config.intensity },
1341
+ transition: transition ? { enabled: true, duration } : { enabled: false },
1342
+ full_config: config,
1343
+ suggested_next_actions: ['set_post_processing', 'describe_visual_state', 'set_visual_parameters']
1277
1344
  };
1278
1345
  }
1279
1346
 
1280
- if (this.engine) {
1281
- this.engine.setParameter('hue', presetData.hue);
1282
- this.engine.setParameter('saturation', presetData.saturation);
1283
- this.engine.setParameter('intensity', presetData.intensity);
1284
- }
1285
-
1347
+ // Fallback: no engine, return preset metadata for artifact mode
1286
1348
  return {
1287
1349
  preset,
1288
- applied: presetData,
1350
+ applied: null,
1351
+ load_code: `const colors = new ColorPresetsSystem((n, v) => engine.setParameter(n, v));\ncolors.applyPreset('${preset}', ${transition}, ${duration});`,
1289
1352
  suggested_next_actions: ['set_post_processing', 'describe_visual_state', 'set_visual_parameters']
1290
1353
  };
1291
1354
  }
@@ -1296,14 +1359,50 @@ export class MCPServer {
1296
1359
  setPostProcessing(args) {
1297
1360
  const { effects, chain_preset, clear_first = true } = args;
1298
1361
 
1362
+ // Try to execute live in browser context
1363
+ let executing = false;
1364
+ if (typeof document !== 'undefined') {
1365
+ try {
1366
+ const target = document.getElementById('viz-container')
1367
+ || document.querySelector('.vib3-container')
1368
+ || document.querySelector('canvas')?.parentElement;
1369
+
1370
+ if (target) {
1371
+ // Lazy-init pipeline, importing dynamically to avoid Node.js issues
1372
+ if (!this._postPipeline) {
1373
+ // PostProcessingPipeline imported statically would fail in Node;
1374
+ // it's already a known browser-only module, so guard at runtime
1375
+ const { PostProcessingPipeline: PPP } = { PostProcessingPipeline: globalThis.PostProcessingPipeline };
1376
+ if (PPP) {
1377
+ this._postPipeline = new PPP(target);
1378
+ }
1379
+ }
1380
+
1381
+ if (this._postPipeline) {
1382
+ if (clear_first) this._postPipeline.clearChain?.();
1383
+ if (chain_preset) {
1384
+ this._postPipeline.loadPresetChain(chain_preset);
1385
+ } else if (effects) {
1386
+ for (const e of effects) {
1387
+ this._postPipeline.addEffect(e.name, { intensity: e.intensity || 0.5, ...e });
1388
+ }
1389
+ }
1390
+ this._postPipeline.apply();
1391
+ executing = true;
1392
+ }
1393
+ }
1394
+ } catch { /* fall through to code generation */ }
1395
+ }
1396
+
1299
1397
  return {
1300
1398
  applied: true,
1399
+ executing,
1301
1400
  effects: effects || [],
1302
1401
  chain_preset: chain_preset || null,
1303
1402
  cleared_previous: clear_first,
1304
- load_code: effects ?
1305
- `const pipeline = new PostProcessingPipeline(gl, canvas);\n${effects.map(e => `pipeline.addEffect('${e.name}', { intensity: ${e.intensity || 0.5} });`).join('\n')}` :
1306
- `pipeline.applyChain('${chain_preset}');`,
1403
+ load_code: executing ? null : (effects ?
1404
+ `const pipeline = new PostProcessingPipeline(document.getElementById('viz-container'));\n${effects.map(e => `pipeline.addEffect('${e.name}', { intensity: ${e.intensity || 0.5} });`).join('\n')}\npipeline.apply();` :
1405
+ `const pipeline = new PostProcessingPipeline(document.getElementById('viz-container'));\npipeline.loadPresetChain('${chain_preset}');\npipeline.apply();`),
1307
1406
  suggested_next_actions: ['describe_visual_state', 'apply_color_preset', 'create_choreography']
1308
1407
  };
1309
1408
  }
@@ -0,0 +1,641 @@
1
+ /**
2
+ * GlyphWarVisualizer.js - VIB3+ Visual Integration for GLYPH_WAR
3
+ *
4
+ * Maps game state (idle, dueling, sudden death, victory) to VIB3+ holographic
5
+ * layer parameters, transitions, post-processing, and a 10-second sudden-death
6
+ * timeline. Chromatic aberration is the primary tension signal.
7
+ *
8
+ * Designed via /vib3-design skill — Artifact Mode.
9
+ *
10
+ * @module games/glyph-war/GlyphWarVisualizer
11
+ * @version 1.0.0
12
+ */
13
+
14
+ import { TransitionAnimator } from '../../creative/TransitionAnimator.js';
15
+ import { ParameterTimeline } from '../../creative/ParameterTimeline.js';
16
+ import { PostProcessingPipeline } from '../../creative/PostProcessingPipeline.js';
17
+ import { ColorPresetsSystem } from '../../creative/ColorPresetsSystem.js';
18
+ import { ChoreographyPlayer } from '../../creative/ChoreographyPlayer.js';
19
+
20
+ // ─────────────────────────────────────────────────────────────────────────────
21
+ // Visual State Presets (designed via /vib3-design Workflow 2)
22
+ // ─────────────────────────────────────────────────────────────────────────────
23
+
24
+ /**
25
+ * 4 game states mapped to VIB3+ parameter snapshots.
26
+ *
27
+ * Each state targets the holographic system with a specific geometry,
28
+ * color preset, post-FX chain, and tuned parameters. Geometry choices:
29
+ * - Idle: 3 (torus) — smooth, waiting
30
+ * - Dueling: 11 (hypersphere+torus) — flowing, organic tension
31
+ * - SuddenDeath: 17 (hypertetra+hypercube) — aggressive, angular
32
+ * - Victory: 8 (hypersphere+tetra) — expansive, resolved
33
+ */
34
+ export const GAME_STATES = {
35
+ idle: {
36
+ system: 'holographic',
37
+ geometry: 3,
38
+ colorPreset: 'Monochrome',
39
+ postFxChain: 'Clean',
40
+ params: {
41
+ hue: 0,
42
+ saturation: 0.0,
43
+ intensity: 0.3,
44
+ speed: 0.3,
45
+ chaos: 0.0,
46
+ morphFactor: 0.0,
47
+ gridDensity: 12,
48
+ dimension: 4.2,
49
+ rot4dXY: 0,
50
+ rot4dXZ: 0,
51
+ rot4dYZ: 0,
52
+ rot4dXW: 0,
53
+ rot4dYW: 0,
54
+ rot4dZW: 0
55
+ }
56
+ },
57
+
58
+ dueling: {
59
+ system: 'holographic',
60
+ geometry: 11,
61
+ colorPreset: 'Cyberpunk',
62
+ postFxChain: 'Holographic',
63
+ params: {
64
+ hue: 280,
65
+ saturation: 0.9,
66
+ intensity: 0.6,
67
+ speed: 1.0,
68
+ chaos: 0.15,
69
+ morphFactor: 0.3,
70
+ gridDensity: 24,
71
+ dimension: 3.8,
72
+ rot4dXY: 0,
73
+ rot4dXZ: 0.3,
74
+ rot4dYZ: 0,
75
+ rot4dXW: 0.8,
76
+ rot4dYW: 0.5,
77
+ rot4dZW: 1.2
78
+ }
79
+ },
80
+
81
+ suddenDeath: {
82
+ system: 'holographic',
83
+ geometry: 17,
84
+ colorPreset: 'Neon',
85
+ postFxChain: 'Glitch Art',
86
+ params: {
87
+ hue: 300,
88
+ saturation: 1.0,
89
+ intensity: 0.9,
90
+ speed: 2.5,
91
+ chaos: 0.7,
92
+ morphFactor: 1.4,
93
+ gridDensity: 60,
94
+ dimension: 3.2,
95
+ rot4dXY: 0,
96
+ rot4dXZ: 0.5,
97
+ rot4dYZ: 0.3,
98
+ rot4dXW: 2.0,
99
+ rot4dYW: 1.5,
100
+ rot4dZW: 2.8
101
+ }
102
+ },
103
+
104
+ victory: {
105
+ system: 'holographic',
106
+ geometry: 8,
107
+ colorPreset: 'Aurora',
108
+ postFxChain: 'Cinematic',
109
+ params: {
110
+ hue: 140,
111
+ saturation: 0.7,
112
+ intensity: 0.8,
113
+ speed: 0.6,
114
+ chaos: 0.0,
115
+ morphFactor: 0.1,
116
+ gridDensity: 8,
117
+ dimension: 4.0,
118
+ rot4dXY: 0,
119
+ rot4dXZ: 0,
120
+ rot4dYZ: 0,
121
+ rot4dXW: 0.3,
122
+ rot4dYW: 0.2,
123
+ rot4dZW: 0.4
124
+ }
125
+ }
126
+ };
127
+
128
+ // ─────────────────────────────────────────────────────────────────────────────
129
+ // Transition Definitions (designed via /vib3-design Workflow 4)
130
+ // ─────────────────────────────────────────────────────────────────────────────
131
+
132
+ /**
133
+ * Transitions between game states.
134
+ * Key: `${fromState}:${toState}`
135
+ */
136
+ export const TRANSITIONS = {
137
+ 'idle:dueling': { duration: 800, easing: 'easeOut' },
138
+ 'dueling:suddenDeath': { duration: 300, easing: 'elastic' },
139
+ 'suddenDeath:victory': { duration: 1200, easing: 'backOut' },
140
+ 'dueling:victory': { duration: 1200, easing: 'backOut' },
141
+ 'victory:idle': { duration: 1000, easing: 'easeInOut' },
142
+ 'idle:idle': { duration: 500, easing: 'easeInOut' }
143
+ };
144
+
145
+ // ─────────────────────────────────────────────────────────────────────────────
146
+ // Sudden Death Timeline (designed via /vib3-design Workflow 3)
147
+ // 10-second escalation from tense → screen-tearing
148
+ // ─────────────────────────────────────────────────────────────────────────────
149
+
150
+ /**
151
+ * ParameterTimeline spec for the 10-second sudden death countdown.
152
+ * Chromatic aberration is driven externally (see TensionMapper).
153
+ */
154
+ export const SUDDEN_DEATH_TIMELINE = {
155
+ duration: 10000,
156
+ loopMode: 'once',
157
+ tracks: {
158
+ hue: {
159
+ keyframes: [
160
+ { time: 0, value: 200, easing: 'linear' },
161
+ { time: 3000, value: 320, easing: 'easeIn' },
162
+ { time: 7000, value: 0, easing: 'exponential' },
163
+ { time: 9000, value: 0, easing: 'elastic' },
164
+ { time: 10000, value: 0, easing: 'linear' }
165
+ ]
166
+ },
167
+ chaos: {
168
+ keyframes: [
169
+ { time: 0, value: 0.3, easing: 'linear' },
170
+ { time: 3000, value: 0.5, easing: 'easeIn' },
171
+ { time: 7000, value: 0.8, easing: 'expoOut' },
172
+ { time: 9000, value: 1.0, easing: 'elastic' },
173
+ { time: 10000, value: 1.0, easing: 'linear' }
174
+ ]
175
+ },
176
+ speed: {
177
+ keyframes: [
178
+ { time: 0, value: 1.5, easing: 'linear' },
179
+ { time: 3000, value: 2.0, easing: 'easeIn' },
180
+ { time: 7000, value: 2.8, easing: 'expoOut' },
181
+ { time: 9000, value: 3.0, easing: 'elastic' },
182
+ { time: 10000, value: 3.0, easing: 'linear' }
183
+ ]
184
+ },
185
+ gridDensity: {
186
+ keyframes: [
187
+ { time: 0, value: 30, easing: 'linear' },
188
+ { time: 3000, value: 45, easing: 'easeIn' },
189
+ { time: 7000, value: 70, easing: 'expoOut' },
190
+ { time: 9000, value: 100, easing: 'elastic' },
191
+ { time: 10000, value: 100, easing: 'linear' }
192
+ ]
193
+ },
194
+ rot4dXW: {
195
+ keyframes: [
196
+ { time: 0, value: 2.0, easing: 'linear' },
197
+ { time: 10000, value: 6.28, easing: 'linear' }
198
+ ]
199
+ },
200
+ intensity: {
201
+ keyframes: [
202
+ { time: 0, value: 0.7, easing: 'linear' },
203
+ { time: 7000, value: 0.9, easing: 'easeIn' },
204
+ { time: 9500, value: 1.0, easing: 'elastic' },
205
+ { time: 10000, value: 1.0, easing: 'linear' }
206
+ ]
207
+ }
208
+ }
209
+ };
210
+
211
+ // ─────────────────────────────────────────────────────────────────────────────
212
+ // Parallax Layer Config (holographic 5-layer stack)
213
+ // ─────────────────────────────────────────────────────────────────────────────
214
+
215
+ /**
216
+ * Parallax multipliers per holographic layer.
217
+ * Applied to spatial input (tilt/mouse) and 4D rotation deltas.
218
+ * Factor 0.0 = screen-fixed, 1.0 = direct tracking, >1.0 = exaggerated.
219
+ */
220
+ export const LAYER_PARALLAX = {
221
+ 'holo-background-canvas': {
222
+ role: 'Deep void',
223
+ parallax: 0.1,
224
+ baseOpacity: 0.4,
225
+ rotationCounterFactor: -0.3 // slow counter-rotation
226
+ },
227
+ 'holo-shadow-canvas': {
228
+ role: 'Interference mesh',
229
+ parallax: 0.5,
230
+ baseOpacity: 0.6,
231
+ rotationCounterFactor: -0.15
232
+ },
233
+ 'holo-content-canvas': {
234
+ role: 'Letter refraction plane',
235
+ parallax: 1.0,
236
+ baseOpacity: 1.0,
237
+ rotationCounterFactor: 0
238
+ },
239
+ 'holo-highlight-canvas': {
240
+ role: 'Contested letter glow',
241
+ parallax: 1.5,
242
+ baseOpacity: 0.0, // only visible during conflicts
243
+ rotationCounterFactor: 0
244
+ },
245
+ 'holo-accent-canvas': {
246
+ role: 'HUD bezel + CA',
247
+ parallax: 0.0, // screen-fixed
248
+ baseOpacity: 0.8,
249
+ rotationCounterFactor: 0
250
+ }
251
+ };
252
+
253
+ // ─────────────────────────────────────────────────────────────────────────────
254
+ // Tension → Chromatic Aberration Mapper
255
+ // ─────────────────────────────────────────────────────────────────────────────
256
+
257
+ /**
258
+ * Compute chromatic aberration intensity from game tension signals.
259
+ *
260
+ * CA is the primary visual metaphor for "system stress":
261
+ * - Base: 0.02 (subtle glass refraction)
262
+ * - +0.15 per contested letter (both players want it)
263
+ * - During sudden death: inversely maps timer to CA (1s left = 2.0)
264
+ *
265
+ * @param {Object} tension - Tension state
266
+ * @param {number} tension.contestedLetters - Count of letters both players want
267
+ * @param {boolean} tension.suddenDeath - Whether sudden death is active
268
+ * @param {number} tension.secondsLeft - Seconds remaining in sudden death (0-10)
269
+ * @returns {number} Chromatic aberration strength (0.02 - ~2.5)
270
+ */
271
+ export function computeChromaticAberration(tension) {
272
+ const BASE_CA = 0.02;
273
+ const PER_CONFLICT = 0.15;
274
+ const MAX_CONFLICT_CA = 0.6;
275
+
276
+ let ca = BASE_CA;
277
+
278
+ // Contested letter tension
279
+ const conflictCA = Math.min(
280
+ (tension.contestedLetters || 0) * PER_CONFLICT,
281
+ MAX_CONFLICT_CA
282
+ );
283
+ ca += conflictCA;
284
+
285
+ // Sudden death timer mapping
286
+ if (tension.suddenDeath && typeof tension.secondsLeft === 'number') {
287
+ const timerPercent = Math.max(0, Math.min(1, tension.secondsLeft / 10));
288
+ const timerCA = (1 - timerPercent) * 2.0;
289
+ ca += timerCA;
290
+ }
291
+
292
+ return ca;
293
+ }
294
+
295
+ // ─────────────────────────────────────────────────────────────────────────────
296
+ // Game Event → VIB3 Parameter Reactive Bindings
297
+ // ─────────────────────────────────────────────────────────────────────────────
298
+
299
+ /**
300
+ * Maps discrete game events to VIB3+ parameter impulses.
301
+ * Each binding returns a partial param object for TransitionAnimator.
302
+ */
303
+ export const EVENT_BINDINGS = {
304
+ /**
305
+ * Player grabs a letter from the pile.
306
+ * Each held letter shifts deeper into 4D hyperspace.
307
+ * @param {number} totalHeld - Total letters held by this player
308
+ */
309
+ letterGrabbed(totalHeld) {
310
+ return {
311
+ params: { rot4dXW: 0.8 + totalHeld * 0.1 },
312
+ duration: 200,
313
+ easing: 'easeOut'
314
+ };
315
+ },
316
+
317
+ /**
318
+ * Both players want the same letter (tether/conflict).
319
+ * Highlight layer pulses, CA spikes.
320
+ * @param {number} contestCount - Number of contested letters
321
+ */
322
+ letterConflict(contestCount) {
323
+ return {
324
+ params: {
325
+ intensity: 0.6 + contestCount * 0.05
326
+ },
327
+ duration: 150,
328
+ easing: 'elastic'
329
+ };
330
+ },
331
+
332
+ /**
333
+ * Player is rapidly placing letters (flow state).
334
+ * Speed increases, shadow layer thickens.
335
+ * @param {number} wordLength - Current word length
336
+ */
337
+ wordGrowing(wordLength) {
338
+ const speedBoost = Math.min(wordLength * 0.15, 1.0);
339
+ return {
340
+ params: {
341
+ speed: 1.0 + speedBoost
342
+ },
343
+ duration: 300,
344
+ easing: 'easeOut'
345
+ };
346
+ },
347
+
348
+ /**
349
+ * Player dissolves their word — letters scatter back to pool.
350
+ * VHS glitch: geometry snaps to points, scanline tear.
351
+ */
352
+ dissolve() {
353
+ return {
354
+ params: {
355
+ morphFactor: 2.0,
356
+ chaos: 0.9,
357
+ speed: 3.0
358
+ },
359
+ duration: 200,
360
+ easing: 'linear',
361
+ // Caller should schedule a snapback after 200ms
362
+ snapback: {
363
+ params: { morphFactor: 0.3, chaos: 0.15, speed: 1.0 },
364
+ duration: 400,
365
+ easing: 'easeOut'
366
+ }
367
+ };
368
+ },
369
+
370
+ /**
371
+ * ATTACK pressed — transition to sudden death.
372
+ */
373
+ attack() {
374
+ return {
375
+ params: GAME_STATES.suddenDeath.params,
376
+ duration: 300,
377
+ easing: 'elastic'
378
+ };
379
+ },
380
+
381
+ /**
382
+ * Round won — transition to victory.
383
+ * @param {string} _winner - 'p1' or 'p2' (for future per-player effects)
384
+ */
385
+ victory(_winner) {
386
+ return {
387
+ params: {
388
+ ...GAME_STATES.victory.params,
389
+ intensity: 1.0 // bloom swell
390
+ },
391
+ duration: 1200,
392
+ easing: 'backOut'
393
+ };
394
+ }
395
+ };
396
+
397
+ // ─────────────────────────────────────────────────────────────────────────────
398
+ // GlyphWarVisualizer Class
399
+ // ─────────────────────────────────────────────────────────────────────────────
400
+
401
+ /**
402
+ * Orchestrates all VIB3+ visual behavior for GLYPH_WAR.
403
+ *
404
+ * Lifecycle:
405
+ * 1. Construct with a VIB3Engine instance
406
+ * 2. Call init() to set up holographic system + post-processing
407
+ * 3. Call setState() on game state transitions
408
+ * 4. Call onGameEvent() for reactive per-frame bindings
409
+ * 5. Call updateTension() each frame with current conflict/timer data
410
+ * 6. Call destroy() on teardown
411
+ *
412
+ * @example
413
+ * const viz = new GlyphWarVisualizer(engine, containerEl);
414
+ * await viz.init();
415
+ * viz.setState('idle');
416
+ * // ... game starts ...
417
+ * viz.setState('dueling');
418
+ * viz.onGameEvent('letterGrabbed', 3);
419
+ * viz.updateTension({ contestedLetters: 2, suddenDeath: false, secondsLeft: 10 });
420
+ */
421
+ export class GlyphWarVisualizer {
422
+ /**
423
+ * @param {Object} engine - VIB3Engine instance
424
+ * @param {HTMLElement} container - DOM container for post-processing target
425
+ */
426
+ constructor(engine, container) {
427
+ if (!engine) throw new Error('GlyphWarVisualizer requires a VIB3Engine');
428
+
429
+ /** @type {Object} */
430
+ this.engine = engine;
431
+
432
+ /** @type {HTMLElement} */
433
+ this.container = container;
434
+
435
+ /** @type {string} Current game state name */
436
+ this.currentState = 'idle';
437
+
438
+ /** @type {TransitionAnimator} */
439
+ this.animator = new TransitionAnimator(
440
+ (name, value) => this.engine.setParameter(name, value),
441
+ (name) => this.engine.getParameter?.(name) ?? 0
442
+ );
443
+
444
+ /** @type {ParameterTimeline|null} Sudden death timeline */
445
+ this.deathTimeline = null;
446
+
447
+ /** @type {PostProcessingPipeline|null} */
448
+ this.postFx = null;
449
+
450
+ /** @type {ColorPresetsSystem|null} */
451
+ this.colors = null;
452
+
453
+ /** @type {number} Current chromatic aberration value */
454
+ this._currentCA = 0.02;
455
+
456
+ /** @type {number|null} rAF ID for tension updates */
457
+ this._tensionFrameId = null;
458
+ }
459
+
460
+ /**
461
+ * Initialize: switch to holographic system, set up creative tooling.
462
+ */
463
+ async init() {
464
+ // Switch to holographic system
465
+ if (this.engine.switchSystem) {
466
+ await this.engine.switchSystem('holographic');
467
+ }
468
+
469
+ // Set up color presets
470
+ this.colors = new ColorPresetsSystem(
471
+ (name, value) => this.engine.setParameter(name, value)
472
+ );
473
+
474
+ // Set up post-processing pipeline
475
+ if (this.container) {
476
+ this.postFx = new PostProcessingPipeline(this.container);
477
+ }
478
+
479
+ // Apply idle state
480
+ this.setState('idle');
481
+ }
482
+
483
+ /**
484
+ * Transition to a new game state.
485
+ *
486
+ * @param {'idle'|'dueling'|'suddenDeath'|'victory'} stateName
487
+ */
488
+ setState(stateName) {
489
+ const state = GAME_STATES[stateName];
490
+ if (!state) {
491
+ console.warn(`GlyphWarVisualizer: Unknown state "${stateName}"`);
492
+ return;
493
+ }
494
+
495
+ const transitionKey = `${this.currentState}:${stateName}`;
496
+ const transition = TRANSITIONS[transitionKey] || { duration: 500, easing: 'easeInOut' };
497
+
498
+ // Stop any running sudden death timeline
499
+ if (stateName !== 'suddenDeath' && this.deathTimeline) {
500
+ this.deathTimeline.stop();
501
+ this.deathTimeline = null;
502
+ }
503
+
504
+ // Set geometry immediately (not interpolatable)
505
+ this.engine.setParameter('geometry', state.geometry);
506
+
507
+ // Apply color preset
508
+ if (this.colors && state.colorPreset) {
509
+ this.colors.applyPreset(state.colorPreset);
510
+ }
511
+
512
+ // Apply post-processing chain
513
+ if (this.postFx && state.postFxChain) {
514
+ this.postFx.loadPresetChain(state.postFxChain);
515
+ this.postFx.apply();
516
+ }
517
+
518
+ // Smooth transition of continuous parameters
519
+ this.animator.cancelAll();
520
+ this.animator.transition(
521
+ state.params,
522
+ transition.duration,
523
+ transition.easing
524
+ );
525
+
526
+ // Start sudden death timeline if entering that state
527
+ if (stateName === 'suddenDeath') {
528
+ this._startSuddenDeathTimeline();
529
+ }
530
+
531
+ this.currentState = stateName;
532
+ }
533
+
534
+ /**
535
+ * Handle a discrete game event with a reactive visual impulse.
536
+ *
537
+ * @param {string} eventName - Key from EVENT_BINDINGS
538
+ * @param {...*} args - Arguments forwarded to the binding function
539
+ */
540
+ onGameEvent(eventName, ...args) {
541
+ const binding = EVENT_BINDINGS[eventName];
542
+ if (!binding) return;
543
+
544
+ const impulse = binding(...args);
545
+ if (!impulse) return;
546
+
547
+ this.animator.transition(
548
+ impulse.params,
549
+ impulse.duration,
550
+ impulse.easing
551
+ );
552
+
553
+ // Handle snapback (e.g., dissolve glitch then recover)
554
+ if (impulse.snapback) {
555
+ setTimeout(() => {
556
+ this.animator.transition(
557
+ impulse.snapback.params,
558
+ impulse.snapback.duration,
559
+ impulse.snapback.easing
560
+ );
561
+ }, impulse.duration);
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Update tension-reactive visuals (call each frame or on state change).
567
+ *
568
+ * @param {Object} tension
569
+ * @param {number} tension.contestedLetters
570
+ * @param {boolean} tension.suddenDeath
571
+ * @param {number} tension.secondsLeft
572
+ */
573
+ updateTension(tension) {
574
+ this._currentCA = computeChromaticAberration(tension);
575
+
576
+ // Apply CA to post-processing pipeline
577
+ if (this.postFx) {
578
+ this.postFx.addEffect('chromaticAberration', {
579
+ strength: this._currentCA
580
+ });
581
+ this.postFx.apply();
582
+ }
583
+
584
+ // Highlight layer opacity tracks conflict count
585
+ // (In a full impl this would target the specific holo-highlight canvas)
586
+ const highlightOpacity = Math.min(0.3 + (tension.contestedLetters || 0) * 0.15, 1.0);
587
+ this.engine.setParameter('intensity',
588
+ GAME_STATES[this.currentState]?.params?.intensity ?? 0.5 +
589
+ highlightOpacity * 0.2
590
+ );
591
+ }
592
+
593
+ /**
594
+ * Get current chromatic aberration value (for UI sync).
595
+ * @returns {number}
596
+ */
597
+ getChromaticAberration() {
598
+ return this._currentCA;
599
+ }
600
+
601
+ /**
602
+ * Clean up all resources.
603
+ */
604
+ destroy() {
605
+ this.animator.cancelAll();
606
+ if (this.deathTimeline) {
607
+ this.deathTimeline.stop();
608
+ this.deathTimeline = null;
609
+ }
610
+ if (this.postFx) {
611
+ this.postFx.clearAll?.();
612
+ }
613
+ if (this._tensionFrameId) {
614
+ cancelAnimationFrame(this._tensionFrameId);
615
+ }
616
+ }
617
+
618
+ // ─── Internal ────────────────────────────────────────────────────────────
619
+
620
+ /**
621
+ * Build and start the 10-second sudden death ParameterTimeline.
622
+ * @private
623
+ */
624
+ _startSuddenDeathTimeline() {
625
+ this.deathTimeline = new ParameterTimeline(
626
+ (name, value) => this.engine.setParameter(name, value)
627
+ );
628
+
629
+ this.deathTimeline.setDuration(SUDDEN_DEATH_TIMELINE.duration);
630
+ this.deathTimeline.setLoopMode(SUDDEN_DEATH_TIMELINE.loopMode);
631
+
632
+ for (const [param, track] of Object.entries(SUDDEN_DEATH_TIMELINE.tracks)) {
633
+ this.deathTimeline.addTrack(param);
634
+ for (const kf of track.keyframes) {
635
+ this.deathTimeline.addKeyframe(param, kf.time, kf.value, kf.easing);
636
+ }
637
+ }
638
+
639
+ this.deathTimeline.play();
640
+ }
641
+ }
@@ -160,21 +160,42 @@ export class Mat4x4 {
160
160
  * @returns {Mat4x4} New matrix = this * m
161
161
  */
162
162
  multiply(m) {
163
+ const out = new Mat4x4();
164
+ const r = out.data;
163
165
  const a = this.data;
164
166
  const b = m.data;
165
- const result = new Float32Array(16);
166
167
 
167
- for (let col = 0; col < 4; col++) {
168
- for (let row = 0; row < 4; row++) {
169
- let sum = 0;
170
- for (let k = 0; k < 4; k++) {
171
- sum += a[k * 4 + row] * b[col * 4 + k];
172
- }
173
- result[col * 4 + row] = sum;
174
- }
175
- }
168
+ const a00 = a[0], a01 = a[4], a02 = a[8], a03 = a[12];
169
+ const a10 = a[1], a11 = a[5], a12 = a[9], a13 = a[13];
170
+ const a20 = a[2], a21 = a[6], a22 = a[10], a23 = a[14];
171
+ const a30 = a[3], a31 = a[7], a32 = a[11], a33 = a[15];
176
172
 
177
- return new Mat4x4(result);
173
+ const b00 = b[0], b01 = b[4], b02 = b[8], b03 = b[12];
174
+ const b10 = b[1], b11 = b[5], b12 = b[9], b13 = b[13];
175
+ const b20 = b[2], b21 = b[6], b22 = b[10], b23 = b[14];
176
+ const b30 = b[3], b31 = b[7], b32 = b[11], b33 = b[15];
177
+
178
+ r[0] = a00 * b00 + a01 * b10 + a02 * b20 + a03 * b30;
179
+ r[1] = a10 * b00 + a11 * b10 + a12 * b20 + a13 * b30;
180
+ r[2] = a20 * b00 + a21 * b10 + a22 * b20 + a23 * b30;
181
+ r[3] = a30 * b00 + a31 * b10 + a32 * b20 + a33 * b30;
182
+
183
+ r[4] = a00 * b01 + a01 * b11 + a02 * b21 + a03 * b31;
184
+ r[5] = a10 * b01 + a11 * b11 + a12 * b21 + a13 * b31;
185
+ r[6] = a20 * b01 + a21 * b11 + a22 * b21 + a23 * b31;
186
+ r[7] = a30 * b01 + a31 * b11 + a32 * b21 + a33 * b31;
187
+
188
+ r[8] = a00 * b02 + a01 * b12 + a02 * b22 + a03 * b32;
189
+ r[9] = a10 * b02 + a11 * b12 + a12 * b22 + a13 * b32;
190
+ r[10] = a20 * b02 + a21 * b12 + a22 * b22 + a23 * b32;
191
+ r[11] = a30 * b02 + a31 * b12 + a32 * b22 + a33 * b32;
192
+
193
+ r[12] = a00 * b03 + a01 * b13 + a02 * b23 + a03 * b33;
194
+ r[13] = a10 * b03 + a11 * b13 + a12 * b23 + a13 * b33;
195
+ r[14] = a20 * b03 + a21 * b13 + a22 * b23 + a23 * b33;
196
+ r[15] = a30 * b03 + a31 * b13 + a32 * b23 + a33 * b33;
197
+
198
+ return out;
178
199
  }
179
200
 
180
201
  /**
@@ -183,8 +204,44 @@ export class Mat4x4 {
183
204
  * @returns {Mat4x4} this
184
205
  */
185
206
  multiplyInPlace(m) {
186
- const result = this.multiply(m);
187
- this.data.set(result.data);
207
+ const a = this.data;
208
+ const b = m.data;
209
+
210
+ // Cache values to avoid aliasing issues and repeated array access
211
+ const a00 = a[0], a01 = a[4], a02 = a[8], a03 = a[12];
212
+ const a10 = a[1], a11 = a[5], a12 = a[9], a13 = a[13];
213
+ const a20 = a[2], a21 = a[6], a22 = a[10], a23 = a[14];
214
+ const a30 = a[3], a31 = a[7], a32 = a[11], a33 = a[15];
215
+
216
+ const b00 = b[0], b01 = b[4], b02 = b[8], b03 = b[12];
217
+ const b10 = b[1], b11 = b[5], b12 = b[9], b13 = b[13];
218
+ const b20 = b[2], b21 = b[6], b22 = b[10], b23 = b[14];
219
+ const b30 = b[3], b31 = b[7], b32 = b[11], b33 = b[15];
220
+
221
+ // Column 0
222
+ a[0] = a00 * b00 + a01 * b10 + a02 * b20 + a03 * b30;
223
+ a[1] = a10 * b00 + a11 * b10 + a12 * b20 + a13 * b30;
224
+ a[2] = a20 * b00 + a21 * b10 + a22 * b20 + a23 * b30;
225
+ a[3] = a30 * b00 + a31 * b10 + a32 * b20 + a33 * b30;
226
+
227
+ // Column 1
228
+ a[4] = a00 * b01 + a01 * b11 + a02 * b21 + a03 * b31;
229
+ a[5] = a10 * b01 + a11 * b11 + a12 * b21 + a13 * b31;
230
+ a[6] = a20 * b01 + a21 * b11 + a22 * b21 + a23 * b31;
231
+ a[7] = a30 * b01 + a31 * b11 + a32 * b21 + a33 * b31;
232
+
233
+ // Column 2
234
+ a[8] = a00 * b02 + a01 * b12 + a02 * b22 + a03 * b32;
235
+ a[9] = a10 * b02 + a11 * b12 + a12 * b22 + a13 * b32;
236
+ a[10] = a20 * b02 + a21 * b12 + a22 * b22 + a23 * b32;
237
+ a[11] = a30 * b02 + a31 * b12 + a32 * b22 + a33 * b32;
238
+
239
+ // Column 3
240
+ a[12] = a00 * b03 + a01 * b13 + a02 * b23 + a03 * b33;
241
+ a[13] = a10 * b03 + a11 * b13 + a12 * b23 + a13 * b33;
242
+ a[14] = a20 * b03 + a21 * b13 + a22 * b23 + a23 * b33;
243
+ a[15] = a30 * b03 + a31 * b13 + a32 * b23 + a33 * b33;
244
+
188
245
  return this;
189
246
  }
190
247