vizcore 0.1.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.
- checksums.yaml +4 -4
- data/README.md +70 -117
- data/docs/.nojekyll +0 -0
- data/docs/assets/playground-worker.js +373 -0
- data/docs/assets/playground.css +440 -0
- data/docs/assets/playground.js +652 -0
- data/docs/assets/site.css +744 -0
- data/docs/assets/vizcore-demo.gif +0 -0
- data/docs/assets/vizcore-poster.png +0 -0
- data/docs/assets/vj-tunnel.js +159 -0
- data/docs/index.html +225 -0
- data/docs/playground.html +81 -0
- data/docs/shape_dsl.md +269 -0
- data/examples/README.md +59 -0
- data/examples/assets/README.md +19 -0
- data/examples/audio_inspector.rb +34 -0
- data/examples/club_intro_drop.rb +78 -0
- data/examples/kansai_rubykaigi_visual.rb +70 -0
- data/examples/live_coding_minimal.rb +22 -0
- data/examples/midi_controller_show.rb +78 -0
- data/examples/midi_scene_switch.rb +3 -1
- data/examples/parser_visualizer.rb +48 -0
- data/examples/readme_demo.rb +17 -0
- data/examples/rhythm_geometry.rb +34 -0
- data/examples/ruby_crystal_show.rb +35 -0
- data/examples/shader_playground.rb +18 -0
- data/examples/unyo_liquid.rb +59 -0
- data/examples/vj_ambient_chill_room.rb +124 -0
- data/examples/vj_dnb_jungle.rb +170 -0
- data/examples/vj_festival_mainstage.rb +245 -0
- data/examples/vj_festival_mainstage.yml +17 -0
- data/examples/vj_glitch_industrial.rb +164 -0
- data/examples/vj_hiphop_cipher.rb +167 -0
- data/examples/vj_jpop_idol_live.rb +210 -0
- data/examples/vj_synthwave_retro.rb +173 -0
- data/examples/vj_techno_warehouse.rb +195 -0
- data/frontend/index.html +494 -2
- data/frontend/src/audio-inspector.js +40 -0
- data/frontend/src/custom-shape-param-controls.js +106 -0
- data/frontend/src/live-controls.js +131 -0
- data/frontend/src/main.js +1060 -16
- data/frontend/src/mapping-target-selector.js +109 -0
- data/frontend/src/midi-learn.js +194 -0
- data/frontend/src/performance-monitor.js +183 -0
- data/frontend/src/plugin-runtime.js +130 -0
- data/frontend/src/projector-mode.js +56 -0
- data/frontend/src/renderer/engine.js +157 -3
- data/frontend/src/renderer/layer-manager.js +442 -30
- data/frontend/src/renderer/shader-manager.js +26 -0
- data/frontend/src/runtime-control-preset.js +11 -0
- data/frontend/src/shader-error-overlay.js +29 -0
- data/frontend/src/shader-param-controls.js +93 -0
- data/frontend/src/shaders/builtins.js +380 -2
- data/frontend/src/shaders/post-effects.js +52 -0
- data/frontend/src/shape-editor-controls.js +157 -0
- data/frontend/src/visual-regression.js +67 -0
- data/frontend/src/visual-settings-preset.js +103 -0
- data/frontend/src/visuals/geometry.js +666 -0
- data/frontend/src/visuals/image-renderer.js +291 -0
- data/frontend/src/visuals/particle-system.js +56 -10
- data/frontend/src/visuals/shape-renderer.js +475 -0
- data/frontend/src/visuals/spectrogram-renderer.js +226 -0
- data/frontend/src/visuals/svg-arc.js +104 -0
- data/frontend/src/visuals/text-renderer.js +112 -11
- data/frontend/src/websocket-client.js +12 -1
- data/lib/vizcore/analysis/adaptive_normalizer.rb +70 -0
- data/lib/vizcore/analysis/beat_detector.rb +4 -2
- data/lib/vizcore/analysis/bpm_estimator.rb +8 -0
- data/lib/vizcore/analysis/feature_recorder.rb +159 -0
- data/lib/vizcore/analysis/feature_replay.rb +84 -0
- data/lib/vizcore/analysis/pipeline.rb +235 -11
- data/lib/vizcore/analysis/tap_tempo.rb +74 -0
- data/lib/vizcore/analysis.rb +4 -0
- data/lib/vizcore/audio/dummy_sine_input.rb +1 -1
- data/lib/vizcore/audio/fixture_input.rb +65 -0
- data/lib/vizcore/audio/input_manager.rb +4 -2
- data/lib/vizcore/audio/mic_input.rb +24 -8
- data/lib/vizcore/audio/portaudio_ffi.rb +106 -1
- data/lib/vizcore/audio.rb +1 -0
- data/lib/vizcore/cli/doctor.rb +159 -0
- data/lib/vizcore/cli/dsl_reference.rb +99 -0
- data/lib/vizcore/cli/layer_docs.rb +46 -0
- data/lib/vizcore/cli/scene_diagnostics.rb +23 -0
- data/lib/vizcore/cli/scene_inspector.rb +136 -0
- data/lib/vizcore/cli/scene_validator.rb +337 -0
- data/lib/vizcore/cli/shader_template.rb +68 -0
- data/lib/vizcore/cli/shader_uniform_docs.rb +54 -0
- data/lib/vizcore/cli.rb +689 -18
- data/lib/vizcore/config.rb +103 -2
- data/lib/vizcore/control_preset.rb +68 -0
- data/lib/vizcore/dsl/engine.rb +277 -5
- data/lib/vizcore/dsl/layer_builder.rb +1280 -23
- data/lib/vizcore/dsl/layer_group_builder.rb +112 -0
- data/lib/vizcore/dsl/mapping_resolver.rb +290 -7
- data/lib/vizcore/dsl/mapping_transform_builder.rb +71 -0
- data/lib/vizcore/dsl/reaction_builder.rb +44 -0
- data/lib/vizcore/dsl/scene_builder.rb +61 -5
- data/lib/vizcore/dsl/shader_source_resolver.rb +67 -6
- data/lib/vizcore/dsl/style_builder.rb +68 -0
- data/lib/vizcore/dsl/timeline_builder.rb +138 -0
- data/lib/vizcore/dsl/transition_controller.rb +77 -0
- data/lib/vizcore/dsl.rb +5 -1
- data/lib/vizcore/layer_catalog.rb +275 -0
- data/lib/vizcore/project_manifest.rb +152 -0
- data/lib/vizcore/renderer/png_writer.rb +57 -0
- data/lib/vizcore/renderer/render_sequence.rb +153 -0
- data/lib/vizcore/renderer/scene_frame_source.rb +132 -0
- data/lib/vizcore/renderer/scene_serializer.rb +36 -3
- data/lib/vizcore/renderer/snapshot.rb +38 -0
- data/lib/vizcore/renderer/snapshot_renderer.rb +938 -0
- data/lib/vizcore/renderer.rb +5 -0
- data/lib/vizcore/server/frame_broadcaster.rb +143 -8
- data/lib/vizcore/server/gallery_app.rb +155 -0
- data/lib/vizcore/server/gallery_page.rb +100 -0
- data/lib/vizcore/server/gallery_runner.rb +48 -0
- data/lib/vizcore/server/rack_app.rb +203 -4
- data/lib/vizcore/server/runner.rb +391 -22
- data/lib/vizcore/server/scene_dependency_watcher.rb +79 -0
- data/lib/vizcore/server/websocket_handler.rb +60 -10
- data/lib/vizcore/server.rb +4 -0
- data/lib/vizcore/shape.rb +719 -0
- data/lib/vizcore/sync/osc_message.rb +103 -0
- data/lib/vizcore/sync/osc_receiver.rb +68 -0
- data/lib/vizcore/sync.rb +4 -0
- data/lib/vizcore/templates/midi_control_scene.rb +3 -1
- data/lib/vizcore/templates/plugin_layer.rb +20 -0
- data/lib/vizcore/templates/plugin_readme.md +23 -0
- data/lib/vizcore/templates/plugin_renderer.js +43 -0
- data/lib/vizcore/templates/plugin_scene.rb +14 -0
- data/lib/vizcore/templates/project_readme.md +7 -23
- data/lib/vizcore/templates/rubykaigi_scene.rb +30 -0
- data/lib/vizcore/version.rb +1 -1
- data/lib/vizcore.rb +28 -0
- data/scripts/browser_capture.mjs +75 -0
- data/sig/vizcore.rbs +461 -0
- metadata +94 -3
- data/docs/GETTING_STARTED.md +0 -105
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { describeSvgArc, svgArcPoint, svgArcSegmentCount } from "./svg-arc.js";
|
|
2
|
+
|
|
1
3
|
const BASE_VERTICES = [
|
|
2
4
|
[-1.0, -1.0, -1.0],
|
|
3
5
|
[1.0, -1.0, -1.0],
|
|
@@ -15,6 +17,63 @@ const EDGES = [
|
|
|
15
17
|
[0, 4], [1, 5], [2, 6], [3, 7]
|
|
16
18
|
];
|
|
17
19
|
|
|
20
|
+
const TETRAHEDRON_VERTICES = [
|
|
21
|
+
[1, 1, 1],
|
|
22
|
+
[-1, -1, 1],
|
|
23
|
+
[-1, 1, -1],
|
|
24
|
+
[1, -1, -1]
|
|
25
|
+
].map(([x, y, z]) => [x / Math.sqrt(3), y / Math.sqrt(3), z / Math.sqrt(3)]);
|
|
26
|
+
|
|
27
|
+
const TETRAHEDRON_EDGES = [
|
|
28
|
+
[0, 1], [0, 2], [0, 3],
|
|
29
|
+
[1, 2], [1, 3], [2, 3]
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const OCTAHEDRON_VERTICES = [
|
|
33
|
+
[1, 0, 0], [-1, 0, 0],
|
|
34
|
+
[0, 1, 0], [0, -1, 0],
|
|
35
|
+
[0, 0, 1], [0, 0, -1]
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const OCTAHEDRON_EDGES = [
|
|
39
|
+
[0, 2], [0, 3], [0, 4], [0, 5],
|
|
40
|
+
[1, 2], [1, 3], [1, 4], [1, 5],
|
|
41
|
+
[2, 4], [2, 5], [3, 4], [3, 5]
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const PHI = (1 + Math.sqrt(5)) / 2;
|
|
45
|
+
const ICOSAHEDRON_SCALE = 1 / Math.sqrt(1 + PHI * PHI);
|
|
46
|
+
const ICOSAHEDRON_VERTICES = [
|
|
47
|
+
[-1, PHI, 0], [1, PHI, 0], [-1, -PHI, 0], [1, -PHI, 0],
|
|
48
|
+
[0, -1, PHI], [0, 1, PHI], [0, -1, -PHI], [0, 1, -PHI],
|
|
49
|
+
[PHI, 0, -1], [PHI, 0, 1], [-PHI, 0, -1], [-PHI, 0, 1]
|
|
50
|
+
].map(([x, y, z]) => [x * ICOSAHEDRON_SCALE, y * ICOSAHEDRON_SCALE, z * ICOSAHEDRON_SCALE]);
|
|
51
|
+
|
|
52
|
+
const ICOSAHEDRON_EDGES = [
|
|
53
|
+
[0, 1], [0, 5], [0, 7], [0, 10], [0, 11],
|
|
54
|
+
[1, 5], [1, 7], [1, 8], [1, 9],
|
|
55
|
+
[2, 3], [2, 4], [2, 6], [2, 10], [2, 11],
|
|
56
|
+
[3, 4], [3, 6], [3, 8], [3, 9],
|
|
57
|
+
[4, 5], [4, 9], [4, 11],
|
|
58
|
+
[5, 9], [5, 11],
|
|
59
|
+
[6, 7], [6, 8], [6, 10],
|
|
60
|
+
[7, 8], [7, 10],
|
|
61
|
+
[8, 9],
|
|
62
|
+
[10, 11]
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const MESH_PRESETS = {
|
|
66
|
+
cube: { vertices: BASE_VERTICES.map(([x, y, z]) => [x * 0.62, y * 0.62, z * 0.62]), edges: EDGES },
|
|
67
|
+
tetrahedron: { vertices: TETRAHEDRON_VERTICES, edges: TETRAHEDRON_EDGES },
|
|
68
|
+
octahedron: { vertices: OCTAHEDRON_VERTICES, edges: OCTAHEDRON_EDGES },
|
|
69
|
+
icosahedron: { vertices: ICOSAHEDRON_VERTICES, edges: ICOSAHEDRON_EDGES }
|
|
70
|
+
};
|
|
71
|
+
const SHAPE_HALF_WIDTH = 640;
|
|
72
|
+
const SHAPE_HALF_HEIGHT = 360;
|
|
73
|
+
const PATH_DEFAULT_MAX_SEGMENTS = 4096;
|
|
74
|
+
const PATH_HARD_MAX_SEGMENTS = 65536;
|
|
75
|
+
const PATH_MAX_RECURSION = 12;
|
|
76
|
+
|
|
18
77
|
export const buildWireframeLines = ({ rotationY, rotationX, deform }) => {
|
|
19
78
|
const amount = clamp(Number(deform || 0), 0, 1);
|
|
20
79
|
const projected = BASE_VERTICES.map((vertex) => {
|
|
@@ -34,6 +93,34 @@ export const buildWireframeLines = ({ rotationY, rotationX, deform }) => {
|
|
|
34
93
|
return lines;
|
|
35
94
|
};
|
|
36
95
|
|
|
96
|
+
export const buildPresetMeshLines = ({ rotationY = 0, rotationX = 0, deform = 0, params = {} } = {}) => {
|
|
97
|
+
const geometry = normalizeMeshGeometry(params.geometry);
|
|
98
|
+
const mesh = MESH_PRESETS[geometry] || MESH_PRESETS.icosahedron;
|
|
99
|
+
const scale = clamp(finiteNumber(params.scale, 1), 0.1, 3.0);
|
|
100
|
+
const amount = clamp(finiteNumber(deform, 0), 0, 1);
|
|
101
|
+
|
|
102
|
+
const projected = mesh.vertices.map((vertex, index) => {
|
|
103
|
+
const radialPulse = 1 + amount * (0.12 + (index % 3) * 0.05);
|
|
104
|
+
const twist = Math.sin(index * 1.618 + amount * Math.PI) * amount * 0.08;
|
|
105
|
+
return projectVertex(
|
|
106
|
+
[
|
|
107
|
+
(vertex[0] + vertex[1] * twist) * scale * radialPulse,
|
|
108
|
+
(vertex[1] + vertex[2] * twist) * scale * (1 + amount * 0.08),
|
|
109
|
+
(vertex[2] + vertex[0] * twist) * scale * radialPulse
|
|
110
|
+
],
|
|
111
|
+
rotationY,
|
|
112
|
+
rotationX
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const lines = [];
|
|
117
|
+
for (const [start, end] of mesh.edges) {
|
|
118
|
+
lines.push(projected[start][0], projected[start][1]);
|
|
119
|
+
lines.push(projected[end][0], projected[end][1]);
|
|
120
|
+
}
|
|
121
|
+
return lines;
|
|
122
|
+
};
|
|
123
|
+
|
|
37
124
|
export const estimateDeformFromSpectrum = (value) => {
|
|
38
125
|
if (Array.isArray(value)) {
|
|
39
126
|
if (value.length === 0) {
|
|
@@ -46,6 +133,574 @@ export const estimateDeformFromSpectrum = (value) => {
|
|
|
46
133
|
return clamp(Number(value || 0), 0, 1);
|
|
47
134
|
};
|
|
48
135
|
|
|
136
|
+
export const buildRadialBlobLines = ({ time, params = {}, audio = {} }) => {
|
|
137
|
+
const segments = clampInt(params.segments || 160, 24, 512);
|
|
138
|
+
const baseRadius = clamp(Number(params.radius ?? 0.46), 0.05, 1.4);
|
|
139
|
+
const wobble = clamp(Number(params.wobble ?? audio?.amplitude ?? 0), 0, 3);
|
|
140
|
+
const spectrum = Array.isArray(params.spectrum) ? params.spectrum : Array.isArray(audio?.fft) ? audio.fft : [];
|
|
141
|
+
const bass = clamp(Number(audio?.bands?.low || 0), 0, 1);
|
|
142
|
+
const mid = clamp(Number(audio?.bands?.mid || 0), 0, 1);
|
|
143
|
+
const pulse = clamp(Number(audio?.beat_pulse || (audio?.beat ? 1 : 0)), 0, 1);
|
|
144
|
+
const points = [];
|
|
145
|
+
|
|
146
|
+
const sample = (index) => {
|
|
147
|
+
if (!spectrum.length) return 0;
|
|
148
|
+
return clamp(Number(spectrum[index % spectrum.length] || 0), 0, 1);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
for (let index = 0; index < segments; index += 1) {
|
|
152
|
+
const next = (index + 1) % segments;
|
|
153
|
+
appendRadialPoint(points, index, segments, baseRadius, wobble, bass, mid, pulse, time, sample(index));
|
|
154
|
+
appendRadialPoint(points, next, segments, baseRadius, wobble, bass, mid, pulse, time, sample(next));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return points;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const normalizeWaveformStyle = (value) => {
|
|
161
|
+
const style = String(value || "line").trim().toLowerCase();
|
|
162
|
+
if (style === "mirror" || style === "ribbon") return style;
|
|
163
|
+
return "line";
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const buildWaveformLines = ({ time = 0, params = {}, audio = {} } = {}) => {
|
|
167
|
+
const detail = clampInt(params.detail || 96, 16, 256);
|
|
168
|
+
const height = clamp(finiteNumber(params.height ?? 0.46, 0.46), 0.05, 1.1);
|
|
169
|
+
const amplitude = clamp(finiteNumber(audio?.amplitude, 0), 0, 1);
|
|
170
|
+
const spectrum = Array.isArray(params.spectrum) ? params.spectrum : Array.isArray(audio?.fft) ? audio.fft : [];
|
|
171
|
+
const style = normalizeWaveformStyle(params.style);
|
|
172
|
+
const samples = buildWaveformSamples({ detail, height, amplitude, spectrum, time });
|
|
173
|
+
const points = [];
|
|
174
|
+
|
|
175
|
+
appendLineSegments(points, samples);
|
|
176
|
+
|
|
177
|
+
if (style === "mirror" || style === "ribbon") {
|
|
178
|
+
const mirrored = samples.map(([x, y]) => [x, -y]);
|
|
179
|
+
appendLineSegments(points, mirrored);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (style === "ribbon") {
|
|
183
|
+
const stride = Math.max(4, Math.round(detail / 16));
|
|
184
|
+
for (let index = 0; index < samples.length; index += stride) {
|
|
185
|
+
const [x, y] = samples[index];
|
|
186
|
+
points.push(x, y, x, -y);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return points;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const buildShapeLines = ({ params = {} } = {}) => {
|
|
194
|
+
const shapes = Array.isArray(params.shapes) ? params.shapes : [];
|
|
195
|
+
const context = shapeCoordinateContext(params);
|
|
196
|
+
const points = [];
|
|
197
|
+
|
|
198
|
+
shapes.forEach((shape) => {
|
|
199
|
+
const kind = String(shape?.kind || shape?.type || "").toLowerCase();
|
|
200
|
+
if (kind === "circle") {
|
|
201
|
+
appendCircleShape(points, shape, context);
|
|
202
|
+
} else if (kind === "line") {
|
|
203
|
+
appendLineShape(points, shape, context);
|
|
204
|
+
} else if (kind === "rect") {
|
|
205
|
+
appendRectShape(points, shape, context);
|
|
206
|
+
} else if (kind === "polygon" || kind === "polyline") {
|
|
207
|
+
appendPolygonShape(points, shape, context, kind === "polygon");
|
|
208
|
+
} else if (kind === "path") {
|
|
209
|
+
appendPathShape(points, shape, context);
|
|
210
|
+
} else if (kind === "star") {
|
|
211
|
+
appendStarShape(points, shape, context);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return points;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const buildWaveformSamples = ({ detail, height, amplitude, spectrum, time }) => {
|
|
219
|
+
const samples = [];
|
|
220
|
+
const safeTime = finiteNumber(time, 0);
|
|
221
|
+
|
|
222
|
+
for (let index = 0; index < detail; index += 1) {
|
|
223
|
+
const progress = detail === 1 ? 0 : index / (detail - 1);
|
|
224
|
+
const x = -0.92 + progress * 1.84;
|
|
225
|
+
const fftValue = sampleSpectrum(spectrum, progress);
|
|
226
|
+
const carrier = Math.sin(index * 0.55 + safeTime * (2.4 + amplitude * 2.0));
|
|
227
|
+
const harmonic = Math.sin(index * 0.13 + safeTime * 1.1);
|
|
228
|
+
const energy = 0.12 + amplitude * 0.35 + fftValue * 0.55;
|
|
229
|
+
const y = clamp((carrier * 0.72 + harmonic * 0.28) * energy * height, -0.92, 0.92);
|
|
230
|
+
samples.push([x, y]);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return samples;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const appendLineSegments = (points, samples) => {
|
|
237
|
+
for (let index = 1; index < samples.length; index += 1) {
|
|
238
|
+
points.push(samples[index - 1][0], samples[index - 1][1], samples[index][0], samples[index][1]);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const appendCircleShape = (points, shape, context) => {
|
|
243
|
+
const count = clampInt(shape.count || 1, 1, 64);
|
|
244
|
+
const segments = clampInt(shape.segments || 96, 12, 256);
|
|
245
|
+
const radius = normalizeShapeLength(shape.radius ?? 100, context, "radius");
|
|
246
|
+
const center = normalizeShapePoint(shape.x ?? 0, shape.y ?? 0, context);
|
|
247
|
+
|
|
248
|
+
for (let ring = 0; ring < count; ring += 1) {
|
|
249
|
+
const ringRadius = radius * ((ring + 1) / count);
|
|
250
|
+
for (let index = 0; index < segments; index += 1) {
|
|
251
|
+
appendSegment(
|
|
252
|
+
points,
|
|
253
|
+
circlePoint(center, ringRadius, index, segments),
|
|
254
|
+
circlePoint(center, ringRadius, index + 1, segments),
|
|
255
|
+
shape,
|
|
256
|
+
context
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const circlePoint = (center, radius, index, segments) => {
|
|
263
|
+
const angle = (index / segments) * Math.PI * 2;
|
|
264
|
+
return [center[0] + Math.cos(angle) * radius, center[1] + Math.sin(angle) * radius];
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const appendLineShape = (points, shape, context) => {
|
|
268
|
+
const [x1, y1, x2, y2] = lineDefaults(context);
|
|
269
|
+
appendSegment(
|
|
270
|
+
points,
|
|
271
|
+
normalizeShapePoint(shape.x1 ?? x1, shape.y1 ?? y1, context),
|
|
272
|
+
normalizeShapePoint(shape.x2 ?? x2, shape.y2 ?? y2, context),
|
|
273
|
+
shape,
|
|
274
|
+
context
|
|
275
|
+
);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const appendRectShape = (points, shape, context) => {
|
|
279
|
+
const center = normalizeShapePoint(shape.x ?? 0, shape.y ?? 0, context);
|
|
280
|
+
const halfWidth = normalizeShapeLength(shape.width ?? 100, context, "x") / 2;
|
|
281
|
+
const halfHeight = normalizeShapeLength(shape.height ?? 100, context, "y") / 2;
|
|
282
|
+
const vertices = [
|
|
283
|
+
[center[0] - halfWidth, center[1] - halfHeight],
|
|
284
|
+
[center[0] + halfWidth, center[1] - halfHeight],
|
|
285
|
+
[center[0] + halfWidth, center[1] + halfHeight],
|
|
286
|
+
[center[0] - halfWidth, center[1] + halfHeight]
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
appendPolylineSegments(points, vertices, shape, context, true);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const appendPolygonShape = (points, shape, context, defaultClosed) => {
|
|
293
|
+
const vertices = normalizeShapePoints(shape.points, context);
|
|
294
|
+
if (vertices.length < (defaultClosed ? 3 : 2)) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
appendPolylineSegments(points, vertices, shape, context, shape.closed ?? defaultClosed);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const appendStarShape = (points, shape, context) => {
|
|
302
|
+
const tips = clampInt(shape.points || 5, 3, 128);
|
|
303
|
+
const center = normalizeShapePoint(shape.x ?? 0, shape.y ?? 0, context);
|
|
304
|
+
const radius = normalizeShapeLength(shape.radius ?? 100, context, "radius");
|
|
305
|
+
const innerRadius = normalizeShapeLength(shape.inner_radius ?? radiusToRawHalf(shape.radius ?? 100), context, "radius");
|
|
306
|
+
const rotation = (finiteNumber(shape.rotation, -90) / 180) * Math.PI;
|
|
307
|
+
const vertices = [];
|
|
308
|
+
|
|
309
|
+
for (let index = 0; index < tips * 2; index += 1) {
|
|
310
|
+
const angle = rotation + (index / (tips * 2)) * Math.PI * 2;
|
|
311
|
+
const pointRadius = index % 2 === 0 ? radius : innerRadius;
|
|
312
|
+
vertices.push([center[0] + Math.cos(angle) * pointRadius, center[1] + Math.sin(angle) * pointRadius]);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
appendPolylineSegments(points, vertices, shape, context, true);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const appendPathShape = (points, shape, context) => {
|
|
319
|
+
const commands = Array.isArray(shape.commands) ? shape.commands : [];
|
|
320
|
+
const detail = clampInt(shape.detail || 32, 4, 128);
|
|
321
|
+
const tolerance = pathTolerance(shape);
|
|
322
|
+
const budget = pathSegmentBudget(shape);
|
|
323
|
+
let current = null;
|
|
324
|
+
let subpathStart = null;
|
|
325
|
+
|
|
326
|
+
commands.forEach((entry) => {
|
|
327
|
+
const command = Array.isArray(entry) ? String(entry[0] || "").toUpperCase() : "";
|
|
328
|
+
const values = Array.isArray(entry) ? entry.slice(1).map((value) => finiteNumber(value, 0)) : [];
|
|
329
|
+
|
|
330
|
+
if (command === "M" && values.length >= 2) {
|
|
331
|
+
current = [values[0], values[1]];
|
|
332
|
+
subpathStart = current;
|
|
333
|
+
} else if (command === "L" && current && values.length >= 2) {
|
|
334
|
+
const next = [values[0], values[1]];
|
|
335
|
+
appendRawSegment(points, current, next, shape, context, budget);
|
|
336
|
+
current = next;
|
|
337
|
+
} else if (command === "H" && current && values.length >= 1) {
|
|
338
|
+
const next = [values[0], current[1]];
|
|
339
|
+
appendRawSegment(points, current, next, shape, context, budget);
|
|
340
|
+
current = next;
|
|
341
|
+
} else if (command === "V" && current && values.length >= 1) {
|
|
342
|
+
const next = [current[0], values[0]];
|
|
343
|
+
appendRawSegment(points, current, next, shape, context, budget);
|
|
344
|
+
current = next;
|
|
345
|
+
} else if (command === "Q" && current && values.length >= 4) {
|
|
346
|
+
current = appendQuadraticPath(points, current, values, detail, tolerance, shape, context, budget);
|
|
347
|
+
} else if (command === "C" && current && values.length >= 6) {
|
|
348
|
+
current = appendCubicPath(points, current, values, detail, tolerance, shape, context, budget);
|
|
349
|
+
} else if (command === "A" && current && values.length >= 7) {
|
|
350
|
+
current = appendArcPath(points, current, values, detail, shape, context, budget);
|
|
351
|
+
} else if (command === "Z" && current && subpathStart) {
|
|
352
|
+
appendRawSegment(points, current, subpathStart, shape, context, budget);
|
|
353
|
+
current = subpathStart;
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const appendQuadraticPath = (points, current, values, detail, tolerance, shape, context, budget) => {
|
|
359
|
+
let previous = current;
|
|
360
|
+
const control = [values[0], values[1]];
|
|
361
|
+
const end = [values[2], values[3]];
|
|
362
|
+
|
|
363
|
+
if (tolerance !== null) {
|
|
364
|
+
appendAdaptiveQuadraticPath(points, current, control, end, tolerance, shape, context, budget);
|
|
365
|
+
return end;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for (let step = 1; step <= detail; step += 1) {
|
|
369
|
+
const t = step / detail;
|
|
370
|
+
const next = [
|
|
371
|
+
quadraticPoint(current[0], control[0], end[0], t),
|
|
372
|
+
quadraticPoint(current[1], control[1], end[1], t)
|
|
373
|
+
];
|
|
374
|
+
if (!appendRawSegment(points, previous, next, shape, context, budget)) break;
|
|
375
|
+
previous = next;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return end;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const appendCubicPath = (points, current, values, detail, tolerance, shape, context, budget) => {
|
|
382
|
+
let previous = current;
|
|
383
|
+
const c1 = [values[0], values[1]];
|
|
384
|
+
const c2 = [values[2], values[3]];
|
|
385
|
+
const end = [values[4], values[5]];
|
|
386
|
+
|
|
387
|
+
if (tolerance !== null) {
|
|
388
|
+
appendAdaptiveCubicPath(points, current, c1, c2, end, tolerance, shape, context, budget);
|
|
389
|
+
return end;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
for (let step = 1; step <= detail; step += 1) {
|
|
393
|
+
const t = step / detail;
|
|
394
|
+
const next = [
|
|
395
|
+
cubicPoint(current[0], c1[0], c2[0], end[0], t),
|
|
396
|
+
cubicPoint(current[1], c1[1], c2[1], end[1], t)
|
|
397
|
+
];
|
|
398
|
+
if (!appendRawSegment(points, previous, next, shape, context, budget)) break;
|
|
399
|
+
previous = next;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return end;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const appendArcPath = (points, current, values, detail, shape, context, budget) => {
|
|
406
|
+
const end = [values[5], values[6]];
|
|
407
|
+
const arc = describeSvgArc({
|
|
408
|
+
from: current,
|
|
409
|
+
to: end,
|
|
410
|
+
rx: values[0],
|
|
411
|
+
ry: values[1],
|
|
412
|
+
xAxisRotation: values[2],
|
|
413
|
+
largeArc: !!values[3],
|
|
414
|
+
sweep: !!values[4]
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
if (!arc) {
|
|
418
|
+
appendRawSegment(points, current, end, shape, context, budget);
|
|
419
|
+
return end;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
let previous = current;
|
|
423
|
+
const segments = svgArcSegmentCount(arc, detail);
|
|
424
|
+
for (let step = 1; step <= segments; step += 1) {
|
|
425
|
+
const next = svgArcPoint(arc, step / segments);
|
|
426
|
+
if (!appendRawSegment(points, previous, next, shape, context, budget)) break;
|
|
427
|
+
previous = next;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return end;
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const appendRawSegment = (points, from, to, shape, context, budget) => {
|
|
434
|
+
if (budget && budget.remaining <= 0) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
appendSegment(points, normalizeShapePoint(from[0], from[1], context), normalizeShapePoint(to[0], to[1], context), shape, context);
|
|
439
|
+
if (budget) {
|
|
440
|
+
budget.remaining -= 1;
|
|
441
|
+
}
|
|
442
|
+
return true;
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const appendAdaptiveQuadraticPath = (points, from, control, to, tolerance, shape, context, budget, depth = 0) => {
|
|
446
|
+
if (budget && budget.remaining <= 0) return;
|
|
447
|
+
|
|
448
|
+
if (depth >= PATH_MAX_RECURSION || pointLineDistance(control, from, to) <= tolerance) {
|
|
449
|
+
appendRawSegment(points, from, to, shape, context, budget);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const leftControl = midpoint(from, control);
|
|
454
|
+
const rightControl = midpoint(control, to);
|
|
455
|
+
const center = midpoint(leftControl, rightControl);
|
|
456
|
+
appendAdaptiveQuadraticPath(points, from, leftControl, center, tolerance, shape, context, budget, depth + 1);
|
|
457
|
+
appendAdaptiveQuadraticPath(points, center, rightControl, to, tolerance, shape, context, budget, depth + 1);
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const appendAdaptiveCubicPath = (points, from, c1, c2, to, tolerance, shape, context, budget, depth = 0) => {
|
|
461
|
+
if (budget && budget.remaining <= 0) return;
|
|
462
|
+
|
|
463
|
+
const flatness = Math.max(pointLineDistance(c1, from, to), pointLineDistance(c2, from, to));
|
|
464
|
+
if (depth >= PATH_MAX_RECURSION || flatness <= tolerance) {
|
|
465
|
+
appendRawSegment(points, from, to, shape, context, budget);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const p01 = midpoint(from, c1);
|
|
470
|
+
const p12 = midpoint(c1, c2);
|
|
471
|
+
const p23 = midpoint(c2, to);
|
|
472
|
+
const p012 = midpoint(p01, p12);
|
|
473
|
+
const p123 = midpoint(p12, p23);
|
|
474
|
+
const center = midpoint(p012, p123);
|
|
475
|
+
appendAdaptiveCubicPath(points, from, p01, p012, center, tolerance, shape, context, budget, depth + 1);
|
|
476
|
+
appendAdaptiveCubicPath(points, center, p123, p23, to, tolerance, shape, context, budget, depth + 1);
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const appendPolylineSegments = (points, vertices, shape, context, closed) => {
|
|
480
|
+
for (let index = 1; index < vertices.length; index += 1) {
|
|
481
|
+
appendSegment(points, vertices[index - 1], vertices[index], shape, context);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (closed && vertices.length > 2) {
|
|
485
|
+
appendSegment(points, vertices[vertices.length - 1], vertices[0], shape, context);
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const appendSegment = (points, from, to, shape, context) => {
|
|
490
|
+
const start = applyShapeTransform(from, shape, context);
|
|
491
|
+
const end = applyShapeTransform(to, shape, context);
|
|
492
|
+
points.push(start[0], start[1], end[0], end[1]);
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const normalizeShapePoints = (value, context) => {
|
|
496
|
+
if (!Array.isArray(value)) {
|
|
497
|
+
return [];
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return value
|
|
501
|
+
.filter((point) => Array.isArray(point) && point.length >= 2)
|
|
502
|
+
.map((point) => normalizeShapePoint(point[0], point[1], context));
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const shapeCoordinateContext = (params) => {
|
|
506
|
+
const requestedUnits = String(params.units || "").trim().toLowerCase();
|
|
507
|
+
if (requestedUnits) {
|
|
508
|
+
return { units: requestedUnits };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const version = Number(params.shape_schema_version ?? params.shapeSchemaVersion ?? 1);
|
|
512
|
+
return { units: version >= 2 ? "logical" : "legacy" };
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const normalizeShapePoint = (x, y, context) => {
|
|
516
|
+
return [
|
|
517
|
+
normalizeShapeCoordinate(x, context, "x"),
|
|
518
|
+
normalizeShapeCoordinate(y, context, "y")
|
|
519
|
+
];
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const normalizeShapeCoordinate = (value, context, axis) => {
|
|
523
|
+
const numeric = finiteNumber(value, 0);
|
|
524
|
+
if (context.units === "ndc") {
|
|
525
|
+
return clamp(numeric, -1.2, 1.2);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (logicalShapeUnits(context.units)) {
|
|
529
|
+
return clamp(numeric / shapeAxisHalf(axis), -1.2, 1.2);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (screenShapeUnits(context.units)) {
|
|
533
|
+
return axis === "y"
|
|
534
|
+
? clamp(1 - numeric / SHAPE_HALF_HEIGHT, -1.2, 1.2)
|
|
535
|
+
: clamp(numeric / SHAPE_HALF_WIDTH - 1, -1.2, 1.2);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return normalizeLegacyShapeCoordinate(numeric, axis);
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const normalizeLegacyShapeCoordinate = (numeric, axis) => {
|
|
542
|
+
if (Math.abs(numeric) <= 1.5) {
|
|
543
|
+
return clamp(numeric, -1.2, 1.2);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return axis === "y"
|
|
547
|
+
? clamp(1 - numeric / SHAPE_HALF_HEIGHT, -1.2, 1.2)
|
|
548
|
+
: clamp(numeric / SHAPE_HALF_WIDTH - 1, -1.2, 1.2);
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const normalizeShapeLength = (value, context, axis) => {
|
|
552
|
+
const numeric = Math.abs(finiteNumber(value, 0));
|
|
553
|
+
if (context.units === "ndc" || Math.abs(numeric) <= 2) {
|
|
554
|
+
return clamp(numeric, 0.005, 1.4);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return clamp(numeric / shapeAxisHalf(axis === "radius" ? "y" : axis), 0.005, 1.4);
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const normalizeShapeVector = (value, context, axis) => {
|
|
561
|
+
const numeric = finiteNumber(value, 0);
|
|
562
|
+
if (context.units === "ndc") {
|
|
563
|
+
return clamp(numeric, -2.0, 2.0);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return clamp(numeric / shapeAxisHalf(axis), -2.0, 2.0);
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const applyShapeTransform = (point, shape, context) => {
|
|
570
|
+
const transform = shapeTransform(shape, context);
|
|
571
|
+
const shiftedX = (point[0] - transform.origin.x) * transform.scale.x;
|
|
572
|
+
const shiftedY = (point[1] - transform.origin.y) * transform.scale.y;
|
|
573
|
+
const radians = (transform.rotate / 180) * Math.PI;
|
|
574
|
+
const cos = Math.cos(radians);
|
|
575
|
+
const sin = Math.sin(radians);
|
|
576
|
+
const rotatedX = shiftedX * cos - shiftedY * sin;
|
|
577
|
+
const rotatedY = shiftedX * sin + shiftedY * cos;
|
|
578
|
+
|
|
579
|
+
return [
|
|
580
|
+
clamp(rotatedX + transform.origin.x + transform.translate.x, -1.2, 1.2),
|
|
581
|
+
clamp(rotatedY + transform.origin.y + transform.translate.y, -1.2, 1.2)
|
|
582
|
+
];
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const shapeTransform = (shape, context) => {
|
|
586
|
+
const transform = shape?.transform || {};
|
|
587
|
+
return {
|
|
588
|
+
translate: normalizeShapeVectorObject(transform.translate || shape.translate, context, { x: 0, y: 0 }),
|
|
589
|
+
origin: normalizeShapeVectorObject(transform.origin || shape.origin, context, { x: 0, y: 0 }),
|
|
590
|
+
rotate: finiteNumber(transform.rotate ?? shape.rotate ?? shape.rotation, 0),
|
|
591
|
+
scale: normalizeShapeScale(transform.scale ?? shape.scale)
|
|
592
|
+
};
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const normalizeShapeVectorObject = (value, context, fallback) => {
|
|
596
|
+
if (Array.isArray(value)) {
|
|
597
|
+
return {
|
|
598
|
+
x: normalizeShapeVector(value[0] ?? fallback.x, context, "x"),
|
|
599
|
+
y: normalizeShapeVector(value[1] ?? fallback.y, context, "y")
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (value && typeof value === "object") {
|
|
604
|
+
return {
|
|
605
|
+
x: normalizeShapeVector(value.x ?? fallback.x, context, "x"),
|
|
606
|
+
y: normalizeShapeVector(value.y ?? fallback.y, context, "y")
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return fallback;
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const normalizeShapeScale = (value) => {
|
|
614
|
+
if (value && typeof value === "object") {
|
|
615
|
+
return {
|
|
616
|
+
x: clamp(finiteNumber(value.x, 1), -8, 8),
|
|
617
|
+
y: clamp(finiteNumber(value.y, 1), -8, 8)
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const scale = clamp(finiteNumber(value, 1), -8, 8);
|
|
622
|
+
return { x: scale, y: scale };
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
const lineDefaults = (context) => {
|
|
626
|
+
if (context.units === "legacy" || context.units === "ndc") {
|
|
627
|
+
return [-0.8, 0, 0.8, 0];
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return [-100, 0, 100, 0];
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
const shapeAxisHalf = (axis) => axis === "x" ? SHAPE_HALF_WIDTH : SHAPE_HALF_HEIGHT;
|
|
634
|
+
|
|
635
|
+
const logicalShapeUnits = (value) => ["logical", "center", "center_origin", "px"].includes(value);
|
|
636
|
+
|
|
637
|
+
const screenShapeUnits = (value) => ["screen", "canvas", "viewport"].includes(value);
|
|
638
|
+
|
|
639
|
+
const radiusToRawHalf = (value) => finiteNumber(value, 100) * 0.5;
|
|
640
|
+
|
|
641
|
+
const pathSegmentBudget = (shape) => {
|
|
642
|
+
const value = shape.max_segments ?? shape.maxSegments ?? PATH_DEFAULT_MAX_SEGMENTS;
|
|
643
|
+
return { remaining: clampInt(value, 1, PATH_HARD_MAX_SEGMENTS) };
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const pathTolerance = (shape) => {
|
|
647
|
+
if (shape.tolerance === undefined && shape.tolerancePx === undefined) return null;
|
|
648
|
+
|
|
649
|
+
const numeric = Number(shape.tolerance ?? shape.tolerancePx);
|
|
650
|
+
return Number.isFinite(numeric) && numeric >= 0 ? numeric : null;
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
const midpoint = (from, to) => [
|
|
654
|
+
(from[0] + to[0]) * 0.5,
|
|
655
|
+
(from[1] + to[1]) * 0.5
|
|
656
|
+
];
|
|
657
|
+
|
|
658
|
+
const pointLineDistance = (point, from, to) => {
|
|
659
|
+
const dx = to[0] - from[0];
|
|
660
|
+
const dy = to[1] - from[1];
|
|
661
|
+
const length = Math.hypot(dx, dy);
|
|
662
|
+
if (length <= 0) return Math.hypot(point[0] - from[0], point[1] - from[1]);
|
|
663
|
+
return Math.abs(dy * point[0] - dx * point[1] + to[0] * from[1] - to[1] * from[0]) / length;
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const quadraticPoint = (from, control, to, t) => {
|
|
667
|
+
const inv = 1 - t;
|
|
668
|
+
return inv * inv * from + 2 * inv * t * control + t * t * to;
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
const cubicPoint = (from, c1, c2, to, t) => {
|
|
672
|
+
const inv = 1 - t;
|
|
673
|
+
return inv * inv * inv * from + 3 * inv * inv * t * c1 + 3 * inv * t * t * c2 + t * t * t * to;
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
const sampleSpectrum = (spectrum, progress) => {
|
|
677
|
+
if (!spectrum.length) return 0;
|
|
678
|
+
|
|
679
|
+
const position = progress * (spectrum.length - 1);
|
|
680
|
+
const left = Math.floor(position);
|
|
681
|
+
const right = Math.min(left + 1, spectrum.length - 1);
|
|
682
|
+
const mix = position - left;
|
|
683
|
+
const from = finiteNumber(spectrum[left], 0);
|
|
684
|
+
const to = finiteNumber(spectrum[right], 0);
|
|
685
|
+
return clamp(from + (to - from) * mix, 0, 1);
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
const appendRadialPoint = (points, index, segments, baseRadius, wobble, bass, mid, pulse, time, fftValue) => {
|
|
689
|
+
const angle = (index / segments) * Math.PI * 2;
|
|
690
|
+
const organic = Math.sin(angle * (3.0 + mid * 5.0) + time * (1.2 + bass * 2.0));
|
|
691
|
+
const radius = baseRadius
|
|
692
|
+
+ bass * 0.14
|
|
693
|
+
+ pulse * 0.10
|
|
694
|
+
+ fftValue * (0.10 + wobble * 0.12)
|
|
695
|
+
+ organic * wobble * 0.035;
|
|
696
|
+
|
|
697
|
+
points.push(Math.cos(angle) * radius, Math.sin(angle) * radius);
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const normalizeMeshGeometry = (value) => {
|
|
701
|
+
return String(value || "icosahedron").trim().toLowerCase();
|
|
702
|
+
};
|
|
703
|
+
|
|
49
704
|
const projectVertex = (vertex, angleY, angleX) => {
|
|
50
705
|
const [x, y, z] = vertex;
|
|
51
706
|
|
|
@@ -63,4 +718,15 @@ const projectVertex = (vertex, angleY, angleX) => {
|
|
|
63
718
|
return [x1 * perspectiveScale, y1 * perspectiveScale];
|
|
64
719
|
};
|
|
65
720
|
|
|
721
|
+
const clampInt = (value, min, max) => {
|
|
722
|
+
const numeric = Number(value);
|
|
723
|
+
if (!Number.isFinite(numeric)) return min;
|
|
724
|
+
return Math.round(Math.min(Math.max(numeric, min), max));
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
const finiteNumber = (value, fallback) => {
|
|
728
|
+
const numeric = Number(value);
|
|
729
|
+
return Number.isFinite(numeric) ? numeric : fallback;
|
|
730
|
+
};
|
|
731
|
+
|
|
66
732
|
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|