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
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
const IMAGE_VERTEX_SHADER = `#version 300 es
|
|
2
|
+
in vec2 a_position;
|
|
3
|
+
in vec2 a_uv;
|
|
4
|
+
out vec2 v_uv;
|
|
5
|
+
void main() {
|
|
6
|
+
v_uv = a_uv;
|
|
7
|
+
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
8
|
+
}
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
const IMAGE_FRAGMENT_SHADER = `#version 300 es
|
|
12
|
+
precision mediump float;
|
|
13
|
+
in vec2 v_uv;
|
|
14
|
+
uniform sampler2D u_texture;
|
|
15
|
+
uniform float u_intensity;
|
|
16
|
+
uniform float u_invert;
|
|
17
|
+
out vec4 outColor;
|
|
18
|
+
|
|
19
|
+
void main() {
|
|
20
|
+
vec4 texel = texture(u_texture, v_uv);
|
|
21
|
+
vec3 color = mix(texel.rgb, vec3(1.0) - texel.rgb, clamp(u_invert, 0.0, 1.0));
|
|
22
|
+
outColor = vec4(color, texel.a * u_intensity);
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
const QUAD_VERTICES = new Float32Array([
|
|
27
|
+
-1.0, -1.0, 0.0, 1.0,
|
|
28
|
+
1.0, -1.0, 1.0, 1.0,
|
|
29
|
+
-1.0, 1.0, 0.0, 0.0,
|
|
30
|
+
1.0, 1.0, 1.0, 0.0
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
export class ImageRenderer {
|
|
34
|
+
constructor(gl, shaderManager) {
|
|
35
|
+
this.gl = gl;
|
|
36
|
+
this.shaderManager = shaderManager;
|
|
37
|
+
this.program = this.shaderManager.getProgram("image-renderer", IMAGE_VERTEX_SHADER, IMAGE_FRAGMENT_SHADER);
|
|
38
|
+
this.positionLocation = this.gl.getAttribLocation(this.program, "a_position");
|
|
39
|
+
this.uvLocation = this.gl.getAttribLocation(this.program, "a_uv");
|
|
40
|
+
this.textureLocation = this.gl.getUniformLocation(this.program, "u_texture");
|
|
41
|
+
this.intensityLocation = this.gl.getUniformLocation(this.program, "u_intensity");
|
|
42
|
+
this.invertLocation = this.gl.getUniformLocation(this.program, "u_invert");
|
|
43
|
+
this.media = new Map();
|
|
44
|
+
|
|
45
|
+
this.buffer = this.gl.createBuffer();
|
|
46
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
|
|
47
|
+
this.gl.bufferData(this.gl.ARRAY_BUFFER, QUAD_VERTICES, this.gl.STATIC_DRAW);
|
|
48
|
+
|
|
49
|
+
this.canvas = document.createElement("canvas");
|
|
50
|
+
this.canvas.width = 1024;
|
|
51
|
+
this.canvas.height = 1024;
|
|
52
|
+
this.ctx = this.canvas.getContext("2d");
|
|
53
|
+
|
|
54
|
+
this.texture = this.gl.createTexture();
|
|
55
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
|
|
56
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
|
|
57
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
|
|
58
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
|
|
59
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
render({ src, audio, fit, scale, rotation, playbackRate, invert }) {
|
|
63
|
+
const source = resolveMediaSource(src);
|
|
64
|
+
if (!source) return;
|
|
65
|
+
|
|
66
|
+
const media = this.loadMedia(source);
|
|
67
|
+
this.ensureVideoPlayback(media, playbackRate);
|
|
68
|
+
const dimensions = resolveMediaDimensions(media);
|
|
69
|
+
if (!isRenderableMedia(media, dimensions)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.syncCanvasSize();
|
|
74
|
+
const amplitude = clamp(Number(audio?.amplitude || 0), 0, 1);
|
|
75
|
+
const pulse = clamp(Number(audio?.beat_pulse || 0), 0, 1);
|
|
76
|
+
this.drawImageToCanvas({
|
|
77
|
+
media,
|
|
78
|
+
dimensions,
|
|
79
|
+
fit,
|
|
80
|
+
scale: normalizeScale(scale) * (1 + amplitude * 0.04 + pulse * 0.03),
|
|
81
|
+
rotation: normalizeRotation(rotation)
|
|
82
|
+
});
|
|
83
|
+
this.uploadTexture();
|
|
84
|
+
this.drawQuad({ intensity: 0.9 + amplitude * 0.1, invert: normalizeInvert(invert) });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
loadMedia(src) {
|
|
88
|
+
return isVideoSource(src) ? this.loadVideo(src) : this.loadImage(src);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
loadImage(src) {
|
|
92
|
+
if (this.media.has(src)) {
|
|
93
|
+
return this.media.get(src);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const image = new Image();
|
|
97
|
+
if (!src.startsWith("data:")) {
|
|
98
|
+
image.crossOrigin = "anonymous";
|
|
99
|
+
}
|
|
100
|
+
image.decoding = "async";
|
|
101
|
+
image.src = src;
|
|
102
|
+
this.media.set(src, image);
|
|
103
|
+
return image;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
loadVideo(src) {
|
|
107
|
+
if (this.media.has(src)) {
|
|
108
|
+
return this.media.get(src);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const video = document.createElement("video");
|
|
112
|
+
if (!src.startsWith("data:")) {
|
|
113
|
+
video.crossOrigin = "anonymous";
|
|
114
|
+
}
|
|
115
|
+
video.muted = true;
|
|
116
|
+
video.loop = true;
|
|
117
|
+
video.playsInline = true;
|
|
118
|
+
video.preload = "auto";
|
|
119
|
+
video.src = src;
|
|
120
|
+
this.media.set(src, video);
|
|
121
|
+
return video;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
ensureVideoPlayback(media, playbackRate) {
|
|
125
|
+
if (!isVideoElement(media)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const rate = normalizePlaybackRate(playbackRate);
|
|
130
|
+
if (media.playbackRate !== rate) {
|
|
131
|
+
media.playbackRate = rate;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!media.paused) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const playback = media.play();
|
|
139
|
+
if (playback?.catch) {
|
|
140
|
+
playback.catch(() => {});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
drawImageToCanvas({ media, dimensions, fit, scale, rotation }) {
|
|
145
|
+
const ctx = this.ctx;
|
|
146
|
+
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
147
|
+
const rect = resolveImageRect({
|
|
148
|
+
canvasWidth: this.canvas.width,
|
|
149
|
+
canvasHeight: this.canvas.height,
|
|
150
|
+
imageWidth: dimensions.width,
|
|
151
|
+
imageHeight: dimensions.height,
|
|
152
|
+
fit,
|
|
153
|
+
scale
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
ctx.save();
|
|
157
|
+
ctx.translate(this.canvas.width / 2, this.canvas.height / 2);
|
|
158
|
+
ctx.rotate(rotation);
|
|
159
|
+
ctx.drawImage(media, -rect.width / 2, -rect.height / 2, rect.width, rect.height);
|
|
160
|
+
ctx.restore();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
syncCanvasSize() {
|
|
164
|
+
const width = clamp(Math.floor(this.gl.drawingBufferWidth || 1024), 640, 2048);
|
|
165
|
+
const height = clamp(Math.floor(this.gl.drawingBufferHeight || 1024), 360, 2048);
|
|
166
|
+
if (this.canvas.width === width && this.canvas.height === height) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
this.canvas.width = width;
|
|
170
|
+
this.canvas.height = height;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
uploadTexture() {
|
|
174
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
|
|
175
|
+
this.gl.texImage2D(
|
|
176
|
+
this.gl.TEXTURE_2D,
|
|
177
|
+
0,
|
|
178
|
+
this.gl.RGBA,
|
|
179
|
+
this.gl.RGBA,
|
|
180
|
+
this.gl.UNSIGNED_BYTE,
|
|
181
|
+
this.canvas
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
drawQuad({ intensity, invert }) {
|
|
186
|
+
const gl = this.gl;
|
|
187
|
+
gl.useProgram(this.program);
|
|
188
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
189
|
+
gl.enableVertexAttribArray(this.positionLocation);
|
|
190
|
+
gl.vertexAttribPointer(this.positionLocation, 2, gl.FLOAT, false, 16, 0);
|
|
191
|
+
gl.enableVertexAttribArray(this.uvLocation);
|
|
192
|
+
gl.vertexAttribPointer(this.uvLocation, 2, gl.FLOAT, false, 16, 8);
|
|
193
|
+
|
|
194
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
195
|
+
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
|
196
|
+
gl.uniform1i(this.textureLocation, 0);
|
|
197
|
+
gl.uniform1f(this.intensityLocation, clamp(Number(intensity || 1), 0, 1));
|
|
198
|
+
gl.uniform1f(this.invertLocation, normalizeInvert(invert));
|
|
199
|
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export const resolveMediaSource = (value) => {
|
|
204
|
+
const source = String(value || "").trim();
|
|
205
|
+
return source || null;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export const normalizeImageFit = (value) => {
|
|
209
|
+
const fit = String(value || "contain").trim().toLowerCase();
|
|
210
|
+
if (fit === "cover" || fit === "stretch") return fit;
|
|
211
|
+
return "contain";
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export const normalizeScale = (value) => {
|
|
215
|
+
const scale = Number(value);
|
|
216
|
+
if (!Number.isFinite(scale)) return 1;
|
|
217
|
+
return clamp(scale, 0.01, 8);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export const normalizeRotation = (value) => {
|
|
221
|
+
const rotation = Number(value);
|
|
222
|
+
return Number.isFinite(rotation) ? rotation : 0;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export const normalizePlaybackRate = (value) => {
|
|
226
|
+
const rate = Number(value);
|
|
227
|
+
if (!Number.isFinite(rate)) return 1;
|
|
228
|
+
return clamp(rate, 0.1, 4);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
export const normalizeInvert = (value) => {
|
|
232
|
+
const amount = Number(value);
|
|
233
|
+
if (!Number.isFinite(amount)) return 0;
|
|
234
|
+
return clamp(amount, 0, 1);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export const isVideoSource = (value) => {
|
|
238
|
+
const source = resolveMediaSource(value);
|
|
239
|
+
if (!source) return false;
|
|
240
|
+
if (/^data:video\//i.test(source)) return true;
|
|
241
|
+
return /\.(mp4|webm|ogv|ogg)(?:[?#].*)?$/i.test(source);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export const resolveMediaDimensions = (media) => {
|
|
245
|
+
if (isVideoElement(media)) {
|
|
246
|
+
return {
|
|
247
|
+
width: Number(media.videoWidth || 0),
|
|
248
|
+
height: Number(media.videoHeight || 0)
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
width: Number(media?.naturalWidth || 0),
|
|
254
|
+
height: Number(media?.naturalHeight || 0)
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export const resolveImageRect = ({ canvasWidth, canvasHeight, imageWidth, imageHeight, fit, scale = 1 }) => {
|
|
259
|
+
const width = Math.max(Number(canvasWidth) || 0, 1);
|
|
260
|
+
const height = Math.max(Number(canvasHeight) || 0, 1);
|
|
261
|
+
const sourceWidth = Math.max(Number(imageWidth) || 0, 1);
|
|
262
|
+
const sourceHeight = Math.max(Number(imageHeight) || 0, 1);
|
|
263
|
+
const resolvedScale = normalizeScale(scale);
|
|
264
|
+
const resolvedFit = normalizeImageFit(fit);
|
|
265
|
+
|
|
266
|
+
if (resolvedFit === "stretch") {
|
|
267
|
+
return { width: width * resolvedScale, height: height * resolvedScale };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const multiplier = resolvedFit === "cover"
|
|
271
|
+
? Math.max(width / sourceWidth, height / sourceHeight)
|
|
272
|
+
: Math.min(width / sourceWidth, height / sourceHeight);
|
|
273
|
+
return {
|
|
274
|
+
width: sourceWidth * multiplier * resolvedScale,
|
|
275
|
+
height: sourceHeight * multiplier * resolvedScale
|
|
276
|
+
};
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const isRenderableMedia = (media, dimensions) => {
|
|
280
|
+
if (isVideoElement(media)) {
|
|
281
|
+
return media.readyState >= 2 && dimensions.width > 0 && dimensions.height > 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return !!media?.complete && dimensions.width > 0 && dimensions.height > 0;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const isVideoElement = (media) => {
|
|
288
|
+
return media?.tagName === "VIDEO" || (typeof media?.play === "function" && "videoWidth" in media);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
@@ -20,6 +20,40 @@ void main() {
|
|
|
20
20
|
}
|
|
21
21
|
`;
|
|
22
22
|
|
|
23
|
+
export const resolveParticleForces = ({ x, y, index, time, forceField, turbulence, bassExplosion, audio }) => {
|
|
24
|
+
const bass = clampNumber(audio?.bands?.low, 0, 1);
|
|
25
|
+
const mid = clampNumber(audio?.bands?.mid, 0, 1);
|
|
26
|
+
const high = clampNumber(audio?.bands?.high, 0, 1);
|
|
27
|
+
const pulse = clampNumber(audio?.beat_pulse || (audio?.beat ? 1 : 0), 0, 1);
|
|
28
|
+
const field = String(forceField || "drift").toLowerCase();
|
|
29
|
+
|
|
30
|
+
let fx = 0;
|
|
31
|
+
let fy = 0;
|
|
32
|
+
|
|
33
|
+
const radius = Math.max(0.001, Math.hypot(x, y));
|
|
34
|
+
const nx = x / radius;
|
|
35
|
+
const ny = y / radius;
|
|
36
|
+
|
|
37
|
+
if (field === "vortex") {
|
|
38
|
+
const strength = 0.00018 + mid * 0.00065;
|
|
39
|
+
fx += -ny * strength;
|
|
40
|
+
fy += nx * strength;
|
|
41
|
+
} else if (field === "pulse") {
|
|
42
|
+
const strength = (bass * 0.0007 + pulse * 0.0012) * (1 + clampNumber(bassExplosion, 0, 3));
|
|
43
|
+
fx += nx * strength;
|
|
44
|
+
fy += ny * strength;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const turb = clampNumber(turbulence, 0, 3);
|
|
48
|
+
if (turb > 0) {
|
|
49
|
+
const noise = Math.sin(time * (1.7 + high * 3.0) + index * 12.9898);
|
|
50
|
+
fx += Math.cos(noise * 6.28318530718) * turb * 0.00018;
|
|
51
|
+
fy += Math.sin(noise * 6.28318530718) * turb * 0.00018;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return [fx, fy];
|
|
55
|
+
};
|
|
56
|
+
|
|
23
57
|
export class ParticleSystem {
|
|
24
58
|
constructor(gl, shaderManager) {
|
|
25
59
|
this.gl = gl;
|
|
@@ -39,11 +73,11 @@ export class ParticleSystem {
|
|
|
39
73
|
this.velocities = new Float32Array(0);
|
|
40
74
|
}
|
|
41
75
|
|
|
42
|
-
render({ count, speed, size, audio, time }) {
|
|
76
|
+
render({ count, speed, size, forceField, turbulence, bassExplosion, sparkle, color, audio, time }) {
|
|
43
77
|
const particleCount = clampInt(count, 200, 20_000);
|
|
44
78
|
this.ensureParticles(particleCount);
|
|
45
|
-
this.updateParticles(speed, audio, time);
|
|
46
|
-
this.draw(size, audio);
|
|
79
|
+
this.updateParticles({ speed, forceField, turbulence, bassExplosion, audio, time });
|
|
80
|
+
this.draw({ size, sparkle, color, audio });
|
|
47
81
|
}
|
|
48
82
|
|
|
49
83
|
ensureParticles(nextCount) {
|
|
@@ -67,9 +101,9 @@ export class ParticleSystem {
|
|
|
67
101
|
}
|
|
68
102
|
}
|
|
69
103
|
|
|
70
|
-
updateParticles(speed, audio, time) {
|
|
104
|
+
updateParticles({ speed, forceField, turbulence, bassExplosion, audio, time }) {
|
|
71
105
|
const motion = 0.4 + clampNumber(speed, 0, 4);
|
|
72
|
-
const beatBoost = audio?.beat ? 1.4 : 1.0;
|
|
106
|
+
const beatBoost = audio?.beat || audio?.beat_pulse ? 1.4 : 1.0;
|
|
73
107
|
const drift = 0.0008 + clampNumber(audio?.amplitude, 0, 1) * 0.0018;
|
|
74
108
|
|
|
75
109
|
for (let index = 0; index < this.count; index += 1) {
|
|
@@ -83,6 +117,12 @@ export class ParticleSystem {
|
|
|
83
117
|
vx += swirl * 0.01;
|
|
84
118
|
vy -= swirl * 0.01;
|
|
85
119
|
|
|
120
|
+
const [fx, fy] = resolveParticleForces({ x, y, index, time, forceField, turbulence, bassExplosion, audio });
|
|
121
|
+
vx += fx;
|
|
122
|
+
vy += fy;
|
|
123
|
+
vx = clampNumber(vx * 0.997, -0.035, 0.035);
|
|
124
|
+
vy = clampNumber(vy * 0.997, -0.035, 0.035);
|
|
125
|
+
|
|
86
126
|
x += vx * motion * beatBoost;
|
|
87
127
|
y += vy * motion * beatBoost;
|
|
88
128
|
|
|
@@ -102,12 +142,13 @@ export class ParticleSystem {
|
|
|
102
142
|
}
|
|
103
143
|
}
|
|
104
144
|
|
|
105
|
-
draw(size, audio) {
|
|
145
|
+
draw({ size, sparkle, color, audio }) {
|
|
106
146
|
const gl = this.gl;
|
|
107
|
-
const pointSize = 1.5 + clampNumber(size, 0, 32);
|
|
108
147
|
const amplitude = clampNumber(audio?.amplitude, 0, 1);
|
|
109
148
|
const bass = clampNumber(audio?.bands?.low, 0, 1);
|
|
110
149
|
const high = clampNumber(audio?.bands?.high, 0, 1);
|
|
150
|
+
const sparkleAmount = clampNumber(sparkle, 0, 3) * high;
|
|
151
|
+
const pointSize = 1.5 + clampNumber(size, 0, 32) + sparkleAmount * 1.5;
|
|
111
152
|
|
|
112
153
|
gl.useProgram(this.program);
|
|
113
154
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
@@ -116,11 +157,16 @@ export class ParticleSystem {
|
|
|
116
157
|
gl.vertexAttribPointer(this.positionLocation, 2, gl.FLOAT, false, 0, 0);
|
|
117
158
|
|
|
118
159
|
gl.uniform1f(this.pointSizeLocation, pointSize);
|
|
160
|
+
const resolvedColor = Array.isArray(color) ? color : [
|
|
161
|
+
0.35 + bass * 0.45,
|
|
162
|
+
0.55 + high * 0.35 + sparkleAmount * 0.08,
|
|
163
|
+
0.95 + amplitude * 0.05 + sparkleAmount * 0.05
|
|
164
|
+
];
|
|
119
165
|
gl.uniform3f(
|
|
120
166
|
this.colorLocation,
|
|
121
|
-
0
|
|
122
|
-
|
|
123
|
-
0
|
|
167
|
+
clampNumber(resolvedColor[0], 0, 1),
|
|
168
|
+
clampNumber(resolvedColor[1], 0, 1),
|
|
169
|
+
clampNumber(resolvedColor[2], 0, 1)
|
|
124
170
|
);
|
|
125
171
|
gl.drawArrays(gl.POINTS, 0, this.count);
|
|
126
172
|
}
|