@vib3code/sdk 2.0.3-canary.0e9a1ac → 2.0.3-canary.3349130
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.
|
|
3
|
+
"version": "2.0.3-canary.3349130",
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
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
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
1306
|
-
`pipeline.
|
|
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
|
+
}
|