@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.
Files changed (38) hide show
  1. package/README.md +138 -319
  2. package/dist/capabilities.d.ts +5 -7
  3. package/dist/capabilities.d.ts.map +1 -1
  4. package/dist/capabilities.js +6 -8
  5. package/dist/compiler.d.ts +3 -3
  6. package/dist/compiler.js +4 -4
  7. package/dist/index.d.ts +33 -30
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +33 -30
  10. package/dist/preview/canvas-scaler.d.ts +70 -0
  11. package/dist/preview/canvas-scaler.d.ts.map +1 -0
  12. package/dist/preview/canvas-scaler.js +112 -0
  13. package/dist/preview/code-renderer.d.ts +11 -28
  14. package/dist/preview/code-renderer.d.ts.map +1 -1
  15. package/dist/preview/code-renderer.js +118 -660
  16. package/dist/preview/frame-budget.d.ts +43 -0
  17. package/dist/preview/frame-budget.d.ts.map +1 -0
  18. package/dist/preview/frame-budget.js +78 -0
  19. package/dist/preview/preview-engine.d.ts +42 -0
  20. package/dist/preview/preview-engine.d.ts.map +1 -0
  21. package/dist/preview/preview-engine.js +204 -0
  22. package/dist/preview/preview-runtime.d.ts +28 -0
  23. package/dist/preview/preview-runtime.d.ts.map +1 -0
  24. package/dist/preview/preview-runtime.js +512 -0
  25. package/dist/preview/preview-types.d.ts +116 -0
  26. package/dist/preview/preview-types.d.ts.map +1 -0
  27. package/dist/preview/preview-types.js +36 -0
  28. package/dist/preview/renderer.d.ts +3 -3
  29. package/dist/preview/renderer.js +3 -3
  30. package/dist/preview/unified-renderer.d.ts.map +1 -1
  31. package/dist/preview/unified-renderer.js +48 -22
  32. package/dist/system.d.ts +2 -2
  33. package/dist/system.d.ts.map +1 -1
  34. package/dist/system.js +8 -10
  35. package/dist/types.d.ts +9 -5
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/types.js +9 -5
  38. package/package.json +2 -2
@@ -1,692 +1,132 @@
1
1
  /**
2
- * @nexart/ui-renderer v0.7.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 v1.4.0 (Protocol v1.2.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
  */
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
- * Validate and normalize VAR array to 10 elements.
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 !== '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.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 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);
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
- 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
- };
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 (mirrors @nexart/codemode-sdk)';
656
- ctx.font = '12px -apple-system, sans-serif';
98
+ const text = '⚠️ Preview';
99
+ ctx.font = '10px -apple-system, sans-serif';
657
100
  const metrics = ctx.measureText(text);
658
- const padding = 8;
101
+ const padding = 6;
659
102
  const badgeWidth = metrics.width + padding * 2;
660
- const badgeHeight = 24;
661
- const x = system.width - badgeWidth - 10;
662
- const y = 10;
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, 4);
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 + 16);
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, system.width, system.height);
118
+ ctx.fillRect(0, 0, scaled.renderWidth, scaled.renderHeight);
676
119
  ctx.fillStyle = '#ff6666';
677
- ctx.font = '14px monospace';
678
- ctx.fillText('[Protocol Error]', 20, 30);
679
- ctx.fillText(error.message, 20, 50);
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
- 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);
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.error('[UIRenderer Protocol Error]', err.message);
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
- 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);
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
- 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;
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.error('[UIRenderer Protocol Error]', err.message);
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.clearRect(0, 0, system.width, system.height);
231
+ clearCanvasIgnoringTransform(ctx, canvas);
232
+ runtime = null;
233
+ setupFn = null;
234
+ drawFn = null;
777
235
  if (activeRendererInstance === renderer) {
778
236
  activeRendererInstance = null;
779
237
  }