exodeui-react-native 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1 -0
- package/package.json +10 -7
- package/src/ExodeUIView.tsx +110 -27
- package/src/__tests__/engine.test.ts +299 -0
- package/src/engine.ts +529 -178
- package/src/physics/MatterPhysics.ts +66 -0
- package/src/physics/PhysicsEngine.ts +42 -0
- package/src/physics/index.ts +2 -0
- package/src/types.ts +159 -16
- package/src/useExodeUI.ts +31 -1
package/src/engine.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { SkCanvas, SkImage, SkPaint, PaintStyle, Skia, SkPath, SkColor, BlurStyle, SkImageFilter, SkMaskFilter } from '@shopify/react-native-skia';
|
|
2
|
-
import { Artboard, Animation, ShapeObject, StateMachine, State, Fit, Alignment, Layout } from './types';
|
|
3
|
-
import
|
|
2
|
+
import { Artboard, Animation as SDKAnimation, ShapeObject, StateMachine, State, Fit, Alignment, Layout, LogicNode, LogicOp, Constraint, ComponentEvent } from './types';
|
|
3
|
+
import { PhysicsEngine, MatterPhysics } from './physics';
|
|
4
4
|
|
|
5
5
|
export class ExodeUIEngine {
|
|
6
6
|
private artboard: Artboard | null = null;
|
|
7
7
|
private objectStates: Map<string, any> = new Map();
|
|
8
8
|
|
|
9
9
|
// Physics State
|
|
10
|
-
private physicsEngine:
|
|
11
|
-
private physicsBodies: Map<string, Matter.Body> = new Map();
|
|
10
|
+
private physicsEngine: PhysicsEngine | null = null;
|
|
12
11
|
|
|
13
12
|
// State Machine State
|
|
14
13
|
private activeStateMachine: StateMachine | null = null;
|
|
@@ -16,9 +15,9 @@ export class ExodeUIEngine {
|
|
|
16
15
|
private inputNameMap: Map<string, string[]> = new Map(); // name -> id[] mapping
|
|
17
16
|
private layerStates: Map<string, {
|
|
18
17
|
currentStateIds: string[];
|
|
19
|
-
animation:
|
|
18
|
+
animations: { animation: SDKAnimation; state: State | null }[];
|
|
20
19
|
time: number;
|
|
21
|
-
|
|
20
|
+
duration: number;
|
|
22
21
|
}> = new Map();
|
|
23
22
|
|
|
24
23
|
private imageCache = new Map<string, SkImage>();
|
|
@@ -27,6 +26,22 @@ export class ExodeUIEngine {
|
|
|
27
26
|
|
|
28
27
|
private onTrigger?: (triggerName: string, animationName: string) => void;
|
|
29
28
|
private onInputUpdate?: (nameOrId: string, value: any) => void;
|
|
29
|
+
private onComponentChange?: (event: ComponentEvent) => void;
|
|
30
|
+
|
|
31
|
+
// Specific Component Listeners
|
|
32
|
+
private onToggle?: (name: string, checked: boolean) => void;
|
|
33
|
+
private onInputChange?: (name: string, text: string) => void;
|
|
34
|
+
private onInputFocus?: (name: string) => void;
|
|
35
|
+
private onInputBlur?: (name: string) => void;
|
|
36
|
+
|
|
37
|
+
// Track triggers that were just fired in the current frame
|
|
38
|
+
private justFiredTriggers: Set<string> = new Set();
|
|
39
|
+
|
|
40
|
+
// Interaction State
|
|
41
|
+
private focusedId: string | null = null;
|
|
42
|
+
|
|
43
|
+
// Physics/Animation State
|
|
44
|
+
private objectVelocities: Map<string, { vx: number; vy: number }> = new Map();
|
|
30
45
|
|
|
31
46
|
setTriggerCallback(cb: (triggerName: string, animationName: string) => void) {
|
|
32
47
|
this.onTrigger = cb;
|
|
@@ -36,6 +51,26 @@ export class ExodeUIEngine {
|
|
|
36
51
|
this.onInputUpdate = cb;
|
|
37
52
|
}
|
|
38
53
|
|
|
54
|
+
setComponentChangeCallback(cb: (event: ComponentEvent) => void) {
|
|
55
|
+
this.onComponentChange = cb;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setToggleCallback(cb: (name: string, checked: boolean) => void) {
|
|
59
|
+
this.onToggle = cb;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
setInputChangeCallback(cb: (name: string, text: string) => void) {
|
|
63
|
+
this.onInputChange = cb;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setInputFocusCallback(cb: (name: string) => void) {
|
|
67
|
+
this.onInputFocus = cb;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setInputBlurCallback(cb: (name: string) => void) {
|
|
71
|
+
this.onInputBlur = cb;
|
|
72
|
+
}
|
|
73
|
+
|
|
39
74
|
constructor() {}
|
|
40
75
|
|
|
41
76
|
// Helper to update Map AND notify listener
|
|
@@ -55,81 +90,155 @@ export class ExodeUIEngine {
|
|
|
55
90
|
return state ? state.currentStateIds : [];
|
|
56
91
|
}
|
|
57
92
|
|
|
93
|
+
getObjectState(id: string): any {
|
|
94
|
+
return this.objectStates.get(id);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
updateConstraint(objectId: string, index: number, properties: Partial<Constraint>) {
|
|
99
|
+
const obj = this.artboard?.objects.find(o => o.id === objectId);
|
|
100
|
+
if (obj && obj.constraints && obj.constraints[index]) {
|
|
101
|
+
obj.constraints[index] = { ...obj.constraints[index], ...properties } as any;
|
|
102
|
+
|
|
103
|
+
// Clear velocity if spring is disabled or reset
|
|
104
|
+
if (properties.useSpring === false) {
|
|
105
|
+
this.objectVelocities.delete(objectId);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
updateObjectOptions(id: string, newOptions: any) {
|
|
111
|
+
const state = this.objectStates.get(id);
|
|
112
|
+
if (state) {
|
|
113
|
+
state.options = { ...state.options, ...newOptions };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public updateGraphData(nameOrId: string, data: number[]) {
|
|
118
|
+
if (!this.artboard) return;
|
|
119
|
+
|
|
120
|
+
// Find the graph object
|
|
121
|
+
const obj = this.artboard.objects.find(o => o.id === nameOrId || o.name === nameOrId);
|
|
122
|
+
if (obj && obj.geometry.type === 'LineGraph') {
|
|
123
|
+
const geo = obj.geometry as any;
|
|
124
|
+
|
|
125
|
+
if (!geo.datasets || geo.datasets.length === 0) {
|
|
126
|
+
geo.datasets = [{
|
|
127
|
+
id: 'default',
|
|
128
|
+
label: 'Runtime Data',
|
|
129
|
+
data: data,
|
|
130
|
+
lineColor: '#3b82f6',
|
|
131
|
+
lineWidth: 2,
|
|
132
|
+
showArea: false
|
|
133
|
+
}];
|
|
134
|
+
} else {
|
|
135
|
+
geo.datasets[0].data = data;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const state = this.objectStates.get(obj.id);
|
|
139
|
+
if (state && state.geometry && state.geometry.type === 'LineGraph') {
|
|
140
|
+
if (!state.geometry.datasets || state.geometry.datasets.length === 0) {
|
|
141
|
+
state.geometry.datasets = JSON.parse(JSON.stringify(geo.datasets));
|
|
142
|
+
} else {
|
|
143
|
+
state.geometry.datasets[0].data = [...data];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (geo.datasets[0].inputId) {
|
|
148
|
+
this.updateInput(geo.datasets[0].inputId, data);
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
this.updateInput(nameOrId, data);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
58
155
|
load(data: Artboard) {
|
|
156
|
+
if (!data) {
|
|
157
|
+
console.warn('[ExodeUIEngine] Attempted to load null artboard');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
console.log(`[ExodeUIEngine] Loading artboard: ${data.name} (${data.width}x${data.height})`);
|
|
59
161
|
this.artboard = data;
|
|
60
162
|
this.reset();
|
|
163
|
+
this.advance(0);
|
|
61
164
|
}
|
|
62
165
|
|
|
63
166
|
reset() {
|
|
64
167
|
if (!this.artboard) return;
|
|
65
168
|
this.objectStates.clear();
|
|
169
|
+
this.objectVelocities.clear();
|
|
66
170
|
this.inputs.clear();
|
|
67
171
|
this.inputNameMap.clear();
|
|
68
172
|
this.layerStates.clear();
|
|
69
|
-
|
|
70
|
-
this.physicsEngine
|
|
173
|
+
|
|
174
|
+
if (this.physicsEngine) {
|
|
175
|
+
this.physicsEngine.destroy();
|
|
176
|
+
this.physicsEngine = null;
|
|
177
|
+
}
|
|
71
178
|
|
|
72
179
|
// Initialize object states
|
|
73
|
-
this.artboard.objects
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
this.artboard.objects.forEach(obj => {
|
|
96
|
-
if (obj.physics?.enabled && obj.type === 'Shape') {
|
|
97
|
-
const state = this.objectStates.get(obj.id);
|
|
98
|
-
const w = (obj.geometry as any).width || 100;
|
|
99
|
-
const h = (obj.geometry as any).height || 100;
|
|
100
|
-
|
|
101
|
-
const options: Matter.IChamferableBodyDefinition = {
|
|
102
|
-
isStatic: obj.physics.bodyType === 'Static',
|
|
103
|
-
label: obj.id,
|
|
104
|
-
friction: obj.physics.friction,
|
|
105
|
-
restitution: obj.physics.restitution,
|
|
106
|
-
mass: obj.physics.mass,
|
|
107
|
-
frictionAir: obj.physics.frictionAir,
|
|
108
|
-
isSensor: obj.physics.isSensor,
|
|
109
|
-
angle: state.rotation * (Math.PI / 180) // Convert deg to rad
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
if (obj.physics.density) {
|
|
113
|
-
options.density = obj.physics.density;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
let body: Matter.Body | null = null;
|
|
117
|
-
|
|
118
|
-
if (obj.geometry.type === 'Rectangle' || obj.geometry.type === 'Image') {
|
|
119
|
-
body = Matter.Bodies.rectangle(state.x, state.y, w, h, options);
|
|
120
|
-
} else if (obj.geometry.type === 'Ellipse') {
|
|
121
|
-
const r = (w + h) / 4;
|
|
122
|
-
body = Matter.Bodies.circle(state.x, state.y, r, options);
|
|
123
|
-
} else {
|
|
124
|
-
body = Matter.Bodies.rectangle(state.x, state.y, w, h, options);
|
|
125
|
-
}
|
|
180
|
+
if (this.artboard.objects) {
|
|
181
|
+
this.artboard.objects?.forEach((obj: ShapeObject) => {
|
|
182
|
+
// Deep copy initial state
|
|
183
|
+
const transform = obj.transform || { x: 0, y: 0, rotation: 0, scale_x: 1, scale_y: 1 };
|
|
184
|
+
this.objectStates.set(obj.id, {
|
|
185
|
+
x: transform.x ?? 0,
|
|
186
|
+
y: transform.y ?? 0,
|
|
187
|
+
rotation: transform.rotation ?? 0,
|
|
188
|
+
scale_x: transform.scale_x ?? 1,
|
|
189
|
+
scale_y: transform.scale_y ?? 1,
|
|
190
|
+
width: (obj as any).width || (obj.geometry as any).width || 100,
|
|
191
|
+
height: (obj as any).height || (obj.geometry as any).height || 100,
|
|
192
|
+
cornerRadius: (obj as any).cornerRadius ?? (obj as any).corner_radius ?? 0,
|
|
193
|
+
opacity: obj.opacity !== undefined ? obj.opacity : 1,
|
|
194
|
+
visible: obj.visible !== undefined ? obj.visible : (obj.isVisible !== undefined ? obj.isVisible : true),
|
|
195
|
+
blendMode: obj.blendMode || 'Normal',
|
|
196
|
+
style: JSON.parse(JSON.stringify(obj.style || {})),
|
|
197
|
+
geometry: JSON.parse(JSON.stringify(obj.geometry || {})),
|
|
198
|
+
options: (obj as any).options ? JSON.parse(JSON.stringify((obj as any).options)) : {}
|
|
199
|
+
});
|
|
200
|
+
});
|
|
126
201
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
202
|
+
// Initialize Physics Engine if any object has physics enabled
|
|
203
|
+
const hasPhysics = this.artboard.objects.some(obj => obj.physics?.enabled);
|
|
204
|
+
if (hasPhysics) {
|
|
205
|
+
this.physicsEngine = new MatterPhysics();
|
|
206
|
+
|
|
207
|
+
const gravity = this.artboard.physics
|
|
208
|
+
? { x: this.artboard.physics.gravity.x, y: this.artboard.physics.gravity.y }
|
|
209
|
+
: { x: 0, y: 1 }; // Default
|
|
210
|
+
|
|
211
|
+
this.physicsEngine.init(gravity).then(() => {
|
|
212
|
+
if (!this.physicsEngine) return;
|
|
213
|
+
|
|
214
|
+
this.artboard?.objects?.forEach(obj => {
|
|
215
|
+
if (obj.physics?.enabled && obj.type === 'Shape') {
|
|
216
|
+
const state = this.objectStates.get(obj.id);
|
|
217
|
+
if (!state) return;
|
|
218
|
+
|
|
219
|
+
const w = (obj.geometry as any).width || 100;
|
|
220
|
+
const h = (obj.geometry as any).height || 100;
|
|
221
|
+
|
|
222
|
+
this.physicsEngine!.createBody({
|
|
223
|
+
id: obj.id,
|
|
224
|
+
type: obj.geometry.type as any,
|
|
225
|
+
x: state.x,
|
|
226
|
+
y: state.y,
|
|
227
|
+
width: w,
|
|
228
|
+
height: h,
|
|
229
|
+
rotation: state.rotation * (Math.PI / 180),
|
|
230
|
+
isStatic: obj.physics.bodyType === 'Static',
|
|
231
|
+
mass: obj.physics.mass,
|
|
232
|
+
friction: obj.physics.friction,
|
|
233
|
+
restitution: obj.physics.restitution,
|
|
234
|
+
frictionAir: obj.physics.frictionAir,
|
|
235
|
+
density: obj.physics.density,
|
|
236
|
+
isSensor: obj.physics.isSensor
|
|
237
|
+
});
|
|
130
238
|
}
|
|
131
|
-
}
|
|
132
|
-
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
133
242
|
}
|
|
134
243
|
|
|
135
244
|
// Initialize State Machine
|
|
@@ -137,7 +246,7 @@ export class ExodeUIEngine {
|
|
|
137
246
|
this.activeStateMachine = this.artboard.stateMachine;
|
|
138
247
|
|
|
139
248
|
// Init Inputs
|
|
140
|
-
this.activeStateMachine.inputs
|
|
249
|
+
this.activeStateMachine.inputs?.forEach((input: any) => {
|
|
141
250
|
this.inputs.set(input.id, input.value.value);
|
|
142
251
|
|
|
143
252
|
// Map Name -> IDs
|
|
@@ -147,7 +256,7 @@ export class ExodeUIEngine {
|
|
|
147
256
|
});
|
|
148
257
|
|
|
149
258
|
// Init Layers
|
|
150
|
-
this.activeStateMachine.layers
|
|
259
|
+
this.activeStateMachine.layers?.forEach((layer: any) => {
|
|
151
260
|
// Initial Entry
|
|
152
261
|
const entryState = layer.states.find((s: State) => s.id === layer.entryStateId);
|
|
153
262
|
|
|
@@ -155,17 +264,18 @@ export class ExodeUIEngine {
|
|
|
155
264
|
this.enterStates(layer.name, [entryState.id]);
|
|
156
265
|
}
|
|
157
266
|
});
|
|
267
|
+
|
|
268
|
+
this.evaluateTransitions();
|
|
158
269
|
}
|
|
159
270
|
|
|
160
271
|
if (!this.artboard.stateMachine && this.artboard.animations.length > 0) {
|
|
161
|
-
|
|
162
|
-
const onLoadAnim = this.artboard.animations.find((a: Animation) => a.name === 'onLoad');
|
|
272
|
+
const onLoadAnim = this.artboard.animations.find((a: SDKAnimation) => a.name === 'onLoad');
|
|
163
273
|
if (onLoadAnim) {
|
|
164
274
|
this.layerStates.set('intro', {
|
|
165
275
|
currentStateIds: ['onLoad'],
|
|
166
|
-
animation: onLoadAnim,
|
|
276
|
+
animations: [{ animation: onLoadAnim, state: { loop: true } as any }],
|
|
167
277
|
time: 0,
|
|
168
|
-
|
|
278
|
+
duration: onLoadAnim.duration
|
|
169
279
|
});
|
|
170
280
|
}
|
|
171
281
|
}
|
|
@@ -174,30 +284,26 @@ export class ExodeUIEngine {
|
|
|
174
284
|
private enterStates(layerName: string, stateIds: string[]) {
|
|
175
285
|
if (stateIds.length === 0) return;
|
|
176
286
|
|
|
177
|
-
|
|
178
|
-
let
|
|
287
|
+
const activeAnims: { animation: SDKAnimation; state: State | null }[] = [];
|
|
288
|
+
let maxDuration = 0;
|
|
179
289
|
|
|
180
290
|
if (this.artboard && this.activeStateMachine) {
|
|
181
291
|
const layer = this.activeStateMachine.layers.find(l => l.name === layerName);
|
|
182
292
|
if (layer) {
|
|
183
|
-
|
|
184
|
-
for (let i = stateIds.length - 1; i >= 0; i--) {
|
|
185
|
-
const sId = stateIds[i];
|
|
293
|
+
for (const sId of stateIds) {
|
|
186
294
|
const state = layer.states.find(s => s.id === sId);
|
|
187
|
-
|
|
188
295
|
if (state) {
|
|
189
|
-
|
|
296
|
+
let anim: SDKAnimation | null = null;
|
|
190
297
|
if (state.animationId) {
|
|
191
298
|
anim = this.artboard.animations.find(a => a.id === state.animationId) || null;
|
|
192
299
|
}
|
|
193
|
-
// Priority 2: Name Match (Legacy)
|
|
194
300
|
if (!anim) {
|
|
195
|
-
|
|
301
|
+
anim = this.artboard.animations.find((a: SDKAnimation) => a.name === state.name || a.id === state.name) || null;
|
|
196
302
|
}
|
|
197
303
|
|
|
198
304
|
if (anim) {
|
|
199
|
-
|
|
200
|
-
|
|
305
|
+
activeAnims.push({ animation: anim, state });
|
|
306
|
+
maxDuration = Math.max(maxDuration, anim.duration);
|
|
201
307
|
}
|
|
202
308
|
}
|
|
203
309
|
}
|
|
@@ -206,9 +312,9 @@ export class ExodeUIEngine {
|
|
|
206
312
|
|
|
207
313
|
this.layerStates.set(layerName, {
|
|
208
314
|
currentStateIds: stateIds,
|
|
209
|
-
|
|
315
|
+
animations: activeAnims,
|
|
210
316
|
time: 0,
|
|
211
|
-
|
|
317
|
+
duration: maxDuration
|
|
212
318
|
});
|
|
213
319
|
}
|
|
214
320
|
|
|
@@ -229,11 +335,17 @@ export class ExodeUIEngine {
|
|
|
229
335
|
this.updateInput(nameOrId, value);
|
|
230
336
|
}
|
|
231
337
|
|
|
338
|
+
setInputNumberArray(nameOrId: string, value: number[]) {
|
|
339
|
+
this.updateInput(nameOrId, value);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
setInputStringArray(nameOrId: string, value: string[]) {
|
|
343
|
+
this.updateInput(nameOrId, value);
|
|
344
|
+
}
|
|
345
|
+
|
|
232
346
|
private updateInput(nameOrId: string, value: any) {
|
|
233
|
-
console.log(`[Engine] updateInput: ${nameOrId} -> ${value}`);
|
|
234
347
|
|
|
235
348
|
let inputType: string | undefined;
|
|
236
|
-
// Resolve Type
|
|
237
349
|
if (this.inputs.has(nameOrId)) {
|
|
238
350
|
const input = this.activeStateMachine?.inputs.find(i => i.id === nameOrId);
|
|
239
351
|
if (input && typeof input.value === 'object') {
|
|
@@ -255,42 +367,36 @@ export class ExodeUIEngine {
|
|
|
255
367
|
else if (value === 0) finalValue = false;
|
|
256
368
|
}
|
|
257
369
|
|
|
258
|
-
// 1. Try treating as ID
|
|
259
370
|
if (this.inputs.has(nameOrId)) {
|
|
260
371
|
this.setInternalInput(nameOrId, finalValue);
|
|
261
372
|
}
|
|
262
373
|
|
|
263
|
-
// 2. Try treating as Name (Broadcast)
|
|
264
374
|
const ids = this.inputNameMap.get(nameOrId);
|
|
265
375
|
if (ids) {
|
|
266
|
-
ids
|
|
376
|
+
ids?.forEach(id => this.setInternalInput(id, finalValue));
|
|
267
377
|
}
|
|
268
378
|
|
|
269
|
-
// TRIGGER HARD RESET logic
|
|
270
379
|
if (inputType === 'Trigger' && finalValue === true) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
380
|
+
this.justFiredTriggers.add(nameOrId);
|
|
381
|
+
if (ids) {
|
|
382
|
+
ids?.forEach(id => this.justFiredTriggers.add(id));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Sync with compatible components (ListView/Dropdown)
|
|
387
|
+
if (Array.isArray(finalValue)) {
|
|
388
|
+
this.artboard?.objects?.forEach(obj => {
|
|
389
|
+
if (obj.inputId === nameOrId || obj.name === nameOrId) {
|
|
390
|
+
const state = this.objectStates.get(obj.id);
|
|
391
|
+
if (state && state.options) {
|
|
392
|
+
if (obj.type === 'ListView' || (obj as any).variant === 'listview') {
|
|
393
|
+
state.options.items = finalValue;
|
|
394
|
+
} else if (obj.type === 'Dropdown' || (obj as any).variant === 'dropdown') {
|
|
395
|
+
state.options.optionsList = finalValue;
|
|
289
396
|
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
292
399
|
});
|
|
293
|
-
return;
|
|
294
400
|
}
|
|
295
401
|
|
|
296
402
|
this.evaluateTransitions();
|
|
@@ -299,7 +405,7 @@ export class ExodeUIEngine {
|
|
|
299
405
|
private evaluateTransitions() {
|
|
300
406
|
if (!this.activeStateMachine) return;
|
|
301
407
|
|
|
302
|
-
this.activeStateMachine.layers
|
|
408
|
+
this.activeStateMachine.layers?.forEach((layer) => {
|
|
303
409
|
const layerState = this.layerStates.get(layer.name);
|
|
304
410
|
if (!layerState || layerState.currentStateIds.length === 0) return;
|
|
305
411
|
|
|
@@ -318,19 +424,12 @@ export class ExodeUIEngine {
|
|
|
318
424
|
transitioned = true;
|
|
319
425
|
hasTransition = true;
|
|
320
426
|
|
|
321
|
-
// Reset Trigger/Number inputs used in this transition
|
|
322
427
|
if (trans.conditions) {
|
|
323
|
-
trans.conditions
|
|
428
|
+
trans.conditions?.forEach((cond: any) => {
|
|
324
429
|
const input = this.activeStateMachine?.inputs.find(i => i.id === cond.inputId);
|
|
325
430
|
if (input && typeof input.value === 'object') {
|
|
326
|
-
if (input.value.type === '
|
|
327
|
-
|
|
328
|
-
if (this.inputNameMap.has(input.name)) {
|
|
329
|
-
const ids = this.inputNameMap.get(input.name);
|
|
330
|
-
ids?.forEach(id => this.setInternalInput(id, false));
|
|
331
|
-
}
|
|
332
|
-
} else if (input.value.type === 'Number') {
|
|
333
|
-
this.setInternalInput(cond.inputId, 0);
|
|
431
|
+
if (input.value.type === 'Number') {
|
|
432
|
+
this.setInternalInput(cond.inputId, 0);
|
|
334
433
|
if (this.inputNameMap.has(input.name)) {
|
|
335
434
|
const ids = this.inputNameMap.get(input.name);
|
|
336
435
|
ids?.forEach(id => this.setInternalInput(id, 0));
|
|
@@ -347,11 +446,63 @@ export class ExodeUIEngine {
|
|
|
347
446
|
}
|
|
348
447
|
}
|
|
349
448
|
|
|
449
|
+
if (!hasTransition) {
|
|
450
|
+
const globalTransition = this.findGlobalTransition(layer, layerState.currentStateIds);
|
|
451
|
+
if (globalTransition) {
|
|
452
|
+
nextStateIds.length = 0;
|
|
453
|
+
nextStateIds.push(globalTransition.targetStateId);
|
|
454
|
+
hasTransition = true;
|
|
455
|
+
|
|
456
|
+
if (globalTransition.conditions) {
|
|
457
|
+
globalTransition.conditions?.forEach((cond: any) => {
|
|
458
|
+
const input = this.activeStateMachine?.inputs.find(i => i.id === cond.inputId);
|
|
459
|
+
if (input && typeof input.value === 'object') {
|
|
460
|
+
if (input.value.type === 'Number') {
|
|
461
|
+
this.setInternalInput(cond.inputId, 0);
|
|
462
|
+
if (this.inputNameMap.has(input.name)) {
|
|
463
|
+
const ids = this.inputNameMap.get(input.name);
|
|
464
|
+
ids?.forEach(id => this.setInternalInput(id, 0));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
350
473
|
if (hasTransition) {
|
|
351
474
|
const uniqueIds = Array.from(new Set(nextStateIds));
|
|
352
475
|
this.enterStates(layer.name, uniqueIds);
|
|
353
476
|
}
|
|
354
477
|
});
|
|
478
|
+
|
|
479
|
+
if (this.justFiredTriggers.size > 0) {
|
|
480
|
+
this.justFiredTriggers?.forEach(id => {
|
|
481
|
+
this.setInternalInput(id, false);
|
|
482
|
+
const input = this.activeStateMachine?.inputs.find(i => i.id === id);
|
|
483
|
+
if (input && this.inputNameMap.has(input.name)) {
|
|
484
|
+
this.inputNameMap.get(input.name)?.forEach(nid => this.setInternalInput(nid, false));
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
this.justFiredTriggers.clear();
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private findGlobalTransition(layer: any, currentStateIds: string[]): any | null {
|
|
492
|
+
for (const state of layer.states) {
|
|
493
|
+
const isActive = currentStateIds.includes(state.id);
|
|
494
|
+
if (isActive && this.justFiredTriggers.size === 0) continue;
|
|
495
|
+
|
|
496
|
+
for (const trans of state.transitions) {
|
|
497
|
+
const targetsActive = currentStateIds.includes(trans.targetStateId);
|
|
498
|
+
if (targetsActive && this.justFiredTriggers.size === 0) continue;
|
|
499
|
+
|
|
500
|
+
if (this.checkConditions(trans.conditions)) {
|
|
501
|
+
return trans;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return null;
|
|
355
506
|
}
|
|
356
507
|
|
|
357
508
|
private checkConditions(conditions: any[]): boolean {
|
|
@@ -362,7 +513,7 @@ export class ExodeUIEngine {
|
|
|
362
513
|
const op = cond.op;
|
|
363
514
|
const targetValue = cond.value;
|
|
364
515
|
|
|
365
|
-
const inputValue = this.
|
|
516
|
+
const inputValue = this.evaluateLogicTree(inputId);
|
|
366
517
|
|
|
367
518
|
if (inputValue === undefined) return false;
|
|
368
519
|
|
|
@@ -382,10 +533,57 @@ export class ExodeUIEngine {
|
|
|
382
533
|
});
|
|
383
534
|
}
|
|
384
535
|
|
|
385
|
-
|
|
536
|
+
private evaluateLogicTree(sourceId: string, sourceHandleId?: string, visited: Set<string> = new Set()): any {
|
|
537
|
+
if (visited.has(sourceId)) return false;
|
|
538
|
+
visited.add(sourceId);
|
|
539
|
+
|
|
540
|
+
const logicNode = this.activeStateMachine?.logicNodes?.find((n: LogicNode) => n.id === sourceId);
|
|
541
|
+
if (logicNode) {
|
|
542
|
+
const getInputValue = (portId: string, defaultValue: any) => {
|
|
543
|
+
const inputPort = logicNode.inputs.find((i: any) => i.id === portId);
|
|
544
|
+
if (!inputPort) return defaultValue;
|
|
545
|
+
|
|
546
|
+
if (inputPort.sourceId) {
|
|
547
|
+
return this.evaluateLogicTree(inputPort.sourceId, inputPort.sourceHandleId, visited);
|
|
548
|
+
}
|
|
549
|
+
return inputPort.value !== undefined ? inputPort.value : defaultValue;
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
switch (logicNode.op) {
|
|
553
|
+
case LogicOp.AND: return getInputValue('a', false) && getInputValue('b', false);
|
|
554
|
+
case LogicOp.OR: return getInputValue('a', false) || getInputValue('b', false);
|
|
555
|
+
case LogicOp.NOT: return !getInputValue('in', false);
|
|
556
|
+
case LogicOp.XOR: return !!getInputValue('a', false) !== !!getInputValue('b', false);
|
|
557
|
+
default: return 0;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
let value = this.inputs.get(sourceId);
|
|
562
|
+
if (this.justFiredTriggers.has(sourceId)) {
|
|
563
|
+
value = true;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (sourceHandleId && value !== undefined) {
|
|
567
|
+
const input = this.activeStateMachine?.inputs.find(i => i.id === sourceId);
|
|
568
|
+
if (input && input.value.type === 'Number' && typeof value === 'number') {
|
|
569
|
+
const threshold = (input.value as any).defaultValue ?.(input.value as any).value;
|
|
570
|
+
if (sourceHandleId === 'source-greater') return value > threshold;
|
|
571
|
+
if (sourceHandleId === 'source-less') return value < threshold;
|
|
572
|
+
if (sourceHandleId === 'source-equal') return value === threshold;
|
|
573
|
+
}
|
|
574
|
+
if (sourceHandleId === 'source-true') return value === true;
|
|
575
|
+
if (sourceHandleId === 'source-false') return value === false;
|
|
576
|
+
if (sourceHandleId === 'source-fire') {
|
|
577
|
+
return this.justFiredTriggers.has(sourceId);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return value !== undefined ? value : 0;
|
|
582
|
+
}
|
|
583
|
+
|
|
386
584
|
private activeTriggers: Map<string, {
|
|
387
585
|
triggerId: string;
|
|
388
|
-
animation:
|
|
586
|
+
animation: SDKAnimation;
|
|
389
587
|
time: number;
|
|
390
588
|
phase: 'entry' | 'hold' | 'exit';
|
|
391
589
|
elapsedHold: number;
|
|
@@ -396,15 +594,22 @@ export class ExodeUIEngine {
|
|
|
396
594
|
|
|
397
595
|
// 1. Step Physics
|
|
398
596
|
if (this.physicsEngine) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
state
|
|
406
|
-
|
|
407
|
-
state
|
|
597
|
+
this.physicsEngine.step(dt);
|
|
598
|
+
|
|
599
|
+
this.artboard.objects?.forEach(obj => {
|
|
600
|
+
if (obj.physics?.enabled) {
|
|
601
|
+
const pos = this.physicsEngine!.getPosition(obj.id);
|
|
602
|
+
const rot = this.physicsEngine!.getRotation(obj.id);
|
|
603
|
+
const state = this.objectStates.get(obj.id);
|
|
604
|
+
|
|
605
|
+
if (state && pos !== null) {
|
|
606
|
+
state.x = pos.x;
|
|
607
|
+
state.y = pos.y;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (state && rot !== null) {
|
|
611
|
+
state.rotation = rot * (180 / Math.PI);
|
|
612
|
+
}
|
|
408
613
|
}
|
|
409
614
|
});
|
|
410
615
|
}
|
|
@@ -413,31 +618,35 @@ export class ExodeUIEngine {
|
|
|
413
618
|
if (this.activeStateMachine) {
|
|
414
619
|
this.evaluateTransitions();
|
|
415
620
|
|
|
416
|
-
this.layerStates
|
|
417
|
-
if (
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const duration = state.animation.duration;
|
|
421
|
-
const shouldLoop = state.currentState?.loop !== false;
|
|
621
|
+
this.layerStates?.forEach(layerState => {
|
|
622
|
+
if (layerState.animations.length > 0) {
|
|
623
|
+
layerState.time += dt;
|
|
422
624
|
|
|
423
|
-
if (
|
|
424
|
-
|
|
425
|
-
|
|
625
|
+
if (layerState.time > layerState.duration) {
|
|
626
|
+
const canLoop = layerState.animations.some(a => a.state?.loop === true);
|
|
627
|
+
if (canLoop) {
|
|
628
|
+
layerState.time %= layerState.duration;
|
|
426
629
|
} else {
|
|
427
|
-
|
|
630
|
+
layerState.time = layerState.duration;
|
|
428
631
|
}
|
|
429
632
|
}
|
|
430
633
|
|
|
431
|
-
|
|
634
|
+
layerState.animations?.forEach(animObj => {
|
|
635
|
+
this.applyAnimation(animObj.animation, layerState.time);
|
|
636
|
+
});
|
|
432
637
|
}
|
|
433
638
|
});
|
|
434
639
|
}
|
|
435
640
|
|
|
641
|
+
// Solve Constraints
|
|
642
|
+
this.solveConstraints(dt);
|
|
643
|
+
|
|
436
644
|
// Advance Active Triggers
|
|
437
|
-
this.activeTriggers
|
|
645
|
+
this.activeTriggers?.forEach((state, objectId) => {
|
|
438
646
|
if (state.phase === 'entry') {
|
|
439
647
|
state.time += dt;
|
|
440
648
|
if (state.time >= state.animation.duration) {
|
|
649
|
+
const trigger = this.artboard?.objects.find(o => o.id === objectId)?.triggers?.find(t => t.id === state.triggerId);
|
|
441
650
|
state.phase = 'hold';
|
|
442
651
|
state.elapsedHold = 0;
|
|
443
652
|
state.time = state.animation.duration;
|
|
@@ -471,6 +680,97 @@ export class ExodeUIEngine {
|
|
|
471
680
|
}
|
|
472
681
|
}
|
|
473
682
|
});
|
|
683
|
+
|
|
684
|
+
// 4. Apply Bindings
|
|
685
|
+
this.applyBindings();
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
private applyBindings() {
|
|
689
|
+
if (!this.artboard) return;
|
|
690
|
+
this.artboard.objects?.forEach(obj => {
|
|
691
|
+
const bindings = (obj as any).bindings || [];
|
|
692
|
+
if (bindings.length > 0) {
|
|
693
|
+
bindings?.forEach((binding: any) => {
|
|
694
|
+
const val = this.evaluateLogicTree(binding.inputId);
|
|
695
|
+
const state = this.objectStates.get(obj.id);
|
|
696
|
+
if (state && val !== undefined) {
|
|
697
|
+
state[binding.property] = val;
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
private solveConstraints(dt: number) {
|
|
705
|
+
if (!this.artboard || !this.artboard.objects) return;
|
|
706
|
+
|
|
707
|
+
this.artboard.objects?.forEach(obj => {
|
|
708
|
+
if (obj.constraints && obj.constraints.length > 0) {
|
|
709
|
+
obj.constraints?.forEach(constraint => {
|
|
710
|
+
this.applyConstraint(obj.id, constraint, dt);
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
private applyConstraint(objectId: string, constraint: Constraint, dt: number) {
|
|
717
|
+
const state = this.objectStates.get(objectId);
|
|
718
|
+
const targetState = this.objectStates.get(constraint.targetId);
|
|
719
|
+
|
|
720
|
+
if (!state || !targetState) return;
|
|
721
|
+
|
|
722
|
+
const strength = constraint.strength ?? 1;
|
|
723
|
+
|
|
724
|
+
switch (constraint.type) {
|
|
725
|
+
case 'Translation':
|
|
726
|
+
if (constraint.useSpring) {
|
|
727
|
+
this.applySpring(objectId, 'x', targetState.x, constraint, dt);
|
|
728
|
+
this.applySpring(objectId, 'y', targetState.y, constraint, dt);
|
|
729
|
+
} else {
|
|
730
|
+
if (constraint.copyX !== false) state.x += (targetState.x - state.x) * strength;
|
|
731
|
+
if (constraint.copyY !== false) state.y += (targetState.y - state.y) * strength;
|
|
732
|
+
}
|
|
733
|
+
break;
|
|
734
|
+
case 'Rotation':
|
|
735
|
+
if (constraint.useSpring) {
|
|
736
|
+
this.applySpring(objectId, 'rotation', targetState.rotation, constraint, dt);
|
|
737
|
+
} else {
|
|
738
|
+
state.rotation += (targetState.rotation - state.rotation) * strength;
|
|
739
|
+
}
|
|
740
|
+
break;
|
|
741
|
+
case 'Scale':
|
|
742
|
+
if (constraint.useSpring) {
|
|
743
|
+
this.applySpring(objectId, 'scale_x', targetState.scale_x, constraint, dt);
|
|
744
|
+
this.applySpring(objectId, 'scale_y', targetState.scale_y, constraint, dt);
|
|
745
|
+
} else {
|
|
746
|
+
state.scale_x += (targetState.scale_x - state.scale_x) * strength;
|
|
747
|
+
state.scale_y += (targetState.scale_y - state.scale_y) * strength;
|
|
748
|
+
}
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private applySpring(objectId: string, property: string, targetValue: number, constraint: Constraint, dt: number) {
|
|
754
|
+
const state = this.objectStates.get(objectId);
|
|
755
|
+
if (!state) return;
|
|
756
|
+
|
|
757
|
+
const current = state[property];
|
|
758
|
+
const velocityKey = `${objectId}_${property}`;
|
|
759
|
+
let v = this.objectVelocities.get(velocityKey) || { vx: 0, vy: 0 };
|
|
760
|
+
|
|
761
|
+
const k = constraint.stiffness ?? 100;
|
|
762
|
+
const d = constraint.damping ?? 10;
|
|
763
|
+
const m = constraint.mass ?? 1;
|
|
764
|
+
|
|
765
|
+
// F = -k * (x - target) - d * v
|
|
766
|
+
const x = current - targetValue;
|
|
767
|
+
const f = -k * x - d * v.vx;
|
|
768
|
+
const a = f / m;
|
|
769
|
+
|
|
770
|
+
v.vx += a * dt;
|
|
771
|
+
state[property] += v.vx * dt;
|
|
772
|
+
|
|
773
|
+
this.objectVelocities.set(velocityKey, v);
|
|
474
774
|
}
|
|
475
775
|
|
|
476
776
|
handlePointerInput(type: string, canvasX: number, canvasY: number, canvasWidth: number, canvasHeight: number) {
|
|
@@ -492,12 +792,15 @@ export class ExodeUIEngine {
|
|
|
492
792
|
private handlePointerEvent(type: string, x: number, y: number) {
|
|
493
793
|
if (!this.artboard) return;
|
|
494
794
|
|
|
795
|
+
let hitId: string | null = null;
|
|
495
796
|
for (let i = this.artboard.objects.length - 1; i >= 0; i--) {
|
|
496
797
|
const obj = this.artboard.objects[i];
|
|
497
798
|
const isHit = this.hitTest(obj, x, y);
|
|
498
799
|
|
|
499
800
|
if (isHit) {
|
|
500
|
-
|
|
801
|
+
hitId = obj.id;
|
|
802
|
+
|
|
803
|
+
// Handle interactions
|
|
501
804
|
const interactions = (obj as any).interactions || [];
|
|
502
805
|
const matchingInteraction = interactions.find((int: any) => int.event === type || (int.event === 'onClick' && type === 'click'));
|
|
503
806
|
|
|
@@ -509,7 +812,23 @@ export class ExodeUIEngine {
|
|
|
509
812
|
}
|
|
510
813
|
}
|
|
511
814
|
|
|
512
|
-
//
|
|
815
|
+
// Handle Component Listeners
|
|
816
|
+
if (type === 'click' || type === 'PointerDown') {
|
|
817
|
+
const state = this.objectStates.get(obj.id);
|
|
818
|
+
const options = state?.options || {};
|
|
819
|
+
|
|
820
|
+
if (obj.type === 'Toggle' || (obj as any).variant === 'toggle') {
|
|
821
|
+
const newValue = !options.checked;
|
|
822
|
+
this.updateObjectOptions(obj.id, { checked: newValue });
|
|
823
|
+
if (this.onToggle) this.onToggle(obj.name, newValue);
|
|
824
|
+
if (obj.inputId) this.updateInput(obj.inputId, newValue);
|
|
825
|
+
} else if ((obj as any).type === 'TextInput' || (obj as any).variant === 'textinput') {
|
|
826
|
+
this.focusedId = obj.id;
|
|
827
|
+
if (this.onInputFocus) this.onInputFocus(obj.name);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Handle triggers (Animations)
|
|
513
832
|
const triggers = obj.triggers || [];
|
|
514
833
|
const matchingTrigger = triggers.find(t => t.eventType === type || (t.eventType === 'onClick' && type === 'click'));
|
|
515
834
|
|
|
@@ -530,28 +849,39 @@ export class ExodeUIEngine {
|
|
|
530
849
|
return;
|
|
531
850
|
}
|
|
532
851
|
}
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (!hitId && (type === 'click' || type === 'PointerDown')) {
|
|
857
|
+
if (this.focusedId) {
|
|
858
|
+
const focusedObj = this.artboard.objects.find(o => o.id === this.focusedId);
|
|
859
|
+
if (focusedObj && this.onInputBlur) this.onInputBlur(focusedObj.name);
|
|
860
|
+
this.focusedId = null;
|
|
533
861
|
}
|
|
534
862
|
}
|
|
535
863
|
}
|
|
536
864
|
|
|
537
865
|
private hitTest(obj: ShapeObject, x: number, y: number): boolean {
|
|
538
866
|
const state = this.objectStates.get(obj.id);
|
|
539
|
-
if (!state) return false;
|
|
867
|
+
if (!state || state.visible === false) return false;
|
|
540
868
|
|
|
541
|
-
const w = (obj.geometry as any).width || 100;
|
|
542
|
-
const h = (obj.geometry as any).height || 100;
|
|
869
|
+
const w = state.width || (obj.geometry as any).width || 100;
|
|
870
|
+
const h = state.height || (obj.geometry as any).height || 100;
|
|
543
871
|
|
|
544
872
|
const dx = x - state.x;
|
|
545
873
|
const dy = y - state.y;
|
|
546
874
|
|
|
875
|
+
// Basic AABB check
|
|
547
876
|
return Math.abs(dx) <= w / 2 && Math.abs(dy) <= h / 2;
|
|
548
877
|
}
|
|
549
878
|
|
|
550
|
-
private applyAnimation(anim:
|
|
551
|
-
anim.tracks
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
879
|
+
private applyAnimation(anim: SDKAnimation, time: number) {
|
|
880
|
+
anim.tracks?.forEach((track: any) => {
|
|
881
|
+
// Skip physics controlled objects if they are dynamic
|
|
882
|
+
const isPhysicsControlled = this.physicsEngine &&
|
|
883
|
+
this.physicsEngine.isDynamic(track.object_id);
|
|
884
|
+
|
|
555
885
|
if (isPhysicsControlled && (['x', 'y', 'rotation'].includes(track.property) || track.property.startsWith('transform.'))) {
|
|
556
886
|
return;
|
|
557
887
|
}
|
|
@@ -574,14 +904,13 @@ export class ExodeUIEngine {
|
|
|
574
904
|
}
|
|
575
905
|
|
|
576
906
|
// Sync State -> Physics Body (for Kinematic/Static animations)
|
|
577
|
-
|
|
578
|
-
if (body && body.isStatic) {
|
|
907
|
+
if (this.physicsEngine && !this.physicsEngine.isDynamic(track.object_id)) {
|
|
579
908
|
if (track.property === 'x' || track.property === 'transform.x') {
|
|
580
|
-
|
|
909
|
+
this.physicsEngine.setPosition(track.object_id, value, objState.y);
|
|
581
910
|
} else if (track.property === 'y' || track.property === 'transform.y') {
|
|
582
|
-
|
|
911
|
+
this.physicsEngine.setPosition(track.object_id, objState.x, value);
|
|
583
912
|
} else if (track.property === 'rotation' || track.property === 'transform.rotation') {
|
|
584
|
-
|
|
913
|
+
this.physicsEngine.setRotation(track.object_id, value * (Math.PI / 180));
|
|
585
914
|
}
|
|
586
915
|
}
|
|
587
916
|
});
|
|
@@ -605,6 +934,7 @@ export class ExodeUIEngine {
|
|
|
605
934
|
}
|
|
606
935
|
|
|
607
936
|
const duration = end.time - bg.time;
|
|
937
|
+
if (duration <= 0) return end.value;
|
|
608
938
|
const t = (time - bg.time) / duration;
|
|
609
939
|
|
|
610
940
|
if (typeof bg.value === 'string' && bg.value.startsWith('#')) {
|
|
@@ -620,6 +950,13 @@ export class ExodeUIEngine {
|
|
|
620
950
|
private interpolateColor(c1: string, c2: string, t: number): string {
|
|
621
951
|
const parse = (c: string) => {
|
|
622
952
|
const hex = c.replace('#', '');
|
|
953
|
+
if (hex.length === 3) {
|
|
954
|
+
return {
|
|
955
|
+
r: parseInt(hex[0] + hex[0], 16),
|
|
956
|
+
g: parseInt(hex[1] + hex[1], 16),
|
|
957
|
+
b: parseInt(hex[2] + hex[2], 16)
|
|
958
|
+
};
|
|
959
|
+
}
|
|
623
960
|
return {
|
|
624
961
|
r: parseInt(hex.substring(0, 2), 16),
|
|
625
962
|
g: parseInt(hex.substring(2, 4), 16),
|
|
@@ -647,7 +984,13 @@ export class ExodeUIEngine {
|
|
|
647
984
|
return;
|
|
648
985
|
}
|
|
649
986
|
|
|
650
|
-
|
|
987
|
+
try {
|
|
988
|
+
const bg = this.artboard.backgroundColor || '#000000';
|
|
989
|
+
canvas.clear(Skia.Color(bg));
|
|
990
|
+
} catch (e) {
|
|
991
|
+
console.error('[ExodeUIEngine] Rendering failed (clear):', e);
|
|
992
|
+
canvas.clear(Skia.Color('#000000'));
|
|
993
|
+
}
|
|
651
994
|
|
|
652
995
|
const abWidth = this.artboard.width;
|
|
653
996
|
const abHeight = this.artboard.height;
|
|
@@ -672,7 +1015,7 @@ export class ExodeUIEngine {
|
|
|
672
1015
|
clipPath.addRect({ x: -abWidth / 2, y: -abHeight / 2, width: abWidth, height: abHeight });
|
|
673
1016
|
canvas.clipPath(clipPath, 1, true);
|
|
674
1017
|
|
|
675
|
-
this.artboard.objects
|
|
1018
|
+
this.artboard.objects?.forEach((obj: ShapeObject) => {
|
|
676
1019
|
this.renderObject(canvas, obj);
|
|
677
1020
|
});
|
|
678
1021
|
|
|
@@ -760,35 +1103,39 @@ export class ExodeUIEngine {
|
|
|
760
1103
|
|
|
761
1104
|
private renderObject(canvas: SkCanvas, obj: ShapeObject) {
|
|
762
1105
|
const state = this.objectStates.get(obj.id);
|
|
763
|
-
if (!state) return;
|
|
1106
|
+
if (!state || state.visible === false) return;
|
|
764
1107
|
|
|
765
1108
|
const geometry = state.geometry || obj.geometry;
|
|
766
1109
|
|
|
767
|
-
const w = (geometry as any).width || 0;
|
|
768
|
-
const h = (geometry as any).height || 0;
|
|
1110
|
+
const w = state.width || (geometry as any).width || 0;
|
|
1111
|
+
const h = state.height || (geometry as any).height || 0;
|
|
769
1112
|
|
|
770
|
-
const cx = state.x;
|
|
771
|
-
const cy = state.y;
|
|
1113
|
+
const cx = state.x || 0;
|
|
1114
|
+
const cy = state.y || 0;
|
|
1115
|
+
const rotation = state.rotation || 0;
|
|
1116
|
+
const scaleX = state.scale_x === undefined ? 1 : state.scale_x;
|
|
1117
|
+
const scaleY = state.scale_y === undefined ? 1 : state.scale_y;
|
|
772
1118
|
|
|
773
1119
|
canvas.save();
|
|
774
1120
|
canvas.translate(cx, cy);
|
|
775
|
-
canvas.rotate(
|
|
776
|
-
canvas.scale(
|
|
1121
|
+
canvas.rotate(rotation, 0, 0);
|
|
1122
|
+
canvas.scale(scaleX, scaleY);
|
|
777
1123
|
|
|
778
1124
|
const style = state.style || obj.style;
|
|
779
1125
|
|
|
780
1126
|
if (geometry.type === 'Text') {
|
|
781
|
-
//
|
|
1127
|
+
// Simple text rendering placeholder
|
|
782
1128
|
} else if (geometry.type === 'Image') {
|
|
783
|
-
// Image rendering
|
|
1129
|
+
// Image rendering placeholder
|
|
784
1130
|
} else {
|
|
785
1131
|
// Shapes
|
|
786
1132
|
const path = Skia.Path.Make();
|
|
787
1133
|
|
|
788
1134
|
if (geometry.type === 'Rectangle') {
|
|
789
1135
|
const rect = { x: -w/2, y: -h/2, width: w, height: h };
|
|
790
|
-
if (geometry.corner_radius) {
|
|
791
|
-
|
|
1136
|
+
if (state.cornerRadius || geometry.corner_radius) {
|
|
1137
|
+
const cr = state.cornerRadius || geometry.corner_radius;
|
|
1138
|
+
path.addRRect(Skia.RRectXY(rect, cr, cr));
|
|
792
1139
|
} else {
|
|
793
1140
|
path.addRect(rect);
|
|
794
1141
|
}
|
|
@@ -800,8 +1147,8 @@ export class ExodeUIEngine {
|
|
|
800
1147
|
path.lineTo(-w/2, h/2);
|
|
801
1148
|
path.close();
|
|
802
1149
|
} else if (geometry.type === 'Star') {
|
|
803
|
-
const ir = geometry.inner_radius;
|
|
804
|
-
const or = geometry.outer_radius;
|
|
1150
|
+
const ir = geometry.inner_radius || 20;
|
|
1151
|
+
const or = geometry.outer_radius || 50;
|
|
805
1152
|
const sp = geometry.points || 5;
|
|
806
1153
|
for (let i = 0; i < sp * 2; i++) {
|
|
807
1154
|
const a = (i * Math.PI / sp) - (Math.PI / 2);
|
|
@@ -816,7 +1163,11 @@ export class ExodeUIEngine {
|
|
|
816
1163
|
|
|
817
1164
|
if (style.fill) {
|
|
818
1165
|
const paint = Skia.Paint();
|
|
819
|
-
|
|
1166
|
+
try {
|
|
1167
|
+
paint.setColor(Skia.Color(style.fill.color || '#000000'));
|
|
1168
|
+
} catch (e) {
|
|
1169
|
+
paint.setColor(Skia.Color('#FF00FF')); // Debug pink
|
|
1170
|
+
}
|
|
820
1171
|
paint.setAlphaf((state.opacity ?? 1) * (style.fill.opacity ?? 1));
|
|
821
1172
|
paint.setStyle(PaintStyle.Fill);
|
|
822
1173
|
|