@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.
- package/CHANGELOG.md +289 -0
- package/CODE_MODE_PROTOCOL.md +471 -0
- package/README.md +237 -50
- package/dist/builder-manifest.d.ts +79 -0
- package/dist/builder-manifest.d.ts.map +1 -0
- package/dist/builder-manifest.js +97 -0
- package/dist/core-index.d.ts +21 -0
- package/dist/core-index.d.ts.map +1 -0
- package/dist/core-index.js +26 -0
- package/dist/engine.d.ts +1 -1
- package/dist/engine.js +1 -1
- package/dist/execute.d.ts +46 -0
- package/dist/execute.d.ts.map +1 -0
- package/dist/execute.js +282 -0
- package/dist/index.d.ts +27 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -19
- package/dist/loop-engine.d.ts +5 -1
- package/dist/loop-engine.d.ts.map +1 -1
- package/dist/loop-engine.js +25 -14
- package/dist/noise-bridge.d.ts +44 -0
- package/dist/noise-bridge.d.ts.map +1 -0
- package/dist/noise-bridge.js +68 -0
- package/dist/noise-engine.d.ts +74 -0
- package/dist/noise-engine.d.ts.map +1 -0
- package/dist/noise-engine.js +132 -0
- package/dist/noise-sketches/fractalNoise.d.ts +11 -0
- package/dist/noise-sketches/fractalNoise.d.ts.map +1 -0
- package/dist/noise-sketches/fractalNoise.js +121 -0
- package/dist/noise-sketches/index.d.ts +21 -0
- package/dist/noise-sketches/index.d.ts.map +1 -0
- package/dist/noise-sketches/index.js +28 -0
- package/dist/p5-runtime.d.ts +56 -4
- package/dist/p5-runtime.d.ts.map +1 -1
- package/dist/p5-runtime.js +743 -32
- package/dist/sound-bridge.d.ts +89 -0
- package/dist/sound-bridge.d.ts.map +1 -0
- package/dist/sound-bridge.js +128 -0
- package/dist/soundart-engine.d.ts +87 -0
- package/dist/soundart-engine.d.ts.map +1 -0
- package/dist/soundart-engine.js +173 -0
- package/dist/soundart-sketches/chladniBloom.d.ts +3 -0
- package/dist/soundart-sketches/chladniBloom.d.ts.map +1 -0
- package/dist/soundart-sketches/chladniBloom.js +53 -0
- package/dist/soundart-sketches/dualVortex.d.ts +3 -0
- package/dist/soundart-sketches/dualVortex.d.ts.map +1 -0
- package/dist/soundart-sketches/dualVortex.js +67 -0
- package/dist/soundart-sketches/geometryIllusion.d.ts +3 -0
- package/dist/soundart-sketches/geometryIllusion.d.ts.map +1 -0
- package/dist/soundart-sketches/geometryIllusion.js +89 -0
- package/dist/soundart-sketches/index.d.ts +39 -0
- package/dist/soundart-sketches/index.d.ts.map +1 -0
- package/dist/soundart-sketches/index.js +72 -0
- package/dist/soundart-sketches/isoflow.d.ts +3 -0
- package/dist/soundart-sketches/isoflow.d.ts.map +1 -0
- package/dist/soundart-sketches/isoflow.js +60 -0
- package/dist/soundart-sketches/loomWeave.d.ts +3 -0
- package/dist/soundart-sketches/loomWeave.d.ts.map +1 -0
- package/dist/soundart-sketches/loomWeave.js +59 -0
- package/dist/soundart-sketches/noiseTerraces.d.ts +3 -0
- package/dist/soundart-sketches/noiseTerraces.d.ts.map +1 -0
- package/dist/soundart-sketches/noiseTerraces.js +53 -0
- package/dist/soundart-sketches/orb.d.ts +3 -0
- package/dist/soundart-sketches/orb.d.ts.map +1 -0
- package/dist/soundart-sketches/orb.js +50 -0
- package/dist/soundart-sketches/pixelGlyphs.d.ts +3 -0
- package/dist/soundart-sketches/pixelGlyphs.d.ts.map +1 -0
- package/dist/soundart-sketches/pixelGlyphs.js +72 -0
- package/dist/soundart-sketches/prismFlowFields.d.ts +3 -0
- package/dist/soundart-sketches/prismFlowFields.d.ts.map +1 -0
- package/dist/soundart-sketches/prismFlowFields.js +51 -0
- package/dist/soundart-sketches/radialBurst.d.ts +3 -0
- package/dist/soundart-sketches/radialBurst.d.ts.map +1 -0
- package/dist/soundart-sketches/radialBurst.js +60 -0
- package/dist/soundart-sketches/resonantSoundBodies.d.ts +3 -0
- package/dist/soundart-sketches/resonantSoundBodies.d.ts.map +1 -0
- package/dist/soundart-sketches/resonantSoundBodies.js +89 -0
- package/dist/soundart-sketches/rings.d.ts +11 -0
- package/dist/soundart-sketches/rings.d.ts.map +1 -0
- package/dist/soundart-sketches/rings.js +89 -0
- package/dist/soundart-sketches/squares.d.ts +3 -0
- package/dist/soundart-sketches/squares.d.ts.map +1 -0
- package/dist/soundart-sketches/squares.js +52 -0
- package/dist/soundart-sketches/waveStripes.d.ts +3 -0
- package/dist/soundart-sketches/waveStripes.d.ts.map +1 -0
- package/dist/soundart-sketches/waveStripes.js +44 -0
- package/dist/static-engine.d.ts +4 -1
- package/dist/static-engine.d.ts.map +1 -1
- package/dist/static-engine.js +13 -8
- package/dist/types.d.ts +43 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/package.json +24 -15
package/dist/p5-runtime.js
CHANGED
|
@@ -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
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
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: '
|
|
69
|
-
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
|
-
|
|
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
|
-
|
|
583
|
+
shapeVertices = [];
|
|
202
584
|
shapeStarted = false;
|
|
203
585
|
},
|
|
204
586
|
vertex: (x, y) => {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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
|
|
701
|
+
return rng();
|
|
249
702
|
if (max === undefined)
|
|
250
|
-
return
|
|
251
|
-
return min +
|
|
703
|
+
return rng() * min;
|
|
704
|
+
return min + rng() * (max - min);
|
|
252
705
|
},
|
|
253
706
|
randomSeed: (seed) => {
|
|
254
|
-
|
|
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
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
}
|