@nexart/codemode-sdk 0.1.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/README.md +304 -0
- package/dist/engine.d.ts +24 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +67 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/loop-engine.d.ts +15 -0
- package/dist/loop-engine.d.ts.map +1 -0
- package/dist/loop-engine.js +208 -0
- package/dist/p5-runtime.d.ts +21 -0
- package/dist/p5-runtime.d.ts.map +1 -0
- package/dist/p5-runtime.js +315 -0
- package/dist/static-engine.d.ts +10 -0
- package/dist/static-engine.d.ts.map +1 -0
- package/dist/static-engine.js +83 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# NexArt Code Mode Runtime SDK
|
|
2
|
+
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
|
|
5
|
+
A minimal, deterministic rendering engine for generative art.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
> **This SDK enforces the NexArt Code Mode Runtime Execution Specification (v0.x).**
|
|
10
|
+
>
|
|
11
|
+
> **This is NOT the protocol-stable GSL v1.**
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @nexart/codemode-sdk
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { createEngine } from '@nexart/codemode-sdk';
|
|
27
|
+
|
|
28
|
+
const engine = createEngine({ mode: 'static' });
|
|
29
|
+
|
|
30
|
+
await engine.run({
|
|
31
|
+
code: `
|
|
32
|
+
function setup() {
|
|
33
|
+
background(255);
|
|
34
|
+
fill(0);
|
|
35
|
+
ellipse(width/2, height/2, 100);
|
|
36
|
+
}
|
|
37
|
+
`,
|
|
38
|
+
onComplete: (result) => {
|
|
39
|
+
console.log(result.type); // 'image'
|
|
40
|
+
console.log(result.blob); // PNG Blob
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## What This SDK Is
|
|
48
|
+
|
|
49
|
+
This SDK provides a headless runtime for executing p5.js-style generative art code and producing deterministic output:
|
|
50
|
+
|
|
51
|
+
- **Static Mode**: Executes `setup()` only, outputs PNG
|
|
52
|
+
- **Loop Mode**: Frame-authoritative rendering, outputs MP4
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## What This SDK Is NOT
|
|
57
|
+
|
|
58
|
+
- **Not a protocol layer** — Does not validate protocol claims, system hashes, or GSL v1
|
|
59
|
+
- **Not a UI library** — No React components, no wallet integration
|
|
60
|
+
- **Not an IPFS client** — Does not handle storage or minting
|
|
61
|
+
- **Not p5.js** — Uses a minimal subset of p5.js-like functions
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Target Version
|
|
66
|
+
|
|
67
|
+
**Code Mode Runtime v0.x**
|
|
68
|
+
|
|
69
|
+
This SDK extracts the current NexArt Code Mode execution logic. Future versions may introduce:
|
|
70
|
+
- GSL v1 SDK (protocol layer) — separate package
|
|
71
|
+
- Extended p5.js compatibility
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## API
|
|
76
|
+
|
|
77
|
+
### `createEngine(config: EngineConfig): Engine`
|
|
78
|
+
|
|
79
|
+
Create a rendering engine instance.
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { createEngine } from '@nexart/codemode-sdk';
|
|
83
|
+
|
|
84
|
+
const engine = createEngine({
|
|
85
|
+
mode: 'static', // 'static' | 'loop'
|
|
86
|
+
width: 1950, // Optional, default: 1950
|
|
87
|
+
height: 2400, // Optional, default: 2400
|
|
88
|
+
duration: 2, // Loop mode only, 1-4 seconds
|
|
89
|
+
fps: 30, // Loop mode only, default: 30
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `engine.run(options: RunOptions): Promise<void>`
|
|
94
|
+
|
|
95
|
+
Execute code and produce output.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
await engine.run({
|
|
99
|
+
code: `
|
|
100
|
+
function setup() {
|
|
101
|
+
background(255);
|
|
102
|
+
fill(0);
|
|
103
|
+
ellipse(width/2, height/2, 100);
|
|
104
|
+
}
|
|
105
|
+
`,
|
|
106
|
+
onPreview: (canvas) => {
|
|
107
|
+
// Optional: called with canvas after first frame
|
|
108
|
+
},
|
|
109
|
+
onProgress: (info) => {
|
|
110
|
+
// Optional: progress updates
|
|
111
|
+
console.log(info.message, info.percent + '%');
|
|
112
|
+
},
|
|
113
|
+
onComplete: (result) => {
|
|
114
|
+
// Required: called with final result
|
|
115
|
+
console.log(result.type); // 'image' | 'video'
|
|
116
|
+
console.log(result.blob); // Blob
|
|
117
|
+
},
|
|
118
|
+
onError: (error) => {
|
|
119
|
+
// Optional: called on error
|
|
120
|
+
console.error(error);
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### `engine.stop(): void`
|
|
126
|
+
|
|
127
|
+
Cancel a running render (Loop mode only).
|
|
128
|
+
|
|
129
|
+
### `engine.getConfig(): EngineConfig`
|
|
130
|
+
|
|
131
|
+
Get the resolved engine configuration.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Execution Rules
|
|
136
|
+
|
|
137
|
+
### Static Mode
|
|
138
|
+
|
|
139
|
+
1. `setup()` is executed once
|
|
140
|
+
2. `draw()` is **NOT** executed
|
|
141
|
+
3. Canvas is captured as PNG
|
|
142
|
+
4. Time variables are all `0`
|
|
143
|
+
|
|
144
|
+
### Loop Mode
|
|
145
|
+
|
|
146
|
+
1. `setup()` is executed once
|
|
147
|
+
2. `draw()` is executed once per frame
|
|
148
|
+
3. Canvas is **cleared** before each `draw()` call
|
|
149
|
+
4. If artist calls `background()` in draw, it paints over the clear
|
|
150
|
+
5. No canvas persistence between frames
|
|
151
|
+
|
|
152
|
+
**Time Variables:**
|
|
153
|
+
|
|
154
|
+
| Variable | Type | Description |
|
|
155
|
+
|----------|------|-------------|
|
|
156
|
+
| `frameCount` | int | Current frame (0, 1, 2, ...) |
|
|
157
|
+
| `t` | float | Normalized time [0.0, 1.0) |
|
|
158
|
+
| `time` | float | Elapsed seconds |
|
|
159
|
+
| `tGlobal` | float | Alias for `t` |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Forbidden Patterns
|
|
164
|
+
|
|
165
|
+
These patterns will throw errors:
|
|
166
|
+
|
|
167
|
+
- `createCanvas()` — Canvas is pre-initialized by the SDK
|
|
168
|
+
- `setTimeout()`, `setInterval()`, `requestAnimationFrame()` — Async timing breaks determinism
|
|
169
|
+
- `noLoop()` in Loop Mode — Incompatible with frame capture
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Example: Static Mode
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { createEngine } from '@nexart/codemode-sdk';
|
|
177
|
+
|
|
178
|
+
const engine = createEngine({ mode: 'static' });
|
|
179
|
+
|
|
180
|
+
await engine.run({
|
|
181
|
+
code: `
|
|
182
|
+
function setup() {
|
|
183
|
+
background(30);
|
|
184
|
+
noStroke();
|
|
185
|
+
for (let i = 0; i < 100; i++) {
|
|
186
|
+
fill(random(255), random(255), random(255));
|
|
187
|
+
ellipse(random(width), random(height), 50);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
`,
|
|
191
|
+
onComplete: (result) => {
|
|
192
|
+
// result.type === 'image'
|
|
193
|
+
// result.blob is a PNG Blob
|
|
194
|
+
const url = URL.createObjectURL(result.blob);
|
|
195
|
+
document.body.innerHTML = `<img src="${url}" />`;
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Example: Loop Mode
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
import { createEngine } from '@nexart/codemode-sdk';
|
|
206
|
+
|
|
207
|
+
const engine = createEngine({
|
|
208
|
+
mode: 'loop',
|
|
209
|
+
duration: 2, // 2 second loop
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
await engine.run({
|
|
213
|
+
code: `
|
|
214
|
+
function setup() {
|
|
215
|
+
// Called once
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function draw() {
|
|
219
|
+
background(30);
|
|
220
|
+
|
|
221
|
+
// t goes from 0 to 1 over the loop duration
|
|
222
|
+
let x = width/2 + cos(t * TWO_PI) * 200;
|
|
223
|
+
let y = height/2 + sin(t * TWO_PI) * 200;
|
|
224
|
+
|
|
225
|
+
fill(255);
|
|
226
|
+
ellipse(x, y, 80);
|
|
227
|
+
}
|
|
228
|
+
`,
|
|
229
|
+
onProgress: (info) => {
|
|
230
|
+
console.log(info.message);
|
|
231
|
+
},
|
|
232
|
+
onComplete: (result) => {
|
|
233
|
+
// result.type === 'video'
|
|
234
|
+
// result.blob is an MP4 Blob
|
|
235
|
+
const url = URL.createObjectURL(result.blob);
|
|
236
|
+
document.body.innerHTML = `<video src="${url}" autoplay loop />`;
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Supported Functions
|
|
244
|
+
|
|
245
|
+
The SDK includes a minimal p5.js-like runtime with:
|
|
246
|
+
|
|
247
|
+
**Drawing:**
|
|
248
|
+
`background`, `clear`, `fill`, `noFill`, `stroke`, `noStroke`, `strokeWeight`
|
|
249
|
+
|
|
250
|
+
**Shapes:**
|
|
251
|
+
`ellipse`, `circle`, `rect`, `square`, `line`, `point`, `triangle`, `quad`, `arc`
|
|
252
|
+
|
|
253
|
+
**Vertex:**
|
|
254
|
+
`beginShape`, `vertex`, `endShape`
|
|
255
|
+
|
|
256
|
+
**Transform:**
|
|
257
|
+
`push`, `pop`, `translate`, `rotate`, `scale`, `resetMatrix`
|
|
258
|
+
|
|
259
|
+
**Color:**
|
|
260
|
+
`colorMode`, `color`, `lerpColor`
|
|
261
|
+
|
|
262
|
+
**Math:**
|
|
263
|
+
`random`, `noise`, `map`, `constrain`, `lerp`, `dist`, `mag`, `norm`
|
|
264
|
+
|
|
265
|
+
**Trig:**
|
|
266
|
+
`sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`, `radians`, `degrees`
|
|
267
|
+
|
|
268
|
+
**Constants:**
|
|
269
|
+
`PI`, `TWO_PI`, `HALF_PI`, `QUARTER_PI`, `width`, `height`, `frameCount`
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Video Encoding
|
|
274
|
+
|
|
275
|
+
Loop Mode requires server-side video encoding. The SDK calls:
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
POST /api/encode-loop
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Ensure your server has this endpoint available, or provide your own encoding solution.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Files
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
@nexart/codemode-sdk/
|
|
289
|
+
├── index.ts # Main export
|
|
290
|
+
├── engine.ts # createEngine entry point
|
|
291
|
+
├── types.ts # TypeScript types
|
|
292
|
+
├── static-engine.ts # Static mode implementation
|
|
293
|
+
├── loop-engine.ts # Loop mode implementation
|
|
294
|
+
├── p5-runtime.ts # p5.js-like runtime
|
|
295
|
+
└── README.md # This file
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## License
|
|
301
|
+
|
|
302
|
+
MIT License
|
|
303
|
+
|
|
304
|
+
Copyright (c) 2024 NexArt
|
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NexArt Code Mode Runtime SDK
|
|
3
|
+
* Version: 0.1.0
|
|
4
|
+
*
|
|
5
|
+
* Main entry point for the Code Mode runtime engine.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const engine = createEngine({ mode: 'static' });
|
|
9
|
+
* engine.run({
|
|
10
|
+
* code: 'function setup() { background(255); ellipse(width/2, height/2, 100); }',
|
|
11
|
+
* onComplete: (result) => console.log(result.blob)
|
|
12
|
+
* });
|
|
13
|
+
*/
|
|
14
|
+
import type { Engine, EngineConfig } from './types';
|
|
15
|
+
/**
|
|
16
|
+
* Create a NexArt Code Mode rendering engine.
|
|
17
|
+
*
|
|
18
|
+
* @param config - Engine configuration
|
|
19
|
+
* @returns Engine instance
|
|
20
|
+
*/
|
|
21
|
+
export declare function createEngine(config: EngineConfig): Engine;
|
|
22
|
+
export type { Engine, EngineConfig, RunOptions, RenderResult, ProgressInfo } from './types';
|
|
23
|
+
export { DEFAULT_CONFIG } from './types';
|
|
24
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAc,MAAM,SAAS,CAAC;AAKhE;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CA+CzD;AAGD,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5F,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/engine.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NexArt Code Mode Runtime SDK
|
|
3
|
+
* Version: 0.1.0
|
|
4
|
+
*
|
|
5
|
+
* Main entry point for the Code Mode runtime engine.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const engine = createEngine({ mode: 'static' });
|
|
9
|
+
* engine.run({
|
|
10
|
+
* code: 'function setup() { background(255); ellipse(width/2, height/2, 100); }',
|
|
11
|
+
* onComplete: (result) => console.log(result.blob)
|
|
12
|
+
* });
|
|
13
|
+
*/
|
|
14
|
+
import { DEFAULT_CONFIG } from './types';
|
|
15
|
+
import { runStaticMode } from './static-engine';
|
|
16
|
+
import { runLoopMode, cancelLoopMode } from './loop-engine';
|
|
17
|
+
/**
|
|
18
|
+
* Create a NexArt Code Mode rendering engine.
|
|
19
|
+
*
|
|
20
|
+
* @param config - Engine configuration
|
|
21
|
+
* @returns Engine instance
|
|
22
|
+
*/
|
|
23
|
+
export function createEngine(config) {
|
|
24
|
+
const resolvedConfig = {
|
|
25
|
+
mode: config.mode,
|
|
26
|
+
width: config.width ?? DEFAULT_CONFIG.width,
|
|
27
|
+
height: config.height ?? DEFAULT_CONFIG.height,
|
|
28
|
+
duration: config.duration ?? DEFAULT_CONFIG.duration,
|
|
29
|
+
fps: config.fps ?? DEFAULT_CONFIG.fps,
|
|
30
|
+
};
|
|
31
|
+
let isRunning = false;
|
|
32
|
+
const run = async (options) => {
|
|
33
|
+
if (isRunning) {
|
|
34
|
+
throw new Error('Engine is already running. Call stop() first.');
|
|
35
|
+
}
|
|
36
|
+
isRunning = true;
|
|
37
|
+
try {
|
|
38
|
+
if (resolvedConfig.mode === 'static') {
|
|
39
|
+
await runStaticMode(resolvedConfig, options);
|
|
40
|
+
}
|
|
41
|
+
else if (resolvedConfig.mode === 'loop') {
|
|
42
|
+
await runLoopMode(resolvedConfig, options);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
throw new Error(`Unknown mode: ${resolvedConfig.mode}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
isRunning = false;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const stop = () => {
|
|
53
|
+
if (resolvedConfig.mode === 'loop') {
|
|
54
|
+
cancelLoopMode();
|
|
55
|
+
}
|
|
56
|
+
isRunning = false;
|
|
57
|
+
};
|
|
58
|
+
const getConfig = () => {
|
|
59
|
+
return { ...resolvedConfig };
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
run,
|
|
63
|
+
stop,
|
|
64
|
+
getConfig,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export { DEFAULT_CONFIG } from './types';
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NexArt Code Mode Runtime SDK
|
|
3
|
+
* Version: 0.1.0
|
|
4
|
+
*
|
|
5
|
+
* A minimal, deterministic rendering engine for generative art.
|
|
6
|
+
* Supports Static (PNG) and Loop (MP4) modes.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { createEngine } from '@nexart/codemode-sdk';
|
|
11
|
+
*
|
|
12
|
+
* const engine = createEngine({ mode: 'static' });
|
|
13
|
+
*
|
|
14
|
+
* engine.run({
|
|
15
|
+
* code: `
|
|
16
|
+
* function setup() {
|
|
17
|
+
* background(255);
|
|
18
|
+
* fill(0);
|
|
19
|
+
* ellipse(width/2, height/2, 100);
|
|
20
|
+
* }
|
|
21
|
+
* `,
|
|
22
|
+
* onComplete: (result) => {
|
|
23
|
+
* console.log('Rendered:', result.type, result.blob.size, 'bytes');
|
|
24
|
+
* }
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export { createEngine } from './engine';
|
|
29
|
+
export type { Engine, EngineConfig, RunOptions, RenderResult, ProgressInfo, RenderMode, TimeVariables, } from './types';
|
|
30
|
+
export { DEFAULT_CONFIG } from './types';
|
|
31
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EACV,MAAM,EACN,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NexArt Code Mode Runtime SDK
|
|
3
|
+
* Version: 0.1.0
|
|
4
|
+
*
|
|
5
|
+
* A minimal, deterministic rendering engine for generative art.
|
|
6
|
+
* Supports Static (PNG) and Loop (MP4) modes.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { createEngine } from '@nexart/codemode-sdk';
|
|
11
|
+
*
|
|
12
|
+
* const engine = createEngine({ mode: 'static' });
|
|
13
|
+
*
|
|
14
|
+
* engine.run({
|
|
15
|
+
* code: `
|
|
16
|
+
* function setup() {
|
|
17
|
+
* background(255);
|
|
18
|
+
* fill(0);
|
|
19
|
+
* ellipse(width/2, height/2, 100);
|
|
20
|
+
* }
|
|
21
|
+
* `,
|
|
22
|
+
* onComplete: (result) => {
|
|
23
|
+
* console.log('Rendered:', result.type, result.blob.size, 'bytes');
|
|
24
|
+
* }
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export { createEngine } from './engine';
|
|
29
|
+
export { DEFAULT_CONFIG } from './types';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NexArt Code Mode Runtime SDK - Loop Engine
|
|
3
|
+
* Version: 0.1.0
|
|
4
|
+
*
|
|
5
|
+
* Loop mode renderer: frame-authoritative, stateless execution.
|
|
6
|
+
* - Executes setup() once
|
|
7
|
+
* - Executes draw() once per frame
|
|
8
|
+
* - Clears canvas before each frame (transparent)
|
|
9
|
+
* - Injects normalized time variables
|
|
10
|
+
* - No canvas persistence between frames
|
|
11
|
+
*/
|
|
12
|
+
import type { EngineConfig, RunOptions } from './types';
|
|
13
|
+
export declare function cancelLoopMode(): void;
|
|
14
|
+
export declare function runLoopMode(config: EngineConfig, options: RunOptions): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=loop-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loop-engine.d.ts","sourceRoot":"","sources":["../loop-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAA+B,MAAM,SAAS,CAAC;AAMrF,wBAAgB,cAAc,IAAI,IAAI,CAErC;AAED,wBAAsB,WAAW,CAC/B,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,IAAI,CAAC,CA4Lf"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NexArt Code Mode Runtime SDK - Loop Engine
|
|
3
|
+
* Version: 0.1.0
|
|
4
|
+
*
|
|
5
|
+
* Loop mode renderer: frame-authoritative, stateless execution.
|
|
6
|
+
* - Executes setup() once
|
|
7
|
+
* - Executes draw() once per frame
|
|
8
|
+
* - Clears canvas before each frame (transparent)
|
|
9
|
+
* - Injects normalized time variables
|
|
10
|
+
* - No canvas persistence between frames
|
|
11
|
+
*/
|
|
12
|
+
import { DEFAULT_CONFIG } from './types';
|
|
13
|
+
import { createP5Runtime } from './p5-runtime';
|
|
14
|
+
let isCancelled = false;
|
|
15
|
+
export function cancelLoopMode() {
|
|
16
|
+
isCancelled = true;
|
|
17
|
+
}
|
|
18
|
+
export async function runLoopMode(config, options) {
|
|
19
|
+
const { code, onPreview, onProgress, onComplete, onError } = options;
|
|
20
|
+
const width = config.width ?? DEFAULT_CONFIG.width;
|
|
21
|
+
const height = config.height ?? DEFAULT_CONFIG.height;
|
|
22
|
+
const duration = Math.max(DEFAULT_CONFIG.minDuration, Math.min(DEFAULT_CONFIG.maxDuration, config.duration ?? DEFAULT_CONFIG.duration));
|
|
23
|
+
const fps = config.fps ?? DEFAULT_CONFIG.fps;
|
|
24
|
+
const totalFrames = Math.floor(duration * fps);
|
|
25
|
+
isCancelled = false;
|
|
26
|
+
try {
|
|
27
|
+
onProgress?.({
|
|
28
|
+
phase: 'setup',
|
|
29
|
+
percent: 0,
|
|
30
|
+
message: 'Initializing canvas...',
|
|
31
|
+
});
|
|
32
|
+
// Create canvas
|
|
33
|
+
const canvas = document.createElement('canvas');
|
|
34
|
+
canvas.width = width;
|
|
35
|
+
canvas.height = height;
|
|
36
|
+
// Create p5 runtime
|
|
37
|
+
const p = createP5Runtime(canvas, width, height);
|
|
38
|
+
// Validate code
|
|
39
|
+
const hasDrawFunction = /function\s+draw\s*\(\s*\)/.test(code);
|
|
40
|
+
if (!hasDrawFunction) {
|
|
41
|
+
throw new Error('Loop Mode requires a draw() function.');
|
|
42
|
+
}
|
|
43
|
+
const forbiddenPatterns = [
|
|
44
|
+
{ pattern: /noLoop\s*\(\s*\)/, name: 'noLoop()' },
|
|
45
|
+
{ pattern: /setTimeout\s*\(/, name: 'setTimeout' },
|
|
46
|
+
{ pattern: /setInterval\s*\(/, name: 'setInterval' },
|
|
47
|
+
{ pattern: /requestAnimationFrame\s*\(/, name: 'requestAnimationFrame' },
|
|
48
|
+
];
|
|
49
|
+
for (const { pattern, name } of forbiddenPatterns) {
|
|
50
|
+
if (pattern.test(code)) {
|
|
51
|
+
throw new Error(`Forbidden function in Loop Mode: ${name}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
onProgress?.({
|
|
55
|
+
phase: 'setup',
|
|
56
|
+
percent: 5,
|
|
57
|
+
message: 'Parsing code...',
|
|
58
|
+
});
|
|
59
|
+
// Extract setup() and draw() functions
|
|
60
|
+
const setupMatch = code.match(/function\s+setup\s*\(\s*\)\s*\{([\s\S]*?)\}(?=\s*function|\s*$)/);
|
|
61
|
+
const drawMatch = code.match(/function\s+draw\s*\(\s*\)\s*\{([\s\S]*?)\}(?=\s*function|\s*$)/);
|
|
62
|
+
const setupCode = setupMatch ? setupMatch[1].trim() : '';
|
|
63
|
+
const drawCode = drawMatch ? drawMatch[1].trim() : '';
|
|
64
|
+
if (!drawCode) {
|
|
65
|
+
throw new Error('Loop Mode requires a draw() function with content.');
|
|
66
|
+
}
|
|
67
|
+
// Create wrapped functions with p5 context and time variables
|
|
68
|
+
const wrappedSetup = new Function('p', 'frameCount', 't', 'time', 'tGlobal', `with(p) { ${setupCode} }`);
|
|
69
|
+
const wrappedDraw = new Function('p', 'frameCount', 't', 'time', 'tGlobal', `with(p) { ${drawCode} }`);
|
|
70
|
+
onProgress?.({
|
|
71
|
+
phase: 'setup',
|
|
72
|
+
percent: 10,
|
|
73
|
+
message: 'Executing setup()...',
|
|
74
|
+
});
|
|
75
|
+
// Execute setup() once with time = 0
|
|
76
|
+
wrappedSetup(p, 0, 0, 0, 0);
|
|
77
|
+
// Capture frames
|
|
78
|
+
const frames = [];
|
|
79
|
+
onProgress?.({
|
|
80
|
+
phase: 'rendering',
|
|
81
|
+
frame: 0,
|
|
82
|
+
totalFrames,
|
|
83
|
+
percent: 10,
|
|
84
|
+
message: `Rendering frames (0/${totalFrames})...`,
|
|
85
|
+
});
|
|
86
|
+
for (let frame = 0; frame < totalFrames; frame++) {
|
|
87
|
+
if (isCancelled) {
|
|
88
|
+
throw new Error('Rendering cancelled');
|
|
89
|
+
}
|
|
90
|
+
// Calculate normalized time variables
|
|
91
|
+
// t = frame / totalFrames (range [0, 1))
|
|
92
|
+
const t = frame / totalFrames;
|
|
93
|
+
const time = t * duration;
|
|
94
|
+
// Update p5 runtime frameCount
|
|
95
|
+
p.frameCount = frame;
|
|
96
|
+
// CRITICAL: Clear canvas before each draw() call
|
|
97
|
+
// This enforces stateless, frame-authoritative rendering
|
|
98
|
+
// Preserves artist-defined backgrounds (if they call background() in draw)
|
|
99
|
+
p.clear();
|
|
100
|
+
// Execute draw() with time variables
|
|
101
|
+
wrappedDraw(p, frame, t, time, t);
|
|
102
|
+
// Capture frame as PNG blob
|
|
103
|
+
const blob = await new Promise((resolve, reject) => {
|
|
104
|
+
canvas.toBlob((b) => b ? resolve(b) : reject(new Error(`Failed to capture frame ${frame}`)), 'image/png');
|
|
105
|
+
});
|
|
106
|
+
frames.push(blob);
|
|
107
|
+
// Provide preview on first frame
|
|
108
|
+
if (frame === 0) {
|
|
109
|
+
onPreview?.(canvas);
|
|
110
|
+
}
|
|
111
|
+
// Update progress
|
|
112
|
+
const percent = 10 + Math.floor((frame / totalFrames) * 60);
|
|
113
|
+
onProgress?.({
|
|
114
|
+
phase: 'rendering',
|
|
115
|
+
frame: frame + 1,
|
|
116
|
+
totalFrames,
|
|
117
|
+
percent,
|
|
118
|
+
message: `Rendering frames (${frame + 1}/${totalFrames})...`,
|
|
119
|
+
});
|
|
120
|
+
// Yield to prevent blocking UI
|
|
121
|
+
if (frame % 10 === 0) {
|
|
122
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
onProgress?.({
|
|
126
|
+
phase: 'encoding',
|
|
127
|
+
frame: totalFrames,
|
|
128
|
+
totalFrames,
|
|
129
|
+
percent: 70,
|
|
130
|
+
message: 'Encoding video...',
|
|
131
|
+
});
|
|
132
|
+
// Encode to video (MP4)
|
|
133
|
+
const videoBlob = await encodeFramesToMP4(frames, fps, width, height, (progress) => {
|
|
134
|
+
const percent = 70 + Math.floor(progress * 30);
|
|
135
|
+
onProgress?.({
|
|
136
|
+
phase: 'encoding',
|
|
137
|
+
frame: totalFrames,
|
|
138
|
+
totalFrames,
|
|
139
|
+
percent,
|
|
140
|
+
message: `Encoding video (${Math.floor(progress * 100)}%)...`,
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
onProgress?.({
|
|
144
|
+
phase: 'complete',
|
|
145
|
+
frame: totalFrames,
|
|
146
|
+
totalFrames,
|
|
147
|
+
percent: 100,
|
|
148
|
+
message: 'Complete',
|
|
149
|
+
});
|
|
150
|
+
const result = {
|
|
151
|
+
type: 'video',
|
|
152
|
+
blob: videoBlob,
|
|
153
|
+
frames: totalFrames,
|
|
154
|
+
duration,
|
|
155
|
+
};
|
|
156
|
+
onComplete(result);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
160
|
+
onError?.(err);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Encode frames to MP4 video
|
|
165
|
+
* Uses server-side encoding endpoint for cross-browser reliability
|
|
166
|
+
*/
|
|
167
|
+
async function encodeFramesToMP4(frames, fps, width, height, onProgress) {
|
|
168
|
+
// Convert frames to base64 for server transport
|
|
169
|
+
const frameDataUrls = [];
|
|
170
|
+
for (let i = 0; i < frames.length; i++) {
|
|
171
|
+
const reader = new FileReader();
|
|
172
|
+
const dataUrl = await new Promise((resolve, reject) => {
|
|
173
|
+
reader.onload = () => resolve(reader.result);
|
|
174
|
+
reader.onerror = reject;
|
|
175
|
+
reader.readAsDataURL(frames[i]);
|
|
176
|
+
});
|
|
177
|
+
frameDataUrls.push(dataUrl);
|
|
178
|
+
onProgress?.(i / frames.length * 0.3);
|
|
179
|
+
}
|
|
180
|
+
// Send to server for encoding
|
|
181
|
+
const response = await fetch('/api/encode-loop', {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
headers: { 'Content-Type': 'application/json' },
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
frames: frameDataUrls,
|
|
186
|
+
fps,
|
|
187
|
+
width,
|
|
188
|
+
height,
|
|
189
|
+
}),
|
|
190
|
+
});
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
const errorText = await response.text();
|
|
193
|
+
throw new Error(`Video encoding failed: ${errorText}`);
|
|
194
|
+
}
|
|
195
|
+
onProgress?.(0.8);
|
|
196
|
+
const data = await response.json();
|
|
197
|
+
if (!data.video) {
|
|
198
|
+
throw new Error('No video data returned from encoder');
|
|
199
|
+
}
|
|
200
|
+
// Convert base64 to blob
|
|
201
|
+
const binaryString = atob(data.video.split(',')[1] || data.video);
|
|
202
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
203
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
204
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
205
|
+
}
|
|
206
|
+
onProgress?.(1);
|
|
207
|
+
return new Blob([bytes], { type: 'video/mp4' });
|
|
208
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NexArt Code Mode Runtime SDK - p5-like Runtime
|
|
3
|
+
* Version: 0.1.0
|
|
4
|
+
*
|
|
5
|
+
* Minimal p5.js-like runtime for deterministic generative art execution.
|
|
6
|
+
* This is a headless runtime - no UI dependencies.
|
|
7
|
+
*/
|
|
8
|
+
import type { TimeVariables } from './types';
|
|
9
|
+
export interface P5Runtime {
|
|
10
|
+
[key: string]: any;
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
frameCount: number;
|
|
14
|
+
PI: number;
|
|
15
|
+
TWO_PI: number;
|
|
16
|
+
HALF_PI: number;
|
|
17
|
+
QUARTER_PI: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function createP5Runtime(canvas: HTMLCanvasElement, width: number, height: number): P5Runtime;
|
|
20
|
+
export declare function injectTimeVariables(p: P5Runtime, time: TimeVariables): void;
|
|
21
|
+
//# sourceMappingURL=p5-runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"p5-runtime.d.ts","sourceRoot":"","sources":["../p5-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C,MAAM,WAAW,SAAS;IACxB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,iBAAiB,EACzB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,SAAS,CAkVX;AAED,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,GAAG,IAAI,CAE3E"}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NexArt Code Mode Runtime SDK - p5-like Runtime
|
|
3
|
+
* Version: 0.1.0
|
|
4
|
+
*
|
|
5
|
+
* Minimal p5.js-like runtime for deterministic generative art execution.
|
|
6
|
+
* This is a headless runtime - no UI dependencies.
|
|
7
|
+
*/
|
|
8
|
+
export function createP5Runtime(canvas, width, height) {
|
|
9
|
+
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
10
|
+
let currentFill = 'rgba(255, 255, 255, 1)';
|
|
11
|
+
let currentStroke = 'rgba(0, 0, 0, 1)';
|
|
12
|
+
let strokeEnabled = true;
|
|
13
|
+
let fillEnabled = true;
|
|
14
|
+
let currentStrokeWeight = 1;
|
|
15
|
+
let colorModeSettings = { mode: 'RGB', maxR: 255, maxG: 255, maxB: 255, maxA: 255 };
|
|
16
|
+
const parseColor = (...args) => {
|
|
17
|
+
if (args.length === 0)
|
|
18
|
+
return 'rgba(0, 0, 0, 1)';
|
|
19
|
+
const { mode, maxR, maxG, maxB, maxA } = colorModeSettings;
|
|
20
|
+
if (args.length === 1) {
|
|
21
|
+
const val = args[0];
|
|
22
|
+
if (typeof val === 'string')
|
|
23
|
+
return val;
|
|
24
|
+
if (mode === 'HSB') {
|
|
25
|
+
return `hsla(${val}, 100%, 50%, 1)`;
|
|
26
|
+
}
|
|
27
|
+
const gray = Math.round((val / maxR) * 255);
|
|
28
|
+
return `rgba(${gray}, ${gray}, ${gray}, 1)`;
|
|
29
|
+
}
|
|
30
|
+
if (args.length === 2) {
|
|
31
|
+
const [gray, alpha] = args;
|
|
32
|
+
const g = Math.round((gray / maxR) * 255);
|
|
33
|
+
const a = alpha / maxA;
|
|
34
|
+
return `rgba(${g}, ${g}, ${g}, ${a})`;
|
|
35
|
+
}
|
|
36
|
+
if (args.length === 3) {
|
|
37
|
+
const [r, g, b] = args;
|
|
38
|
+
if (mode === 'HSB') {
|
|
39
|
+
return `hsla(${(r / maxR) * 360}, ${(g / maxG) * 100}%, ${(b / maxB) * 100}%, 1)`;
|
|
40
|
+
}
|
|
41
|
+
return `rgba(${Math.round((r / maxR) * 255)}, ${Math.round((g / maxG) * 255)}, ${Math.round((b / maxB) * 255)}, 1)`;
|
|
42
|
+
}
|
|
43
|
+
if (args.length === 4) {
|
|
44
|
+
const [r, g, b, a] = args;
|
|
45
|
+
if (mode === 'HSB') {
|
|
46
|
+
return `hsla(${(r / maxR) * 360}, ${(g / maxG) * 100}%, ${(b / maxB) * 100}%, ${a / maxA})`;
|
|
47
|
+
}
|
|
48
|
+
return `rgba(${Math.round((r / maxR) * 255)}, ${Math.round((g / maxG) * 255)}, ${Math.round((b / maxB) * 255)}, ${a / maxA})`;
|
|
49
|
+
}
|
|
50
|
+
return 'rgba(0, 0, 0, 1)';
|
|
51
|
+
};
|
|
52
|
+
const p = {
|
|
53
|
+
width,
|
|
54
|
+
height,
|
|
55
|
+
frameCount: 0,
|
|
56
|
+
// Constants
|
|
57
|
+
PI: Math.PI,
|
|
58
|
+
TWO_PI: Math.PI * 2,
|
|
59
|
+
HALF_PI: Math.PI / 2,
|
|
60
|
+
QUARTER_PI: Math.PI / 4,
|
|
61
|
+
// Shape mode constants
|
|
62
|
+
CORNER: 'corner',
|
|
63
|
+
CENTER: 'center',
|
|
64
|
+
CORNERS: 'corners',
|
|
65
|
+
RADIUS: 'radius',
|
|
66
|
+
ROUND: 'round',
|
|
67
|
+
SQUARE: 'square',
|
|
68
|
+
PROJECT: 'project',
|
|
69
|
+
MITER: 'miter',
|
|
70
|
+
BEVEL: 'bevel',
|
|
71
|
+
CLOSE: 'close',
|
|
72
|
+
// Canvas operations
|
|
73
|
+
background: (...args) => {
|
|
74
|
+
ctx.save();
|
|
75
|
+
ctx.fillStyle = parseColor(...args);
|
|
76
|
+
ctx.fillRect(0, 0, width, height);
|
|
77
|
+
ctx.restore();
|
|
78
|
+
},
|
|
79
|
+
clear: () => {
|
|
80
|
+
ctx.clearRect(0, 0, width, height);
|
|
81
|
+
},
|
|
82
|
+
// Color functions
|
|
83
|
+
fill: (...args) => {
|
|
84
|
+
fillEnabled = true;
|
|
85
|
+
currentFill = parseColor(...args);
|
|
86
|
+
ctx.fillStyle = currentFill;
|
|
87
|
+
},
|
|
88
|
+
noFill: () => {
|
|
89
|
+
fillEnabled = false;
|
|
90
|
+
},
|
|
91
|
+
stroke: (...args) => {
|
|
92
|
+
strokeEnabled = true;
|
|
93
|
+
currentStroke = parseColor(...args);
|
|
94
|
+
ctx.strokeStyle = currentStroke;
|
|
95
|
+
},
|
|
96
|
+
noStroke: () => {
|
|
97
|
+
strokeEnabled = false;
|
|
98
|
+
},
|
|
99
|
+
strokeWeight: (weight) => {
|
|
100
|
+
currentStrokeWeight = weight;
|
|
101
|
+
ctx.lineWidth = weight;
|
|
102
|
+
},
|
|
103
|
+
colorMode: (mode, max1, max2, max3, maxA) => {
|
|
104
|
+
colorModeSettings = {
|
|
105
|
+
mode: mode.toUpperCase(),
|
|
106
|
+
maxR: max1 ?? 255,
|
|
107
|
+
maxG: max2 ?? max1 ?? 255,
|
|
108
|
+
maxB: max3 ?? max1 ?? 255,
|
|
109
|
+
maxA: maxA ?? 255,
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
color: (...args) => parseColor(...args),
|
|
113
|
+
lerpColor: (c1, c2, amt) => {
|
|
114
|
+
return c1; // Simplified - full implementation would blend colors
|
|
115
|
+
},
|
|
116
|
+
// Shape functions
|
|
117
|
+
ellipse: (x, y, w, h) => {
|
|
118
|
+
const rw = w / 2;
|
|
119
|
+
const rh = (h ?? w) / 2;
|
|
120
|
+
ctx.beginPath();
|
|
121
|
+
ctx.ellipse(x, y, rw, rh, 0, 0, Math.PI * 2);
|
|
122
|
+
if (fillEnabled)
|
|
123
|
+
ctx.fill();
|
|
124
|
+
if (strokeEnabled)
|
|
125
|
+
ctx.stroke();
|
|
126
|
+
},
|
|
127
|
+
circle: (x, y, d) => {
|
|
128
|
+
p.ellipse(x, y, d, d);
|
|
129
|
+
},
|
|
130
|
+
rect: (x, y, w, h, r) => {
|
|
131
|
+
const height = h ?? w;
|
|
132
|
+
ctx.beginPath();
|
|
133
|
+
if (r && r > 0) {
|
|
134
|
+
ctx.roundRect(x, y, w, height, r);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
ctx.rect(x, y, w, height);
|
|
138
|
+
}
|
|
139
|
+
if (fillEnabled)
|
|
140
|
+
ctx.fill();
|
|
141
|
+
if (strokeEnabled)
|
|
142
|
+
ctx.stroke();
|
|
143
|
+
},
|
|
144
|
+
square: (x, y, s, r) => {
|
|
145
|
+
p.rect(x, y, s, s, r);
|
|
146
|
+
},
|
|
147
|
+
line: (x1, y1, x2, y2) => {
|
|
148
|
+
ctx.beginPath();
|
|
149
|
+
ctx.moveTo(x1, y1);
|
|
150
|
+
ctx.lineTo(x2, y2);
|
|
151
|
+
if (strokeEnabled)
|
|
152
|
+
ctx.stroke();
|
|
153
|
+
},
|
|
154
|
+
point: (x, y) => {
|
|
155
|
+
ctx.beginPath();
|
|
156
|
+
ctx.arc(x, y, currentStrokeWeight / 2, 0, Math.PI * 2);
|
|
157
|
+
ctx.fillStyle = currentStroke;
|
|
158
|
+
ctx.fill();
|
|
159
|
+
},
|
|
160
|
+
triangle: (x1, y1, x2, y2, x3, y3) => {
|
|
161
|
+
ctx.beginPath();
|
|
162
|
+
ctx.moveTo(x1, y1);
|
|
163
|
+
ctx.lineTo(x2, y2);
|
|
164
|
+
ctx.lineTo(x3, y3);
|
|
165
|
+
ctx.closePath();
|
|
166
|
+
if (fillEnabled)
|
|
167
|
+
ctx.fill();
|
|
168
|
+
if (strokeEnabled)
|
|
169
|
+
ctx.stroke();
|
|
170
|
+
},
|
|
171
|
+
quad: (x1, y1, x2, y2, x3, y3, x4, y4) => {
|
|
172
|
+
ctx.beginPath();
|
|
173
|
+
ctx.moveTo(x1, y1);
|
|
174
|
+
ctx.lineTo(x2, y2);
|
|
175
|
+
ctx.lineTo(x3, y3);
|
|
176
|
+
ctx.lineTo(x4, y4);
|
|
177
|
+
ctx.closePath();
|
|
178
|
+
if (fillEnabled)
|
|
179
|
+
ctx.fill();
|
|
180
|
+
if (strokeEnabled)
|
|
181
|
+
ctx.stroke();
|
|
182
|
+
},
|
|
183
|
+
arc: (x, y, w, h, start, stop, mode) => {
|
|
184
|
+
ctx.beginPath();
|
|
185
|
+
ctx.ellipse(x, y, w / 2, h / 2, 0, start, stop);
|
|
186
|
+
if (mode === 'pie' || mode === 'PIE') {
|
|
187
|
+
ctx.lineTo(x, y);
|
|
188
|
+
ctx.closePath();
|
|
189
|
+
}
|
|
190
|
+
else if (mode === 'chord' || mode === 'CHORD') {
|
|
191
|
+
ctx.closePath();
|
|
192
|
+
}
|
|
193
|
+
if (fillEnabled)
|
|
194
|
+
ctx.fill();
|
|
195
|
+
if (strokeEnabled)
|
|
196
|
+
ctx.stroke();
|
|
197
|
+
},
|
|
198
|
+
// Vertex-based shapes
|
|
199
|
+
beginShape: () => {
|
|
200
|
+
ctx.beginPath();
|
|
201
|
+
},
|
|
202
|
+
vertex: (x, y) => {
|
|
203
|
+
if (ctx.isPointInPath(x, y)) {
|
|
204
|
+
ctx.lineTo(x, y);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
ctx.moveTo(x, y);
|
|
208
|
+
ctx.lineTo(x, y);
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
endShape: (mode) => {
|
|
212
|
+
if (mode === 'close' || mode === 'CLOSE') {
|
|
213
|
+
ctx.closePath();
|
|
214
|
+
}
|
|
215
|
+
if (fillEnabled)
|
|
216
|
+
ctx.fill();
|
|
217
|
+
if (strokeEnabled)
|
|
218
|
+
ctx.stroke();
|
|
219
|
+
},
|
|
220
|
+
// Transform functions
|
|
221
|
+
push: () => {
|
|
222
|
+
ctx.save();
|
|
223
|
+
},
|
|
224
|
+
pop: () => {
|
|
225
|
+
ctx.restore();
|
|
226
|
+
ctx.fillStyle = currentFill;
|
|
227
|
+
ctx.strokeStyle = currentStroke;
|
|
228
|
+
ctx.lineWidth = currentStrokeWeight;
|
|
229
|
+
},
|
|
230
|
+
translate: (x, y) => {
|
|
231
|
+
ctx.translate(x, y);
|
|
232
|
+
},
|
|
233
|
+
rotate: (angle) => {
|
|
234
|
+
ctx.rotate(angle);
|
|
235
|
+
},
|
|
236
|
+
scale: (sx, sy) => {
|
|
237
|
+
ctx.scale(sx, sy ?? sx);
|
|
238
|
+
},
|
|
239
|
+
resetMatrix: () => {
|
|
240
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
241
|
+
},
|
|
242
|
+
// Math functions
|
|
243
|
+
random: (min, max) => {
|
|
244
|
+
if (min === undefined)
|
|
245
|
+
return Math.random();
|
|
246
|
+
if (max === undefined)
|
|
247
|
+
return Math.random() * min;
|
|
248
|
+
return min + Math.random() * (max - min);
|
|
249
|
+
},
|
|
250
|
+
randomSeed: (seed) => {
|
|
251
|
+
// Simplified - would need proper seeded random
|
|
252
|
+
},
|
|
253
|
+
noise: (x, y, z) => {
|
|
254
|
+
// Simplified Perlin-like noise
|
|
255
|
+
const hash = (n) => {
|
|
256
|
+
const s = Math.sin(n) * 43758.5453123;
|
|
257
|
+
return s - Math.floor(s);
|
|
258
|
+
};
|
|
259
|
+
const xVal = x ?? 0;
|
|
260
|
+
const yVal = y ?? 0;
|
|
261
|
+
const zVal = z ?? 0;
|
|
262
|
+
return hash(xVal * 12.9898 + yVal * 78.233 + zVal * 37.719);
|
|
263
|
+
},
|
|
264
|
+
noiseSeed: (seed) => { },
|
|
265
|
+
noiseDetail: (lod, falloff) => { },
|
|
266
|
+
map: (value, start1, stop1, start2, stop2) => {
|
|
267
|
+
return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1));
|
|
268
|
+
},
|
|
269
|
+
constrain: (n, low, high) => {
|
|
270
|
+
return Math.max(low, Math.min(high, n));
|
|
271
|
+
},
|
|
272
|
+
lerp: (start, stop, amt) => {
|
|
273
|
+
return start + (stop - start) * amt;
|
|
274
|
+
},
|
|
275
|
+
dist: (x1, y1, x2, y2) => {
|
|
276
|
+
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
|
277
|
+
},
|
|
278
|
+
mag: (x, y) => {
|
|
279
|
+
return Math.sqrt(x * x + y * y);
|
|
280
|
+
},
|
|
281
|
+
norm: (value, start, stop) => {
|
|
282
|
+
return (value - start) / (stop - start);
|
|
283
|
+
},
|
|
284
|
+
// Trig functions
|
|
285
|
+
sin: Math.sin,
|
|
286
|
+
cos: Math.cos,
|
|
287
|
+
tan: Math.tan,
|
|
288
|
+
asin: Math.asin,
|
|
289
|
+
acos: Math.acos,
|
|
290
|
+
atan: Math.atan,
|
|
291
|
+
atan2: Math.atan2,
|
|
292
|
+
radians: (degrees) => degrees * (Math.PI / 180),
|
|
293
|
+
degrees: (radians) => radians * (180 / Math.PI),
|
|
294
|
+
// Utility functions
|
|
295
|
+
abs: Math.abs,
|
|
296
|
+
ceil: Math.ceil,
|
|
297
|
+
floor: Math.floor,
|
|
298
|
+
round: Math.round,
|
|
299
|
+
sqrt: Math.sqrt,
|
|
300
|
+
pow: Math.pow,
|
|
301
|
+
exp: Math.exp,
|
|
302
|
+
log: Math.log,
|
|
303
|
+
min: Math.min,
|
|
304
|
+
max: Math.max,
|
|
305
|
+
// Loop control (no-ops for SDK)
|
|
306
|
+
noLoop: () => { },
|
|
307
|
+
loop: () => { },
|
|
308
|
+
redraw: () => { },
|
|
309
|
+
frameRate: (fps) => { },
|
|
310
|
+
};
|
|
311
|
+
return p;
|
|
312
|
+
}
|
|
313
|
+
export function injectTimeVariables(p, time) {
|
|
314
|
+
p.frameCount = time.frameCount;
|
|
315
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NexArt Code Mode Runtime SDK - Static Engine
|
|
3
|
+
* Version: 0.1.0
|
|
4
|
+
*
|
|
5
|
+
* Static mode renderer: executes setup() only, captures single PNG.
|
|
6
|
+
* Does NOT execute draw() - per NexArt Execution Specification v1.
|
|
7
|
+
*/
|
|
8
|
+
import type { EngineConfig, RunOptions } from './types';
|
|
9
|
+
export declare function runStaticMode(config: EngineConfig, options: RunOptions): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=static-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"static-engine.d.ts","sourceRoot":"","sources":["../static-engine.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAgB,MAAM,SAAS,CAAC;AAItE,wBAAsB,aAAa,CACjC,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,IAAI,CAAC,CA+Ff"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NexArt Code Mode Runtime SDK - Static Engine
|
|
3
|
+
* Version: 0.1.0
|
|
4
|
+
*
|
|
5
|
+
* Static mode renderer: executes setup() only, captures single PNG.
|
|
6
|
+
* Does NOT execute draw() - per NexArt Execution Specification v1.
|
|
7
|
+
*/
|
|
8
|
+
import { DEFAULT_CONFIG } from './types';
|
|
9
|
+
import { createP5Runtime, injectTimeVariables } from './p5-runtime';
|
|
10
|
+
export async function runStaticMode(config, options) {
|
|
11
|
+
const { code, onPreview, onProgress, onComplete, onError } = options;
|
|
12
|
+
const width = config.width ?? DEFAULT_CONFIG.width;
|
|
13
|
+
const height = config.height ?? DEFAULT_CONFIG.height;
|
|
14
|
+
try {
|
|
15
|
+
onProgress?.({
|
|
16
|
+
phase: 'setup',
|
|
17
|
+
percent: 0,
|
|
18
|
+
message: 'Initializing canvas...',
|
|
19
|
+
});
|
|
20
|
+
// Create offscreen canvas
|
|
21
|
+
const canvas = document.createElement('canvas');
|
|
22
|
+
canvas.width = width;
|
|
23
|
+
canvas.height = height;
|
|
24
|
+
// Create p5 runtime
|
|
25
|
+
const p = createP5Runtime(canvas, width, height);
|
|
26
|
+
// Inject time variables (static = frame 0, t = 0)
|
|
27
|
+
injectTimeVariables(p, {
|
|
28
|
+
frameCount: 0,
|
|
29
|
+
t: 0,
|
|
30
|
+
time: 0,
|
|
31
|
+
tGlobal: 0,
|
|
32
|
+
});
|
|
33
|
+
onProgress?.({
|
|
34
|
+
phase: 'setup',
|
|
35
|
+
percent: 10,
|
|
36
|
+
message: 'Parsing code...',
|
|
37
|
+
});
|
|
38
|
+
// Extract setup() function only - draw() is NOT executed in Static Mode
|
|
39
|
+
const setupMatch = code.match(/function\s+setup\s*\(\s*\)\s*\{([\s\S]*?)\}(?=\s*function|\s*$)/);
|
|
40
|
+
const setupCode = setupMatch ? setupMatch[1].trim() : code;
|
|
41
|
+
// Validate - no forbidden patterns
|
|
42
|
+
const forbiddenPatterns = ['setTimeout', 'setInterval', 'requestAnimationFrame'];
|
|
43
|
+
for (const pattern of forbiddenPatterns) {
|
|
44
|
+
if (code.includes(pattern)) {
|
|
45
|
+
throw new Error(`Forbidden async timing function: ${pattern}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
onProgress?.({
|
|
49
|
+
phase: 'rendering',
|
|
50
|
+
percent: 30,
|
|
51
|
+
message: 'Executing setup()...',
|
|
52
|
+
});
|
|
53
|
+
// Create wrapped setup function with p5 context
|
|
54
|
+
const wrappedSetup = new Function('p', 'frameCount', 't', 'time', 'tGlobal', `with(p) { ${setupCode} }`);
|
|
55
|
+
// Execute setup() only
|
|
56
|
+
wrappedSetup(p, 0, 0, 0, 0);
|
|
57
|
+
// Provide preview callback
|
|
58
|
+
onPreview?.(canvas);
|
|
59
|
+
onProgress?.({
|
|
60
|
+
phase: 'encoding',
|
|
61
|
+
percent: 70,
|
|
62
|
+
message: 'Capturing PNG...',
|
|
63
|
+
});
|
|
64
|
+
// Capture as PNG
|
|
65
|
+
const blob = await new Promise((resolve, reject) => {
|
|
66
|
+
canvas.toBlob((b) => b ? resolve(b) : reject(new Error('Failed to capture PNG')), 'image/png');
|
|
67
|
+
});
|
|
68
|
+
onProgress?.({
|
|
69
|
+
phase: 'complete',
|
|
70
|
+
percent: 100,
|
|
71
|
+
message: 'Complete',
|
|
72
|
+
});
|
|
73
|
+
const result = {
|
|
74
|
+
type: 'image',
|
|
75
|
+
blob,
|
|
76
|
+
};
|
|
77
|
+
onComplete(result);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
81
|
+
onError?.(err);
|
|
82
|
+
}
|
|
83
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NexArt Code Mode Runtime SDK - Types
|
|
3
|
+
* Version: 0.1.0
|
|
4
|
+
*
|
|
5
|
+
* Type definitions for the Code Mode runtime engine.
|
|
6
|
+
*/
|
|
7
|
+
export type RenderMode = 'static' | 'loop';
|
|
8
|
+
export interface EngineConfig {
|
|
9
|
+
mode: RenderMode;
|
|
10
|
+
width?: number;
|
|
11
|
+
height?: number;
|
|
12
|
+
duration?: number;
|
|
13
|
+
fps?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface RenderResult {
|
|
16
|
+
type: 'image' | 'video';
|
|
17
|
+
blob: Blob;
|
|
18
|
+
frames?: number;
|
|
19
|
+
duration?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface RunOptions {
|
|
22
|
+
code: string;
|
|
23
|
+
onPreview?: (canvas: HTMLCanvasElement) => void;
|
|
24
|
+
onProgress?: (progress: ProgressInfo) => void;
|
|
25
|
+
onComplete: (result: RenderResult) => void;
|
|
26
|
+
onError?: (error: Error) => void;
|
|
27
|
+
}
|
|
28
|
+
export interface ProgressInfo {
|
|
29
|
+
phase: 'setup' | 'rendering' | 'encoding' | 'complete';
|
|
30
|
+
frame?: number;
|
|
31
|
+
totalFrames?: number;
|
|
32
|
+
percent: number;
|
|
33
|
+
message: string;
|
|
34
|
+
}
|
|
35
|
+
export interface Engine {
|
|
36
|
+
run: (options: RunOptions) => Promise<void>;
|
|
37
|
+
stop: () => void;
|
|
38
|
+
getConfig: () => Readonly<EngineConfig>;
|
|
39
|
+
}
|
|
40
|
+
export interface TimeVariables {
|
|
41
|
+
frameCount: number;
|
|
42
|
+
t: number;
|
|
43
|
+
time: number;
|
|
44
|
+
tGlobal: number;
|
|
45
|
+
}
|
|
46
|
+
export declare const DEFAULT_CONFIG: {
|
|
47
|
+
readonly width: 1950;
|
|
48
|
+
readonly height: 2400;
|
|
49
|
+
readonly duration: 2;
|
|
50
|
+
readonly fps: 30;
|
|
51
|
+
readonly minDuration: 1;
|
|
52
|
+
readonly maxDuration: 4;
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE3C,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACxB,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAChD,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAC;IAC9C,UAAU,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;IAC3C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,OAAO,GAAG,WAAW,GAAG,UAAU,GAAG,UAAU,CAAC;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,SAAS,EAAE,MAAM,QAAQ,CAAC,YAAY,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,CAAC,EAAE,MAAM,CAAC;IACV,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,eAAO,MAAM,cAAc;;;;;;;CAOjB,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NexArt Code Mode Runtime SDK - Types
|
|
3
|
+
* Version: 0.1.0
|
|
4
|
+
*
|
|
5
|
+
* Type definitions for the Code Mode runtime engine.
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_CONFIG = {
|
|
8
|
+
width: 1950,
|
|
9
|
+
height: 2400,
|
|
10
|
+
duration: 2,
|
|
11
|
+
fps: 30,
|
|
12
|
+
minDuration: 1,
|
|
13
|
+
maxDuration: 4,
|
|
14
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nexart/codemode-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "NexArt Code Mode Runtime SDK - Deterministic generative art rendering engine",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"typescript": "^5.0.0"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"generative-art",
|
|
21
|
+
"p5js",
|
|
22
|
+
"canvas",
|
|
23
|
+
"rendering",
|
|
24
|
+
"deterministic",
|
|
25
|
+
"nft",
|
|
26
|
+
"creative-coding"
|
|
27
|
+
],
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/artnames/nexart-codemode-sdk.git"
|
|
31
|
+
},
|
|
32
|
+
"author": "NexArt",
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
}
|
|
36
|
+
}
|