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