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.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +70 -117
  3. data/docs/.nojekyll +0 -0
  4. data/docs/assets/playground-worker.js +373 -0
  5. data/docs/assets/playground.css +440 -0
  6. data/docs/assets/playground.js +652 -0
  7. data/docs/assets/site.css +744 -0
  8. data/docs/assets/vizcore-demo.gif +0 -0
  9. data/docs/assets/vizcore-poster.png +0 -0
  10. data/docs/assets/vj-tunnel.js +159 -0
  11. data/docs/index.html +225 -0
  12. data/docs/playground.html +81 -0
  13. data/docs/shape_dsl.md +269 -0
  14. data/examples/README.md +59 -0
  15. data/examples/assets/README.md +19 -0
  16. data/examples/audio_inspector.rb +34 -0
  17. data/examples/club_intro_drop.rb +78 -0
  18. data/examples/kansai_rubykaigi_visual.rb +70 -0
  19. data/examples/live_coding_minimal.rb +22 -0
  20. data/examples/midi_controller_show.rb +78 -0
  21. data/examples/midi_scene_switch.rb +3 -1
  22. data/examples/parser_visualizer.rb +48 -0
  23. data/examples/readme_demo.rb +17 -0
  24. data/examples/rhythm_geometry.rb +34 -0
  25. data/examples/ruby_crystal_show.rb +35 -0
  26. data/examples/shader_playground.rb +18 -0
  27. data/examples/unyo_liquid.rb +59 -0
  28. data/examples/vj_ambient_chill_room.rb +124 -0
  29. data/examples/vj_dnb_jungle.rb +170 -0
  30. data/examples/vj_festival_mainstage.rb +245 -0
  31. data/examples/vj_festival_mainstage.yml +17 -0
  32. data/examples/vj_glitch_industrial.rb +164 -0
  33. data/examples/vj_hiphop_cipher.rb +167 -0
  34. data/examples/vj_jpop_idol_live.rb +210 -0
  35. data/examples/vj_synthwave_retro.rb +173 -0
  36. data/examples/vj_techno_warehouse.rb +195 -0
  37. data/frontend/index.html +494 -2
  38. data/frontend/src/audio-inspector.js +40 -0
  39. data/frontend/src/custom-shape-param-controls.js +106 -0
  40. data/frontend/src/live-controls.js +131 -0
  41. data/frontend/src/main.js +1060 -16
  42. data/frontend/src/mapping-target-selector.js +109 -0
  43. data/frontend/src/midi-learn.js +194 -0
  44. data/frontend/src/performance-monitor.js +183 -0
  45. data/frontend/src/plugin-runtime.js +130 -0
  46. data/frontend/src/projector-mode.js +56 -0
  47. data/frontend/src/renderer/engine.js +157 -3
  48. data/frontend/src/renderer/layer-manager.js +442 -30
  49. data/frontend/src/renderer/shader-manager.js +26 -0
  50. data/frontend/src/runtime-control-preset.js +11 -0
  51. data/frontend/src/shader-error-overlay.js +29 -0
  52. data/frontend/src/shader-param-controls.js +93 -0
  53. data/frontend/src/shaders/builtins.js +380 -2
  54. data/frontend/src/shaders/post-effects.js +52 -0
  55. data/frontend/src/shape-editor-controls.js +157 -0
  56. data/frontend/src/visual-regression.js +67 -0
  57. data/frontend/src/visual-settings-preset.js +103 -0
  58. data/frontend/src/visuals/geometry.js +666 -0
  59. data/frontend/src/visuals/image-renderer.js +291 -0
  60. data/frontend/src/visuals/particle-system.js +56 -10
  61. data/frontend/src/visuals/shape-renderer.js +475 -0
  62. data/frontend/src/visuals/spectrogram-renderer.js +226 -0
  63. data/frontend/src/visuals/svg-arc.js +104 -0
  64. data/frontend/src/visuals/text-renderer.js +112 -11
  65. data/frontend/src/websocket-client.js +12 -1
  66. data/lib/vizcore/analysis/adaptive_normalizer.rb +70 -0
  67. data/lib/vizcore/analysis/beat_detector.rb +4 -2
  68. data/lib/vizcore/analysis/bpm_estimator.rb +8 -0
  69. data/lib/vizcore/analysis/feature_recorder.rb +159 -0
  70. data/lib/vizcore/analysis/feature_replay.rb +84 -0
  71. data/lib/vizcore/analysis/pipeline.rb +235 -11
  72. data/lib/vizcore/analysis/tap_tempo.rb +74 -0
  73. data/lib/vizcore/analysis.rb +4 -0
  74. data/lib/vizcore/audio/dummy_sine_input.rb +1 -1
  75. data/lib/vizcore/audio/fixture_input.rb +65 -0
  76. data/lib/vizcore/audio/input_manager.rb +4 -2
  77. data/lib/vizcore/audio/mic_input.rb +24 -8
  78. data/lib/vizcore/audio/portaudio_ffi.rb +106 -1
  79. data/lib/vizcore/audio.rb +1 -0
  80. data/lib/vizcore/cli/doctor.rb +159 -0
  81. data/lib/vizcore/cli/dsl_reference.rb +99 -0
  82. data/lib/vizcore/cli/layer_docs.rb +46 -0
  83. data/lib/vizcore/cli/scene_diagnostics.rb +23 -0
  84. data/lib/vizcore/cli/scene_inspector.rb +136 -0
  85. data/lib/vizcore/cli/scene_validator.rb +337 -0
  86. data/lib/vizcore/cli/shader_template.rb +68 -0
  87. data/lib/vizcore/cli/shader_uniform_docs.rb +54 -0
  88. data/lib/vizcore/cli.rb +689 -18
  89. data/lib/vizcore/config.rb +103 -2
  90. data/lib/vizcore/control_preset.rb +68 -0
  91. data/lib/vizcore/dsl/engine.rb +277 -5
  92. data/lib/vizcore/dsl/layer_builder.rb +1280 -23
  93. data/lib/vizcore/dsl/layer_group_builder.rb +112 -0
  94. data/lib/vizcore/dsl/mapping_resolver.rb +290 -7
  95. data/lib/vizcore/dsl/mapping_transform_builder.rb +71 -0
  96. data/lib/vizcore/dsl/reaction_builder.rb +44 -0
  97. data/lib/vizcore/dsl/scene_builder.rb +61 -5
  98. data/lib/vizcore/dsl/shader_source_resolver.rb +67 -6
  99. data/lib/vizcore/dsl/style_builder.rb +68 -0
  100. data/lib/vizcore/dsl/timeline_builder.rb +138 -0
  101. data/lib/vizcore/dsl/transition_controller.rb +77 -0
  102. data/lib/vizcore/dsl.rb +5 -1
  103. data/lib/vizcore/layer_catalog.rb +275 -0
  104. data/lib/vizcore/project_manifest.rb +152 -0
  105. data/lib/vizcore/renderer/png_writer.rb +57 -0
  106. data/lib/vizcore/renderer/render_sequence.rb +153 -0
  107. data/lib/vizcore/renderer/scene_frame_source.rb +132 -0
  108. data/lib/vizcore/renderer/scene_serializer.rb +36 -3
  109. data/lib/vizcore/renderer/snapshot.rb +38 -0
  110. data/lib/vizcore/renderer/snapshot_renderer.rb +938 -0
  111. data/lib/vizcore/renderer.rb +5 -0
  112. data/lib/vizcore/server/frame_broadcaster.rb +143 -8
  113. data/lib/vizcore/server/gallery_app.rb +155 -0
  114. data/lib/vizcore/server/gallery_page.rb +100 -0
  115. data/lib/vizcore/server/gallery_runner.rb +48 -0
  116. data/lib/vizcore/server/rack_app.rb +203 -4
  117. data/lib/vizcore/server/runner.rb +391 -22
  118. data/lib/vizcore/server/scene_dependency_watcher.rb +79 -0
  119. data/lib/vizcore/server/websocket_handler.rb +60 -10
  120. data/lib/vizcore/server.rb +4 -0
  121. data/lib/vizcore/shape.rb +719 -0
  122. data/lib/vizcore/sync/osc_message.rb +103 -0
  123. data/lib/vizcore/sync/osc_receiver.rb +68 -0
  124. data/lib/vizcore/sync.rb +4 -0
  125. data/lib/vizcore/templates/midi_control_scene.rb +3 -1
  126. data/lib/vizcore/templates/plugin_layer.rb +20 -0
  127. data/lib/vizcore/templates/plugin_readme.md +23 -0
  128. data/lib/vizcore/templates/plugin_renderer.js +43 -0
  129. data/lib/vizcore/templates/plugin_scene.rb +14 -0
  130. data/lib/vizcore/templates/project_readme.md +7 -23
  131. data/lib/vizcore/templates/rubykaigi_scene.rb +30 -0
  132. data/lib/vizcore/version.rb +1 -1
  133. data/lib/vizcore.rb +28 -0
  134. data/scripts/browser_capture.mjs +75 -0
  135. data/sig/vizcore.rbs +461 -0
  136. metadata +94 -3
  137. 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.35 + bass * 0.45,
122
- 0.55 + high * 0.35,
123
- 0.95 + amplitude * 0.05
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
  }