@nexart/codemode-sdk 1.4.0 → 1.5.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 (93) hide show
  1. package/CHANGELOG.md +289 -0
  2. package/CODE_MODE_PROTOCOL.md +471 -0
  3. package/README.md +237 -50
  4. package/dist/builder-manifest.d.ts +79 -0
  5. package/dist/builder-manifest.d.ts.map +1 -0
  6. package/dist/builder-manifest.js +97 -0
  7. package/dist/core-index.d.ts +21 -0
  8. package/dist/core-index.d.ts.map +1 -0
  9. package/dist/core-index.js +26 -0
  10. package/dist/engine.d.ts +1 -1
  11. package/dist/engine.js +1 -1
  12. package/dist/execute.d.ts +46 -0
  13. package/dist/execute.d.ts.map +1 -0
  14. package/dist/execute.js +282 -0
  15. package/dist/index.d.ts +27 -19
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +34 -19
  18. package/dist/loop-engine.d.ts +5 -1
  19. package/dist/loop-engine.d.ts.map +1 -1
  20. package/dist/loop-engine.js +25 -14
  21. package/dist/noise-bridge.d.ts +44 -0
  22. package/dist/noise-bridge.d.ts.map +1 -0
  23. package/dist/noise-bridge.js +68 -0
  24. package/dist/noise-engine.d.ts +74 -0
  25. package/dist/noise-engine.d.ts.map +1 -0
  26. package/dist/noise-engine.js +132 -0
  27. package/dist/noise-sketches/fractalNoise.d.ts +11 -0
  28. package/dist/noise-sketches/fractalNoise.d.ts.map +1 -0
  29. package/dist/noise-sketches/fractalNoise.js +121 -0
  30. package/dist/noise-sketches/index.d.ts +21 -0
  31. package/dist/noise-sketches/index.d.ts.map +1 -0
  32. package/dist/noise-sketches/index.js +28 -0
  33. package/dist/p5-runtime.d.ts +56 -4
  34. package/dist/p5-runtime.d.ts.map +1 -1
  35. package/dist/p5-runtime.js +743 -32
  36. package/dist/sound-bridge.d.ts +89 -0
  37. package/dist/sound-bridge.d.ts.map +1 -0
  38. package/dist/sound-bridge.js +128 -0
  39. package/dist/soundart-engine.d.ts +87 -0
  40. package/dist/soundart-engine.d.ts.map +1 -0
  41. package/dist/soundart-engine.js +173 -0
  42. package/dist/soundart-sketches/chladniBloom.d.ts +3 -0
  43. package/dist/soundart-sketches/chladniBloom.d.ts.map +1 -0
  44. package/dist/soundart-sketches/chladniBloom.js +53 -0
  45. package/dist/soundart-sketches/dualVortex.d.ts +3 -0
  46. package/dist/soundart-sketches/dualVortex.d.ts.map +1 -0
  47. package/dist/soundart-sketches/dualVortex.js +67 -0
  48. package/dist/soundart-sketches/geometryIllusion.d.ts +3 -0
  49. package/dist/soundart-sketches/geometryIllusion.d.ts.map +1 -0
  50. package/dist/soundart-sketches/geometryIllusion.js +89 -0
  51. package/dist/soundart-sketches/index.d.ts +39 -0
  52. package/dist/soundart-sketches/index.d.ts.map +1 -0
  53. package/dist/soundart-sketches/index.js +72 -0
  54. package/dist/soundart-sketches/isoflow.d.ts +3 -0
  55. package/dist/soundart-sketches/isoflow.d.ts.map +1 -0
  56. package/dist/soundart-sketches/isoflow.js +60 -0
  57. package/dist/soundart-sketches/loomWeave.d.ts +3 -0
  58. package/dist/soundart-sketches/loomWeave.d.ts.map +1 -0
  59. package/dist/soundart-sketches/loomWeave.js +59 -0
  60. package/dist/soundart-sketches/noiseTerraces.d.ts +3 -0
  61. package/dist/soundart-sketches/noiseTerraces.d.ts.map +1 -0
  62. package/dist/soundart-sketches/noiseTerraces.js +53 -0
  63. package/dist/soundart-sketches/orb.d.ts +3 -0
  64. package/dist/soundart-sketches/orb.d.ts.map +1 -0
  65. package/dist/soundart-sketches/orb.js +50 -0
  66. package/dist/soundart-sketches/pixelGlyphs.d.ts +3 -0
  67. package/dist/soundart-sketches/pixelGlyphs.d.ts.map +1 -0
  68. package/dist/soundart-sketches/pixelGlyphs.js +72 -0
  69. package/dist/soundart-sketches/prismFlowFields.d.ts +3 -0
  70. package/dist/soundart-sketches/prismFlowFields.d.ts.map +1 -0
  71. package/dist/soundart-sketches/prismFlowFields.js +51 -0
  72. package/dist/soundart-sketches/radialBurst.d.ts +3 -0
  73. package/dist/soundart-sketches/radialBurst.d.ts.map +1 -0
  74. package/dist/soundart-sketches/radialBurst.js +60 -0
  75. package/dist/soundart-sketches/resonantSoundBodies.d.ts +3 -0
  76. package/dist/soundart-sketches/resonantSoundBodies.d.ts.map +1 -0
  77. package/dist/soundart-sketches/resonantSoundBodies.js +89 -0
  78. package/dist/soundart-sketches/rings.d.ts +11 -0
  79. package/dist/soundart-sketches/rings.d.ts.map +1 -0
  80. package/dist/soundart-sketches/rings.js +89 -0
  81. package/dist/soundart-sketches/squares.d.ts +3 -0
  82. package/dist/soundart-sketches/squares.d.ts.map +1 -0
  83. package/dist/soundart-sketches/squares.js +52 -0
  84. package/dist/soundart-sketches/waveStripes.d.ts +3 -0
  85. package/dist/soundart-sketches/waveStripes.d.ts.map +1 -0
  86. package/dist/soundart-sketches/waveStripes.js +44 -0
  87. package/dist/static-engine.d.ts +4 -1
  88. package/dist/static-engine.d.ts.map +1 -1
  89. package/dist/static-engine.js +13 -8
  90. package/dist/types.d.ts +43 -2
  91. package/dist/types.d.ts.map +1 -1
  92. package/dist/types.js +1 -1
  93. package/package.json +24 -15
