@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.
Files changed (40) hide show
  1. package/README.md +133 -322
  2. package/dist/capabilities.d.ts +5 -6
  3. package/dist/capabilities.d.ts.map +1 -1
  4. package/dist/capabilities.js +6 -7
  5. package/dist/compiler.d.ts +3 -2
  6. package/dist/compiler.d.ts.map +1 -1
  7. package/dist/compiler.js +4 -3
  8. package/dist/index.d.ts +33 -29
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +33 -29
  11. package/dist/preview/canvas-scaler.d.ts +49 -0
  12. package/dist/preview/canvas-scaler.d.ts.map +1 -0
  13. package/dist/preview/canvas-scaler.js +74 -0
  14. package/dist/preview/code-renderer.d.ts +11 -28
  15. package/dist/preview/code-renderer.d.ts.map +1 -1
  16. package/dist/preview/code-renderer.js +116 -661
  17. package/dist/preview/frame-budget.d.ts +43 -0
  18. package/dist/preview/frame-budget.d.ts.map +1 -0
  19. package/dist/preview/frame-budget.js +78 -0
  20. package/dist/preview/preview-engine.d.ts +42 -0
  21. package/dist/preview/preview-engine.d.ts.map +1 -0
  22. package/dist/preview/preview-engine.js +201 -0
  23. package/dist/preview/preview-runtime.d.ts +28 -0
  24. package/dist/preview/preview-runtime.d.ts.map +1 -0
  25. package/dist/preview/preview-runtime.js +512 -0
  26. package/dist/preview/preview-types.d.ts +116 -0
  27. package/dist/preview/preview-types.d.ts.map +1 -0
  28. package/dist/preview/preview-types.js +36 -0
  29. package/dist/preview/renderer.d.ts +3 -2
  30. package/dist/preview/renderer.d.ts.map +1 -1
  31. package/dist/preview/renderer.js +3 -2
  32. package/dist/preview/unified-renderer.d.ts.map +1 -1
  33. package/dist/preview/unified-renderer.js +45 -22
  34. package/dist/system.d.ts +2 -2
  35. package/dist/system.d.ts.map +1 -1
  36. package/dist/system.js +8 -10
  37. package/dist/types.d.ts +9 -5
  38. package/dist/types.d.ts.map +1 -1
  39. package/dist/types.js +9 -5
  40. package/package.json +3 -3
