vizcore 1.0.0 → 1.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.
@@ -0,0 +1,81 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="description" content="Vizcore Ruby wasm playground for editing Ruby scene DSL and previewing audio-reactive visuals in the browser." />
7
+ <meta name="theme-color" content="#08110f" />
8
+ <link rel="canonical" href="https://ydah.github.io/vizcore/playground.html" />
9
+ <meta property="og:type" content="website" />
10
+ <meta property="og:title" content="Vizcore Playground" />
11
+ <meta property="og:description" content="Edit Vizcore Ruby DSL and preview audio-reactive scenes in the browser." />
12
+ <meta property="og:url" content="https://ydah.github.io/vizcore/playground.html" />
13
+ <meta property="og:image" content="https://ydah.github.io/vizcore/assets/vizcore-poster.png" />
14
+ <title>Vizcore Playground</title>
15
+ <link rel="stylesheet" href="assets/playground.css" />
16
+ </head>
17
+ <body>
18
+ <a class="skip-link" href="#editor">Skip to editor</a>
19
+
20
+ <header class="topbar" aria-label="Playground header">
21
+ <a class="brand" href="index.html">Vizcore</a>
22
+ <nav class="topbar-nav" aria-label="Playground navigation">
23
+ <a href="index.html">Home</a>
24
+ <a href="https://github.com/ydah/vizcore">GitHub</a>
25
+ </nav>
26
+ </header>
27
+
28
+ <main class="playground-shell">
29
+ <section class="editor-panel" aria-labelledby="editor-title">
30
+ <div class="panel-header">
31
+ <div>
32
+ <p class="eyebrow">Ruby wasm</p>
33
+ <h1 id="editor-title">Playground</h1>
34
+ </div>
35
+ <div class="toolbar">
36
+ <label class="preset-label" for="preset-select">Preset</label>
37
+ <select id="preset-select" class="preset-select"></select>
38
+ <button id="run-button" class="button primary" type="button">Run</button>
39
+ <button id="reset-button" class="button secondary" type="button">Reset</button>
40
+ </div>
41
+ </div>
42
+
43
+ <textarea id="editor" class="code-editor" spellcheck="false" autocomplete="off" autocapitalize="off"></textarea>
44
+
45
+ <div class="output-row">
46
+ <div class="status-line" aria-live="polite">
47
+ <span id="ruby-status">Ruby wasm idle</span>
48
+ <span id="compile-status">Waiting</span>
49
+ </div>
50
+ <details class="json-output">
51
+ <summary>Scene JSON</summary>
52
+ <pre id="json-output">{}</pre>
53
+ </details>
54
+ </div>
55
+ </section>
56
+
57
+ <section class="preview-panel" aria-labelledby="preview-title">
58
+ <div class="preview-header">
59
+ <div>
60
+ <p class="eyebrow">Canvas preview</p>
61
+ <h2 id="preview-title">Output</h2>
62
+ </div>
63
+ <div id="scene-tabs" class="scene-tabs" aria-label="Scenes"></div>
64
+ </div>
65
+
66
+ <div class="canvas-wrap">
67
+ <canvas id="preview-canvas" width="1280" height="720" aria-label="Vizcore visual preview"></canvas>
68
+ <div class="preview-meter" aria-live="polite">
69
+ <span id="scene-stat">Scene: --</span>
70
+ <span id="audio-stat">Amplitude: --</span>
71
+ <span id="beat-stat">Beat: --</span>
72
+ </div>
73
+ </div>
74
+
75
+ <div id="error-output" class="error-output" role="alert" hidden></div>
76
+ </section>
77
+ </main>
78
+
79
+ <script type="module" src="assets/playground.js"></script>
80
+ </body>
81
+ </html>
data/docs/shape_dsl.md ADDED
@@ -0,0 +1,269 @@
1
+ # Vizcore Shape DSL
2
+
3
+ Vizcore shape layers can declare 2D vector primitives directly in Ruby. Calling a
4
+ shape primitive inside a layer automatically marks that layer as `type: :shape`.
5
+
6
+ ```ruby
7
+ Vizcore.define do
8
+ scene :logo do
9
+ layer :badge do
10
+ rect :panel, width: 360, height: 160, radius: 24 do
11
+ fill "#111827"
12
+ stroke width: 2, color: "#38bdf8"
13
+ opacity 0.8
14
+ end
15
+
16
+ star :spark, points: 5, radius: 72, inner_radius: 28 do
17
+ translate x: 180, y: 0
18
+ map beat_pulse, to: :scale, range: 0.8..1.4
19
+ end
20
+ end
21
+ end
22
+ end
23
+ ```
24
+
25
+ ## Primitives
26
+
27
+ Supported primitives:
28
+
29
+ - `circle id = nil, x: 0, y: 0, radius: 100, count: 1, segments: 96`
30
+ - `line id = nil, x1: -100, y1: 0, x2: 100, y2: 0`
31
+ - `rect id = nil, x: 0, y: 0, width:, height:, radius: 0`
32
+ - `polygon id = nil, points: [[x, y], ...], closed: true`
33
+ - `polyline id = nil, points: [[x, y], ...]`
34
+ - `path id = nil, detail: 32, tolerance: nil, max_segments: 4096 do ... end`
35
+ - `bezier id = nil, from:, control:, to:` for quadratic curves
36
+ - `bezier id = nil, from:, c1:, c2:, to:` for cubic curves
37
+ - `star id = nil, points: 5, radius: 100, inner_radius: 50`
38
+ - `custom_shape name_or_class, **params`
39
+
40
+ `draw do ... end` may be used to group declarations for readability.
41
+ `group do ... end` applies shared style and transform to child primitives.
42
+
43
+ ## Path Commands
44
+
45
+ Path blocks support an SVG-like command subset:
46
+
47
+ ```ruby
48
+ path :blob, detail: 48 do
49
+ move_to 0, 120
50
+ line_to 120, 0
51
+ quad_to 80, -100, 0, -120
52
+ cubic_to -80, -100, -120, 80, 0, 120
53
+ close
54
+ end
55
+ ```
56
+
57
+ Commands are serialized as `M`, `L`, `Q`, `C`, `H`, `V`, `A`, and `Z`. The
58
+ Canvas2D renderer draws `arc_to` as an SVG-style elliptical arc, and the line
59
+ fallback/snapshot renderer flattens curves and arcs to line segments.
60
+ `detail` controls the default curve/arc subdivision count. When `tolerance` is
61
+ provided, quadratic and cubic curves use adaptive flattening instead. `max_segments`
62
+ caps the number of flattened line segments per path; the DSL raises when the
63
+ estimated flattened path would exceed that budget.
64
+
65
+ ## Style And Transform
66
+
67
+ Inside a shape block, these methods target the shape rather than the layer:
68
+
69
+ ```ruby
70
+ fill "#f472b6"
71
+ stroke 2
72
+ stroke width: 3, color: "#ffffff"
73
+ blend :add
74
+ opacity 0.75
75
+
76
+ translate x: 100, y: 40
77
+ translate 100, 40
78
+ rotate 30
79
+ scale 1.2
80
+ scale x: 1.4, y: 0.8
81
+ origin x: 0, y: 0
82
+ ```
83
+
84
+ The transform order is origin adjustment, scale, rotate, then translate.
85
+
86
+ ## Groups
87
+
88
+ Groups are flattened into child primitives during DSL evaluation. Child shapes
89
+ inherit style values they do not set themselves, and group opacity is multiplied
90
+ with child opacity.
91
+
92
+ ```ruby
93
+ group :spinner do
94
+ translate x: 0, y: 80
95
+ rotate 15
96
+ scale 1.2
97
+ opacity 0.75
98
+ stroke width: 2, color: "#38bdf8"
99
+
100
+ 12.times do |index|
101
+ rect width: 12, height: 80 do
102
+ translate x: 0, y: 160
103
+ rotate index * 30
104
+ end
105
+ end
106
+ end
107
+ ```
108
+
109
+ ## Mapping
110
+
111
+ Mappings declared inside a shape block are scoped to that shape:
112
+
113
+ ```ruby
114
+ circle :ring, radius: 120 do
115
+ map bass, to: :radius, range: 80..240
116
+ map beat_pulse, to: :scale, range: 1.0..1.4
117
+ map high, to: :opacity, range: 0.2..1.0
118
+ end
119
+ ```
120
+
121
+ Transform aliases:
122
+
123
+ - `:translate_x` -> `transform.translate.x`
124
+ - `:translate_y` -> `transform.translate.y`
125
+ - `:rotate` and `:rotation` -> `transform.rotate`
126
+ - `:scale` -> `transform.scale`
127
+ - `:scale_x` -> `transform.scale.x`
128
+ - `:scale_y` -> `transform.scale.y`
129
+ - `:origin_x` -> `transform.origin.x`
130
+ - `:origin_y` -> `transform.origin.y`
131
+
132
+ You can also map to a named shape from the layer scope:
133
+
134
+ ```ruby
135
+ rect :panel, width: 360, height: 160
136
+ map bass, to: shape(:panel).rotate, range: -12..12
137
+ ```
138
+
139
+ Mappings inside a static `custom_shape` block are applied to each primitive
140
+ generated by that custom shape:
141
+
142
+ ```ruby
143
+ custom_shape :badge, radius: 120 do
144
+ fill "#22d3ee"
145
+ map beat_pulse, to: :scale, range: 0.8..1.2
146
+ end
147
+ ```
148
+
149
+ ## Custom Shapes
150
+
151
+ Custom shapes are registered in Ruby and expanded into regular shape
152
+ primitives before the browser receives the scene.
153
+
154
+ ```ruby
155
+ class DiamondShape
156
+ include Vizcore::Shape
157
+
158
+ param :radius, default: 100, min: 10, max: 400
159
+
160
+ def draw(ctx)
161
+ radius = ctx.param(:radius)
162
+
163
+ ctx.polygon points: [
164
+ [0, radius],
165
+ [radius, 0],
166
+ [0, -radius],
167
+ [-radius, 0]
168
+ ]
169
+ end
170
+ end
171
+
172
+ Vizcore.register_shape :diamond, DiamondShape
173
+ ```
174
+
175
+ Use the registered shape from a layer:
176
+
177
+ ```ruby
178
+ layer :generated_logo do
179
+ custom_shape :diamond, radius: 140 do
180
+ rotate 45
181
+ stroke width: 2, color: "#ffffff"
182
+ end
183
+ end
184
+ ```
185
+
186
+ Block registration is supported for small generators:
187
+
188
+ ```ruby
189
+ Vizcore.register_shape :spark do |ctx|
190
+ ctx.star points: 5, radius: ctx.param(:radius, 80), inner_radius: 32
191
+ end
192
+ ```
193
+
194
+ `draw(ctx)` may return a primitive hash, an array of primitive hashes, or build
195
+ primitives with `ctx.draw`, `ctx.circle`, `ctx.rect`, `ctx.path`, and the other
196
+ shape methods. Values declared with `param` are validated against their
197
+ `min`/`max` metadata before `draw(ctx)` runs, and dynamic custom shape
198
+ descriptors include the same schema for editor tooling.
199
+
200
+ By default, custom shapes are expanded when the DSL is evaluated. Use
201
+ `dynamic: true` when the generator needs mapped params, `ctx.time`, `ctx.frame`,
202
+ or `ctx.audio` at runtime:
203
+
204
+ ```ruby
205
+ custom_shape :orbit, count: 32, radius: 260, dynamic: true do
206
+ fill "#22d3ee"
207
+ map bass, to: :radius, range: 120..280
208
+ map beat_pulse, to: :scale, range: 0.8..1.2
209
+ end
210
+ ```
211
+
212
+ Inside a dynamic custom shape block, mappings to custom params such as `:radius`
213
+ are applied before `draw(ctx)` runs. Transform aliases such as `:scale` and
214
+ `:rotate` are applied to the generated primitives after expansion.
215
+
216
+ Use `static: true` for generators that are independent of runtime context. Static
217
+ custom shapes with identical renderer, params, layer context, palette, and
218
+ resolution reuse a cached expansion, while each call still receives its own copy
219
+ of the generated primitives.
220
+
221
+ ## Coordinates
222
+
223
+ New shape schema layers use center-origin logical coordinates by default:
224
+
225
+ - `x` increases to the right.
226
+ - `y` increases upward.
227
+ - `x: 0, y: 0` is the center of the canvas.
228
+
229
+ Legacy `circle` and `line` layers without `shape_schema_version: 2` keep the old
230
+ coordinate heuristic for compatibility with existing examples.
231
+
232
+ Set `units :ndc` on the layer to use normalized device coordinates directly.
233
+
234
+ ## Validation
235
+
236
+ The DSL raises early for malformed primitives:
237
+
238
+ - duplicate shape IDs within a layer
239
+ - negative `radius`, `inner_radius`, `width`, `height`, or `stroke_width`
240
+ - `polygon` with fewer than 3 points
241
+ - `polyline` with fewer than 2 points
242
+ - `path` without commands
243
+ - `path` whose estimated flattened segment count exceeds `max_segments`
244
+ - unknown `custom_shape`
245
+ - custom shapes that return unsupported primitive kinds
246
+
247
+ `vizcore validate` also emits warnings for shape payloads that are renderable but
248
+ likely surprising: unsupported primitive kinds in raw `params[:shapes]`, fills
249
+ that line fallback will ignore, opacity values outside `0..1`, and zero or very
250
+ large scale values that will collapse or clamp.
251
+
252
+ ## Renderer Notes
253
+
254
+ The browser renderer uses a Canvas2D shape backend composited through the
255
+ existing WebGL layer pipeline. It supports fill, stroke color/width, opacity,
256
+ dash, line caps/joins, transforms, and logical/ndc coordinates for the shape
257
+ schema.
258
+
259
+ If Canvas2D is unavailable, the browser falls back to the existing line renderer.
260
+ That fallback ignores fill and only approximates stroke geometry. The software
261
+ snapshot renderer also uses line flattening.
262
+
263
+ The browser HUD includes local shape editor controls for resolved shape layers:
264
+ primitive kind, translate, rotate, scale, opacity, fill, stroke color, and stroke
265
+ width can be adjusted in the running view without mutating the source DSL file.
266
+ Dynamic custom shape params with `param` metadata are also exposed as HUD
267
+ controls; changing one sends a runtime override to the server so the Ruby custom
268
+ shape is re-expanded on the next frame. The HUD also lists valid mapping target
269
+ paths for layer params, primitive params/transforms, and custom shape params.
data/frontend/index.html CHANGED
@@ -409,6 +409,29 @@
409
409
  text-align: right;