@@ -1,11 +1,97 @@
1
1
  /**
2
2
  * NexArt Code Mode Runtime SDK - p5-like Runtime
3
- * Version: 1.4.0 (Protocol v1.2.0)
4
3
  *
5
- * Minimal p5.js-like runtime for deterministic generative art execution.
6
- * This is a headless runtime - no UI dependencies.
4
+ * ╔══════════════════════════════════════════════════════════════════════════╗
5
+ * ║ CODE MODE PROTOCOL v1.2.0 (Phase 3) STABLE ║
6
+ * ║ ║
7
+ * ║ Status: HARD PROTOCOL ENFORCEMENT ║
8
+ * ║ This is the stable, canonical execution surface. ║
9
+ * ║ SDKs, ByX, and external builders can depend on this API. ║
10
+ * ║ ║
11
+ * ║ Phase 1 Surface: ║
12
+ * ║ - VAR[0..9]: 10 read-only protocol variables (0-100 range) ║
13
+ * ║ - Drawing: line, rect, ellipse, circle, triangle, quad, arc, etc. ║
14
+ * ║ - Style: fill, stroke, colorMode, strokeWeight ║
15
+ * ║ - Transform: push, pop, translate, rotate, scale ║
16
+ * ║ - Random: random(), randomSeed(), randomGaussian() (seeded) ║
17
+ * ║ - Noise: noise(), noiseSeed(), noiseDetail() (seeded) ║
18
+ * ║ - Math: map, constrain, lerp, lerpColor, dist, mag, norm ║
19
+ * ║ - Color: Full CSS format support, color extraction functions ║
20
+ * ║ - Time: frameCount, t, time, tGlobal ║
21
+ * ║ ║
22
+ * ║ Determinism Guarantees: ║
23
+ * ║ - Same code + same seed + same VARs = identical output ║
24
+ * ║ - No external state, no browser entropy, no time-based drift ║
25
+ * ║ - Randomness ONLY from: random(), noise() (both seeded) ║
26
+ * ║ ║
27
+ * ║ ⚠️ Future changes require Phase 2+ ║
28
+ * ╚══════════════════════════════════════════════════════════════════════════╝
7
29
  */
