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