@@ -1,692 +1,129 @@
1
1
  /**
2
- * @nexart/ui-renderer v0.6.0 - Code Mode Renderer
2
+ * @nexart/ui-renderer v0.8.0 - Code Mode Renderer
3
3
  *
4
4
  * ╔══════════════════════════════════════════════════════════════════════════╗
5
- * ║ PREVIEW RENDERER — MIRRORS @nexart/codemode-sdk BEHAVIOR
5
+ * ║ PREVIEW RENDERER — LIGHTWEIGHT, NON-AUTHORITATIVE
6
6
  * ║ ║
7
- * ║ This file is a MIRROR, not an authority. ║
7
+ * ║ This renderer is a preview-only runtime. ║
8
+ * ║ It does not guarantee determinism or protocol compliance. ║
8
9
  * ║ ║
9
- * ║ Authority: @nexart/codemode-sdk (Protocol v1.0.0)
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
- const PROTOCOL_VERSION = '1.0.0';
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
- * Validate and normalize VAR array to 10 elements.
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 !== 'number' || !Number.isFinite(v)) {
57
- throw new Error(`[Code Mode Protocol Error] VAR[${i}] must be a finite number, got ${typeof v === 'number' ? v : typeof v}`);
36
+ if (typeof v === 'number' && Number.isFinite(v)) {
37
+ result.push(Math.max(0, Math.min(100, v)));
58
38
  }
59
- if (v < 0 || v > 100) {
60
- throw new Error(`[Code Mode Protocol Error] VAR[${i}] = ${v} is out of range. Values must be 0-100.`);
39
+ else {
40
+ result.push(0);
61
41
  }
62
- result.push(v);
63
42
  }
64
- // Pad with zeros to always have 10 elements for protocol consistency
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 delegation@nexart/codemode-sdk');
630
- console.log(`[UIRenderer] Protocol version: ${PROTOCOL_VERSION}`);
631
- console.log(`[UIRenderer] Mode: ${system.mode}`);
48
+ console.log('[UIRenderer] Preview modelightweight 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
- canvas.width = system.width;
638
- canvas.height = system.height;
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
- const extractFunctions = (code) => {
645
- const setupMatch = code.match(/function\s+setup\s*\(\s*\)\s*\{([\s\S]*?)\}(?=\s*function|\s*$)/);
646
- const drawMatch = code.match(/function\s+draw\s*\(\s*\)\s*\{([\s\S]*?)\}(?=\s*function|\s*$)/);
647
- return {
648
- setupCode: setupMatch ? setupMatch[1].trim() : code,
649
- drawCode: drawMatch ? drawMatch[1].trim() : null,
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 (mirrors @nexart/codemode-sdk)';
656
- ctx.font = '12px -apple-system, sans-serif';
95
+ const text = '⚠️ Preview';
96
+ ctx.font = '10px -apple-system, sans-serif';
657
97
  const metrics = ctx.measureText(text);
658
- const padding = 8;
98
+ const padding = 6;
659
99
  const badgeWidth = metrics.width + padding * 2;
660
- const badgeHeight = 24;
661
- const x = system.width - badgeWidth - 10;
662
- const y = 10;
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, 4);
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 + 16);
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, system.width, system.height);
115
+ ctx.fillRect(0, 0, scaled.renderWidth, scaled.renderHeight);
676
116
  ctx.fillStyle = '#ff6666';
677
- ctx.font = '14px monospace';
678
- ctx.fillText('[Protocol Error]', 20, 30);
679
- ctx.fillText(error.message, 20, 50);
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
- console.log('[UIRenderer] Delegating static render via protocol-aligned runtime');
684
- validateForbiddenPatterns(system.source);
685
- const { setupCode } = extractFunctions(system.source);
686
- const p = createP5Runtime(canvas, system.width, system.height, system.seed, normalizedVars);
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.error('[UIRenderer Protocol Error]', err.message);
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
- console.log('[UIRenderer] Delegating loop render via protocol-aligned runtime');
710
- validateForbiddenPatterns(system.source);
711
- const { setupCode, drawCode } = extractFunctions(system.source);
712
- if (!drawCode) {
713
- throw new Error('[Protocol Error] Loop mode requires a draw() function');
714
- }
715
- const totalFrames = system.totalFrames ?? 60;
716
- let frame = 0;
717
- const p = createP5Runtime(canvas, system.width, system.height, system.seed, normalizedVars);
718
- const wrappedSetup = new Function('p', 'frameCount', 't', 'time', 'tGlobal', 'VAR', `with(p) { ${setupCode} }`);
719
- const wrappedDraw = new Function('p', 'frameCount', 't', 'time', 'tGlobal', 'VAR', `with(p) { ${drawCode} }`);
720
- wrappedSetup(p, 0, 0, 0, 0, p.VAR);
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
- const t = frame / totalFrames;
725
- const time = t * (system.totalFrames ? system.totalFrames / 30 : 2);
726
- ctx.clearRect(0, 0, system.width, system.height);
727
- injectTimeVariables(p, { frameCount: frame, t, time, tGlobal: t });
728
- p.randomSeed(system.seed);
729
- p.noiseSeed(system.seed);
730
- wrappedDraw(p, frame, t, time, t, p.VAR);
731
- drawBadge();
732
- frame = (frame + 1) % totalFrames;
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.error('[UIRenderer Protocol Error]', err.message);
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, system.width, system.height);
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
  }