410
410
  }
411
411
 
412
+ .shader-param-controls details {
413
+ margin-top: 0.35rem;
414
+ padding-top: 0.3rem;
415
+ border-top: 1px solid rgba(153, 195, 255, 0.12);
416
+ }
417
+
418
+ .shader-param-controls summary {
419
+ cursor: pointer;
420
+ color: #d9e8ff;
421
+ font-size: 0.78rem;
422
+ font-weight: 700;
423
+ }
424
+
425
+ .shader-param-controls select,
426
+ .shader-param-controls input[type="color"] {
427
+ width: 100%;
428
+ min-height: 1.65rem;
429
+ border: 1px solid rgba(153, 195, 255, 0.28);
430
+ border-radius: 0.35rem;
431
+ background: rgba(2, 6, 14, 0.72);
432
+ color: var(--fg);
433
+ }
434
+
412
435
  @media (max-width: 520px) {
413
436
  .hud {
414
437
  right: 1rem;
@@ -507,6 +530,9 @@
507
530
  </div>
508
531
  </div>
509
532
  <div id="shader-param-controls" class="shader-param-controls" hidden aria-label="Shader parameter controls"></div>
533
+ <div id="shape-editor-controls" class="shader-param-controls" hidden aria-label="Shape editor controls"></div>
534
+ <div id="custom-shape-param-controls" class="shader-param-controls" hidden aria-label="Custom shape parameter controls"></div>
535
+ <div id="mapping-target-selector" class="shader-param-controls" hidden aria-label="Mapping target selector"></div>
510
536
  <div id="scene-switcher" class="scene-switcher" hidden aria-label="Scene switcher"></div>
511
537
  <button id="audio-toggle" type="button" hidden>Play Audio</button>
512
538
  </section>
@@ -0,0 +1,106 @@
1
+ export const customShapeParamControlEntries = (layers, overrides = {}) => {
2
+ const entries = [];
3
+ const layerList = Array.isArray(layers) ? layers : [];
4
+
5
+ layerList.forEach((layer, layerIndex) => {
6
+ const controls = Array.isArray(layer?.params?.custom_shape_controls) ? layer.params.custom_shape_controls : [];
7
+ if (!controls.length) return;
8
+
9
+ const layerKey = layerControlKey(layer, layerIndex);
10
+ const layerName = String(layer?.name || `layer_${layerIndex + 1}`);
11
+ controls.forEach((control, controlIndex) => {
12
+ const customShapeIndex = finiteIndex(control?.index, controlIndex);
13
+ const customShapeName = String(control?.name || `custom_shape_${customShapeIndex + 1}`);
14
+ const params = control?.params && typeof control.params === "object" ? control.params : {};
15
+ const schemaByName = paramSchemaByName(control?.param_schema);
16
+ const paramNames = [...new Set([...Object.keys(schemaByName), ...Object.keys(params)])].sort();
17
+
18
+ paramNames.forEach((paramName) => {
19
+ const schema = schemaByName[paramName] || {};
20
+ const baseValue = finiteNumber(params[paramName], finiteNumber(schema.default, 0));
21
+ const min = finiteNumber(schema.min, Math.min(0, baseValue));
22
+ const fallbackMax = Math.max(min + 1, Math.abs(baseValue) * 2, 1);
23
+ const max = Math.max(min, finiteNumber(schema.max, fallbackMax));
24
+ const step = positiveNumber(schema.step, Math.max((max - min) / 100, 0.01));
25
+ const value = finiteNumber(overrides?.[layerKey]?.[customShapeIndex]?.[paramName], baseValue);
26
+
27
+ entries.push({
28
+ key: `${layerKey}:custom_shapes.${customShapeIndex}.params.${paramName}`,
29
+ layerKey,
30
+ layerName,
31
+ customShapeIndex,
32
+ customShapeName,
33
+ paramName,
34
+ label: `${layerName}.${customShapeName}.${paramName}`,
35
+ target: `custom_shapes.${customShapeIndex}.params.${paramName}`,
36
+ min,
37
+ max,
38
+ step,
39
+ value: clamp(value, min, max),
40
+ });
41
+ });
42
+ });
43
+ });
44
+
45
+ return entries;
46
+ };
47
+
48
+ export const pruneCustomShapeParamOverrides = (overrides = {}, entries = []) => {
49
+ const validKeys = new Set(entries.map((entry) => `${entry.layerKey}:${entry.customShapeIndex}:${entry.paramName}`));
50
+ const next = {};
51
+
52
+ Object.entries(overrides || {}).forEach(([layerKey, controls]) => {
53
+ if (!controls || typeof controls !== "object") return;
54
+
55
+ Object.entries(controls).forEach(([customShapeIndex, params]) => {
56
+ if (!params || typeof params !== "object") return;
57
+
58
+ Object.entries(params).forEach(([paramName, value]) => {
59
+ if (!validKeys.has(`${layerKey}:${customShapeIndex}:${paramName}`) || !Number.isFinite(Number(value))) return;
60
+
61
+ next[layerKey] ||= {};
62
+ next[layerKey][customShapeIndex] ||= {};
63
+ next[layerKey][customShapeIndex][paramName] = Number(value);
64
+ });
65
+ });
66
+ });
67
+
68
+ return next;
69
+ };
70
+
71
+ export const customShapeParamMessage = (entry, value) => ({
72
+ layer: entry.layerName,
73
+ custom_shape_index: entry.customShapeIndex,
74
+ param: entry.paramName,
75
+ value: clamp(finiteNumber(value, entry.value), entry.min, entry.max),
76
+ });
77
+
78
+ const layerControlKey = (layer, index) => `${index}:${String(layer?.name || "layer")}`;
79
+
80
+ const paramSchemaByName = (schema) => {
81
+ const output = {};
82
+ (Array.isArray(schema) ? schema : []).forEach((entry) => {
83
+ const name = String(entry?.name || "").trim();
84
+ if (!name) return;
85
+
86
+ output[name] = entry;
87
+ });
88
+ return output;
89
+ };
90
+
91
+ const finiteIndex = (value, fallback) => {
92
+ const numeric = Number(value);
93
+ return Number.isInteger(numeric) && numeric >= 0 ? numeric : fallback;
94
+ };
95
+
96
+ const finiteNumber = (value, fallback) => {
97
+ const numeric = Number(value);
98
+ return Number.isFinite(numeric) ? numeric : fallback;
99
+ };
100
+
101
+ const positiveNumber = (value, fallback) => {
102
+ const numeric = finiteNumber(value, fallback);
103
+ return numeric > 0 ? numeric : fallback;
104
+ };
105
+
106
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);