vizcore 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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +170 -0
- data/docs/GETTING_STARTED.md +105 -0
- data/examples/assets/complex_demo_loop.wav +0 -0
- data/examples/basic.rb +9 -0
- data/examples/complex_audio_showcase.rb +261 -0
- data/examples/custom_shader.rb +21 -0
- data/examples/file_audio_demo.rb +74 -0
- data/examples/intro_drop.rb +38 -0
- data/examples/midi_scene_switch.rb +32 -0
- data/examples/shaders/custom_wave.frag +30 -0
- data/exe/vizcore +6 -0
- data/frontend/index.html +148 -0
- data/frontend/src/main.js +304 -0
- data/frontend/src/renderer/engine.js +135 -0
- data/frontend/src/renderer/layer-manager.js +456 -0
- data/frontend/src/renderer/shader-manager.js +69 -0
- data/frontend/src/shaders/builtins.js +244 -0
- data/frontend/src/shaders/post-effects.js +85 -0
- data/frontend/src/visuals/geometry.js +66 -0
- data/frontend/src/visuals/particle-system.js +148 -0
- data/frontend/src/visuals/text-renderer.js +143 -0
- data/frontend/src/visuals/vj-effects.js +56 -0
- data/frontend/src/websocket-client.js +131 -0
- data/lib/vizcore/analysis/band_splitter.rb +63 -0
- data/lib/vizcore/analysis/beat_detector.rb +70 -0
- data/lib/vizcore/analysis/bpm_estimator.rb +86 -0
- data/lib/vizcore/analysis/fft_processor.rb +224 -0
- data/lib/vizcore/analysis/fftw_ffi.rb +50 -0
- data/lib/vizcore/analysis/pipeline.rb +72 -0
- data/lib/vizcore/analysis/smoother.rb +74 -0
- data/lib/vizcore/analysis.rb +14 -0
- data/lib/vizcore/audio/base_input.rb +39 -0
- data/lib/vizcore/audio/dummy_sine_input.rb +40 -0
- data/lib/vizcore/audio/file_input.rb +163 -0
- data/lib/vizcore/audio/input_manager.rb +133 -0
- data/lib/vizcore/audio/mic_input.rb +121 -0
- data/lib/vizcore/audio/midi_input.rb +246 -0
- data/lib/vizcore/audio/portaudio_ffi.rb +243 -0
- data/lib/vizcore/audio/ring_buffer.rb +92 -0
- data/lib/vizcore/audio.rb +16 -0
- data/lib/vizcore/cli.rb +115 -0
- data/lib/vizcore/config.rb +46 -0
- data/lib/vizcore/dsl/engine.rb +229 -0
- data/lib/vizcore/dsl/file_watcher.rb +108 -0
- data/lib/vizcore/dsl/layer_builder.rb +182 -0
- data/lib/vizcore/dsl/mapping_resolver.rb +81 -0
- data/lib/vizcore/dsl/midi_map_executor.rb +188 -0
- data/lib/vizcore/dsl/scene_builder.rb +44 -0
- data/lib/vizcore/dsl/shader_source_resolver.rb +71 -0
- data/lib/vizcore/dsl/transition_controller.rb +166 -0
- data/lib/vizcore/dsl.rb +16 -0
- data/lib/vizcore/errors.rb +27 -0
- data/lib/vizcore/renderer/frame_scheduler.rb +75 -0
- data/lib/vizcore/renderer/scene_serializer.rb +73 -0
- data/lib/vizcore/renderer.rb +10 -0
- data/lib/vizcore/server/frame_broadcaster.rb +351 -0
- data/lib/vizcore/server/rack_app.rb +183 -0
- data/lib/vizcore/server/runner.rb +357 -0
- data/lib/vizcore/server/websocket_handler.rb +163 -0
- data/lib/vizcore/server.rb +12 -0
- data/lib/vizcore/templates/basic_scene.rb +10 -0
- data/lib/vizcore/templates/custom_shader_scene.rb +22 -0
- data/lib/vizcore/templates/custom_wave.frag +31 -0
- data/lib/vizcore/templates/intro_drop_scene.rb +40 -0
- data/lib/vizcore/templates/midi_control_scene.rb +33 -0
- data/lib/vizcore/templates/project_readme.md +35 -0
- data/lib/vizcore/version.rb +6 -0
- data/lib/vizcore.rb +37 -0
- metadata +186 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
const DEFAULT_FRAGMENT_SHADER = `#version 300 es
|
|
2
|
+
precision mediump float;
|
|
3
|
+
uniform vec2 u_resolution;
|
|
4
|
+
uniform float u_time;
|
|
5
|
+
uniform float u_amplitude;
|
|
6
|
+
uniform float u_bass;
|
|
7
|
+
uniform float u_mid;
|
|
8
|
+
uniform float u_high;
|
|
9
|
+
uniform float u_beat;
|
|
10
|
+
uniform float u_bpm;
|
|
11
|
+
out vec4 outColor;
|
|
12
|
+
|
|
13
|
+
void main() {
|
|
14
|
+
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
|
|
15
|
+
float pulse = 0.5 + 0.5 * sin(u_time * 1.8 + uv.x * 5.0);
|
|
16
|
+
vec3 color = vec3(0.08, 0.12, 0.24);
|
|
17
|
+
color += vec3(u_bass, u_mid, u_high) * 0.25;
|
|
18
|
+
color += pulse * (0.08 + u_amplitude * 0.25);
|
|
19
|
+
color += vec3(u_beat * 0.15);
|
|
20
|
+
outColor = vec4(color, 1.0);
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
export const BUILTIN_FRAGMENT_SHADERS = {
|
|
25
|
+
default: DEFAULT_FRAGMENT_SHADER,
|
|
26
|
+
gradient_pulse: `#version 300 es
|
|
27
|
+
precision mediump float;
|
|
28
|
+
uniform vec2 u_resolution;
|
|
29
|
+
uniform float u_time;
|
|
30
|
+
uniform float u_amplitude;
|
|
31
|
+
uniform float u_bass;
|
|
32
|
+
uniform float u_mid;
|
|
33
|
+
uniform float u_high;
|
|
34
|
+
uniform float u_beat;
|
|
35
|
+
out vec4 outColor;
|
|
36
|
+
|
|
37
|
+
void main() {
|
|
38
|
+
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
|
|
39
|
+
vec3 baseA = vec3(0.04, 0.08, 0.15);
|
|
40
|
+
vec3 baseB = vec3(0.20 + u_high * 0.35, 0.16 + u_mid * 0.3, 0.28 + u_bass * 0.25);
|
|
41
|
+
float sweep = 0.5 + 0.5 * sin(u_time * 2.4 + uv.y * 8.0);
|
|
42
|
+
float flash = u_beat * 0.35;
|
|
43
|
+
vec3 color = mix(baseA, baseB, uv.y + sweep * 0.2);
|
|
44
|
+
color += vec3(flash + u_amplitude * 0.2);
|
|
45
|
+
outColor = vec4(color, 1.0);
|
|
46
|
+
}
|
|
47
|
+
`,
|
|
48
|
+
bass_tunnel: `#version 300 es
|
|
49
|
+
precision mediump float;
|
|
50
|
+
uniform vec2 u_resolution;
|
|
51
|
+
uniform float u_time;
|
|
52
|
+
uniform float u_bass;
|
|
53
|
+
uniform float u_mid;
|
|
54
|
+
uniform float u_high;
|
|
55
|
+
uniform float u_amplitude;
|
|
56
|
+
out vec4 outColor;
|
|
57
|
+
|
|
58
|
+
void main() {
|
|
59
|
+
vec2 uv = (gl_FragCoord.xy / u_resolution.xy) * 2.0 - 1.0;
|
|
60
|
+
uv.x *= u_resolution.x / max(u_resolution.y, 1.0);
|
|
61
|
+
float r = length(uv);
|
|
62
|
+
float angle = atan(uv.y, uv.x);
|
|
63
|
+
float tunnel = sin(angle * 6.0 + u_time * (2.0 + u_bass * 6.0));
|
|
64
|
+
float rings = smoothstep(0.8, 0.0, abs(r - (0.3 + 0.2 * tunnel)));
|
|
65
|
+
vec3 color = vec3(0.03, 0.05, 0.08);
|
|
66
|
+
color += vec3(0.65, 0.45, 0.2) * rings * (0.4 + u_bass * 0.8);
|
|
67
|
+
color += vec3(0.2, 0.55, 0.9) * (1.0 - r) * (0.2 + u_high * 0.6);
|
|
68
|
+
color += u_amplitude * 0.08;
|
|
69
|
+
outColor = vec4(color, 1.0);
|
|
70
|
+
}
|
|
71
|
+
`,
|
|
72
|
+
neon_grid: `#version 300 es
|
|
73
|
+
precision mediump float;
|
|
74
|
+
uniform vec2 u_resolution;
|
|
75
|
+
uniform float u_time;
|
|
76
|
+
uniform float u_bass;
|
|
77
|
+
uniform float u_mid;
|
|
78
|
+
uniform float u_high;
|
|
79
|
+
uniform float u_beat;
|
|
80
|
+
out vec4 outColor;
|
|
81
|
+
|
|
82
|
+
float line(float p, float width) {
|
|
83
|
+
return smoothstep(width, 0.0, abs(fract(p) - 0.5));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
void main() {
|
|
87
|
+
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
|
|
88
|
+
float zoom = 8.0 + u_bass * 12.0;
|
|
89
|
+
float move = u_time * (0.5 + u_mid * 2.0);
|
|
90
|
+
float gx = line(uv.x * zoom + move, 0.03);
|
|
91
|
+
float gy = line(uv.y * zoom - move, 0.03);
|
|
92
|
+
float glow = max(gx, gy);
|
|
93
|
+
vec3 color = vec3(0.01, 0.02, 0.04);
|
|
94
|
+
color += vec3(0.1, 0.95, 0.85) * glow * (0.5 + u_high * 0.7);
|
|
95
|
+
color += vec3(u_beat * 0.2);
|
|
96
|
+
outColor = vec4(color, 1.0);
|
|
97
|
+
}
|
|
98
|
+
`,
|
|
99
|
+
kaleidoscope: `#version 300 es
|
|
100
|
+
precision mediump float;
|
|
101
|
+
uniform vec2 u_resolution;
|
|
102
|
+
uniform float u_time;
|
|
103
|
+
uniform float u_amplitude;
|
|
104
|
+
uniform float u_bass;
|
|
105
|
+
uniform float u_mid;
|
|
106
|
+
uniform float u_high;
|
|
107
|
+
out vec4 outColor;
|
|
108
|
+
|
|
109
|
+
void main() {
|
|
110
|
+
vec2 uv = (gl_FragCoord.xy / u_resolution.xy) * 2.0 - 1.0;
|
|
111
|
+
uv.x *= u_resolution.x / max(u_resolution.y, 1.0);
|
|
112
|
+
float slices = 6.0 + floor(u_mid * 6.0);
|
|
113
|
+
float angle = atan(uv.y, uv.x);
|
|
114
|
+
float radius = length(uv);
|
|
115
|
+
angle = mod(angle, 6.28318530718 / slices);
|
|
116
|
+
angle = abs(angle - 3.14159265359 / slices);
|
|
117
|
+
vec2 p = vec2(cos(angle), sin(angle)) * radius;
|
|
118
|
+
float wave = sin(p.x * 16.0 + u_time * (1.5 + u_high * 5.0));
|
|
119
|
+
vec3 color = vec3(0.04, 0.02, 0.08);
|
|
120
|
+
color += vec3(0.8, 0.3, 0.95) * (0.5 + 0.5 * wave) * (0.3 + u_amplitude * 0.7);
|
|
121
|
+
color += vec3(0.2, 0.6, 0.95) * (1.0 - radius) * (0.2 + u_bass * 0.6);
|
|
122
|
+
outColor = vec4(color, 1.0);
|
|
123
|
+
}
|
|
124
|
+
`,
|
|
125
|
+
spectrum_rings: `#version 300 es
|
|
126
|
+
precision mediump float;
|
|
127
|
+
uniform vec2 u_resolution;
|
|
128
|
+
uniform float u_time;
|
|
129
|
+
uniform float u_bass;
|
|
130
|
+
uniform float u_mid;
|
|
131
|
+
uniform float u_high;
|
|
132
|
+
uniform float u_bpm;
|
|
133
|
+
out vec4 outColor;
|
|
134
|
+
|
|
135
|
+
void main() {
|
|
136
|
+
vec2 uv = (gl_FragCoord.xy / u_resolution.xy) * 2.0 - 1.0;
|
|
137
|
+
uv.x *= u_resolution.x / max(u_resolution.y, 1.0);
|
|
138
|
+
float r = length(uv);
|
|
139
|
+
float bpmPhase = u_time * (u_bpm / 60.0);
|
|
140
|
+
float rings = sin((r * 18.0) - bpmPhase * 6.28318530718);
|
|
141
|
+
float pulse = smoothstep(0.15, 0.0, abs(rings));
|
|
142
|
+
vec3 color = vec3(0.02, 0.02, 0.05);
|
|
143
|
+
color += vec3(0.95, 0.3, 0.22) * pulse * (0.2 + u_bass * 0.9);
|
|
144
|
+
color += vec3(0.2, 0.55, 0.95) * pulse * (0.2 + u_mid * 0.6);
|
|
145
|
+
color += vec3(0.8, 0.85, 1.0) * pulse * (0.1 + u_high * 0.4);
|
|
146
|
+
outColor = vec4(color, 1.0);
|
|
147
|
+
}
|
|
148
|
+
`,
|
|
149
|
+
audio_bars: `#version 300 es
|
|
150
|
+
precision mediump float;
|
|
151
|
+
uniform vec2 u_resolution;
|
|
152
|
+
uniform float u_time;
|
|
153
|
+
uniform float u_amplitude;
|
|
154
|
+
uniform float u_bass;
|
|
155
|
+
uniform float u_mid;
|
|
156
|
+
uniform float u_high;
|
|
157
|
+
uniform float u_beat;
|
|
158
|
+
uniform float u_param_bar_count;
|
|
159
|
+
uniform float u_param_floor_glow;
|
|
160
|
+
out vec4 outColor;
|
|
161
|
+
|
|
162
|
+
float hash11(float p) {
|
|
163
|
+
return fract(sin(p * 127.1) * 43758.5453123);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
void main() {
|
|
167
|
+
vec2 uv = gl_FragCoord.xy / max(u_resolution.xy, vec2(1.0));
|
|
168
|
+
float bars = floor(max(u_param_bar_count, 18.0));
|
|
169
|
+
float col = uv.x * bars;
|
|
170
|
+
float id = floor(col);
|
|
171
|
+
float x = fract(col) - 0.5;
|
|
172
|
+
float gap = 0.13;
|
|
173
|
+
float barMask = smoothstep(0.48, 0.48 - 0.02, abs(x)) * (1.0 - smoothstep(0.48 - gap, 0.48 - gap - 0.02, abs(x)));
|
|
174
|
+
|
|
175
|
+
float n = hash11(id + 1.7);
|
|
176
|
+
float region = id / max(bars - 1.0, 1.0);
|
|
177
|
+
float lowW = 1.0 - smoothstep(0.0, 0.42, region);
|
|
178
|
+
float highW = smoothstep(0.58, 1.0, region);
|
|
179
|
+
float midW = max(0.0, 1.0 - lowW - highW);
|
|
180
|
+
float ripple = 0.5 + 0.5 * sin(u_time * (1.8 + n * 2.4) + id * (0.35 + n * 0.4));
|
|
181
|
+
float spark = 0.5 + 0.5 * sin(u_time * 7.0 + id * 1.31);
|
|
182
|
+
float height = 0.05
|
|
183
|
+
+ u_amplitude * (0.14 + n * 0.18)
|
|
184
|
+
+ lowW * u_bass * (0.22 + 0.14 * ripple)
|
|
185
|
+
+ midW * u_mid * (0.20 + 0.12 * ripple)
|
|
186
|
+
+ highW * u_high * (0.18 + 0.16 * ripple)
|
|
187
|
+
+ u_beat * (0.04 + 0.04 * spark);
|
|
188
|
+
height = clamp(height, 0.03, 0.95);
|
|
189
|
+
|
|
190
|
+
float fill = smoothstep(0.0, 0.01, height - uv.y);
|
|
191
|
+
float topGlow = exp(-abs(uv.y - height) * 180.0) * 0.4;
|
|
192
|
+
float floorGlow = exp(-uv.y * 20.0) * (0.08 + max(u_param_floor_glow, 0.0));
|
|
193
|
+
|
|
194
|
+
vec3 bg = vec3(0.012, 0.018, 0.032);
|
|
195
|
+
bg += vec3(0.02, 0.03, 0.055) * floorGlow;
|
|
196
|
+
|
|
197
|
+
vec3 barA = vec3(0.09, 0.88, 0.95);
|
|
198
|
+
vec3 barB = vec3(0.92, 0.32, 1.0);
|
|
199
|
+
vec3 barColor = mix(barA, barB, smoothstep(0.2, 0.95, region));
|
|
200
|
+
barColor *= 0.65 + 0.75 * (lowW * u_bass + midW * u_mid + highW * u_high + u_amplitude * 0.35);
|
|
201
|
+
barColor += vec3(0.9, 0.95, 1.0) * topGlow;
|
|
202
|
+
|
|
203
|
+
float active = barMask * fill;
|
|
204
|
+
vec3 color = bg + barColor * active;
|
|
205
|
+
color += barColor * barMask * topGlow * 0.45;
|
|
206
|
+
|
|
207
|
+
outColor = vec4(color, 1.0);
|
|
208
|
+
}
|
|
209
|
+
`,
|
|
210
|
+
glitch_flash: `#version 300 es
|
|
211
|
+
precision mediump float;
|
|
212
|
+
uniform vec2 u_resolution;
|
|
213
|
+
uniform float u_time;
|
|
214
|
+
uniform float u_amplitude;
|
|
215
|
+
uniform float u_beat;
|
|
216
|
+
uniform float u_high;
|
|
217
|
+
uniform float u_param_intensity;
|
|
218
|
+
out vec4 outColor;
|
|
219
|
+
|
|
220
|
+
float random(vec2 p) {
|
|
221
|
+
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
void main() {
|
|
225
|
+
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
|
|
226
|
+
float intensity = max(u_param_intensity, 0.15 + u_amplitude * 0.8);
|
|
227
|
+
float line = step(0.92, fract(uv.y * (20.0 + u_high * 90.0) + u_time * 6.0));
|
|
228
|
+
float noise = random(vec2(floor(uv.y * 140.0), floor(u_time * 8.0)));
|
|
229
|
+
float flash = min(1.0, u_beat * 1.3 + noise * intensity * 0.35);
|
|
230
|
+
vec3 color = vec3(0.03, 0.04, 0.08);
|
|
231
|
+
color += vec3(0.1, 0.9, 0.95) * line * intensity;
|
|
232
|
+
color += vec3(0.95, 0.2, 0.4) * flash;
|
|
233
|
+
outColor = vec4(color, 1.0);
|
|
234
|
+
}
|
|
235
|
+
`
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export const getBuiltinShader = (name) => {
|
|
239
|
+
const key = String(name || "").trim();
|
|
240
|
+
if (!key) {
|
|
241
|
+
return BUILTIN_FRAGMENT_SHADERS.default;
|
|
242
|
+
}
|
|
243
|
+
return BUILTIN_FRAGMENT_SHADERS[key] || BUILTIN_FRAGMENT_SHADERS.default;
|
|
244
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export const POST_EFFECT_SHADERS = {
|
|
2
|
+
bloom: `#version 300 es
|
|
3
|
+
precision mediump float;
|
|
4
|
+
in vec2 v_uv;
|
|
5
|
+
uniform sampler2D u_texture;
|
|
6
|
+
uniform vec2 u_resolution;
|
|
7
|
+
uniform float u_time;
|
|
8
|
+
uniform float u_intensity;
|
|
9
|
+
out vec4 outColor;
|
|
10
|
+
|
|
11
|
+
void main() {
|
|
12
|
+
vec2 texel = 1.0 / max(u_resolution, vec2(1.0));
|
|
13
|
+
vec4 base = texture(u_texture, v_uv);
|
|
14
|
+
vec4 blur = vec4(0.0);
|
|
15
|
+
blur += texture(u_texture, v_uv + texel * vec2( 1.0, 0.0));
|
|
16
|
+
blur += texture(u_texture, v_uv + texel * vec2(-1.0, 0.0));
|
|
17
|
+
blur += texture(u_texture, v_uv + texel * vec2( 0.0, 1.0));
|
|
18
|
+
blur += texture(u_texture, v_uv + texel * vec2( 0.0, -1.0));
|
|
19
|
+
blur *= 0.25;
|
|
20
|
+
float glow = max(0.0, dot(base.rgb, vec3(0.333)) - 0.35);
|
|
21
|
+
vec3 color = base.rgb + blur.rgb * glow * (0.4 + u_intensity * 1.5);
|
|
22
|
+
outColor = vec4(color, base.a);
|
|
23
|
+
}
|
|
24
|
+
`,
|
|
25
|
+
glitch: `#version 300 es
|
|
26
|
+
precision mediump float;
|
|
27
|
+
in vec2 v_uv;
|
|
28
|
+
uniform sampler2D u_texture;
|
|
29
|
+
uniform float u_time;
|
|
30
|
+
uniform float u_intensity;
|
|
31
|
+
out vec4 outColor;
|
|
32
|
+
|
|
33
|
+
float rand(vec2 p) {
|
|
34
|
+
return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
void main() {
|
|
38
|
+
float line = floor(v_uv.y * 120.0 + u_time * 7.0);
|
|
39
|
+
float jitter = (rand(vec2(line, u_time)) - 0.5) * 0.02 * (0.5 + u_intensity);
|
|
40
|
+
vec2 uv = vec2(v_uv.x + jitter, v_uv.y);
|
|
41
|
+
vec4 color = texture(u_texture, uv);
|
|
42
|
+
color.r = texture(u_texture, uv + vec2(0.003 * u_intensity, 0.0)).r;
|
|
43
|
+
color.b = texture(u_texture, uv - vec2(0.003 * u_intensity, 0.0)).b;
|
|
44
|
+
outColor = color;
|
|
45
|
+
}
|
|
46
|
+
`,
|
|
47
|
+
chromatic: `#version 300 es
|
|
48
|
+
precision mediump float;
|
|
49
|
+
in vec2 v_uv;
|
|
50
|
+
uniform sampler2D u_texture;
|
|
51
|
+
uniform float u_intensity;
|
|
52
|
+
out vec4 outColor;
|
|
53
|
+
|
|
54
|
+
void main() {
|
|
55
|
+
vec2 shift = vec2(0.004 * (0.3 + u_intensity), 0.0);
|
|
56
|
+
float r = texture(u_texture, v_uv + shift).r;
|
|
57
|
+
float g = texture(u_texture, v_uv).g;
|
|
58
|
+
float b = texture(u_texture, v_uv - shift).b;
|
|
59
|
+
float a = texture(u_texture, v_uv).a;
|
|
60
|
+
outColor = vec4(r, g, b, a);
|
|
61
|
+
}
|
|
62
|
+
`,
|
|
63
|
+
feedback: `#version 300 es
|
|
64
|
+
precision mediump float;
|
|
65
|
+
in vec2 v_uv;
|
|
66
|
+
uniform sampler2D u_texture;
|
|
67
|
+
uniform float u_time;
|
|
68
|
+
uniform float u_intensity;
|
|
69
|
+
out vec4 outColor;
|
|
70
|
+
|
|
71
|
+
void main() {
|
|
72
|
+
vec2 center = v_uv - vec2(0.5);
|
|
73
|
+
float zoom = 1.0 + 0.01 * (0.5 + u_intensity);
|
|
74
|
+
vec2 uv = center / zoom + vec2(0.5);
|
|
75
|
+
vec4 base = texture(u_texture, uv);
|
|
76
|
+
float flicker = 0.96 + 0.04 * sin(u_time * 4.0);
|
|
77
|
+
outColor = vec4(base.rgb * flicker, base.a);
|
|
78
|
+
}
|
|
79
|
+
`
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const getPostEffectShader = (name) => {
|
|
83
|
+
const key = String(name || "").trim().toLowerCase();
|
|
84
|
+
return POST_EFFECT_SHADERS[key] || null;
|
|
85
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const BASE_VERTICES = [
|
|
2
|
+
[-1.0, -1.0, -1.0],
|
|
3
|
+
[1.0, -1.0, -1.0],
|
|
4
|
+
[1.0, 1.0, -1.0],
|
|
5
|
+
[-1.0, 1.0, -1.0],
|
|
6
|
+
[-1.0, -1.0, 1.0],
|
|
7
|
+
[1.0, -1.0, 1.0],
|
|
8
|
+
[1.0, 1.0, 1.0],
|
|
9
|
+
[-1.0, 1.0, 1.0]
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const EDGES = [
|
|
13
|
+
[0, 1], [1, 2], [2, 3], [3, 0],
|
|
14
|
+
[4, 5], [5, 6], [6, 7], [7, 4],
|
|
15
|
+
[0, 4], [1, 5], [2, 6], [3, 7]
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export const buildWireframeLines = ({ rotationY, rotationX, deform }) => {
|
|
19
|
+
const amount = clamp(Number(deform || 0), 0, 1);
|
|
20
|
+
const projected = BASE_VERTICES.map((vertex) => {
|
|
21
|
+
const scaled = [
|
|
22
|
+
vertex[0] * (1 + amount * 0.35),
|
|
23
|
+
vertex[1] * (1 + amount * 0.2),
|
|
24
|
+
vertex[2] * (1 + amount * 0.35)
|
|
25
|
+
];
|
|
26
|
+
return projectVertex(scaled, rotationY, rotationX);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const lines = [];
|
|
30
|
+
for (const [start, end] of EDGES) {
|
|
31
|
+
lines.push(projected[start][0], projected[start][1]);
|
|
32
|
+
lines.push(projected[end][0], projected[end][1]);
|
|
33
|
+
}
|
|
34
|
+
return lines;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const estimateDeformFromSpectrum = (value) => {
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
if (value.length === 0) {
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
const sample = value.slice(0, Math.min(24, value.length));
|
|
43
|
+
const sum = sample.reduce((total, entry) => total + Number(entry || 0), 0);
|
|
44
|
+
return clamp(sum / sample.length, 0, 1);
|
|
45
|
+
}
|
|
46
|
+
return clamp(Number(value || 0), 0, 1);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const projectVertex = (vertex, angleY, angleX) => {
|
|
50
|
+
const [x, y, z] = vertex;
|
|
51
|
+
|
|
52
|
+
const cosY = Math.cos(angleY);
|
|
53
|
+
const sinY = Math.sin(angleY);
|
|
54
|
+
const x1 = x * cosY - z * sinY;
|
|
55
|
+
const z1 = x * sinY + z * cosY;
|
|
56
|
+
|
|
57
|
+
const cosX = Math.cos(angleX);
|
|
58
|
+
const sinX = Math.sin(angleX);
|
|
59
|
+
const y1 = y * cosX - z1 * sinX;
|
|
60
|
+
const z2 = y * sinX + z1 * cosX + 4.2;
|
|
61
|
+
|
|
62
|
+
const perspectiveScale = 1.6 / z2;
|
|
63
|
+
return [x1 * perspectiveScale, y1 * perspectiveScale];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const PARTICLE_VERTEX_SHADER = `#version 300 es
|
|
2
|
+
in vec2 a_position;
|
|
3
|
+
uniform float u_point_size;
|
|
4
|
+
void main() {
|
|
5
|
+
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
6
|
+
gl_PointSize = u_point_size;
|
|
7
|
+
}
|
|
8
|
+
`;
|
|
9
|
+
|
|
10
|
+
const PARTICLE_FRAGMENT_SHADER = `#version 300 es
|
|
11
|
+
precision mediump float;
|
|
12
|
+
uniform vec3 u_color;
|
|
13
|
+
out vec4 outColor;
|
|
14
|
+
|
|
15
|
+
void main() {
|
|
16
|
+
vec2 center = gl_PointCoord - vec2(0.5, 0.5);
|
|
17
|
+
float distance = length(center);
|
|
18
|
+
float alpha = smoothstep(0.55, 0.0, distance);
|
|
19
|
+
outColor = vec4(u_color, alpha);
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
export class ParticleSystem {
|
|
24
|
+
constructor(gl, shaderManager) {
|
|
25
|
+
this.gl = gl;
|
|
26
|
+
this.shaderManager = shaderManager;
|
|
27
|
+
this.program = this.shaderManager.getProgram(
|
|
28
|
+
"particle-field",
|
|
29
|
+
PARTICLE_VERTEX_SHADER,
|
|
30
|
+
PARTICLE_FRAGMENT_SHADER
|
|
31
|
+
);
|
|
32
|
+
this.positionLocation = this.gl.getAttribLocation(this.program, "a_position");
|
|
33
|
+
this.pointSizeLocation = this.gl.getUniformLocation(this.program, "u_point_size");
|
|
34
|
+
this.colorLocation = this.gl.getUniformLocation(this.program, "u_color");
|
|
35
|
+
this.buffer = this.gl.createBuffer();
|
|
36
|
+
|
|
37
|
+
this.count = 0;
|
|
38
|
+
this.positions = new Float32Array(0);
|
|
39
|
+
this.velocities = new Float32Array(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
render({ count, speed, size, audio, time }) {
|
|
43
|
+
const particleCount = clampInt(count, 200, 20_000);
|
|
44
|
+
this.ensureParticles(particleCount);
|
|
45
|
+
this.updateParticles(speed, audio, time);
|
|
46
|
+
this.draw(size, audio);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
ensureParticles(nextCount) {
|
|
50
|
+
if (this.count === nextCount) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.count = nextCount;
|
|
55
|
+
this.positions = new Float32Array(this.count * 2);
|
|
56
|
+
this.velocities = new Float32Array(this.count * 2);
|
|
57
|
+
|
|
58
|
+
for (let index = 0; index < this.count; index += 1) {
|
|
59
|
+
const x = pseudoRandom(index * 17.13) * 2 - 1;
|
|
60
|
+
const y = pseudoRandom(index * 31.91) * 2 - 1;
|
|
61
|
+
const vx = (pseudoRandom(index * 71.17) * 2 - 1) * 0.004;
|
|
62
|
+
const vy = (pseudoRandom(index * 91.37) * 2 - 1) * 0.004;
|
|
63
|
+
this.positions[index * 2] = x;
|
|
64
|
+
this.positions[index * 2 + 1] = y;
|
|
65
|
+
this.velocities[index * 2] = vx;
|
|
66
|
+
this.velocities[index * 2 + 1] = vy;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
updateParticles(speed, audio, time) {
|
|
71
|
+
const motion = 0.4 + clampNumber(speed, 0, 4);
|
|
72
|
+
const beatBoost = audio?.beat ? 1.4 : 1.0;
|
|
73
|
+
const drift = 0.0008 + clampNumber(audio?.amplitude, 0, 1) * 0.0018;
|
|
74
|
+
|
|
75
|
+
for (let index = 0; index < this.count; index += 1) {
|
|
76
|
+
const i = index * 2;
|
|
77
|
+
let x = this.positions[i];
|
|
78
|
+
let y = this.positions[i + 1];
|
|
79
|
+
let vx = this.velocities[i];
|
|
80
|
+
let vy = this.velocities[i + 1];
|
|
81
|
+
|
|
82
|
+
const swirl = Math.sin(time * 0.8 + index * 0.013) * drift;
|
|
83
|
+
vx += swirl * 0.01;
|
|
84
|
+
vy -= swirl * 0.01;
|
|
85
|
+
|
|
86
|
+
x += vx * motion * beatBoost;
|
|
87
|
+
y += vy * motion * beatBoost;
|
|
88
|
+
|
|
89
|
+
if (x > 1 || x < -1) {
|
|
90
|
+
vx *= -1;
|
|
91
|
+
x = clampNumber(x, -1, 1);
|
|
92
|
+
}
|
|
93
|
+
if (y > 1 || y < -1) {
|
|
94
|
+
vy *= -1;
|
|
95
|
+
y = clampNumber(y, -1, 1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.positions[i] = x;
|
|
99
|
+
this.positions[i + 1] = y;
|
|
100
|
+
this.velocities[i] = vx;
|
|
101
|
+
this.velocities[i + 1] = vy;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
draw(size, audio) {
|
|
106
|
+
const gl = this.gl;
|
|
107
|
+
const pointSize = 1.5 + clampNumber(size, 0, 32);
|
|
108
|
+
const amplitude = clampNumber(audio?.amplitude, 0, 1);
|
|
109
|
+
const bass = clampNumber(audio?.bands?.low, 0, 1);
|
|
110
|
+
const high = clampNumber(audio?.bands?.high, 0, 1);
|
|
111
|
+
|
|
112
|
+
gl.useProgram(this.program);
|
|
113
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
114
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.positions, gl.DYNAMIC_DRAW);
|
|
115
|
+
gl.enableVertexAttribArray(this.positionLocation);
|
|
116
|
+
gl.vertexAttribPointer(this.positionLocation, 2, gl.FLOAT, false, 0, 0);
|
|
117
|
+
|
|
118
|
+
gl.uniform1f(this.pointSizeLocation, pointSize);
|
|
119
|
+
gl.uniform3f(
|
|
120
|
+
this.colorLocation,
|
|
121
|
+
0.35 + bass * 0.45,
|
|
122
|
+
0.55 + high * 0.35,
|
|
123
|
+
0.95 + amplitude * 0.05
|
|
124
|
+
);
|
|
125
|
+
gl.drawArrays(gl.POINTS, 0, this.count);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const clampInt = (value, min, max) => {
|
|
130
|
+
const numeric = Number(value);
|
|
131
|
+
if (!Number.isFinite(numeric)) {
|
|
132
|
+
return min;
|
|
133
|
+
}
|
|
134
|
+
return Math.round(Math.min(Math.max(numeric, min), max));
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const clampNumber = (value, min, max) => {
|
|
138
|
+
const numeric = Number(value);
|
|
139
|
+
if (!Number.isFinite(numeric)) {
|
|
140
|
+
return min;
|
|
141
|
+
}
|
|
142
|
+
return Math.min(Math.max(numeric, min), max);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const pseudoRandom = (seed) => {
|
|
146
|
+
const value = Math.sin(seed * 12.9898) * 43758.5453;
|
|
147
|
+
return value - Math.floor(value);
|
|
148
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const TEXT_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 TEXT_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
|
+
out vec4 outColor;
|
|
17
|
+
|
|
18
|
+
void main() {
|
|
19
|
+
vec4 texel = texture(u_texture, v_uv);
|
|
20
|
+
outColor = vec4(texel.rgb, texel.a * u_intensity);
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const QUAD_VERTICES = new Float32Array([
|
|
25
|
+
-1.0, -1.0, 0.0, 1.0,
|
|
26
|
+
1.0, -1.0, 1.0, 1.0,
|
|
27
|
+
-1.0, 1.0, 0.0, 0.0,
|
|
28
|
+
1.0, 1.0, 1.0, 0.0
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export class TextRenderer {
|
|
32
|
+
constructor(gl, shaderManager) {
|
|
33
|
+
this.gl = gl;
|
|
34
|
+
this.shaderManager = shaderManager;
|
|
35
|
+
this.program = this.shaderManager.getProgram("text-renderer", TEXT_VERTEX_SHADER, TEXT_FRAGMENT_SHADER);
|
|
36
|
+
this.positionLocation = this.gl.getAttribLocation(this.program, "a_position");
|
|
37
|
+
this.uvLocation = this.gl.getAttribLocation(this.program, "a_uv");
|
|
38
|
+
this.textureLocation = this.gl.getUniformLocation(this.program, "u_texture");
|
|
39
|
+
this.intensityLocation = this.gl.getUniformLocation(this.program, "u_intensity");
|
|
40
|
+
|
|
41
|
+
this.buffer = this.gl.createBuffer();
|
|
42
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
|
|
43
|
+
this.gl.bufferData(this.gl.ARRAY_BUFFER, QUAD_VERTICES, this.gl.STATIC_DRAW);
|
|
44
|
+
|
|
45
|
+
this.canvas = document.createElement("canvas");
|
|
46
|
+
this.canvas.width = 1024;
|
|
47
|
+
this.canvas.height = 512;
|
|
48
|
+
this.ctx = this.canvas.getContext("2d");
|
|
49
|
+
|
|
50
|
+
this.texture = this.gl.createTexture();
|
|
51
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
|
|
52
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
|
|
53
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
|
|
54
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
|
|
55
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
render({ content, fontSize, audio, time, color, glowStrength }) {
|
|
59
|
+
const text = String(content || "").trim();
|
|
60
|
+
if (!text) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.syncCanvasSize();
|
|
65
|
+
|
|
66
|
+
const amp = clamp(Number(audio?.amplitude || 0), 0, 1);
|
|
67
|
+
const beatBoost = audio?.beat ? 1.0 : 0.0;
|
|
68
|
+
const maxFontSize = Math.max(48, Math.floor(this.canvas.height * 0.22));
|
|
69
|
+
const dynamicSize = Math.round(
|
|
70
|
+
clamp(Number(fontSize || 96), 18, maxFontSize) * (1 + amp * 0.08 + beatBoost * 0.04)
|
|
71
|
+
);
|
|
72
|
+
this.drawTextToCanvas({
|
|
73
|
+
text,
|
|
74
|
+
fontSize: dynamicSize,
|
|
75
|
+
time,
|
|
76
|
+
color,
|
|
77
|
+
amplitude: amp,
|
|
78
|
+
glowStrength: Number(glowStrength ?? 0.15)
|
|
79
|
+
});
|
|
80
|
+
this.uploadTexture();
|
|
81
|
+
this.drawQuad({ intensity: 0.85 + amp * 0.15 });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
drawTextToCanvas({ text, fontSize, time, color, amplitude, glowStrength }) {
|
|
85
|
+
const ctx = this.ctx;
|
|
86
|
+
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
87
|
+
|
|
88
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0.0)";
|
|
89
|
+
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
90
|
+
|
|
91
|
+
const safeColor = typeof color === "string" && color.trim() ? color : "#e5f3ff";
|
|
92
|
+
const glow = clamp(Number(glowStrength || 0), 0, 1) * (1.5 + amplitude * 5.0);
|
|
93
|
+
const xShift = Math.sin(time * 2.0) * (2 + amplitude * 4);
|
|
94
|
+
|
|
95
|
+
ctx.textAlign = "center";
|
|
96
|
+
ctx.textBaseline = "middle";
|
|
97
|
+
ctx.font = `700 ${fontSize}px "IBM Plex Sans", "Noto Sans JP", sans-serif`;
|
|
98
|
+
ctx.shadowColor = "rgba(110, 208, 255, 0.35)";
|
|
99
|
+
ctx.shadowBlur = glow;
|
|
100
|
+
ctx.fillStyle = safeColor;
|
|
101
|
+
ctx.fillText(text, this.canvas.width / 2 + xShift, this.canvas.height / 2);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
syncCanvasSize() {
|
|
105
|
+
const width = clamp(Math.floor(this.gl.drawingBufferWidth || 1024), 640, 2048);
|
|
106
|
+
const height = clamp(Math.floor(this.gl.drawingBufferHeight || 512), 360, 2048);
|
|
107
|
+
if (this.canvas.width === width && this.canvas.height === height) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
this.canvas.width = width;
|
|
111
|
+
this.canvas.height = height;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
uploadTexture() {
|
|
115
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
|
|
116
|
+
this.gl.texImage2D(
|
|
117
|
+
this.gl.TEXTURE_2D,
|
|
118
|
+
0,
|
|
119
|
+
this.gl.RGBA,
|
|
120
|
+
this.gl.RGBA,
|
|
121
|
+
this.gl.UNSIGNED_BYTE,
|
|
122
|
+
this.canvas
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
drawQuad({ intensity }) {
|
|
127
|
+
const gl = this.gl;
|
|
128
|
+
gl.useProgram(this.program);
|
|
129
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
130
|
+
gl.enableVertexAttribArray(this.positionLocation);
|
|
131
|
+
gl.vertexAttribPointer(this.positionLocation, 2, gl.FLOAT, false, 16, 0);
|
|
132
|
+
gl.enableVertexAttribArray(this.uvLocation);
|
|
133
|
+
gl.vertexAttribPointer(this.uvLocation, 2, gl.FLOAT, false, 16, 8);
|
|
134
|
+
|
|
135
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
136
|
+
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
|
137
|
+
gl.uniform1i(this.textureLocation, 0);
|
|
138
|
+
gl.uniform1f(this.intensityLocation, clamp(Number(intensity || 1), 0, 1));
|
|
139
|
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|