jailedthreejs 0.9.2-beta.0

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/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # JailedThreeJS
2
+
3
+ CSS-first scaffolding for Three.js. Drop a `<cell>` on the page, describe your scene in HTML, tweak it with CSS custom properties, sprinkle behaviour with plain JS. The engine does the wiring (renderer, cameras, raycasters, resize handling) so you can stay inside web platform muscle memory.
4
+
5
+ ---
6
+
7
+ ## TL;DR
8
+
9
+ - **Structure**: Every tag under a `<cell>` becomes its matching `THREE.Object3D` (`<mesh>`, `<group>`, `<perspectivecamera>`, `<axeshelper>`, …).
10
+ - **Style**: Custom properties map to object state: `--position`, `--rotation`, `--scale`, `--geometry`, `--material-color`, `--transition`, etc.
11
+ - **Interact**: Native DOM events (`onclick`, `onmouseover`, `ondblclick`, `oncontextmenu`, …) and pseudo-classes (`:hover`, `:focus`, `:active`) work on meshes.
12
+ - **Animate**: Declare transitions or CSS `@keyframes` and the painter lerps the underlying Three.js values.
13
+ - **Assets**: Reference built-ins (`@cube`, `@plane`, `@sphere`, `@torus`) or define custom `@asset` rules that fetch GLTF/GLB/FBX/textures/audio.
14
+ - **API access**: You can always grab the underlying `Cell`/`THREE.Object3D` to run imperative code when needed.
15
+
16
+ > Philosophy: embrace HTML for structure, CSS for look, JS for intent. No DSLs, no scene JSON. When you want raw Three.js, it’s still there.
17
+
18
+ ---
19
+
20
+ ## Installation
21
+
22
+ The repo ships as ES modules. Point your bundler (or `<script type="module">`) to the `src/module` folder.
23
+
24
+ ```html
25
+ <script type="module">
26
+ import { JThree } from './module/index.js';
27
+ // All <cell> elements are converted on import.
28
+ </script>
29
+ ```
30
+
31
+ If you add cells later (e.g. via a framework), call `JThree.init_convert()` to rescan the DOM.
32
+
33
+ ---
34
+
35
+ ## Quick start
36
+
37
+ ```html
38
+ <cell id="demo" style="display:block;width:560px;height:320px;background:#05070f">
39
+ <perspectivecamera render></perspectivecamera>
40
+ <mesh id="box" class="box" onclick="spin(this)"></mesh>
41
+ </cell>
42
+
43
+ <style>
44
+ .box {
45
+ --geometry: @cube;
46
+ --position: (0,0,-5);
47
+ --scale: (1,1,1);
48
+ --material-color: (0.8,0.2,0.2);
49
+ --transition: 400ms ease-in-out;
50
+ }
51
+ .box:hover { --material-color: (0.2,0.8,0.9); }
52
+ .box:active { --scale: (1.2,1.2,1.2); }
53
+ </style>
54
+
55
+ <script type="module">
56
+ import { Cell } from './module/index.js';
57
+ const cell = Cell.getCell(document.getElementById('demo'));
58
+ const box = cell.getConvictById('box');
59
+
60
+ cell.addUpdateFunction(function () { box.rotation.y += 0.01; });
61
+ window.spin = (mesh) => mesh.style.setProperty('--rotation-y', box.rotation.y + Math.PI);
62
+ </script>
63
+ ```
64
+
65
+ That is the entire scene: no manual `WebGLRenderer`, no raycaster setup, no resize bookkeeping.
66
+
67
+ ---
68
+
69
+ ## Styling cheat sheet
70
+
71
+ | Purpose | Custom property examples |
72
+ |--------------------|--------------------------------------------------------------------------------|
73
+ | Transform | `--position: (x,y,z)` · `--rotation: (radX,radY,radZ)` · `--scale: (sx,sy,sz)` |
74
+ | Geometry/material | `--geometry: @cube` · `--material-color: (r,g,b)` · `--material-roughness: 0.5` |
75
+ | Cameras | `--fov` · `--near` · `--far` · `--aspect` (auto-updated unless overridden) |
76
+ | Lights | `--intensity` · `--distance` · `--color` · `--decay` |
77
+ | Transitions | `--transition: 300ms ease` (or set `object.transition = { duration, timing }`) |
78
+ | Animations | Use CSS `@keyframes` where frames set the same custom props. |
79
+ | Assets | `--geometry: @MySpaceship` (load via `@MySpaceship { url: './ship.glb'; }`) |
80
+
81
+ > Any custom property prefixed with `--` is forwarded to the object through a best-effort parser (numbers/arrays/assets/refs). Unknown props are ignored with a console warning.
82
+
83
+ ---
84
+
85
+ ## Runtime API
86
+
87
+ | API | Description |
88
+ |---------------------|-------------------------------------------------------------------------------------------------------------------|
89
+ | `JThree.init_convert()` | Rescan the DOM for `<cell>` tags and boot them. Automatically run once on import. |
90
+ | `Cell.getCell(element)` | Retrieve the `Cell` controller attached to a `<cell>` DOM node. |
91
+ | `cell.addUpdateFunction(fn)` | Register a per-frame callback (runs inside the render loop). |
92
+ | `cell.getConvictById(id)` / `getConvictByDom(dom)` | Fetch the underlying `THREE.Object3D` created for a DOM element. |
93
+ | `cell.getConvictsByClass(cls)` | Batch lookup by class name (mirrors `document.getElementsByClassName`). |
94
+ | `cell.removeConvict(object)` | Remove an object and clean up book-keeping. |
95
+ | `object.transition = { duration, timing }` | Enable JS-driven interpolation for subsequent style changes. |
96
+ | `object.animation = { name, duration, iteration }` | Apply a CSS `@keyframes` animation to an object’s custom props. |
97
+
98
+ You always have access to the raw `THREE.Object3D` instance (`convict`). Use it for low-level operations, loaders, shaders, etc.
99
+
100
+ ---
101
+
102
+ ## Events & pseudo-classes
103
+
104
+ - Add DOM event attributes (`onclick`, `onmouseover`, `ondblclick`, `onmousedown`, `onmouseup`, `oncontextmenu`). They receive a synthetic event with `{ target3d, targetCell, pointerPosition, originalEvt }`.
105
+ - Use CSS pseudo-classes (`:hover`, `:focus`, `:active`). The runtime keeps class flags in sync with pointer/click states and repaints affected objects.
106
+ - `object.layers` is managed automatically: pickable meshes are moved to layer 3 only when they need interaction, saving raycast cost.
107
+
108
+ ---
109
+
110
+ ## Assets & `@rules`
111
+
112
+ Declare assets in CSS alongside the rest of your styles:
113
+
114
+ ```css
115
+ @MySpaceship {
116
+ url: "./models/ship.glb";
117
+ }
118
+
119
+ .hero {
120
+ --geometry: @MySpaceship;
121
+ --position: (0,0,-10);
122
+ }
123
+ ```
124
+
125
+ First time the painter sees `@MySpaceship`, it loads the GLB (or FBX, texture, audio, material JSON), caches it, and applies it wherever referenced. Built-ins `@cube`, `@sphere`, `@plane`, and `@torus` ship for quick sketches.
126
+
127
+ ---
128
+
129
+
130
+ ## Tips & gotchas
131
+
132
+ - **Inline styles vs stylesheet rules**: inline `style=""` attributes on `<mesh>` nodes work the same as CSS rules. The painter merges them in the order: base class → id → pseudo-class → inline.
133
+ - **Transitions**: if you define `object.transition`, only numeric/array props are animated. Non-numeric values (like `@asset` references) swap instantly.
134
+ - **Async assets**: `--geometry: @MyGLTF` assigns a promise until the loader resolves. The painter applies final values when the promise settles; don’t mutate those props synchronously.
135
+ - **Cleanup**: If you inject scripts that schedule loops/intervals, register a cleanup handler via `registerSceneCleanup(fn)` so the editor or export can dispose them reliably.
136
+
137
+ ---
138
+
139
+ ## License
140
+
141
+ MIT © 2025. Use it in games, demos, dashboards, art toys, education—wherever CSS-driven 3D makes sense. Contributions welcome!
@@ -0,0 +1,208 @@
1
+ // NoScope.js
2
+ //
3
+ // Centralised event handling.
4
+ // Shared THREE.Raycaster + NDC pointer for all cells; all pickable
5
+ // objects live on layer 3.
6
+
7
+ import * as THREE from 'three';
8
+ import { paintExtraCell, paintSpecificMuse } from './artist.js';
9
+ import { fastRemove_arry } from './utils.js';
10
+
11
+ const raycaster = new THREE.Raycaster();
12
+ const ndcPointer = new THREE.Vector2();
13
+
14
+ // Only objects on layer 3 are considered pickable.
15
+ raycaster.layers.set(3);
16
+
17
+ /* Flag helpers */
18
+
19
+ function addFlag(arr, flag) {
20
+ if (!arr.includes(flag)) arr.push(flag);
21
+ }
22
+
23
+ function delFlag(arr, flag) {
24
+ fastRemove_arry(arr, flag);
25
+ }
26
+
27
+ /* Public handlers */
28
+
29
+ export function default_onCellClick_method(domEvt, cell) {
30
+ const hit = cell._last_cast_caught;
31
+ if (!hit) return;
32
+
33
+ addFlag(hit.userData.extraParams, ':focus');
34
+
35
+ const synth = {
36
+ type: 'cellclick',
37
+ originalEvt: domEvt,
38
+ target3d: hit,
39
+ targetCell: cell,
40
+ targetElement: hit.userData.domEl,
41
+ pointerPosition: cell._lastHitPosition
42
+ };
43
+
44
+ hit.userData.domEl.onclick?.call(hit.userData.domEl, synth);
45
+ paintExtraCell(cell);
46
+ }
47
+
48
+ export function default_onCellPointerMove_method(domEvt, cell) {
49
+ if (!cell.focusedCamera) return;
50
+
51
+ _raycast(domEvt, cell.focusedCamera, cell.cellElm);
52
+
53
+ const hitResult = raycaster.intersectObjects(cell.loadedScene.children, true)[0];
54
+ const lastHit = cell._last_cast_caught;
55
+
56
+ if (hitResult) {
57
+ const hitObject = hitResult.object;
58
+
59
+ if (hitObject !== lastHit) {
60
+ if (lastHit) {
61
+ delFlag(lastHit.userData.extraParams, ':hover');
62
+ lastHit.userData.domEl.onmouseleave?.call(lastHit.userData.domEl, {
63
+ type: 'cellmouseleave',
64
+ originalEvt: domEvt,
65
+ target3d: lastHit,
66
+ targetCell: cell,
67
+ targetElement: lastHit.userData.domEl,
68
+ pointerPosition: cell._lastHitPosition
69
+ });
70
+ paintSpecificMuse(lastHit);
71
+ }
72
+
73
+ cell._last_cast_caught = hitObject;
74
+
75
+ hitObject.userData.domEl.onmouseenter?.call(hitObject.userData.domEl, {
76
+ type: 'cellmouseenter',
77
+ originalEvt: domEvt,
78
+ target3d: hitObject,
79
+ targetCell: cell,
80
+ targetElement: hitObject.userData.domEl,
81
+ pointerPosition: hitResult.point
82
+ });
83
+ }
84
+
85
+ addFlag(hitObject.userData.extraParams, ':hover');
86
+ cell._lastHitPosition = hitResult.point;
87
+
88
+ hitObject.userData.domEl.onmouseover?.call(hitObject.userData.domEl, {
89
+ type: 'cellhover',
90
+ originalEvt: domEvt,
91
+ target3d: hitObject,
92
+ targetCell: cell,
93
+ targetElement: hitObject.userData.domEl,
94
+ pointerPosition: hitResult.point
95
+ });
96
+
97
+ paintExtraCell(cell);
98
+ } else if (lastHit) {
99
+ delFlag(lastHit.userData.extraParams, ':hover');
100
+ lastHit.userData.domEl.onmouseleave?.call(lastHit.userData.domEl, {
101
+ type: 'cellmouseleave',
102
+ originalEvt: domEvt,
103
+ target3d: lastHit,
104
+ targetCell: cell,
105
+ targetElement: lastHit.userData.domEl,
106
+ pointerPosition: cell._lastHitPosition
107
+ });
108
+ paintSpecificMuse(lastHit);
109
+ cell._last_cast_caught = null;
110
+ }
111
+ }
112
+
113
+ export function default_onCellMouseDown_method(domEvt, cell) {
114
+ const hit = cell._last_cast_caught;
115
+ if (!hit) return;
116
+
117
+ addFlag(hit.userData.extraParams, ':active');
118
+
119
+ const synth = {
120
+ type: 'celldown',
121
+ originalEvt: domEvt,
122
+ target3d: hit,
123
+ targetCell: cell,
124
+ targetElement: hit.userData.domEl,
125
+ pointerPosition: cell._lastHitPosition
126
+ };
127
+
128
+ hit.userData.domEl.onmousedown?.call(hit.userData.domEl, synth);
129
+ paintExtraCell(cell);
130
+ }
131
+
132
+ export function default_onCellMouseUp_method(domEvt, cell) {
133
+ const hit = cell._last_cast_caught;
134
+ if (!hit) return;
135
+
136
+ delFlag(hit.userData.extraParams, ':active');
137
+
138
+ const synth = {
139
+ type: 'cellup',
140
+ originalEvt: domEvt,
141
+ target3d: hit,
142
+ targetCell: cell,
143
+ targetElement: hit.userData.domEl,
144
+ pointerPosition: cell._lastHitPosition
145
+ };
146
+
147
+ hit.userData.domEl.onmouseup?.call(hit.userData.domEl, synth);
148
+ paintExtraCell(cell);
149
+ }
150
+
151
+ export function default_onCellDoubleClick_method(domEvt, cell) {
152
+ const hit = cell._last_cast_caught;
153
+ if (!hit) return;
154
+
155
+ addFlag(hit.userData.extraParams, ':focus');
156
+
157
+ const synth = {
158
+ type: 'celldblclick',
159
+ originalEvt: domEvt,
160
+ target3d: hit,
161
+ targetCell: cell,
162
+ targetElement: hit.userData.domEl,
163
+ pointerPosition: cell._lastHitPosition
164
+ };
165
+
166
+ hit.userData.domEl.ondblclick?.call(hit.userData.domEl, synth);
167
+ paintExtraCell(cell);
168
+ }
169
+
170
+ export function default_onCellContextMenu_method(domEvt, cell) {
171
+ const hit = cell._last_cast_caught;
172
+ if (!hit) return;
173
+
174
+ const synth = {
175
+ type: 'cellcontextmenu',
176
+ originalEvt: domEvt,
177
+ target3d: hit,
178
+ targetCell: cell,
179
+ targetElement: hit.userData.domEl,
180
+ pointerPosition: cell._lastHitPosition
181
+ };
182
+
183
+ hit.userData.domEl.oncontextmenu?.call(hit.userData.domEl, synth);
184
+ paintExtraCell(cell);
185
+ }
186
+
187
+ /* Internal raycast helper */
188
+
189
+ /**
190
+ * @param {MouseEvent} domEvt
191
+ * @param {THREE.Camera} camera
192
+ * @param {HTMLElement} referenceEl
193
+ */
194
+ function _raycast(domEvt, camera, referenceEl) {
195
+ if (!camera) return;
196
+
197
+ const targetEl = referenceEl || domEvt.currentTarget || domEvt.target;
198
+ const rect = targetEl.getBoundingClientRect();
199
+
200
+ camera.updateMatrixWorld();
201
+
202
+ ndcPointer.set(
203
+ ((domEvt.clientX - rect.left) / rect.width) * 2 - 1,
204
+ (-(domEvt.clientY - rect.top) / rect.height) * 2 + 1
205
+ );
206
+
207
+ raycaster.setFromCamera(ndcPointer, camera);
208
+ }
package/dist/Train.js ADDED
@@ -0,0 +1,349 @@
1
+ // Train.js
2
+ //
3
+ // Interpolation / animation helpers used by JailedThreeJS.
4
+ // - Numeric lerping
5
+ // - Cubic-bezier easing
6
+ // - Generic value interpolation (numbers + arrays)
7
+ // - Time-based transitions over JS values
8
+ // - CSS keyframe-driven animation for custom props
9
+
10
+ import { exchange_rule, deep_searchParms, CSSValueTo3JSValue } from './artist.js';
11
+ import { getAnimationMap } from './utils.js';
12
+
13
+ /**
14
+ * Linearly interpolate between two numbers.
15
+ * @param {number} a
16
+ * @param {number} b
17
+ * @param {number} t in [0, 1]
18
+ */
19
+ export function lerpNumber(a, b, t) {
20
+ return a + (b - a) * t;
21
+ }
22
+
23
+ /**
24
+ * Interpolate arrays with tolerant length handling.
25
+ *
26
+ * Length rule:
27
+ * - For indices where both arrays have a value → lerp them.
28
+ * - For indices that exist only in `from` → keep `from[i]`.
29
+ * - For indices that exist only in `to` → keep `to[i]`.
30
+ *
31
+ * Example:
32
+ * [x1,y1,z1,q1] → [x2,y2,z2]
33
+ * ==>
34
+ * [lerp(x1,x2), lerp(y1,y2), lerp(z1,z2), q1]
35
+ *
36
+ * @param {Array<number>} from
37
+ * @param {Array<number>} to
38
+ * @param {number} t
39
+ * @param {(a:number,b:number,t:number)=>number} lerpMethod
40
+ */
41
+ function lerpArray(from, to, t, lerpMethod) {
42
+ const maxLen = Math.max(from.length, to.length);
43
+ const out = new Array(maxLen);
44
+ for (let i = 0; i < maxLen; i++) {
45
+ const hasFrom = i < from.length;
46
+ const hasTo = i < to.length;
47
+ if (hasFrom && hasTo) {
48
+ out[i] = lerpMethod(from[i], to[i], t);
49
+ } else if (hasFrom) {
50
+ out[i] = from[i];
51
+ } else {
52
+ out[i] = to[i];
53
+ }
54
+ }
55
+ return out;
56
+ }
57
+
58
+ /**
59
+ * Interpolate numbers or arrays using the provided lerp method.
60
+ *
61
+ * @param {number|Array<number>} from
62
+ * @param {number|Array<number>} to
63
+ * @param {number} t
64
+ * @param {(a:number,b:number,t:number)=>number} lerpMethod
65
+ */
66
+ export function lerpValue(from, to, t, lerpMethod = lerpNumber) {
67
+ const isNum = v => typeof v === 'number';
68
+ const isArr = Array.isArray;
69
+
70
+ if (isNum(from) && isNum(to)) {
71
+ return lerpMethod(from, to, t);
72
+ }
73
+ if (isArr(from) && isArr(to)) {
74
+ return lerpArray(from, to, t, lerpMethod);
75
+ }
76
+ // Mixed or unsupported types: just return `to` instantly.
77
+ return to;
78
+ }
79
+
80
+ /**
81
+ * Create a cubic-bezier easing function.
82
+ * Implementation adapted from https://github.com/gre/bezier-easing
83
+ */
84
+ function cubicBezier(p0, p1, p2, p3) {
85
+ const cx = 3 * p0;
86
+ const bx = 3 * (p2 - p0) - cx;
87
+ const ax = 1 - cx - bx;
88
+ const cy = 3 * p1;
89
+ const by = 3 * (p3 - p1) - cy;
90
+ const ay = 1 - cy - by;
91
+
92
+ function sampleCurveX(t) {
93
+ return ((ax * t + bx) * t + cx) * t;
94
+ }
95
+ function sampleCurveY(t) {
96
+ return ((ay * t + by) * t + cy) * t;
97
+ }
98
+ function sampleCurveDerivativeX(t) {
99
+ return (3 * ax * t + 2 * bx) * t + cx;
100
+ }
101
+ function solveTforX(x) {
102
+ let t = x;
103
+ for (let i = 0; i < 4; i++) {
104
+ const dx = sampleCurveX(t) - x;
105
+ if (Math.abs(dx) < 1e-6) return t;
106
+ t -= dx / sampleCurveDerivativeX(t);
107
+ }
108
+ return t;
109
+ }
110
+ return x => sampleCurveY(solveTforX(x));
111
+ }
112
+
113
+ /**
114
+ * Resolve an easing function from a CSS timing function string.
115
+ *
116
+ * @param {string} timingFunction
117
+ * @returns {(t:number)=>number}
118
+ */
119
+ function _get_Equation(timingFunction) {
120
+ switch (timingFunction) {
121
+ case 'linear':
122
+ return t => t;
123
+ case 'ease':
124
+ return cubicBezier(0.25, 0.1, 0.25, 1.0);
125
+ case 'ease-in':
126
+ return cubicBezier(0.42, 0, 1.0, 1.0);
127
+ case 'ease-out':
128
+ return cubicBezier(0, 0, 0.58, 1.0);
129
+ case 'ease-in-out':
130
+ return cubicBezier(0.42, 0, 0.58, 1.0);
131
+ default:
132
+ break;
133
+ }
134
+
135
+ if (typeof timingFunction === 'string' && timingFunction.startsWith('cubic-bezier')) {
136
+ const match = timingFunction.match(/cubic-bezier\(([^)]+)\)/);
137
+ if (match) {
138
+ const nums = match[1]
139
+ .split(/[, ]+/)
140
+ .map(Number)
141
+ .filter(n => !Number.isNaN(n));
142
+ if (nums.length === 4) {
143
+ return cubicBezier(nums[0], nums[1], nums[2], nums[3]);
144
+ }
145
+ }
146
+ }
147
+
148
+ // Fallback to linear on garbage.
149
+ return t => t;
150
+ }
151
+
152
+ /**
153
+ * Animate between two values over time.
154
+ *
155
+ * - Supports numbers and arrays (arrays follow the tolerant rule above).
156
+ * - Non-animatable values resolve instantly (onUpdate + onComplete with `to`).
157
+ *
158
+ * @param {number|Array<number>} from
159
+ * @param {number|Array<number>} to
160
+ * @param {number} durationMs
161
+ * @param {(value:any, easedT:number) => void} onUpdate
162
+ * @param {(finalValue:any) => void} [onComplete]
163
+ * @param {string} [timingFunction='linear']
164
+ */
165
+ export function animateLerp(
166
+ from,
167
+ to,
168
+ durationMs,
169
+ onUpdate,
170
+ onComplete,
171
+ timingFunction = 'linear'
172
+ ) {
173
+ const isAnimatable = v =>
174
+ typeof v === 'number' || Array.isArray(v);
175
+
176
+ const finishInstant = () => {
177
+ if (onUpdate) onUpdate(to, 1);
178
+ if (onComplete) onComplete(to);
179
+ };
180
+
181
+ if (!isAnimatable(from) || !isAnimatable(to)) {
182
+ finishInstant();
183
+ return;
184
+ }
185
+
186
+ if (!Number.isFinite(durationMs) || durationMs <= 0) {
187
+ finishInstant();
188
+ return;
189
+ }
190
+
191
+ const start = performance.now();
192
+ const ease = _get_Equation(timingFunction);
193
+
194
+ function step(now) {
195
+ let t = (now - start) / durationMs;
196
+ if (t >= 1) t = 1;
197
+ const easedT = ease(t);
198
+ const value = lerpValue(from, to, easedT, lerpNumber);
199
+ if (onUpdate) onUpdate(value, easedT);
200
+ if (t < 1) {
201
+ requestAnimationFrame(step);
202
+ } else if (onComplete) {
203
+ onComplete(value);
204
+ }
205
+ }
206
+
207
+ requestAnimationFrame(step);
208
+ }
209
+
210
+ /**
211
+ * Parse a keyframe time string into milliseconds.
212
+ *
213
+ * Supports:
214
+ * - 'from' / 'to'
215
+ * - percentages ('0%', '50%', '100%')
216
+ * - '123ms'
217
+ * - bare numbers treated as ms
218
+ *
219
+ * @param {string} keyText
220
+ * @param {number} totalDuration
221
+ */
222
+ function parseKeyframeTime(keyText, totalDuration) {
223
+ if (!keyText) return 0;
224
+ const text = String(keyText).trim().toLowerCase();
225
+
226
+ if (text === 'from') return 0;
227
+ if (text === 'to') return totalDuration;
228
+
229
+ if (text.endsWith('%')) {
230
+ const v = parseFloat(text);
231
+ return Number.isFinite(v) ? (v / 100) * totalDuration : 0;
232
+ }
233
+ if (text.endsWith('ms')) {
234
+ const v = parseFloat(text);
235
+ return Number.isFinite(v) ? v : 0;
236
+ }
237
+ const n = Number(text);
238
+ return Number.isFinite(n) ? n : 0;
239
+ }
240
+
241
+ /**
242
+ * Apply a CSS @keyframes animation to a Three.js object.
243
+ *
244
+ * @param {THREE.Object3D} object
245
+ * @param {{
246
+ * name: string,
247
+ * duration: number,
248
+ * timing?: { fun?: string },
249
+ * iteration?: { count?: number | string }
250
+ * }} animationObj
251
+ */
252
+ export async function KeyFrameAnimationLerp(object, animationObj) {
253
+ if (!object || !animationObj?.name || !animationObj?.duration) return;
254
+
255
+ const keyFramesRule = getAnimationMap(animationObj.name);
256
+ if (!keyFramesRule || !keyFramesRule.cssRules) {
257
+ console.error(`Animation "${animationObj.name}" not found or has no rules.`);
258
+ return;
259
+ }
260
+
261
+ const duration = animationObj.duration;
262
+
263
+ const rules = Array.from(keyFramesRule.cssRules).slice();
264
+ rules.sort(
265
+ (a, b) =>
266
+ parseKeyframeTime(a.keyText, duration) -
267
+ parseKeyframeTime(b.keyText, duration)
268
+ );
269
+
270
+ const runOnce = async () => {
271
+ for (let i = 0; i < rules.length - 1; i++) {
272
+ const fromRule = rules[i];
273
+ const toRule = rules[i + 1];
274
+ const t0 = parseKeyframeTime(fromRule.keyText, duration);
275
+ const t1 = parseKeyframeTime(toRule.keyText, duration);
276
+ const segmentMs = t1 - t0;
277
+ if (segmentMs <= 0) continue;
278
+
279
+ const fromProps = {};
280
+ const toProps = {};
281
+
282
+ // Collect animatable custom props from "from" frame
283
+ for (const propName of fromRule.style) {
284
+ const raw = fromRule.style.getPropertyValue(propName);
285
+ // custom props are expected to be `--foo-bar`
286
+ if (!propName.startsWith('--')) continue;
287
+ fromProps[propName.slice(2)] = CSSValueTo3JSValue(raw, object);
288
+ }
289
+
290
+ // Collect matching props from "to" frame
291
+ for (const propName of toRule.style) {
292
+ const raw = toRule.style.getPropertyValue(propName);
293
+ if (!propName.startsWith('--')) continue;
294
+ toProps[propName.slice(2)] = CSSValueTo3JSValue(raw, object);
295
+ }
296
+
297
+ const keys = Object.keys(fromProps).filter(k => k in toProps);
298
+ if (!keys.length) continue;
299
+
300
+ await Promise.all(
301
+ keys.map(key => {
302
+ const fromVal = fromProps[key];
303
+ const toVal = toProps[key];
304
+
305
+ // Resolve async assets before animating.
306
+ const resolveValue = v =>
307
+ (v && typeof v.then === 'function')
308
+ ? v
309
+ : Promise.resolve(v);
310
+
311
+ return Promise.all([resolveValue(fromVal), resolveValue(toVal)])
312
+ .then(([resolvedFrom, resolvedTo]) => new Promise(resolve => {
313
+ animateLerp(
314
+ resolvedFrom,
315
+ resolvedTo,
316
+ segmentMs,
317
+ v => {
318
+ const { parent, key: finalKey } = deep_searchParms(object, key.split('-'));
319
+ exchange_rule(parent, finalKey, v);
320
+ },
321
+ resolve,
322
+ animationObj.timing?.fun || 'linear'
323
+ );
324
+ }));
325
+ })
326
+ );
327
+ }
328
+ };
329
+
330
+ let iterationCount = animationObj.iteration?.count ?? 1;
331
+ if (iterationCount === 'infinity' || iterationCount === 'infinite') {
332
+ iterationCount = Infinity;
333
+ }
334
+
335
+ if (iterationCount === Infinity) {
336
+ // infinite loop – deliberately never resolves
337
+ // eslint-disable-next-line no-constant-condition
338
+ while (true) {
339
+ // eslint-disable-next-line no-await-in-loop
340
+ await runOnce();
341
+ }
342
+ } else {
343
+ const total = Number(iterationCount) || 1;
344
+ for (let i = 0; i < total; i++) {
345
+ // eslint-disable-next-line no-await-in-loop
346
+ await runOnce();
347
+ }
348
+ }
349
+ }