@nexart/ui-renderer 0.6.0 → 0.8.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 +133 -322
- package/dist/capabilities.d.ts +5 -6
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +6 -7
- package/dist/compiler.d.ts +3 -2
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +4 -3
- package/dist/index.d.ts +33 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -29
- package/dist/preview/canvas-scaler.d.ts +49 -0
- package/dist/preview/canvas-scaler.d.ts.map +1 -0
- package/dist/preview/canvas-scaler.js +74 -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 +116 -661
- 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 +201 -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 -2
- package/dist/preview/renderer.d.ts.map +1 -1
- package/dist/preview/renderer.js +3 -2
- package/dist/preview/unified-renderer.d.ts.map +1 -1
- package/dist/preview/unified-renderer.js +45 -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 +3 -3
|
@@ -1,692 +1,129 @@
|
|
|
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
|
*/
|
|
27
|
-
|
|
19
|
+
import { PREVIEW_BUDGET, } from './preview-types';
|
|
20
|
+
import { createFrameBudget, canRenderFrame, recordFrame, resetBudget, shouldSkipFrame, } from './frame-budget';
|
|
21
|
+
import { calculateScaledDimensions, applyScaledDimensions, } from './canvas-scaler';
|
|
22
|
+
import { createPreviewRuntime } from './preview-runtime';
|
|
23
|
+
const PROTOCOL_VERSION = '1.2.0';
|
|
28
24
|
let activeRendererInstance = null;
|
|
29
25
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* ╔════════════════════════════════════════════════════════════════════════════╗
|
|
33
|
-
* ║ MUST MATCH @nexart/codemode-sdk v1.1.0 EXACTLY ║
|
|
34
|
-
* ║ Any change here requires updating codemode-sdk's normalizeVars() too. ║
|
|
35
|
-
* ╚════════════════════════════════════════════════════════════════════════════╝
|
|
36
|
-
*
|
|
37
|
-
* Rules (SDK v1.1.0, Protocol v1.0.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.1.0 EXACTLY ║
|
|
124
|
-
* ║ Any change here requires updating codemode-sdk's createProtocolVAR() too. ║
|
|
125
|
-
* ╚════════════════════════════════════════════════════════════════════════════╝
|
|
126
|
-
*
|
|
127
|
-
* SDK v1.1.0 Rules (Protocol v1.0.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);
|
|
639
60
|
const ctx = canvas.getContext('2d');
|
|
640
61
|
let animationId = null;
|
|
641
62
|
let isRunning = false;
|
|
642
63
|
let isDestroyed = false;
|
|
64
|
+
const budget = createFrameBudget();
|
|
643
65
|
const normalizedVars = normalizeVars(system.vars);
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
66
|
+
let runtime = null;
|
|
67
|
+
let setupFn = null;
|
|
68
|
+
let drawFn = null;
|
|
69
|
+
const compileSource = () => {
|
|
70
|
+
runtime = createPreviewRuntime(canvas, scaled.renderWidth, scaled.renderHeight, system.seed ?? 12345, normalizedVars);
|
|
71
|
+
const totalFrames = system.totalFrames ?? 120;
|
|
72
|
+
runtime.totalFrames = totalFrames;
|
|
73
|
+
try {
|
|
74
|
+
const globalVars = Object.keys(runtime);
|
|
75
|
+
const globalValues = Object.values(runtime);
|
|
76
|
+
const wrappedSource = `
|
|
77
|
+
${system.source}
|
|
78
|
+
if (typeof setup === 'function') __registerSetup(setup);
|
|
79
|
+
if (typeof draw === 'function') __registerDraw(draw);
|
|
80
|
+
`;
|
|
81
|
+
const registerSetup = (fn) => { setupFn = fn; };
|
|
82
|
+
const registerDraw = (fn) => { drawFn = fn; };
|
|
83
|
+
globalVars.push('__registerSetup', '__registerDraw');
|
|
84
|
+
globalValues.push(registerSetup, registerDraw);
|
|
85
|
+
const fn = new Function(...globalVars, wrappedSource);
|
|
86
|
+
fn(...globalValues);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.warn('[UIRenderer] Compile error:', error);
|
|
90
|
+
}
|
|
651
91
|
};
|
|
652
92
|
const drawBadge = () => {
|
|
653
93
|
if (!showBadge)
|
|
654
94
|
return;
|
|
655
|
-
const text = '⚠️ Preview
|
|
656
|
-
ctx.font = '
|
|
95
|
+
const text = '⚠️ Preview';
|
|
96
|
+
ctx.font = '10px -apple-system, sans-serif';
|
|
657
97
|
const metrics = ctx.measureText(text);
|
|
658
|
-
const padding =
|
|
98
|
+
const padding = 6;
|
|
659
99
|
const badgeWidth = metrics.width + padding * 2;
|
|
660
|
-
const badgeHeight =
|
|
661
|
-
const x =
|
|
662
|
-
const y =
|
|
100
|
+
const badgeHeight = 18;
|
|
101
|
+
const x = scaled.renderWidth - badgeWidth - 6;
|
|
102
|
+
const y = 6;
|
|
663
103
|
ctx.fillStyle = 'rgba(255, 100, 100, 0.15)';
|
|
664
104
|
ctx.strokeStyle = 'rgba(255, 100, 100, 0.4)';
|
|
665
105
|
ctx.lineWidth = 1;
|
|
666
106
|
ctx.beginPath();
|
|
667
|
-
ctx.roundRect(x, y, badgeWidth, badgeHeight,
|
|
107
|
+
ctx.roundRect(x, y, badgeWidth, badgeHeight, 3);
|
|
668
108
|
ctx.fill();
|
|
669
109
|
ctx.stroke();
|
|
670
110
|
ctx.fillStyle = '#ff9999';
|
|
671
|
-
ctx.fillText(text, x + padding, y +
|
|
111
|
+
ctx.fillText(text, x + padding, y + 13);
|
|
672
112
|
};
|
|
673
113
|
const renderBlackCanvas = (error) => {
|
|
674
114
|
ctx.fillStyle = '#000000';
|
|
675
|
-
ctx.fillRect(0, 0,
|
|
115
|
+
ctx.fillRect(0, 0, scaled.renderWidth, scaled.renderHeight);
|
|
676
116
|
ctx.fillStyle = '#ff6666';
|
|
677
|
-
ctx.font = '
|
|
678
|
-
ctx.fillText('[
|
|
679
|
-
ctx.fillText(error.message,
|
|
117
|
+
ctx.font = '12px monospace';
|
|
118
|
+
ctx.fillText('[Preview Error]', 10, 20);
|
|
119
|
+
ctx.fillText(error.message.slice(0, 60), 10, 36);
|
|
680
120
|
};
|
|
681
121
|
const renderStatic = () => {
|
|
682
122
|
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);
|
|
123
|
+
compileSource();
|
|
124
|
+
if (setupFn) {
|
|
125
|
+
setupFn();
|
|
126
|
+
}
|
|
690
127
|
drawBadge();
|
|
691
128
|
onPreview?.(canvas);
|
|
692
129
|
canvas.toBlob((blob) => {
|
|
@@ -697,7 +134,7 @@ export function renderCodeModeSystem(system, canvas, options = {}) {
|
|
|
697
134
|
}
|
|
698
135
|
catch (error) {
|
|
699
136
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
700
|
-
console.
|
|
137
|
+
console.warn('[UIRenderer] Static render error:', err.message);
|
|
701
138
|
renderBlackCanvas(err);
|
|
702
139
|
onError?.(err);
|
|
703
140
|
}
|
|
@@ -706,38 +143,53 @@ export function renderCodeModeSystem(system, canvas, options = {}) {
|
|
|
706
143
|
if (isDestroyed)
|
|
707
144
|
return;
|
|
708
145
|
try {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
}
|
|
715
|
-
const totalFrames = system.totalFrames ??
|
|
716
|
-
let
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
146
|
+
compileSource();
|
|
147
|
+
if (!drawFn) {
|
|
148
|
+
console.warn('[UIRenderer] Loop mode: no draw() function found, using static');
|
|
149
|
+
renderStatic();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const totalFrames = system.totalFrames ?? 120;
|
|
153
|
+
let frameCount = 0;
|
|
154
|
+
if (setupFn) {
|
|
155
|
+
setupFn();
|
|
156
|
+
}
|
|
157
|
+
resetBudget(budget);
|
|
158
|
+
isRunning = true;
|
|
721
159
|
const loop = () => {
|
|
722
160
|
if (!isRunning || isDestroyed)
|
|
723
161
|
return;
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
162
|
+
if (!canRenderFrame(budget)) {
|
|
163
|
+
console.log(`[UIRenderer] Budget exhausted: ${budget.exhaustionReason}`);
|
|
164
|
+
isRunning = false;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
frameCount++;
|
|
168
|
+
if (!shouldSkipFrame(frameCount)) {
|
|
169
|
+
if (runtime) {
|
|
170
|
+
runtime.frameCount = frameCount;
|
|
171
|
+
runtime.t = (frameCount % totalFrames) / totalFrames;
|
|
172
|
+
runtime.time = runtime.t;
|
|
173
|
+
runtime.tGlobal = runtime.t;
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
ctx.clearRect(0, 0, scaled.renderWidth, scaled.renderHeight);
|
|
177
|
+
if (drawFn)
|
|
178
|
+
drawFn();
|
|
179
|
+
drawBadge();
|
|
180
|
+
recordFrame(budget);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
console.warn('[UIRenderer] Draw error:', error);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
733
186
|
animationId = requestAnimationFrame(loop);
|
|
734
187
|
};
|
|
735
|
-
isRunning = true;
|
|
736
188
|
animationId = requestAnimationFrame(loop);
|
|
737
189
|
}
|
|
738
190
|
catch (error) {
|
|
739
191
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
740
|
-
console.
|
|
192
|
+
console.warn('[UIRenderer] Loop render error:', err.message);
|
|
741
193
|
renderBlackCanvas(err);
|
|
742
194
|
onError?.(err);
|
|
743
195
|
}
|
|
@@ -773,7 +225,10 @@ export function renderCodeModeSystem(system, canvas, options = {}) {
|
|
|
773
225
|
const destroy = () => {
|
|
774
226
|
isDestroyed = true;
|
|
775
227
|
stop();
|
|
776
|
-
ctx.clearRect(0, 0,
|
|
228
|
+
ctx.clearRect(0, 0, scaled.renderWidth, scaled.renderHeight);
|
|
229
|
+
runtime = null;
|
|
230
|
+
setupFn = null;
|
|
231
|
+
drawFn = null;
|
|
777
232
|
if (activeRendererInstance === renderer) {
|
|
778
233
|
activeRendererInstance = null;
|
|
779
234
|
}
|