8
- export function createP5Runtime(canvas, width, height) {
30
+ /**
31
+ * Code Mode Protocol Version
32
+ * This constant defines the locked protocol version.
33
+ * Changes to the execution surface require a version bump.
34
+ */
35
+ export const CODE_MODE_PROTOCOL_VERSION = '1.2.0';
36
+ export const CODE_MODE_PROTOCOL_PHASE = 3;
37
+ export const CODE_MODE_ENFORCEMENT = 'HARD';
38
+ /**
39
+ * Create a seeded random number generator (Mulberry32)
40
+ * Same algorithm used in SoundArt for consistency
41
+ */
42
+ function createSeededRNG(seed = 123456) {
43
+ let a = seed >>> 0;
44
+ return () => {
45
+ a += 0x6D2B79F5;
46
+ let t = Math.imul(a ^ (a >>> 15), a | 1);
47
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
48
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
49
+ };
50
+ }
51
+ /**
52
+ * Create improved Perlin-like noise with seeding support
53
+ */
54
+ function createSeededNoise(seed = 0) {
55
+ const permutation = [];
56
+ const rng = createSeededRNG(seed);
57
+ for (let i = 0; i < 256; i++) {
58
+ permutation[i] = i;
59
+ }
60
+ for (let i = 255; i > 0; i--) {
61
+ const j = Math.floor(rng() * (i + 1));
62
+ [permutation[i], permutation[j]] = [permutation[j], permutation[i]];
63
+ }
64
+ for (let i = 0; i < 256; i++) {
65
+ permutation[256 + i] = permutation[i];
66
+ }
67
+ const fade = (t) => t * t * t * (t * (t * 6 - 15) + 10);
68
+ const lerp = (a, b, t) => a + t * (b - a);
69
+ const grad = (hash, x, y, z) => {
70
+ const h = hash & 15;
71
+ const u = h < 8 ? x : y;
72
+ const v = h < 4 ? y : h === 12 || h === 14 ? x : z;
73
+ return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
74
+ };
75
+ return (x, y = 0, z = 0) => {
76
+ const X = Math.floor(x) & 255;
77
+ const Y = Math.floor(y) & 255;
78
+ const Z = Math.floor(z) & 255;
79
+ x -= Math.floor(x);
80
+ y -= Math.floor(y);
81
+ z -= Math.floor(z);
82
+ const u = fade(x);
83
+ const v = fade(y);
84
+ const w = fade(z);
85
+ const A = permutation[X] + Y;
86
+ const AA = permutation[A] + Z;
87
+ const AB = permutation[A + 1] + Z;
88
+ const B = permutation[X + 1] + Y;
89
+ const BA = permutation[B] + Z;
90
+ const BB = permutation[B + 1] + Z;
91
+ 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;
92
+ };
93
+ }
94
+ export function createP5Runtime(canvas, width, height, config) {
9
95
  const ctx = canvas.getContext('2d', { willReadFrequently: true });
10
96
  let currentFill = 'rgba(255, 255, 255, 1)';
11
97
  let currentStroke = 'rgba(0, 0, 0, 1)';
@@ -14,14 +100,128 @@ export function createP5Runtime(canvas, width, height) {
14
100
  let currentStrokeWeight = 1;
15
101
  let colorModeSettings = { mode: 'RGB', maxR: 255, maxG: 255, maxB: 255, maxA: 255 };
16
102
  let shapeStarted = false;
103
+ let shapeVertices = [];
104
+ // Pixel array for pixel manipulation
105
+ let pixelData = null;
106
+ let imageDataObj = null;
107
+ // Text state
108
+ let currentTextSize = 12;
109
+ let currentTextFont = 'sans-serif';
110
+ let currentTextAlignH = 'left';
111
+ let currentTextAlignV = 'alphabetic';
112
+ // Seeded random state
113
+ let randomSeedValue = config?.seed ?? Math.floor(Math.random() * 2147483647);
114
+ let rng = createSeededRNG(randomSeedValue);
115
+ // Seeded noise state
116
+ let noiseSeedValue = config?.seed ?? 0;
117
+ let noiseFunc = createSeededNoise(noiseSeedValue);
118
+ let noiseOctaves = 4;
119
+ let noiseFalloff = 0.5;
120
+ /**
121
+ * Parse CSS color string to normalized RGBA values
122
+ * Supports: hex (#RGB, #RRGGBB, #RRGGBBAA), rgb(), rgba(), hsl(), hsla()
123
+ */
124
+ const parseCssColor = (str) => {
125
+ const s = str.trim();
126
+ // Hex format: #RGB, #RRGGBB, #RRGGBBAA
127
+ if (s.startsWith('#')) {
128
+ const hex = s.slice(1);
129
+ if (hex.length === 3) {
130
+ const r = parseInt(hex[0] + hex[0], 16);
131
+ const g = parseInt(hex[1] + hex[1], 16);
132
+ const b = parseInt(hex[2] + hex[2], 16);
133
+ return { r, g, b, a: 1 };
134
+ }
135
+ else if (hex.length === 6) {
136
+ const r = parseInt(hex.slice(0, 2), 16);
137
+ const g = parseInt(hex.slice(2, 4), 16);
138
+ const b = parseInt(hex.slice(4, 6), 16);
139
+ return { r, g, b, a: 1 };
140
+ }
141
+ else if (hex.length === 8) {
142
+ const r = parseInt(hex.slice(0, 2), 16);
143
+ const g = parseInt(hex.slice(2, 4), 16);
144
+ const b = parseInt(hex.slice(4, 6), 16);
145
+ const a = parseInt(hex.slice(6, 8), 16) / 255;
146
+ return { r, g, b, a };
147
+ }
148
+ }
149
+ // rgb(r, g, b) or rgb(r g b)
150
+ const rgbMatch = s.match(/^rgb\s*\(\s*(\d+)\s*[,\s]\s*(\d+)\s*[,\s]\s*(\d+)\s*\)$/i);
151
+ if (rgbMatch) {
152
+ return {
153
+ r: parseInt(rgbMatch[1]),
154
+ g: parseInt(rgbMatch[2]),
155
+ b: parseInt(rgbMatch[3]),
156
+ a: 1
157
+ };
158
+ }
159
+ // rgba(r, g, b, a) or rgba(r g b / a)
160
+ const rgbaMatch = s.match(/^rgba\s*\(\s*(\d+)\s*[,\s]\s*(\d+)\s*[,\s]\s*(\d+)\s*[,\/\s]\s*([\d.]+)\s*\)$/i);
161
+ if (rgbaMatch) {
162
+ return {
163
+ r: parseInt(rgbaMatch[1]),
164
+ g: parseInt(rgbaMatch[2]),
165
+ b: parseInt(rgbaMatch[3]),
166
+ a: parseFloat(rgbaMatch[4])
167
+ };
168
+ }
169
+ // hsl(h, s%, l%) or hsla(h, s%, l%, a)
170
+ const hslMatch = s.match(/^hsla?\s*\(\s*([\d.]+)\s*[,\s]\s*([\d.]+)%?\s*[,\s]\s*([\d.]+)%?\s*(?:[,\/\s]\s*([\d.]+))?\s*\)$/i);
171
+ if (hslMatch) {
172
+ const h = parseFloat(hslMatch[1]) / 360;
173
+ const sat = parseFloat(hslMatch[2]) / 100;
174
+ const l = parseFloat(hslMatch[3]) / 100;
175
+ const a = hslMatch[4] ? parseFloat(hslMatch[4]) : 1;
176
+ // HSL to RGB conversion
177
+ let r, g, b;
178
+ if (sat === 0) {
179
+ r = g = b = l;
180
+ }
181
+ else {
182
+ const hue2rgb = (p, q, t) => {
183
+ if (t < 0)
184
+ t += 1;
185
+ if (t > 1)
186
+ t -= 1;
187
+ if (t < 1 / 6)
188
+ return p + (q - p) * 6 * t;
189
+ if (t < 1 / 2)
190
+ return q;
191
+ if (t < 2 / 3)
192
+ return p + (q - p) * (2 / 3 - t) * 6;
193
+ return p;
194
+ };
195
+ const q = l < 0.5 ? l * (1 + sat) : l + sat - l * sat;
196
+ const p = 2 * l - q;
197
+ r = hue2rgb(p, q, h + 1 / 3);
198
+ g = hue2rgb(p, q, h);
199
+ b = hue2rgb(p, q, h - 1 / 3);
200
+ }
201
+ return {
202
+ r: Math.round(r * 255),
203
+ g: Math.round(g * 255),
204
+ b: Math.round(b * 255),
205
+ a
206
+ };
207
+ }
208
+ return null;
209
+ };
17
210
  const parseColor = (...args) => {
18
211
  if (args.length === 0)
19
212
  return 'rgba(0, 0, 0, 1)';
20
213
  const { mode, maxR, maxG, maxB, maxA } = colorModeSettings;
21
214
  if (args.length === 1) {
22
215
  const val = args[0];
23
- if (typeof val === 'string')
216
+ if (typeof val === 'string') {
217
+ // Try to parse CSS color formats
218
+ const parsed = parseCssColor(val);
219
+ if (parsed) {
220
+ return `rgba(${parsed.r}, ${parsed.g}, ${parsed.b}, ${parsed.a})`;
221
+ }
222
+ // Return as-is for named colors (canvas handles them)
24
223
  return val;
224
+ }
25
225
  if (mode === 'HSB') {
26
226
  return `hsla(${val}, 100%, 50%, 1)`;
27
227
  }
@@ -57,6 +257,7 @@ export function createP5Runtime(canvas, width, height) {
57
257
  // Constants
58
258
  PI: Math.PI,
59
259
  TWO_PI: Math.PI * 2,
260
+ TAU: Math.PI * 2,
60
261
  HALF_PI: Math.PI / 2,
61
262
  QUARTER_PI: Math.PI / 4,
62
263
  // Shape mode constants
@@ -65,11 +266,25 @@ export function createP5Runtime(canvas, width, height) {
65
266
  CORNERS: 'corners',
66
267
  RADIUS: 'radius',
67
268
  ROUND: 'round',
68
- SQUARE: 'square',
69
- PROJECT: 'project',
269
+ SQUARE: 'butt',
270
+ PROJECT: 'square',
70
271
  MITER: 'miter',
71
272
  BEVEL: 'bevel',
72
273
  CLOSE: 'close',
274
+ PIE: 'pie',
275
+ CHORD: 'chord',
276
+ OPEN: 'open',
277
+ // Blend mode constants (v1.1)
278
+ NORMAL: 'source-over',
279
+ ADD: 'lighter',
280
+ MULTIPLY: 'multiply',
281
+ SCREEN: 'screen',
282
+ // Text alignment constants
283
+ LEFT: 'left',
284
+ RIGHT: 'right',
285
+ TOP: 'top',
286
+ BOTTOM: 'bottom',
287
+ BASELINE: 'alphabetic',
73
288
  // Canvas operations
74
289
  background: (...args) => {
75
290
  ctx.save();
@@ -80,6 +295,23 @@ export function createP5Runtime(canvas, width, height) {
80
295
  clear: () => {
81
296
  ctx.clearRect(0, 0, width, height);
82
297
  },
298
+ blendMode: (mode) => {
299
+ const modeMap = {
300
+ 'source-over': 'source-over',
301
+ 'NORMAL': 'source-over',
302
+ 'lighter': 'lighter',
303
+ 'ADD': 'lighter',
304
+ 'multiply': 'multiply',
305
+ 'MULTIPLY': 'multiply',
306
+ 'screen': 'screen',
307
+ 'SCREEN': 'screen',
308
+ };
309
+ const compositeOp = modeMap[mode];
310
+ if (!compositeOp) {
311
+ throw new Error(`[Code Mode Protocol Error] Unsupported blend mode: ${mode}. Supported: NORMAL, ADD, MULTIPLY, SCREEN`);
312
+ }
313
+ ctx.globalCompositeOperation = compositeOp;
314
+ },
83
315
  // Color functions
84
316
  fill: (...args) => {
85
317
  fillEnabled = true;
@@ -101,6 +333,29 @@ export function createP5Runtime(canvas, width, height) {
101
333
  currentStrokeWeight = weight;
102
334
  ctx.lineWidth = weight;
103
335
  },
336
+ strokeCap: (cap) => {
337
+ const capMap = {
338
+ 'round': 'round',
339
+ 'ROUND': 'round',
340
+ 'square': 'butt',
341
+ 'SQUARE': 'butt',
342
+ 'project': 'square',
343
+ 'PROJECT': 'square',
344
+ 'butt': 'butt',
345
+ };
346
+ ctx.lineCap = capMap[cap] || 'round';
347
+ },
348
+ strokeJoin: (join) => {
349
+ const joinMap = {
350
+ 'miter': 'miter',
351
+ 'MITER': 'miter',
352
+ 'bevel': 'bevel',
353
+ 'BEVEL': 'bevel',
354
+ 'round': 'round',
355
+ 'ROUND': 'round',
356
+ };
357
+ ctx.lineJoin = joinMap[join] || 'miter';
358
+ },
104
359
  colorMode: (mode, max1, max2, max3, maxA) => {
105
360
  colorModeSettings = {
106
361
  mode: mode.toUpperCase(),
@@ -112,7 +367,71 @@ export function createP5Runtime(canvas, width, height) {
112
367
  },
113
368
  color: (...args) => parseColor(...args),
114
369
  lerpColor: (c1, c2, amt) => {
115
- return c1; // Simplified - full implementation would blend colors
370
+ // Parse both colors
371
+ const color1 = parseCssColor(c1) || { r: 0, g: 0, b: 0, a: 1 };
372
+ const color2 = parseCssColor(c2) || { r: 255, g: 255, b: 255, a: 1 };
373
+ // Linearly interpolate each channel
374
+ const r = Math.round(color1.r + (color2.r - color1.r) * amt);
375
+ const g = Math.round(color1.g + (color2.g - color1.g) * amt);
376
+ const b = Math.round(color1.b + (color2.b - color1.b) * amt);
377
+ const a = color1.a + (color2.a - color1.a) * amt;
378
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
379
+ },
380
+ red: (color) => {
381
+ const parsed = parseCssColor(color);
382
+ return parsed ? parsed.r : 0;
383
+ },
384
+ green: (color) => {
385
+ const parsed = parseCssColor(color);
386
+ return parsed ? parsed.g : 0;
387
+ },
388
+ blue: (color) => {
389
+ const parsed = parseCssColor(color);
390
+ return parsed ? parsed.b : 0;
391
+ },
392
+ alpha: (color) => {
393
+ const parsed = parseCssColor(color);
394
+ return parsed ? parsed.a * 255 : 255;
395
+ },
396
+ brightness: (color) => {
397
+ const parsed = parseCssColor(color);
398
+ if (!parsed)
399
+ return 0;
400
+ return Math.max(parsed.r, parsed.g, parsed.b) / 255 * 100;
401
+ },
402
+ saturation: (color) => {
403
+ const parsed = parseCssColor(color);
404
+ if (!parsed)
405
+ return 0;
406
+ const max = Math.max(parsed.r, parsed.g, parsed.b);
407
+ const min = Math.min(parsed.r, parsed.g, parsed.b);
408
+ if (max === 0)
409
+ return 0;
410
+ return ((max - min) / max) * 100;
411
+ },
412
+ hue: (color) => {
413
+ const parsed = parseCssColor(color);
414
+ if (!parsed)
415
+ return 0;
416
+ const { r, g, b } = parsed;
417
+ const max = Math.max(r, g, b);
418
+ const min = Math.min(r, g, b);
419
+ if (max === min)
420
+ return 0;
421
+ let h = 0;
422
+ const d = max - min;
423
+ switch (max) {
424
+ case r:
425
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
426
+ break;
427
+ case g:
428
+ h = ((b - r) / d + 2) / 6;
429
+ break;
430
+ case b:
431
+ h = ((r - g) / d + 4) / 6;
432
+ break;
433
+ }
434
+ return h * 360;
116
435
  },
117
436
  // Shape functions
118
437
  ellipse: (x, y, w, h) => {
@@ -196,21 +515,144 @@ export function createP5Runtime(canvas, width, height) {
196
515
  if (strokeEnabled)
197
516
  ctx.stroke();
198
517
  },
518
+ // Bezier and curve functions
519
+ bezier: (x1, y1, cx1, cy1, cx2, cy2, x2, y2) => {
520
+ ctx.beginPath();
521
+ ctx.moveTo(x1, y1);
522
+ ctx.bezierCurveTo(cx1, cy1, cx2, cy2, x2, y2);
523
+ if (strokeEnabled)
524
+ ctx.stroke();
525
+ },
526
+ curve: (x1, y1, x2, y2, x3, y3, x4, y4) => {
527
+ // Catmull-Rom spline conversion to cubic bezier
528
+ // The curve is drawn from (x2,y2) to (x3,y3) using (x1,y1) and (x4,y4) as control points
529
+ const tension = 1 / 6;
530
+ const cp1x = x2 + (x3 - x1) * tension;
531
+ const cp1y = y2 + (y3 - y1) * tension;
532
+ const cp2x = x3 - (x4 - x2) * tension;
533
+ const cp2y = y3 - (y4 - y2) * tension;
534
+ ctx.beginPath();
535
+ ctx.moveTo(x2, y2);
536
+ ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x3, y3);
537
+ if (strokeEnabled)
538
+ ctx.stroke();
539
+ },
540
+ // Shape helpers (v1.1)
541
+ polygon: (cx, cy, radius, sides, rotation = 0) => {
542
+ ctx.beginPath();
543
+ for (let i = 0; i < sides; i++) {
544
+ const angle = rotation + (i / sides) * Math.PI * 2 - Math.PI / 2;
545
+ const x = cx + Math.cos(angle) * radius;
546
+ const y = cy + Math.sin(angle) * radius;
547
+ if (i === 0) {
548
+ ctx.moveTo(x, y);
549
+ }
550
+ else {
551
+ ctx.lineTo(x, y);
552
+ }
553
+ }
554
+ ctx.closePath();
555
+ if (fillEnabled)
556
+ ctx.fill();
557
+ if (strokeEnabled)
558
+ ctx.stroke();
559
+ },
560
+ star: (cx, cy, innerRadius, outerRadius, points, rotation = 0) => {
561
+ ctx.beginPath();
562
+ const totalPoints = points * 2;
563
+ for (let i = 0; i < totalPoints; i++) {
564
+ const angle = rotation + (i / totalPoints) * Math.PI * 2 - Math.PI / 2;
565
+ const radius = i % 2 === 0 ? outerRadius : innerRadius;
566
+ const x = cx + Math.cos(angle) * radius;
567
+ const y = cy + Math.sin(angle) * radius;
568
+ if (i === 0) {
569
+ ctx.moveTo(x, y);
570
+ }
571
+ else {
572
+ ctx.lineTo(x, y);
573
+ }
574
+ }
575
+ ctx.closePath();
576
+ if (fillEnabled)
577
+ ctx.fill();
578
+ if (strokeEnabled)
579
+ ctx.stroke();
580
+ },
199
581
  // Vertex-based shapes
200
582
  beginShape: () => {
201
- ctx.beginPath();
583
+ shapeVertices = [];
202
584
  shapeStarted = false;
203
585
  },
204
586
  vertex: (x, y) => {
205
- if (!shapeStarted) {
206
- ctx.moveTo(x, y);
207
- shapeStarted = true;
208
- }
209
- else {
210
- ctx.lineTo(x, y);
211
- }
587
+ shapeVertices.push({ x, y, type: 'vertex' });
588
+ },
589
+ curveVertex: (x, y) => {
590
+ shapeVertices.push({ x, y, type: 'curve' });
591
+ },
592
+ bezierVertex: (cx1, cy1, cx2, cy2, x, y) => {
593
+ shapeVertices.push({ x, y, type: 'bezier', cx1, cy1, cx2, cy2 });
212
594
  },
213
595
  endShape: (mode) => {
596
+ if (shapeVertices.length === 0)
597
+ return;
598
+ ctx.beginPath();
599
+ // Process vertices in sequence, handling mixed types correctly
600
+ let started = false;
601
+ let curveBuffer = [];
602
+ const tension = 1 / 6;
603
+ const flushCurveBuffer = () => {
604
+ if (curveBuffer.length >= 4) {
605
+ // Draw Catmull-Rom spline from buffered curve vertices
606
+ // First and last points are control points only
607
+ if (!started) {
608
+ ctx.moveTo(curveBuffer[1].x, curveBuffer[1].y);
609
+ started = true;
610
+ }
611
+ else {
612
+ ctx.lineTo(curveBuffer[1].x, curveBuffer[1].y);
613
+ }
614
+ for (let i = 1; i < curveBuffer.length - 2; i++) {
615
+ const p0 = curveBuffer[i - 1];
616
+ const p1 = curveBuffer[i];
617
+ const p2 = curveBuffer[i + 1];
618
+ const p3 = curveBuffer[i + 2];
619
+ const cp1x = p1.x + (p2.x - p0.x) * tension;
620
+ const cp1y = p1.y + (p2.y - p0.y) * tension;
621
+ const cp2x = p2.x - (p3.x - p1.x) * tension;
622
+ const cp2y = p2.y - (p3.y - p1.y) * tension;
623
+ ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y);
624
+ }
625
+ }
626
+ curveBuffer = [];
627
+ };
628
+ for (let i = 0; i < shapeVertices.length; i++) {
629
+ const v = shapeVertices[i];
630
+ if (v.type === 'curve') {
631
+ curveBuffer.push({ x: v.x, y: v.y });
632
+ }
633
+ else {
634
+ // Flush any pending curve vertices before processing non-curve vertex
635
+ if (curveBuffer.length > 0) {
636
+ flushCurveBuffer();
637
+ }
638
+ if (v.type === 'vertex') {
639
+ if (!started) {
640
+ ctx.moveTo(v.x, v.y);
641
+ started = true;
642
+ }
643
+ else {
644
+ ctx.lineTo(v.x, v.y);
645
+ }
646
+ }
647
+ else if (v.type === 'bezier' && started) {
648
+ ctx.bezierCurveTo(v.cx1, v.cy1, v.cx2, v.cy2, v.x, v.y);
649
+ }
650
+ }
651
+ }
652
+ // Flush any remaining curve vertices at the end
653
+ if (curveBuffer.length > 0) {
654
+ flushCurveBuffer();
655
+ }
214
656
  if (mode === 'close' || mode === 'CLOSE') {
215
657
  ctx.closePath();
216
658
  }
@@ -218,6 +660,7 @@ export function createP5Runtime(canvas, width, height) {
218
660
  ctx.fill();
219
661
  if (strokeEnabled)
220
662
  ctx.stroke();
663
+ shapeVertices = [];
221
664
  shapeStarted = false;
222
665
  },
223
666
  // Transform functions
@@ -242,30 +685,100 @@ export function createP5Runtime(canvas, width, height) {
242
685
  resetMatrix: () => {
243
686
  ctx.setTransform(1, 0, 0, 1, 0, 0);
244
687
  },
245
- // Math functions
688
+ shearX: (angle) => {
689
+ ctx.transform(1, 0, Math.tan(angle), 1, 0, 0);
690
+ },
691
+ shearY: (angle) => {
692
+ ctx.transform(1, Math.tan(angle), 0, 1, 0, 0);
693
+ },
694
+ // Math functions - SEEDED for determinism
246
695
  random: (min, max) => {
696
+ // Support random() with arrays
697
+ if (Array.isArray(min)) {
698
+ return min[Math.floor(rng() * min.length)];
699
+ }
247
700
  if (min === undefined)
248
- return Math.random();
701
+ return rng();
249
702
  if (max === undefined)
250
- return Math.random() * min;
251
- return min + Math.random() * (max - min);
703
+ return rng() * min;
704
+ return min + rng() * (max - min);
252
705
  },
253
706
  randomSeed: (seed) => {
254
- // Simplified - would need proper seeded random
707
+ randomSeedValue = seed;
708
+ rng = createSeededRNG(seed);
709
+ },
710
+ randomGaussian: (mean = 0, sd = 1) => {
711
+ // Box-Muller transform for Gaussian distribution
712
+ const u1 = rng();
713
+ const u2 = rng();
714
+ const z0 = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
715
+ return z0 * sd + mean;
255
716
  },
256
717
  noise: (x, y, z) => {
257
- // Simplified Perlin-like noise
258
- const hash = (n) => {
259
- const s = Math.sin(n) * 43758.5453123;
260
- return s - Math.floor(s);
261
- };
262
- const xVal = x ?? 0;
263
- const yVal = y ?? 0;
264
- const zVal = z ?? 0;
265
- return hash(xVal * 12.9898 + yVal * 78.233 + zVal * 37.719);
718
+ // Use seeded Perlin noise with octaves
719
+ let total = 0;
720
+ let frequency = 1;
721
+ let amplitude = 1;
722
+ let maxValue = 0;
723
+ for (let i = 0; i < noiseOctaves; i++) {
724
+ total += noiseFunc(x * frequency, (y ?? 0) * frequency, (z ?? 0) * frequency) * amplitude;
725
+ maxValue += amplitude;
726
+ amplitude *= noiseFalloff;
727
+ frequency *= 2;
728
+ }
729
+ return total / maxValue;
730
+ },
731
+ noiseSeed: (seed) => {
732
+ noiseSeedValue = seed;
733
+ noiseFunc = createSeededNoise(seed);
734
+ },
735
+ noiseDetail: (lod, falloff) => {
736
+ noiseOctaves = Math.max(1, Math.min(8, lod));
737
+ if (falloff !== undefined) {
738
+ noiseFalloff = Math.max(0, Math.min(1, falloff));
739
+ }
740
+ },
741
+ // Noise extensions (v1.1) - use seeded noise internally
742
+ fbm: (x, y, octaves = 4, falloff = 0.5) => {
743
+ let total = 0;
744
+ let frequency = 1;
745
+ let amplitude = 1;
746
+ let maxValue = 0;
747
+ for (let i = 0; i < octaves; i++) {
748
+ total += noiseFunc(x * frequency, y * frequency) * amplitude;
749
+ maxValue += amplitude;
750
+ amplitude *= falloff;
751
+ frequency *= 2;
752
+ }
753
+ return total / maxValue;
754
+ },
755
+ ridgedNoise: (x, y) => {
756
+ // Ridged noise: absolute value creates ridge-like patterns
757
+ let total = 0;
758
+ let frequency = 1;
759
+ let amplitude = 1;
760
+ let maxValue = 0;
761
+ for (let i = 0; i < 4; i++) {
762
+ const n = noiseFunc(x * frequency, y * frequency);
763
+ total += (1 - Math.abs(n * 2 - 1)) * amplitude;
764
+ maxValue += amplitude;
765
+ amplitude *= 0.5;
766
+ frequency *= 2;
767
+ }
768
+ return total / maxValue;
769
+ },
770
+ curlNoise: (x, y) => {
771
+ // Curl noise: compute gradient and rotate 90 degrees
772
+ const eps = 0.0001;
773
+ const n1 = noiseFunc(x + eps, y);
774
+ const n2 = noiseFunc(x - eps, y);
775
+ const n3 = noiseFunc(x, y + eps);
776
+ const n4 = noiseFunc(x, y - eps);
777
+ const dx = (n1 - n2) / (2 * eps);
778
+ const dy = (n3 - n4) / (2 * eps);
779
+ // Rotate 90 degrees: (dx, dy) -> (-dy, dx)
780
+ return { x: -dy, y: dx };
266
781
  },
267
- noiseSeed: (seed) => { },
268
- noiseDetail: (lod, falloff) => { },
269
782
  map: (value, start1, stop1, start2, stop2) => {
270
783
  return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1));
271
784
  },
@@ -305,14 +818,212 @@ export function createP5Runtime(canvas, width, height) {
305
818
  log: Math.log,
306
819
  min: Math.min,
307
820
  max: Math.max,
821
+ int: (n) => Math.floor(n),
822
+ sq: (n) => n * n,
823
+ fract: (n) => n - Math.floor(n),
824
+ sign: (n) => n > 0 ? 1 : n < 0 ? -1 : 0,
825
+ // Vector helpers (v1.1) - plain objects, no mutation
826
+ vec: (x, y) => ({ x, y }),
827
+ vecAdd: (a, b) => ({
828
+ x: a.x + b.x,
829
+ y: a.y + b.y
830
+ }),
831
+ vecSub: (a, b) => ({
832
+ x: a.x - b.x,
833
+ y: a.y - b.y
834
+ }),
835
+ vecMult: (v, s) => ({
836
+ x: v.x * s,
837
+ y: v.y * s
838
+ }),
839
+ vecMag: (v) => Math.sqrt(v.x * v.x + v.y * v.y),
840
+ vecNorm: (v) => {
841
+ const m = Math.sqrt(v.x * v.x + v.y * v.y);
842
+ return m === 0 ? { x: 0, y: 0 } : { x: v.x / m, y: v.y / m };
843
+ },
844
+ vecDist: (a, b) => {
845
+ const dx = b.x - a.x;
846
+ const dy = b.y - a.y;
847
+ return Math.sqrt(dx * dx + dy * dy);
848
+ },
849
+ // Easing functions (v1.1) - pure functions, t ∈ [0,1] → [0,1]
850
+ easeIn: (t) => t * t,
851
+ easeOut: (t) => t * (2 - t),
852
+ easeInOut: (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
853
+ easeCubic: (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2,
854
+ easeExpo: (t) => {
855
+ if (t === 0)
856
+ return 0;
857
+ if (t === 1)
858
+ return 1;
859
+ if (t < 0.5) {
860
+ return Math.pow(2, 20 * t - 10) / 2;
861
+ }
862
+ return (2 - Math.pow(2, -20 * t + 10)) / 2;
863
+ },
864
+ // Text functions
865
+ text: (str, x, y) => {
866
+ ctx.font = `${currentTextSize}px ${currentTextFont}`;
867
+ ctx.textAlign = currentTextAlignH;
868
+ ctx.textBaseline = currentTextAlignV;
869
+ if (fillEnabled) {
870
+ ctx.fillStyle = currentFill;
871
+ ctx.fillText(String(str), x, y);
872
+ }
873
+ if (strokeEnabled) {
874
+ ctx.strokeStyle = currentStroke;
875
+ ctx.strokeText(String(str), x, y);
876
+ }
877
+ },
878
+ textSize: (size) => {
879
+ currentTextSize = size;
880
+ ctx.font = `${currentTextSize}px ${currentTextFont}`;
881
+ },
882
+ textFont: (font) => {
883
+ currentTextFont = font;
884
+ ctx.font = `${currentTextSize}px ${currentTextFont}`;
885
+ },
886
+ textAlign: (horizAlign, vertAlign) => {
887
+ const hMap = {
888
+ 'left': 'left', 'LEFT': 'left',
889
+ 'center': 'center', 'CENTER': 'center',
890
+ 'right': 'right', 'RIGHT': 'right',
891
+ };
892
+ const vMap = {
893
+ 'top': 'top', 'TOP': 'top',
894
+ 'bottom': 'bottom', 'BOTTOM': 'bottom',
895
+ 'center': 'middle', 'CENTER': 'middle',
896
+ 'baseline': 'alphabetic', 'BASELINE': 'alphabetic',
897
+ };
898
+ currentTextAlignH = hMap[horizAlign] || 'left';
899
+ if (vertAlign) {
900
+ currentTextAlignV = vMap[vertAlign] || 'alphabetic';
901
+ }
902
+ },
903
+ textWidth: (str) => {
904
+ ctx.font = `${currentTextSize}px ${currentTextFont}`;
905
+ return ctx.measureText(String(str)).width;
906
+ },
907
+ // Pixel manipulation (v1.2)
908
+ loadPixels: () => {
909
+ imageDataObj = ctx.getImageData(0, 0, width, height);
910
+ pixelData = imageDataObj.data;
911
+ p.pixels = pixelData;
912
+ },
913
+ updatePixels: () => {
914
+ if (imageDataObj && pixelData) {
915
+ ctx.putImageData(imageDataObj, 0, 0);
916
+ }
917
+ },
918
+ pixels: null,
919
+ get: (x, y) => {
920
+ const imgData = ctx.getImageData(Math.floor(x), Math.floor(y), 1, 1);
921
+ return [imgData.data[0], imgData.data[1], imgData.data[2], imgData.data[3]];
922
+ },
923
+ set: (x, y, c) => {
924
+ const fx = Math.floor(x);
925
+ const fy = Math.floor(y);
926
+ if (Array.isArray(c)) {
927
+ const imgData = ctx.createImageData(1, 1);
928
+ imgData.data[0] = c[0];
929
+ imgData.data[1] = c[1];
930
+ imgData.data[2] = c[2];
931
+ imgData.data[3] = c[3] ?? 255;
932
+ ctx.putImageData(imgData, fx, fy);
933
+ }
934
+ else {
935
+ const parsed = parseCssColor(c);
936
+ if (parsed) {
937
+ const imgData = ctx.createImageData(1, 1);
938
+ imgData.data[0] = parsed.r;
939
+ imgData.data[1] = parsed.g;
940
+ imgData.data[2] = parsed.b;
941
+ imgData.data[3] = Math.round(parsed.a * 255);
942
+ ctx.putImageData(imgData, fx, fy);
943
+ }
944
+ }
945
+ },
946
+ // Offscreen graphics (v1.2)
947
+ createGraphics: (w, h) => {
948
+ const offscreenCanvas = document.createElement('canvas');
949
+ offscreenCanvas.width = w;
950
+ offscreenCanvas.height = h;
951
+ const pg = createP5Runtime(offscreenCanvas, w, h, config);
952
+ // Add reference to canvas for image() drawing
953
+ pg._canvas = offscreenCanvas;
954
+ return pg;
955
+ },
956
+ // Draw image or graphics object to canvas
957
+ image: (src, x, y, w, h) => {
958
+ const srcCanvas = src._canvas || src;
959
+ if (srcCanvas instanceof HTMLCanvasElement) {
960
+ const dw = w ?? srcCanvas.width;
961
+ const dh = h ?? srcCanvas.height;
962
+ ctx.drawImage(srcCanvas, x, y, dw, dh);
963
+ }
964
+ },
308
965
  // Loop control (no-ops for SDK)
309
966
  noLoop: () => { },
310
967
  loop: () => { },
311
968
  redraw: () => { },
312
969
  frameRate: (fps) => { },
970
+ // totalFrames placeholder (injected by engine)
971
+ totalFrames: 0,
313
972
  };
314
973
  return p;
315
974
  }
316
975
  export function injectTimeVariables(p, time) {
317
976
  p.frameCount = time.frameCount;
977
+ p.t = time.t;
978
+ p.time = time.time;
979
+ p.tGlobal = time.tGlobal;
980
+ p.totalFrames = time.totalFrames;
981
+ }
982
+ /**
983
+ * VAR Protocol Constants (Phase 1 — Protocol v1.0.0)
984
+ * SDK v1.0.2: VAR input is optional (0-10 elements), but runtime always has 10
985
+ */
986
+ export const VAR_COUNT = 10; // Exactly 10 protocol variables: VAR[0..9]
987
+ export const VAR_MIN = 0; // Minimum value
988
+ export const VAR_MAX = 100; // Maximum value (normalized range)
989
+ /**
990
+ * Create a protected, read-only VAR array for protocol execution.
991
+ *
992
+ * SDK v1.0.2 Rules (Protocol v1.0.0):
993
+ * - Input accepts 0-10 elements
994
+ * - Runtime VAR is ALWAYS 10 elements (padded with zeros)
995
+ * - Values are numeric, must be in 0-100 range (validated upstream)
996
+ * - Read-only: writes throw descriptive errors
997
+ * - Available in both setup() and draw()
998
+ */
999
+ export function createProtocolVAR(vars) {
1000
+ // Create frozen 10-element array (upstream normalizeVars ensures this)
1001
+ const normalizedVars = [];
1002
+ for (let i = 0; i < VAR_COUNT; i++) {
1003
+ normalizedVars[i] = vars?.[i] ?? 0;
1004
+ }
1005
+ // Freeze the array to prevent modifications
1006
+ const frozenVars = Object.freeze(normalizedVars);
1007
+ // Wrap in Proxy for descriptive error messages on write attempts
1008
+ return new Proxy(frozenVars, {
1009
+ set(_target, prop, _value) {
1010
+ const propName = typeof prop === 'symbol' ? prop.toString() : prop;
1011
+ throw new Error(`[Code Mode Protocol Error] VAR is read-only. ` +
1012
+ `Cannot write to VAR[${propName}]. ` +
1013
+ `VAR[0..9] are protocol inputs, not sketch state.`);
1014
+ },
1015
+ deleteProperty(_target, prop) {
1016
+ const propName = typeof prop === 'symbol' ? prop.toString() : prop;
1017
+ throw new Error(`[Code Mode Protocol Error] VAR is read-only. ` +
1018
+ `Cannot delete VAR[${propName}].`);
1019
+ },
1020
+ defineProperty(_target, prop) {
1021
+ const propName = typeof prop === 'symbol' ? prop.toString() : prop;
1022
+ throw new Error(`[Code Mode Protocol Error] Cannot define new VAR properties. ` +
1023
+ `VAR is fixed at 10 elements (VAR[0..9]). Attempted: ${propName}`);
1024
+ },
1025
+ });
1026
+ }
1027
+ export function injectProtocolVariables(p, vars) {
1028
+ p.VAR = createProtocolVAR(vars);
318
1029
  }