@nexart/ui-renderer 0.7.0 → 0.8.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/README.md +138 -319
- package/dist/capabilities.d.ts +5 -7
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +6 -8
- package/dist/compiler.d.ts +3 -3
- package/dist/compiler.js +4 -4
- package/dist/index.d.ts +33 -30
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -30
- package/dist/preview/canvas-scaler.d.ts +70 -0
- package/dist/preview/canvas-scaler.d.ts.map +1 -0
- package/dist/preview/canvas-scaler.js +112 -0
- package/dist/preview/code-renderer.d.ts +11 -28
- package/dist/preview/code-renderer.d.ts.map +1 -1
- package/dist/preview/code-renderer.js +118 -660
- package/dist/preview/frame-budget.d.ts +43 -0
- package/dist/preview/frame-budget.d.ts.map +1 -0
- package/dist/preview/frame-budget.js +78 -0
- package/dist/preview/preview-engine.d.ts +42 -0
- package/dist/preview/preview-engine.d.ts.map +1 -0
- package/dist/preview/preview-engine.js +204 -0
- package/dist/preview/preview-runtime.d.ts +28 -0
- package/dist/preview/preview-runtime.d.ts.map +1 -0
- package/dist/preview/preview-runtime.js +512 -0
- package/dist/preview/preview-types.d.ts +116 -0
- package/dist/preview/preview-types.d.ts.map +1 -0
- package/dist/preview/preview-types.js +36 -0
- package/dist/preview/renderer.d.ts +3 -3
- package/dist/preview/renderer.js +3 -3
- package/dist/preview/unified-renderer.d.ts.map +1 -1
- package/dist/preview/unified-renderer.js +48 -22
- package/dist/system.d.ts +2 -2
- package/dist/system.d.ts.map +1 -1
- package/dist/system.js +8 -10
- package/dist/types.d.ts +9 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -5
- package/package.json +2 -2
|
@@ -1,692 +1,132 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @nexart/ui-renderer v0.
|
|
2
|
+
* @nexart/ui-renderer v0.8.0 - Code Mode Renderer
|
|
3
3
|
*
|
|
4
4
|
* ╔══════════════════════════════════════════════════════════════════════════╗
|
|
5
|
-
* ║ PREVIEW RENDERER —
|
|
5
|
+
* ║ PREVIEW RENDERER — LIGHTWEIGHT, NON-AUTHORITATIVE ║
|
|
6
6
|
* ║ ║
|
|
7
|
-
* ║ This
|
|
7
|
+
* ║ This renderer is a preview-only runtime. ║
|
|
8
|
+
* ║ It does not guarantee determinism or protocol compliance. ║
|
|
8
9
|
* ║ ║
|
|
9
|
-
* ║
|
|
10
|
+
* ║ Performance Limits (MANDATORY): ║
|
|
11
|
+
* ║ - Max frames: 30 ║
|
|
12
|
+
* ║ - Max total time: 500ms ║
|
|
13
|
+
* ║ - Max canvas dimension: 900px ║
|
|
14
|
+
* ║ - Frame stride: render every 3rd frame ║
|
|
15
|
+
* ║ ║
|
|
16
|
+
* ║ For minting, export, or validation: use @nexart/codemode-sdk ║
|
|
10
17
|
* ╚══════════════════════════════════════════════════════════════════════════╝
|
|
11
|
-
*
|
|
12
|
-
* ARCHITECTURAL NOTE:
|
|
13
|
-
* -------------------
|
|
14
|
-
* Live preview animation requires local p5 runtime execution because:
|
|
15
|
-
* - @nexart/codemode-sdk.executeCodeMode() returns blobs (PNG/MP4), not frames
|
|
16
|
-
* - Real-time animation in the browser requires frame-by-frame canvas updates
|
|
17
|
-
* - The SDK's loop mode produces video files, not real-time rendering
|
|
18
|
-
*
|
|
19
|
-
* To ensure faithful mirroring, this renderer:
|
|
20
|
-
* 1. Uses identical forbidden pattern validation as the SDK
|
|
21
|
-
* 2. Uses identical VAR handling (read-only, 0-10 input → 10 runtime, 0-100 strict, errors not clamps)
|
|
22
|
-
* 3. Uses identical seeded RNG (Mulberry32) and Perlin noise
|
|
23
|
-
* 4. Uses identical time variable semantics (frameCount, t, time, tGlobal)
|
|
24
|
-
*
|
|
25
|
-
* For archival/canonical output, use @nexart/codemode-sdk directly.
|
|
26
18
|
*/
|
|
19
|
+
import { PREVIEW_BUDGET, } from './preview-types';
|
|
20
|
+
import { createFrameBudget, canRenderFrame, recordFrame, resetBudget, shouldSkipFrame, } from './frame-budget';
|
|
21
|
+
import { calculateScaledDimensions, applyScaledDimensions, reapplyContextScale, clearCanvasIgnoringTransform, } from './canvas-scaler';
|
|
22
|
+
import { createPreviewRuntime } from './preview-runtime';
|
|
27
23
|
const PROTOCOL_VERSION = '1.2.0';
|
|
28
24
|
let activeRendererInstance = null;
|
|
29
25
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* ╔════════════════════════════════════════════════════════════════════════════╗
|
|
33
|
-
* ║ MUST MATCH @nexart/codemode-sdk v1.4.0 EXACTLY ║
|
|
34
|
-
* ║ Any change here requires updating codemode-sdk's normalizeVars() too. ║
|
|
35
|
-
* ╚════════════════════════════════════════════════════════════════════════════╝
|
|
36
|
-
*
|
|
37
|
-
* Rules (SDK v1.4.0, Protocol v1.2.0):
|
|
38
|
-
* - VAR is OPTIONAL: omit or pass [] for empty (defaults to all zeros)
|
|
39
|
-
* - VAR input length MUST be 0-10 elements (protocol error if > 10)
|
|
40
|
-
* - VAR values MUST be finite numbers (protocol error if not)
|
|
41
|
-
* - VAR values MUST be in range 0-100 (protocol error if out of range, NO clamping)
|
|
42
|
-
* - VAR is read-only inside sketches
|
|
43
|
-
* - Output is ALWAYS 10 elements (padded with zeros) for protocol consistency
|
|
26
|
+
* Normalize VAR array.
|
|
27
|
+
* Preview version clamps instead of throwing for better UX.
|
|
44
28
|
*/
|
|
45
29
|
function normalizeVars(vars) {
|
|
46
30
|
if (!vars || !Array.isArray(vars)) {
|
|
47
|
-
console.log('[UIRenderer] No vars provided, using defaults [0,0,0,0,0,0,0,0,0,0]');
|
|
48
31
|
return [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
|
49
32
|
}
|
|
50
|
-
if (vars.length > 10) {
|
|
51
|
-
throw new Error(`[Code Mode Protocol Error] VAR array must have at most 10 elements, got ${vars.length}`);
|
|
52
|
-
}
|
|
53
33
|
const result = [];
|
|
54
|
-
for (let i = 0; i < vars.length; i++) {
|
|
34
|
+
for (let i = 0; i < Math.min(vars.length, 10); i++) {
|
|
55
35
|
const v = vars[i];
|
|
56
|
-
if (typeof v
|
|
57
|
-
|
|
36
|
+
if (typeof v === 'number' && Number.isFinite(v)) {
|
|
37
|
+
result.push(Math.max(0, Math.min(100, v)));
|
|
58
38
|
}
|
|
59
|
-
|
|
60
|
-
|
|
39
|
+
else {
|
|
40
|
+
result.push(0);
|
|
61
41
|
}
|
|
62
|
-
result.push(v);
|
|
63
42
|
}
|
|
64
|
-
|
|
65
|
-
while (result.length < 10) {
|
|
43
|
+
while (result.length < 10)
|
|
66
44
|
result.push(0);
|
|
67
|
-
}
|
|
68
45
|
return result;
|
|
69
46
|
}
|
|
70
|
-
function createSeededRNG(seed = 123456) {
|
|
71
|
-
let a = seed >>> 0;
|
|
72
|
-
return () => {
|
|
73
|
-
a += 0x6D2B79F5;
|
|
74
|
-
let t = Math.imul(a ^ (a >>> 15), a | 1);
|
|
75
|
-
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
76
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
function createSeededNoise(seed = 0) {
|
|
80
|
-
const permutation = [];
|
|
81
|
-
const rng = createSeededRNG(seed);
|
|
82
|
-
for (let i = 0; i < 256; i++) {
|
|
83
|
-
permutation[i] = i;
|
|
84
|
-
}
|
|
85
|
-
for (let i = 255; i > 0; i--) {
|
|
86
|
-
const j = Math.floor(rng() * (i + 1));
|
|
87
|
-
[permutation[i], permutation[j]] = [permutation[j], permutation[i]];
|
|
88
|
-
}
|
|
89
|
-
for (let i = 0; i < 256; i++) {
|
|
90
|
-
permutation[256 + i] = permutation[i];
|
|
91
|
-
}
|
|
92
|
-
const fade = (t) => t * t * t * (t * (t * 6 - 15) + 10);
|
|
93
|
-
const lerp = (a, b, t) => a + t * (b - a);
|
|
94
|
-
const grad = (hash, x, y, z) => {
|
|
95
|
-
const h = hash & 15;
|
|
96
|
-
const u = h < 8 ? x : y;
|
|
97
|
-
const v = h < 4 ? y : h === 12 || h === 14 ? x : z;
|
|
98
|
-
return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
|
|
99
|
-
};
|
|
100
|
-
return (x, y = 0, z = 0) => {
|
|
101
|
-
const X = Math.floor(x) & 255;
|
|
102
|
-
const Y = Math.floor(y) & 255;
|
|
103
|
-
const Z = Math.floor(z) & 255;
|
|
104
|
-
x -= Math.floor(x);
|
|
105
|
-
y -= Math.floor(y);
|
|
106
|
-
z -= Math.floor(z);
|
|
107
|
-
const u = fade(x);
|
|
108
|
-
const v = fade(y);
|
|
109
|
-
const w = fade(z);
|
|
110
|
-
const A = permutation[X] + Y;
|
|
111
|
-
const AA = permutation[A] + Z;
|
|
112
|
-
const AB = permutation[A + 1] + Z;
|
|
113
|
-
const B = permutation[X + 1] + Y;
|
|
114
|
-
const BA = permutation[B] + Z;
|
|
115
|
-
const BB = permutation[B + 1] + Z;
|
|
116
|
-
return (lerp(lerp(lerp(grad(permutation[AA], x, y, z), grad(permutation[BA], x - 1, y, z), u), lerp(grad(permutation[AB], x, y - 1, z), grad(permutation[BB], x - 1, y - 1, z), u), v), lerp(lerp(grad(permutation[AA + 1], x, y, z - 1), grad(permutation[BA + 1], x - 1, y, z - 1), u), lerp(grad(permutation[AB + 1], x, y - 1, z - 1), grad(permutation[BB + 1], x - 1, y - 1, z - 1), u), v), w) + 1) / 2;
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Create a protected, read-only VAR array for protocol execution.
|
|
121
|
-
*
|
|
122
|
-
* ╔════════════════════════════════════════════════════════════════════════════╗
|
|
123
|
-
* ║ MUST MATCH @nexart/codemode-sdk v1.4.0 EXACTLY ║
|
|
124
|
-
* ║ Any change here requires updating codemode-sdk's createProtocolVAR() too. ║
|
|
125
|
-
* ╚════════════════════════════════════════════════════════════════════════════╝
|
|
126
|
-
*
|
|
127
|
-
* SDK v1.4.0 Rules (Protocol v1.2.0):
|
|
128
|
-
* - Input accepts 0-10 elements
|
|
129
|
-
* - Runtime VAR is ALWAYS 10 elements (padded with zeros)
|
|
130
|
-
* - Values are numeric, must be in 0-100 range (validated upstream)
|
|
131
|
-
* - Read-only: writes throw descriptive errors
|
|
132
|
-
*/
|
|
133
|
-
function createProtectedVAR(vars) {
|
|
134
|
-
// Create frozen 10-element array (upstream normalizeVars ensures this)
|
|
135
|
-
const normalizedVars = [];
|
|
136
|
-
for (let i = 0; i < 10; i++) {
|
|
137
|
-
normalizedVars[i] = vars?.[i] ?? 0;
|
|
138
|
-
}
|
|
139
|
-
// Freeze the array to prevent modifications
|
|
140
|
-
const frozenVars = Object.freeze(normalizedVars);
|
|
141
|
-
// Wrap in Proxy for descriptive error messages on write attempts
|
|
142
|
-
return new Proxy(frozenVars, {
|
|
143
|
-
set(_target, prop, _value) {
|
|
144
|
-
const propName = typeof prop === 'symbol' ? prop.toString() : prop;
|
|
145
|
-
throw new Error(`[Code Mode Protocol Error] VAR is read-only. ` +
|
|
146
|
-
`Cannot write to VAR[${propName}]. ` +
|
|
147
|
-
`VAR[0..9] are protocol inputs, not sketch state.`);
|
|
148
|
-
},
|
|
149
|
-
deleteProperty(_target, prop) {
|
|
150
|
-
const propName = typeof prop === 'symbol' ? prop.toString() : prop;
|
|
151
|
-
throw new Error(`[Code Mode Protocol Error] VAR is read-only. ` +
|
|
152
|
-
`Cannot delete VAR[${propName}].`);
|
|
153
|
-
},
|
|
154
|
-
defineProperty(_target, prop) {
|
|
155
|
-
const propName = typeof prop === 'symbol' ? prop.toString() : prop;
|
|
156
|
-
throw new Error(`[Code Mode Protocol Error] Cannot define new VAR properties. ` +
|
|
157
|
-
`VAR is fixed at 10 elements (VAR[0..9]). Attempted: ${propName}`);
|
|
158
|
-
},
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
export function createP5Runtime(canvas, width, height, seed, vars = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) {
|
|
162
|
-
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
163
|
-
let currentFill = 'rgba(255, 255, 255, 1)';
|
|
164
|
-
let currentStroke = 'rgba(0, 0, 0, 1)';
|
|
165
|
-
let strokeEnabled = true;
|
|
166
|
-
let fillEnabled = true;
|
|
167
|
-
let currentStrokeWeight = 1;
|
|
168
|
-
let colorModeSettings = { mode: 'RGB', maxR: 255, maxG: 255, maxB: 255, maxA: 255 };
|
|
169
|
-
let shapeStarted = false;
|
|
170
|
-
let randomSeedValue = seed;
|
|
171
|
-
let rng = createSeededRNG(randomSeedValue);
|
|
172
|
-
let noiseSeedValue = seed;
|
|
173
|
-
let noiseFunc = createSeededNoise(noiseSeedValue);
|
|
174
|
-
let noiseOctaves = 4;
|
|
175
|
-
let noiseFalloff = 0.5;
|
|
176
|
-
const parseColor = (...args) => {
|
|
177
|
-
if (args.length === 0)
|
|
178
|
-
return 'rgba(0, 0, 0, 1)';
|
|
179
|
-
const { mode, maxR, maxG, maxB, maxA } = colorModeSettings;
|
|
180
|
-
if (args.length === 1) {
|
|
181
|
-
const val = args[0];
|
|
182
|
-
if (typeof val === 'string')
|
|
183
|
-
return val;
|
|
184
|
-
if (mode === 'HSB') {
|
|
185
|
-
return `hsla(${val}, 100%, 50%, 1)`;
|
|
186
|
-
}
|
|
187
|
-
const gray = Math.round((val / maxR) * 255);
|
|
188
|
-
return `rgba(${gray}, ${gray}, ${gray}, 1)`;
|
|
189
|
-
}
|
|
190
|
-
if (args.length === 2) {
|
|
191
|
-
const [gray, alpha] = args;
|
|
192
|
-
const g = Math.round((gray / maxR) * 255);
|
|
193
|
-
const a = alpha / maxA;
|
|
194
|
-
return `rgba(${g}, ${g}, ${g}, ${a})`;
|
|
195
|
-
}
|
|
196
|
-
if (args.length === 3) {
|
|
197
|
-
const [r, g, b] = args;
|
|
198
|
-
if (mode === 'HSB') {
|
|
199
|
-
return `hsla(${(r / maxR) * 360}, ${(g / maxG) * 100}%, ${(b / maxB) * 100}%, 1)`;
|
|
200
|
-
}
|
|
201
|
-
return `rgba(${Math.round((r / maxR) * 255)}, ${Math.round((g / maxG) * 255)}, ${Math.round((b / maxB) * 255)}, 1)`;
|
|
202
|
-
}
|
|
203
|
-
if (args.length === 4) {
|
|
204
|
-
const [r, g, b, a] = args;
|
|
205
|
-
if (mode === 'HSB') {
|
|
206
|
-
return `hsla(${(r / maxR) * 360}, ${(g / maxG) * 100}%, ${(b / maxB) * 100}%, ${a / maxA})`;
|
|
207
|
-
}
|
|
208
|
-
return `rgba(${Math.round((r / maxR) * 255)}, ${Math.round((g / maxG) * 255)}, ${Math.round((b / maxB) * 255)}, ${a / maxA})`;
|
|
209
|
-
}
|
|
210
|
-
return 'rgba(0, 0, 0, 1)';
|
|
211
|
-
};
|
|
212
|
-
const protectedVAR = createProtectedVAR(vars);
|
|
213
|
-
const p = {
|
|
214
|
-
width,
|
|
215
|
-
height,
|
|
216
|
-
frameCount: 0,
|
|
217
|
-
VAR: protectedVAR,
|
|
218
|
-
PI: Math.PI,
|
|
219
|
-
TWO_PI: Math.PI * 2,
|
|
220
|
-
HALF_PI: Math.PI / 2,
|
|
221
|
-
QUARTER_PI: Math.PI / 4,
|
|
222
|
-
CORNER: 'corner',
|
|
223
|
-
CENTER: 'center',
|
|
224
|
-
CORNERS: 'corners',
|
|
225
|
-
RADIUS: 'radius',
|
|
226
|
-
ROUND: 'round',
|
|
227
|
-
SQUARE: 'square',
|
|
228
|
-
PROJECT: 'butt',
|
|
229
|
-
MITER: 'miter',
|
|
230
|
-
BEVEL: 'bevel',
|
|
231
|
-
CLOSE: 'close',
|
|
232
|
-
background: (...args) => {
|
|
233
|
-
ctx.fillStyle = parseColor(...args);
|
|
234
|
-
ctx.fillRect(0, 0, width, height);
|
|
235
|
-
},
|
|
236
|
-
fill: (...args) => {
|
|
237
|
-
currentFill = parseColor(...args);
|
|
238
|
-
fillEnabled = true;
|
|
239
|
-
},
|
|
240
|
-
noFill: () => {
|
|
241
|
-
fillEnabled = false;
|
|
242
|
-
},
|
|
243
|
-
stroke: (...args) => {
|
|
244
|
-
currentStroke = parseColor(...args);
|
|
245
|
-
strokeEnabled = true;
|
|
246
|
-
},
|
|
247
|
-
noStroke: () => {
|
|
248
|
-
strokeEnabled = false;
|
|
249
|
-
},
|
|
250
|
-
strokeWeight: (w) => {
|
|
251
|
-
currentStrokeWeight = w;
|
|
252
|
-
ctx.lineWidth = w;
|
|
253
|
-
},
|
|
254
|
-
strokeCap: (cap) => {
|
|
255
|
-
ctx.lineCap = cap;
|
|
256
|
-
},
|
|
257
|
-
strokeJoin: (join) => {
|
|
258
|
-
ctx.lineJoin = join;
|
|
259
|
-
},
|
|
260
|
-
colorMode: (mode, max1, max2, max3, maxA) => {
|
|
261
|
-
colorModeSettings.mode = mode;
|
|
262
|
-
if (max1 !== undefined) {
|
|
263
|
-
colorModeSettings.maxR = max1;
|
|
264
|
-
colorModeSettings.maxG = max2 ?? max1;
|
|
265
|
-
colorModeSettings.maxB = max3 ?? max1;
|
|
266
|
-
colorModeSettings.maxA = maxA ?? max1;
|
|
267
|
-
}
|
|
268
|
-
},
|
|
269
|
-
push: () => {
|
|
270
|
-
ctx.save();
|
|
271
|
-
},
|
|
272
|
-
pop: () => {
|
|
273
|
-
ctx.restore();
|
|
274
|
-
},
|
|
275
|
-
translate: (x, y) => {
|
|
276
|
-
ctx.translate(x, y);
|
|
277
|
-
},
|
|
278
|
-
rotate: (angle) => {
|
|
279
|
-
ctx.rotate(angle);
|
|
280
|
-
},
|
|
281
|
-
scale: (sx, sy) => {
|
|
282
|
-
ctx.scale(sx, sy ?? sx);
|
|
283
|
-
},
|
|
284
|
-
ellipse: (x, y, w, h) => {
|
|
285
|
-
const rw = w / 2;
|
|
286
|
-
const rh = (h ?? w) / 2;
|
|
287
|
-
ctx.beginPath();
|
|
288
|
-
ctx.ellipse(x, y, rw, rh, 0, 0, Math.PI * 2);
|
|
289
|
-
if (fillEnabled) {
|
|
290
|
-
ctx.fillStyle = currentFill;
|
|
291
|
-
ctx.fill();
|
|
292
|
-
}
|
|
293
|
-
if (strokeEnabled) {
|
|
294
|
-
ctx.strokeStyle = currentStroke;
|
|
295
|
-
ctx.lineWidth = currentStrokeWeight;
|
|
296
|
-
ctx.stroke();
|
|
297
|
-
}
|
|
298
|
-
},
|
|
299
|
-
circle: (x, y, d) => {
|
|
300
|
-
p.ellipse(x, y, d, d);
|
|
301
|
-
},
|
|
302
|
-
rect: (x, y, w, h, r) => {
|
|
303
|
-
const height = h ?? w;
|
|
304
|
-
ctx.beginPath();
|
|
305
|
-
if (r && r > 0) {
|
|
306
|
-
ctx.roundRect(x, y, w, height, r);
|
|
307
|
-
}
|
|
308
|
-
else {
|
|
309
|
-
ctx.rect(x, y, w, height);
|
|
310
|
-
}
|
|
311
|
-
if (fillEnabled) {
|
|
312
|
-
ctx.fillStyle = currentFill;
|
|
313
|
-
ctx.fill();
|
|
314
|
-
}
|
|
315
|
-
if (strokeEnabled) {
|
|
316
|
-
ctx.strokeStyle = currentStroke;
|
|
317
|
-
ctx.lineWidth = currentStrokeWeight;
|
|
318
|
-
ctx.stroke();
|
|
319
|
-
}
|
|
320
|
-
},
|
|
321
|
-
square: (x, y, s, r) => {
|
|
322
|
-
p.rect(x, y, s, s, r);
|
|
323
|
-
},
|
|
324
|
-
line: (x1, y1, x2, y2) => {
|
|
325
|
-
ctx.beginPath();
|
|
326
|
-
ctx.moveTo(x1, y1);
|
|
327
|
-
ctx.lineTo(x2, y2);
|
|
328
|
-
if (strokeEnabled) {
|
|
329
|
-
ctx.strokeStyle = currentStroke;
|
|
330
|
-
ctx.lineWidth = currentStrokeWeight;
|
|
331
|
-
ctx.stroke();
|
|
332
|
-
}
|
|
333
|
-
},
|
|
334
|
-
point: (x, y) => {
|
|
335
|
-
ctx.beginPath();
|
|
336
|
-
ctx.arc(x, y, currentStrokeWeight / 2, 0, Math.PI * 2);
|
|
337
|
-
ctx.fillStyle = currentStroke;
|
|
338
|
-
ctx.fill();
|
|
339
|
-
},
|
|
340
|
-
triangle: (x1, y1, x2, y2, x3, y3) => {
|
|
341
|
-
ctx.beginPath();
|
|
342
|
-
ctx.moveTo(x1, y1);
|
|
343
|
-
ctx.lineTo(x2, y2);
|
|
344
|
-
ctx.lineTo(x3, y3);
|
|
345
|
-
ctx.closePath();
|
|
346
|
-
if (fillEnabled) {
|
|
347
|
-
ctx.fillStyle = currentFill;
|
|
348
|
-
ctx.fill();
|
|
349
|
-
}
|
|
350
|
-
if (strokeEnabled) {
|
|
351
|
-
ctx.strokeStyle = currentStroke;
|
|
352
|
-
ctx.lineWidth = currentStrokeWeight;
|
|
353
|
-
ctx.stroke();
|
|
354
|
-
}
|
|
355
|
-
},
|
|
356
|
-
quad: (x1, y1, x2, y2, x3, y3, x4, y4) => {
|
|
357
|
-
ctx.beginPath();
|
|
358
|
-
ctx.moveTo(x1, y1);
|
|
359
|
-
ctx.lineTo(x2, y2);
|
|
360
|
-
ctx.lineTo(x3, y3);
|
|
361
|
-
ctx.lineTo(x4, y4);
|
|
362
|
-
ctx.closePath();
|
|
363
|
-
if (fillEnabled) {
|
|
364
|
-
ctx.fillStyle = currentFill;
|
|
365
|
-
ctx.fill();
|
|
366
|
-
}
|
|
367
|
-
if (strokeEnabled) {
|
|
368
|
-
ctx.strokeStyle = currentStroke;
|
|
369
|
-
ctx.lineWidth = currentStrokeWeight;
|
|
370
|
-
ctx.stroke();
|
|
371
|
-
}
|
|
372
|
-
},
|
|
373
|
-
arc: (x, y, w, h, start, stop, mode) => {
|
|
374
|
-
ctx.beginPath();
|
|
375
|
-
ctx.ellipse(x, y, w / 2, h / 2, 0, start, stop);
|
|
376
|
-
if (mode === 'close' || mode === 'chord') {
|
|
377
|
-
ctx.closePath();
|
|
378
|
-
}
|
|
379
|
-
else if (mode === 'pie') {
|
|
380
|
-
ctx.lineTo(x, y);
|
|
381
|
-
ctx.closePath();
|
|
382
|
-
}
|
|
383
|
-
if (fillEnabled) {
|
|
384
|
-
ctx.fillStyle = currentFill;
|
|
385
|
-
ctx.fill();
|
|
386
|
-
}
|
|
387
|
-
if (strokeEnabled) {
|
|
388
|
-
ctx.strokeStyle = currentStroke;
|
|
389
|
-
ctx.lineWidth = currentStrokeWeight;
|
|
390
|
-
ctx.stroke();
|
|
391
|
-
}
|
|
392
|
-
},
|
|
393
|
-
beginShape: () => {
|
|
394
|
-
ctx.beginPath();
|
|
395
|
-
shapeStarted = true;
|
|
396
|
-
},
|
|
397
|
-
vertex: (x, y) => {
|
|
398
|
-
if (!shapeStarted) {
|
|
399
|
-
ctx.beginPath();
|
|
400
|
-
ctx.moveTo(x, y);
|
|
401
|
-
shapeStarted = true;
|
|
402
|
-
}
|
|
403
|
-
else {
|
|
404
|
-
ctx.lineTo(x, y);
|
|
405
|
-
}
|
|
406
|
-
},
|
|
407
|
-
curveVertex: (x, y) => {
|
|
408
|
-
ctx.lineTo(x, y);
|
|
409
|
-
},
|
|
410
|
-
bezierVertex: (x2, y2, x3, y3, x4, y4) => {
|
|
411
|
-
ctx.bezierCurveTo(x2, y2, x3, y3, x4, y4);
|
|
412
|
-
},
|
|
413
|
-
quadraticVertex: (cx, cy, x3, y3) => {
|
|
414
|
-
ctx.quadraticCurveTo(cx, cy, x3, y3);
|
|
415
|
-
},
|
|
416
|
-
endShape: (close) => {
|
|
417
|
-
if (close === 'close') {
|
|
418
|
-
ctx.closePath();
|
|
419
|
-
}
|
|
420
|
-
if (fillEnabled) {
|
|
421
|
-
ctx.fillStyle = currentFill;
|
|
422
|
-
ctx.fill();
|
|
423
|
-
}
|
|
424
|
-
if (strokeEnabled) {
|
|
425
|
-
ctx.strokeStyle = currentStroke;
|
|
426
|
-
ctx.lineWidth = currentStrokeWeight;
|
|
427
|
-
ctx.stroke();
|
|
428
|
-
}
|
|
429
|
-
shapeStarted = false;
|
|
430
|
-
},
|
|
431
|
-
bezier: (x1, y1, x2, y2, x3, y3, x4, y4) => {
|
|
432
|
-
ctx.beginPath();
|
|
433
|
-
ctx.moveTo(x1, y1);
|
|
434
|
-
ctx.bezierCurveTo(x2, y2, x3, y3, x4, y4);
|
|
435
|
-
if (strokeEnabled) {
|
|
436
|
-
ctx.strokeStyle = currentStroke;
|
|
437
|
-
ctx.lineWidth = currentStrokeWeight;
|
|
438
|
-
ctx.stroke();
|
|
439
|
-
}
|
|
440
|
-
},
|
|
441
|
-
curve: (x1, y1, x2, y2, x3, y3, x4, y4) => {
|
|
442
|
-
ctx.beginPath();
|
|
443
|
-
ctx.moveTo(x2, y2);
|
|
444
|
-
ctx.bezierCurveTo(x2, y2, x3, y3, x3, y3);
|
|
445
|
-
if (strokeEnabled) {
|
|
446
|
-
ctx.strokeStyle = currentStroke;
|
|
447
|
-
ctx.lineWidth = currentStrokeWeight;
|
|
448
|
-
ctx.stroke();
|
|
449
|
-
}
|
|
450
|
-
},
|
|
451
|
-
text: (str, x, y) => {
|
|
452
|
-
if (fillEnabled) {
|
|
453
|
-
ctx.fillStyle = currentFill;
|
|
454
|
-
ctx.fillText(String(str), x, y);
|
|
455
|
-
}
|
|
456
|
-
if (strokeEnabled) {
|
|
457
|
-
ctx.strokeStyle = currentStroke;
|
|
458
|
-
ctx.strokeText(String(str), x, y);
|
|
459
|
-
}
|
|
460
|
-
},
|
|
461
|
-
textSize: (size) => {
|
|
462
|
-
ctx.font = `${size}px sans-serif`;
|
|
463
|
-
},
|
|
464
|
-
textAlign: (horizAlign, vertAlign) => {
|
|
465
|
-
ctx.textAlign = horizAlign;
|
|
466
|
-
if (vertAlign) {
|
|
467
|
-
ctx.textBaseline = vertAlign;
|
|
468
|
-
}
|
|
469
|
-
},
|
|
470
|
-
random: (min, max) => {
|
|
471
|
-
if (min === undefined) {
|
|
472
|
-
return rng();
|
|
473
|
-
}
|
|
474
|
-
if (max === undefined) {
|
|
475
|
-
return rng() * min;
|
|
476
|
-
}
|
|
477
|
-
return min + rng() * (max - min);
|
|
478
|
-
},
|
|
479
|
-
randomSeed: (s) => {
|
|
480
|
-
randomSeedValue = s;
|
|
481
|
-
rng = createSeededRNG(s);
|
|
482
|
-
},
|
|
483
|
-
noise: (x, y, z) => {
|
|
484
|
-
let total = 0;
|
|
485
|
-
let freq = 1;
|
|
486
|
-
let amp = 1;
|
|
487
|
-
let maxValue = 0;
|
|
488
|
-
for (let i = 0; i < noiseOctaves; i++) {
|
|
489
|
-
total += noiseFunc(x * freq, (y ?? 0) * freq, (z ?? 0) * freq) * amp;
|
|
490
|
-
maxValue += amp;
|
|
491
|
-
amp *= noiseFalloff;
|
|
492
|
-
freq *= 2;
|
|
493
|
-
}
|
|
494
|
-
return total / maxValue;
|
|
495
|
-
},
|
|
496
|
-
noiseSeed: (s) => {
|
|
497
|
-
noiseSeedValue = s;
|
|
498
|
-
noiseFunc = createSeededNoise(s);
|
|
499
|
-
},
|
|
500
|
-
noiseDetail: (octaves, falloff) => {
|
|
501
|
-
noiseOctaves = octaves;
|
|
502
|
-
if (falloff !== undefined) {
|
|
503
|
-
noiseFalloff = falloff;
|
|
504
|
-
}
|
|
505
|
-
},
|
|
506
|
-
sin: Math.sin,
|
|
507
|
-
cos: Math.cos,
|
|
508
|
-
tan: Math.tan,
|
|
509
|
-
asin: Math.asin,
|
|
510
|
-
acos: Math.acos,
|
|
511
|
-
atan: Math.atan,
|
|
512
|
-
atan2: Math.atan2,
|
|
513
|
-
abs: Math.abs,
|
|
514
|
-
ceil: Math.ceil,
|
|
515
|
-
floor: Math.floor,
|
|
516
|
-
round: Math.round,
|
|
517
|
-
min: Math.min,
|
|
518
|
-
max: Math.max,
|
|
519
|
-
pow: Math.pow,
|
|
520
|
-
sqrt: Math.sqrt,
|
|
521
|
-
exp: Math.exp,
|
|
522
|
-
log: Math.log,
|
|
523
|
-
sq: (n) => n * n,
|
|
524
|
-
map: (value, start1, stop1, start2, stop2) => {
|
|
525
|
-
return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1));
|
|
526
|
-
},
|
|
527
|
-
constrain: (value, low, high) => {
|
|
528
|
-
return Math.max(low, Math.min(high, value));
|
|
529
|
-
},
|
|
530
|
-
lerp: (start, stop, amt) => {
|
|
531
|
-
return start + (stop - start) * amt;
|
|
532
|
-
},
|
|
533
|
-
dist: (x1, y1, x2, y2) => {
|
|
534
|
-
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
|
535
|
-
},
|
|
536
|
-
mag: (x, y) => {
|
|
537
|
-
return Math.sqrt(x * x + y * y);
|
|
538
|
-
},
|
|
539
|
-
norm: (value, start, stop) => {
|
|
540
|
-
return (value - start) / (stop - start);
|
|
541
|
-
},
|
|
542
|
-
radians: (degrees) => degrees * (Math.PI / 180),
|
|
543
|
-
degrees: (radians) => radians * (180 / Math.PI),
|
|
544
|
-
color: (...args) => parseColor(...args),
|
|
545
|
-
lerpColor: (c1, c2, amt) => {
|
|
546
|
-
return c1;
|
|
547
|
-
},
|
|
548
|
-
red: (c) => {
|
|
549
|
-
const match = c.match(/rgba?\((\d+)/);
|
|
550
|
-
if (match)
|
|
551
|
-
return parseInt(match[1], 10);
|
|
552
|
-
const rgbMatch = c.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
553
|
-
if (rgbMatch)
|
|
554
|
-
return parseInt(rgbMatch[1], 10);
|
|
555
|
-
return 0;
|
|
556
|
-
},
|
|
557
|
-
green: (c) => {
|
|
558
|
-
const match = c.match(/rgba?\(\d+,\s*(\d+)/);
|
|
559
|
-
if (match)
|
|
560
|
-
return parseInt(match[1], 10);
|
|
561
|
-
const rgbMatch = c.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
562
|
-
if (rgbMatch)
|
|
563
|
-
return parseInt(rgbMatch[2], 10);
|
|
564
|
-
return 0;
|
|
565
|
-
},
|
|
566
|
-
blue: (c) => {
|
|
567
|
-
const match = c.match(/rgba?\(\d+,\s*\d+,\s*(\d+)/);
|
|
568
|
-
if (match)
|
|
569
|
-
return parseInt(match[1], 10);
|
|
570
|
-
const rgbMatch = c.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
571
|
-
if (rgbMatch)
|
|
572
|
-
return parseInt(rgbMatch[3], 10);
|
|
573
|
-
return 0;
|
|
574
|
-
},
|
|
575
|
-
alpha: (c) => {
|
|
576
|
-
const match = c.match(/rgba\(\d+,\s*\d+,\s*\d+,\s*([\d.]+)\)/);
|
|
577
|
-
if (match)
|
|
578
|
-
return Math.round(parseFloat(match[1]) * 255);
|
|
579
|
-
return 255;
|
|
580
|
-
},
|
|
581
|
-
hue: (c) => 0,
|
|
582
|
-
saturation: (c) => 0,
|
|
583
|
-
brightness: (c) => 0,
|
|
584
|
-
blendMode: (mode) => {
|
|
585
|
-
ctx.globalCompositeOperation = mode;
|
|
586
|
-
},
|
|
587
|
-
clear: () => {
|
|
588
|
-
ctx.clearRect(0, 0, width, height);
|
|
589
|
-
},
|
|
590
|
-
print: console.log,
|
|
591
|
-
println: console.log,
|
|
592
|
-
};
|
|
593
|
-
return p;
|
|
594
|
-
}
|
|
595
|
-
function injectTimeVariables(p, time) {
|
|
596
|
-
p.frameCount = time.frameCount;
|
|
597
|
-
p.t = time.t;
|
|
598
|
-
p.time = time.time;
|
|
599
|
-
p.tGlobal = time.tGlobal;
|
|
600
|
-
}
|
|
601
|
-
/**
|
|
602
|
-
* Validate forbidden patterns in code.
|
|
603
|
-
* MIRRORS @nexart/codemode-sdk forbidden patterns exactly.
|
|
604
|
-
* Error messages match SDK wording for consistency.
|
|
605
|
-
*/
|
|
606
|
-
function validateForbiddenPatterns(code) {
|
|
607
|
-
const forbiddenPatterns = [
|
|
608
|
-
{ pattern: /setTimeout\s*\(/, name: 'setTimeout' },
|
|
609
|
-
{ pattern: /setInterval\s*\(/, name: 'setInterval' },
|
|
610
|
-
{ pattern: /requestAnimationFrame\s*\(/, name: 'requestAnimationFrame' },
|
|
611
|
-
{ pattern: /Date\.now\s*\(/, name: 'Date.now() — use time variable instead' },
|
|
612
|
-
{ pattern: /new\s+Date\s*\(/, name: 'new Date() — use time variable instead' },
|
|
613
|
-
{ pattern: /Math\.random\s*\(/, name: 'Math.random() — use random() instead (seeded)' },
|
|
614
|
-
{ pattern: /fetch\s*\(/, name: 'fetch() — external IO forbidden' },
|
|
615
|
-
{ pattern: /XMLHttpRequest/, name: 'XMLHttpRequest — external IO forbidden' },
|
|
616
|
-
{ pattern: /createCanvas\s*\(/, name: 'createCanvas() — canvas is pre-initialized' },
|
|
617
|
-
{ pattern: /document\./, name: 'DOM access — document.* forbidden' },
|
|
618
|
-
{ pattern: /window\./, name: 'DOM access — window.* forbidden' },
|
|
619
|
-
{ pattern: /\bimport\s+/, name: 'import — external imports forbidden' },
|
|
620
|
-
{ pattern: /\brequire\s*\(/, name: 'require() — external imports forbidden' },
|
|
621
|
-
];
|
|
622
|
-
for (const { pattern, name } of forbiddenPatterns) {
|
|
623
|
-
if (pattern.test(code)) {
|
|
624
|
-
throw new Error(`[Code Mode Protocol Error] Forbidden pattern: ${name}`);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
47
|
export function renderCodeModeSystem(system, canvas, options = {}) {
|
|
629
|
-
console.log('[UIRenderer] Preview
|
|
630
|
-
console.log(`[UIRenderer]
|
|
631
|
-
console.log(`[UIRenderer] Mode: ${system.mode}`);
|
|
48
|
+
console.log('[UIRenderer] Preview mode → lightweight runtime with budget limits');
|
|
49
|
+
console.log(`[UIRenderer] Budget: max ${PREVIEW_BUDGET.MAX_FRAMES} frames, ${PREVIEW_BUDGET.MAX_TOTAL_TIME_MS}ms`);
|
|
632
50
|
if (activeRendererInstance) {
|
|
633
51
|
activeRendererInstance.destroy();
|
|
634
52
|
activeRendererInstance = null;
|
|
635
53
|
}
|
|
636
54
|
const { showBadge = true, onPreview, onComplete, onError } = options;
|
|
637
|
-
|
|
638
|
-
|
|
55
|
+
const scaled = calculateScaledDimensions(system.width, system.height);
|
|
56
|
+
if (scaled.wasScaled) {
|
|
57
|
+
console.log(`[UIRenderer] Canvas scaled: ${system.width}x${system.height} → ${scaled.renderWidth}x${scaled.renderHeight}`);
|
|
58
|
+
}
|
|
59
|
+
applyScaledDimensions(canvas, scaled);
|
|
60
|
+
// NOTE: Canvas resizing resets the 2D context transform.
|
|
61
|
+
// Reapply scale factor once after resize for correct rendering.
|
|
62
|
+
reapplyContextScale(canvas, scaled);
|
|
639
63
|
const ctx = canvas.getContext('2d');
|
|
640
64
|
let animationId = null;
|
|
641
65
|
let isRunning = false;
|
|
642
66
|
let isDestroyed = false;
|
|
67
|
+
const budget = createFrameBudget();
|
|
643
68
|
const normalizedVars = normalizeVars(system.vars);
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
69
|
+
let runtime = null;
|
|
70
|
+
let setupFn = null;
|
|
71
|
+
let drawFn = null;
|
|
72
|
+
const compileSource = () => {
|
|
73
|
+
runtime = createPreviewRuntime(canvas, scaled.renderWidth, scaled.renderHeight, system.seed ?? 12345, normalizedVars);
|
|
74
|
+
const totalFrames = system.totalFrames ?? 120;
|
|
75
|
+
runtime.totalFrames = totalFrames;
|
|
76
|
+
try {
|
|
77
|
+
const globalVars = Object.keys(runtime);
|
|
78
|
+
const globalValues = Object.values(runtime);
|
|
79
|
+
const wrappedSource = `
|
|
80
|
+
${system.source}
|
|
81
|
+
if (typeof setup === 'function') __registerSetup(setup);
|
|
82
|
+
if (typeof draw === 'function') __registerDraw(draw);
|
|
83
|
+
`;
|
|
84
|
+
const registerSetup = (fn) => { setupFn = fn; };
|
|
85
|
+
const registerDraw = (fn) => { drawFn = fn; };
|
|
86
|
+
globalVars.push('__registerSetup', '__registerDraw');
|
|
87
|
+
globalValues.push(registerSetup, registerDraw);
|
|
88
|
+
const fn = new Function(...globalVars, wrappedSource);
|
|
89
|
+
fn(...globalValues);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.warn('[UIRenderer] Compile error:', error);
|
|
93
|
+
}
|
|
651
94
|
};
|
|
652
95
|
const drawBadge = () => {
|
|
653
96
|
if (!showBadge)
|
|
654
97
|
return;
|
|
655
|
-
const text = '⚠️ Preview
|
|
656
|
-
ctx.font = '
|
|
98
|
+
const text = '⚠️ Preview';
|
|
99
|
+
ctx.font = '10px -apple-system, sans-serif';
|
|
657
100
|
const metrics = ctx.measureText(text);
|
|
658
|
-
const padding =
|
|
101
|
+
const padding = 6;
|
|
659
102
|
const badgeWidth = metrics.width + padding * 2;
|
|
660
|
-
const badgeHeight =
|
|
661
|
-
const x =
|
|
662
|
-
const y =
|
|
103
|
+
const badgeHeight = 18;
|
|
104
|
+
const x = scaled.renderWidth - badgeWidth - 6;
|
|
105
|
+
const y = 6;
|
|
663
106
|
ctx.fillStyle = 'rgba(255, 100, 100, 0.15)';
|
|
664
107
|
ctx.strokeStyle = 'rgba(255, 100, 100, 0.4)';
|
|
665
108
|
ctx.lineWidth = 1;
|
|
666
109
|
ctx.beginPath();
|
|
667
|
-
ctx.roundRect(x, y, badgeWidth, badgeHeight,
|
|
110
|
+
ctx.roundRect(x, y, badgeWidth, badgeHeight, 3);
|
|
668
111
|
ctx.fill();
|
|
669
112
|
ctx.stroke();
|
|
670
113
|
ctx.fillStyle = '#ff9999';
|
|
671
|
-
ctx.fillText(text, x + padding, y +
|
|
114
|
+
ctx.fillText(text, x + padding, y + 13);
|
|
672
115
|
};
|
|
673
116
|
const renderBlackCanvas = (error) => {
|
|
674
117
|
ctx.fillStyle = '#000000';
|
|
675
|
-
ctx.fillRect(0, 0,
|
|
118
|
+
ctx.fillRect(0, 0, scaled.renderWidth, scaled.renderHeight);
|
|
676
119
|
ctx.fillStyle = '#ff6666';
|
|
677
|
-
ctx.font = '
|
|
678
|
-
ctx.fillText('[
|
|
679
|
-
ctx.fillText(error.message,
|
|
120
|
+
ctx.font = '12px monospace';
|
|
121
|
+
ctx.fillText('[Preview Error]', 10, 20);
|
|
122
|
+
ctx.fillText(error.message.slice(0, 60), 10, 36);
|
|
680
123
|
};
|
|
681
124
|
const renderStatic = () => {
|
|
682
125
|
try {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
injectTimeVariables(p, { frameCount: 0, t: 0, time: 0, tGlobal: 0 });
|
|
688
|
-
const wrappedSetup = new Function('p', 'frameCount', 't', 'time', 'tGlobal', 'VAR', `with(p) { ${setupCode} }`);
|
|
689
|
-
wrappedSetup(p, 0, 0, 0, 0, p.VAR);
|
|
126
|
+
compileSource();
|
|
127
|
+
if (setupFn) {
|
|
128
|
+
setupFn();
|
|
129
|
+
}
|
|
690
130
|
drawBadge();
|
|
691
131
|
onPreview?.(canvas);
|
|
692
132
|
canvas.toBlob((blob) => {
|
|
@@ -697,7 +137,7 @@ export function renderCodeModeSystem(system, canvas, options = {}) {
|
|
|
697
137
|
}
|
|
698
138
|
catch (error) {
|
|
699
139
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
700
|
-
console.
|
|
140
|
+
console.warn('[UIRenderer] Static render error:', err.message);
|
|
701
141
|
renderBlackCanvas(err);
|
|
702
142
|
onError?.(err);
|
|
703
143
|
}
|
|
@@ -706,38 +146,53 @@ export function renderCodeModeSystem(system, canvas, options = {}) {
|
|
|
706
146
|
if (isDestroyed)
|
|
707
147
|
return;
|
|
708
148
|
try {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
}
|
|
715
|
-
const totalFrames = system.totalFrames ??
|
|
716
|
-
let
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
149
|
+
compileSource();
|
|
150
|
+
if (!drawFn) {
|
|
151
|
+
console.warn('[UIRenderer] Loop mode: no draw() function found, using static');
|
|
152
|
+
renderStatic();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const totalFrames = system.totalFrames ?? 120;
|
|
156
|
+
let frameCount = 0;
|
|
157
|
+
if (setupFn) {
|
|
158
|
+
setupFn();
|
|
159
|
+
}
|
|
160
|
+
resetBudget(budget);
|
|
161
|
+
isRunning = true;
|
|
721
162
|
const loop = () => {
|
|
722
163
|
if (!isRunning || isDestroyed)
|
|
723
164
|
return;
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
165
|
+
if (!canRenderFrame(budget)) {
|
|
166
|
+
console.log(`[UIRenderer] Budget exhausted: ${budget.exhaustionReason}`);
|
|
167
|
+
isRunning = false;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
frameCount++;
|
|
171
|
+
if (!shouldSkipFrame(frameCount)) {
|
|
172
|
+
if (runtime) {
|
|
173
|
+
runtime.frameCount = frameCount;
|
|
174
|
+
runtime.t = (frameCount % totalFrames) / totalFrames;
|
|
175
|
+
runtime.time = runtime.t;
|
|
176
|
+
runtime.tGlobal = runtime.t;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
clearCanvasIgnoringTransform(ctx, canvas);
|
|
180
|
+
if (drawFn)
|
|
181
|
+
drawFn();
|
|
182
|
+
drawBadge();
|
|
183
|
+
recordFrame(budget);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
console.warn('[UIRenderer] Draw error:', error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
733
189
|
animationId = requestAnimationFrame(loop);
|
|
734
190
|
};
|
|
735
|
-
isRunning = true;
|
|
736
191
|
animationId = requestAnimationFrame(loop);
|
|
737
192
|
}
|
|
738
193
|
catch (error) {
|
|
739
194
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
740
|
-
console.
|
|
195
|
+
console.warn('[UIRenderer] Loop render error:', err.message);
|
|
741
196
|
renderBlackCanvas(err);
|
|
742
197
|
onError?.(err);
|
|
743
198
|
}
|
|
@@ -773,7 +228,10 @@ export function renderCodeModeSystem(system, canvas, options = {}) {
|
|
|
773
228
|
const destroy = () => {
|
|
774
229
|
isDestroyed = true;
|
|
775
230
|
stop();
|
|
776
|
-
ctx
|
|
231
|
+
clearCanvasIgnoringTransform(ctx, canvas);
|
|
232
|
+
runtime = null;
|
|
233
|
+
setupFn = null;
|
|
234
|
+
drawFn = null;
|
|
777
235
|
if (activeRendererInstance === renderer) {
|
|
778
236
|
activeRendererInstance = null;
|
|
779
237
|
}
|