[READ-ONLY] Mirror of https://github.com/probablykasper/ferrum. Music library app for Mac, Linux and Windows ferrum.kasper.space
electron linux macos music music-library music-player napi windows
0

Configure Feed

Select the types of activity you want to include in your feed.

Add ShaderToyLite canvas

Kasper (Oct 2, 2025, 11:46 PM +0200) f403f80e 45c28e79

+515 -5
+2
src/App.svelte
··· 24 24 import CheckForUpdates from './components/CheckForUpdates.svelte' 25 25 import Settings from './components/Settings.svelte' 26 26 import Button from './components/Button.svelte' 27 + import Visualizer from '$lib/visualizer/Visualizer.svelte' 27 28 28 29 ipc_renderer.invoke('app_loaded').catch(() => { 29 30 ipc_renderer.invoke('showMessageBox', false, { ··· 292 293 ></div> 293 294 {/if} 294 295 </main> 296 + <Visualizer /> 295 297 296 298 <QuickNav /> 297 299 {#if track_info_state.instance}
+2 -1
src/lib/player.ts
··· 16 16 import { ipc_renderer } from './window' 17 17 import { queue, set_new_queue, next as queueNext, prev as queuePrev } from './queue' 18 18 import { tracks_page_item_ids } from '$components/TrackList.svelte' 19 - // import { start_visualizer } from './vis' 20 19 21 20 const audio = new Audio() 21 + export const audioContext = new AudioContext() 22 + export const mediaElementSource = audioContext.createMediaElementSource(audio) 22 23 23 24 let is_stopped = true 24 25 export const stopped = (() => {
+13 -4
src/lib/vis.ts src/lib/visualizer/visualizer.ts
··· 1 + // Based on kaleidosync code 2 + 1 3 import { scaleLinear } from 'd3-scale' 2 4 3 5 const visualizer_settings = { ··· 10 12 const FILTER_FREQUENCY = 7500 11 13 const FILTER_Q = 0.75 12 14 13 - export function start_visualizer(element: HTMLAudioElement) { 14 - const audioContext = new AudioContext() 15 - const mediaElementSource = audioContext.createMediaElementSource(element) 15 + export function start_visualizer( 16 + audioContext: AudioContext, 17 + mediaElementSource: MediaElementAudioSourceNode, 18 + on_update: (info: { stream: number; volume: number }) => void, 19 + ) { 20 + console.log('createMediaElementSource') 16 21 17 22 const analyser = audioContext.createAnalyser() 18 23 const filter = audioContext.createBiquadFilter() ··· 28 33 filter.Q.value = FILTER_Q 29 34 30 35 let stream = 0 31 - let volume = 1 36 + let volume = 0 32 37 33 38 const def = visualizer_settings?.def || [2.5, 0.09] 34 39 const volumeBuffer: number[] = [] ··· 79 84 80 85 stream = stream + Math.pow(vol, 0.75) / (playing ? 10 : 100) 81 86 87 + on_update({ stream, volume }) 88 + 82 89 if (!destroyed) { 83 90 raf_id = requestAnimationFrame(measure) 84 91 } ··· 94 101 } 95 102 analyser.disconnect() 96 103 filter.disconnect() 104 + mediaElementSource.disconnect() 105 + audioContext.close() 97 106 }, 98 107 } 99 108 }
+429
src/lib/visualizer/ShaderToyLite.js
··· 1 + // github.com/chipweinberger/ShaderToyLite.js/blob/main/ShaderToyLite.js 2 + 3 + export function ShaderToyLite(canvasId) { 4 + var hdr = `#version 300 es 5 + #ifdef GL_ES 6 + precision highp float; 7 + precision highp int; 8 + precision mediump sampler3D; 9 + #endif 10 + #define texture2D texture 11 + uniform vec3 iResolution; // viewport resolution (in pixels) 12 + uniform float iTime; // shader playback time (in seconds) 13 + uniform float iTimeDelta; // render time (in seconds) 14 + uniform float iFrameRate; // shader frame rate 15 + uniform int iFrame; // shader playback frame 16 + uniform float iChannelTime[4]; // channel playback time (in seconds) 17 + uniform vec3 iChannelResolution[4]; // channel resolution (in pixels) 18 + uniform vec4 iMouse; // mouse pixel coords. xy: current (if MLB down), zw: click 19 + uniform sampler2D iChannel0; // input channel 0 20 + uniform sampler2D iChannel1; // input channel 1 21 + uniform sampler2D iChannel2; // input channel 2 22 + uniform sampler2D iChannel3; // input channel 3 23 + uniform vec4 iDate; // (year, month, day, unixtime in seconds) 24 + uniform float iSampleRate; // sound sample rate (i.e., 44100) 25 + out vec4 frag_out_color; 26 + void mainImage( out vec4 c, in vec2 f ); 27 + void main( void ) 28 + { 29 + vec4 color = vec4(0.0,0.0,0.0,0.0); 30 + mainImage( color, gl_FragCoord.xy ); 31 + frag_out_color = vec4(color); 32 + } 33 + ` 34 + 35 + var basicFragShader = `void mainImage( out vec4 fragColor, in vec2 fragCoord ) { 36 + fragColor = texture2D(iChannel0, gl_FragCoord.xy / iResolution.xy); 37 + } 38 + ` 39 + 40 + const basicVertexShader = `#version 300 es 41 + #ifdef GL_ES 42 + precision highp float; 43 + precision highp int; 44 + precision mediump sampler3D; 45 + #endif 46 + in vec2 vertexInPosition; 47 + void main() { 48 + gl_Position = vec4(vertexInPosition, 0.0, 1.0); 49 + } 50 + ` 51 + 52 + const quadVertices = new Float32Array([ 53 + -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 54 + ]) 55 + 56 + var opts = { 57 + alpha: false, 58 + depth: false, 59 + stencil: false, 60 + premultipliedAlpha: false, 61 + antialias: true, 62 + preserveDrawingBuffer: false, 63 + powerPreference: 'high-performance', 64 + } 65 + 66 + var gl = document.getElementById(canvasId).getContext('webgl2', opts) 67 + 68 + // timing 69 + var isPlaying = false 70 + var firstDrawTime = 0 71 + var prevDrawTime = 0 72 + 73 + // callback 74 + var onDrawCallback 75 + 76 + // uniforms 77 + var iFrame = 0 78 + var iMouse = { x: 0, y: 0, clickX: 0, clickY: 0 } 79 + 80 + // shader common source 81 + var common = '' 82 + 83 + // render passes variables. valid keys: 84 + // 'A', 'B', 'C', 'D', 'Image' 85 + var sourcecode = {} // fragment shader code 86 + var ichannels = {} // texture inputs 87 + var atexture = {} // front texture (input/output) 88 + var btexture = {} // back texture (input/output) 89 + var aframebuf = {} // front buffer (output) 90 + var bframebuf = {} // back buffer (output) 91 + var program = {} // webgl program 92 + var location = {} // uniform location 93 + var flip = {} // a b flip 94 + var quadBuffer // two full screen triangles 95 + 96 + var setup = () => { 97 + gl.getExtension('OES_texture_float_linear') 98 + gl.getExtension('OES_texture_half_float_linear') 99 + gl.getExtension('EXT_color_buffer_float') 100 + gl.getExtension('WEBGL_debug_shaders') 101 + ;['A', 'B', 'C', 'D', 'Image'].forEach((key) => { 102 + sourcecode[key] = '' 103 + ichannels[key] = {} 104 + program[key] = null 105 + location[key] = {} 106 + if (key != 'Image') { 107 + atexture[key] = createTexture() 108 + btexture[key] = createTexture() 109 + aframebuf[key] = createFrameBuffer(atexture[key]) 110 + bframebuf[key] = createFrameBuffer(btexture[key]) 111 + flip[key] = false 112 + } 113 + }) 114 + 115 + // bind the geometry 116 + quadBuffer = gl.createBuffer() 117 + gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer) 118 + gl.bufferData(gl.ARRAY_BUFFER, quadVertices, gl.STATIC_DRAW) 119 + 120 + // Set viewport size 121 + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height) 122 + 123 + var canvas = document.getElementById(canvasId) 124 + 125 + window.addEventListener('resize', function () { 126 + gl.canvas.width = canvas.width 127 + gl.canvas.height = canvas.height 128 + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height) 129 + }) 130 + 131 + canvas.addEventListener('mousemove', (event) => { 132 + iMouse.x = event.offsetX 133 + iMouse.y = canvas.height - event.offsetY 134 + }) 135 + 136 + canvas.addEventListener('mousedown', (event) => { 137 + iMouse.clickX = event.offsetX 138 + iMouse.clickY = canvas.height - event.offsetY 139 + }) 140 + 141 + canvas.addEventListener('mouseup', () => { 142 + iMouse.clickX = 0 143 + iMouse.clickY = 0 144 + }) 145 + } 146 + 147 + var createTexture = () => { 148 + var texture = gl.createTexture() 149 + gl.bindTexture(gl.TEXTURE_2D, texture) 150 + gl.texImage2D( 151 + gl.TEXTURE_2D, 152 + 0, 153 + gl.RGBA32F, 154 + gl.canvas.width, 155 + gl.canvas.height, 156 + 0, 157 + gl.RGBA, 158 + gl.FLOAT, 159 + null, 160 + ) 161 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) 162 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) 163 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) 164 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) 165 + return texture 166 + } 167 + 168 + var createFrameBuffer = (texture) => { 169 + var framebuffer = gl.createFramebuffer() 170 + gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer) 171 + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0) 172 + gl.bindTexture(gl.TEXTURE_2D, null) 173 + gl.bindFramebuffer(gl.FRAMEBUFFER, null) 174 + return framebuffer 175 + } 176 + 177 + var compileProgram = (key) => { 178 + var vert = gl.createShader(gl.VERTEX_SHADER) 179 + gl.shaderSource(vert, basicVertexShader) 180 + gl.compileShader(vert) 181 + 182 + if (!gl.getShaderParameter(vert, gl.COMPILE_STATUS)) { 183 + console.error('Vertex Shader compilation failed: ' + gl.getShaderInfoLog(vert)) 184 + gl.deleteShader(vert) 185 + return null 186 + } 187 + 188 + var source = hdr + common + sourcecode[key] 189 + var frag = gl.createShader(gl.FRAGMENT_SHADER) 190 + gl.shaderSource(frag, source) 191 + gl.compileShader(frag) 192 + 193 + if (!gl.getShaderParameter(frag, gl.COMPILE_STATUS)) { 194 + console.error('Fragment Shader compilation failed: ' + gl.getShaderInfoLog(frag)) 195 + console.error(source) 196 + gl.deleteShader(frag) 197 + return null 198 + } 199 + 200 + var program = gl.createProgram() 201 + gl.attachShader(program, vert) 202 + gl.attachShader(program, frag) 203 + gl.linkProgram(program) 204 + 205 + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 206 + console.error('Program initialization failed: ' + gl.getProgramInfoLog(program)) 207 + return null 208 + } 209 + 210 + // uniform locations 211 + location[key]['iResolution'] = gl.getUniformLocation(program, 'iResolution') 212 + location[key]['iTime'] = gl.getUniformLocation(program, 'iTime') 213 + location[key]['iTimeDelta'] = gl.getUniformLocation(program, 'iTimeDelta') 214 + location[key]['iFrameRate'] = gl.getUniformLocation(program, 'iFrameRate') 215 + location[key]['iFrame'] = gl.getUniformLocation(program, 'iFrame') 216 + location[key]['iChannelTime'] = gl.getUniformLocation(program, 'iChannelTime[0]') 217 + location[key]['iChannelResolution'] = gl.getUniformLocation(program, 'iChannelResolution[0]') 218 + location[key]['iChannel0'] = gl.getUniformLocation(program, 'iChannel0') 219 + location[key]['iChannel1'] = gl.getUniformLocation(program, 'iChannel1') 220 + location[key]['iChannel2'] = gl.getUniformLocation(program, 'iChannel2') 221 + location[key]['iChannel3'] = gl.getUniformLocation(program, 'iChannel3') 222 + location[key]['iMouse'] = gl.getUniformLocation(program, 'iMouse') 223 + location[key]['iDate'] = gl.getUniformLocation(program, 'iDate') 224 + location[key]['iSampleRate'] = gl.getUniformLocation(program, 'iSampleRate') 225 + location[key]['vertexInPosition'] = gl.getAttribLocation(program, 'vertexInPosition') 226 + 227 + return program 228 + } 229 + 230 + var repeat = (times, arr) => { 231 + let result = [] 232 + for (let i = 0; i < times; i++) { 233 + result = [...result, ...arr] 234 + } 235 + return result 236 + } 237 + 238 + var setShader = (config, key) => { 239 + if (config) { 240 + if (config.source) { 241 + sourcecode[key] = config.source 242 + program[key] = compileProgram(key) 243 + if (program[key] == null) { 244 + console.error('Failed to compile ' + key) 245 + } 246 + } 247 + for (let i = 0; i < 4; i++) { 248 + var s = config[`iChannel${i}`] 249 + if (s == 'A' || s == 'B' || s == 'C' || s == 'D') { 250 + ichannels[key][i] = s 251 + } 252 + } 253 + } else { 254 + sourcecode[key] = '' 255 + program[key] = null 256 + } 257 + } 258 + 259 + var draw = () => { 260 + // current time 261 + var now = isPlaying ? Date.now() : prevDrawTime 262 + var date = new Date(now) 263 + 264 + // first draw? 265 + if (firstDrawTime == 0) { 266 + firstDrawTime = now 267 + } 268 + 269 + // call callback 270 + if (onDrawCallback) { 271 + onDrawCallback() 272 + } 273 + 274 + // time difference between frames in seconds 275 + var iTimeDelta = (now - prevDrawTime) * 0.001 276 + 277 + // time in seconds 278 + var iTime = (now - firstDrawTime) * 0.001 279 + var iDate = [date.getFullYear(), date.getMonth(), date.getDate(), date.getTime() * 0.001] 280 + 281 + // channel uniforms 282 + var iChannelTimes = new Float32Array(repeat(4, [iTime])) 283 + var iChannelResolutions = new Float32Array(repeat(4, [gl.canvas.width, gl.canvas.height, 0])) 284 + 285 + ;['A', 'B', 'C', 'D', 'Image'].forEach((key) => { 286 + if (program[key]) { 287 + // framebuffer 288 + if (key === 'Image') { 289 + gl.bindFramebuffer(gl.FRAMEBUFFER, null) 290 + } else { 291 + var output = flip[key] ? bframebuf[key] : aframebuf[key] 292 + gl.bindFramebuffer(gl.FRAMEBUFFER, output) 293 + } 294 + 295 + // textures 296 + for (let i = 0; i < 4; i++) { 297 + var chkey = ichannels[key][i] 298 + if (chkey) { 299 + var input = flip[chkey] ? atexture[chkey] : btexture[chkey] 300 + gl.activeTexture(gl[`TEXTURE${i}`]) 301 + gl.bindTexture(gl.TEXTURE_2D, input) 302 + } 303 + } 304 + 305 + // program 306 + gl.useProgram(program[key]) 307 + 308 + // uniforms 309 + gl.uniform3f(location[key]['iResolution'], gl.canvas.width, gl.canvas.height, 1.0) 310 + gl.uniform1f(location[key]['iTime'], iTime) 311 + gl.uniform1f(location[key]['iTimeDelta'], iTimeDelta) 312 + gl.uniform1f(location[key]['iFrameRate'], 60) 313 + gl.uniform1i(location[key]['iFrame'], iFrame) 314 + gl.uniform1fv(location[key]['iChannelTime'], iChannelTimes) 315 + gl.uniform3fv(location[key]['iChannelResolution'], iChannelResolutions) 316 + gl.uniform1i(location[key]['iChannel0'], 0) 317 + gl.uniform1i(location[key]['iChannel1'], 1) 318 + gl.uniform1i(location[key]['iChannel2'], 2) 319 + gl.uniform1i(location[key]['iChannel3'], 3) 320 + gl.uniform4f(location[key]['iMouse'], iMouse.x, iMouse.y, iMouse.clickX, iMouse.clickY) 321 + gl.uniform4f(location[key]['iDate'], iDate[0], iDate[1], iDate[2], iDate[3]) 322 + gl.uniform1f(location[key]['iSampleRate'], 44100) 323 + 324 + // viewport 325 + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height) 326 + 327 + // vertexs 328 + gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer) 329 + gl.vertexAttribPointer(location[key]['vertexInPosition'], 2, gl.FLOAT, false, 0, 0) 330 + gl.enableVertexAttribArray(location[key]['vertexInPosition']) 331 + 332 + // draw 333 + gl.drawArrays(gl.TRIANGLES, 0, 6) 334 + 335 + flip[key] = !flip[key] 336 + } 337 + }) 338 + 339 + // time of last draw 340 + prevDrawTime = now 341 + 342 + // frame counter 343 + iFrame++ 344 + } 345 + 346 + // Animation loop 347 + var animate = () => { 348 + if (isPlaying) { 349 + draw() 350 + requestAnimationFrame(animate) 351 + } 352 + } 353 + 354 + this.setCommon = (source) => { 355 + if (source === undefined) { 356 + source = '' 357 + } 358 + if (source === null) { 359 + source = '' 360 + } 361 + common = source 362 + ;['A', 'B', 'C', 'D', 'Image'].forEach((key) => { 363 + if (program[key]) { 364 + program[key] = compileProgram(key) 365 + } 366 + }) 367 + } 368 + 369 + this.setBufferA = (config) => { 370 + setShader(config, 'A') 371 + } 372 + 373 + this.setBufferB = (config) => { 374 + setShader(config, 'B') 375 + } 376 + 377 + this.setBufferC = (config) => { 378 + setShader(config, 'C') 379 + } 380 + 381 + this.setBufferD = (config) => { 382 + setShader(config, 'D') 383 + } 384 + 385 + this.setImage = (config) => { 386 + setShader(config, 'Image') 387 + } 388 + 389 + this.setOnDraw = (callback) => { 390 + onDrawCallback = callback 391 + } 392 + 393 + this.addTexture = (texture, key) => { 394 + atexture[key] = texture 395 + btexture[key] = texture 396 + flip[key] = false 397 + } 398 + 399 + this.time = () => { 400 + return (prevDrawTime - firstDrawTime) * 0.001 401 + } 402 + 403 + this.isPlaying = () => isPlaying 404 + 405 + this.reset = () => { 406 + var now = new Date() 407 + firstDrawTime = now 408 + prevDrawTime = now 409 + iFrame = 0 410 + draw() 411 + } 412 + 413 + this.pause = () => { 414 + isPlaying = false 415 + } 416 + 417 + this.play = () => { 418 + if (!isPlaying) { 419 + isPlaying = true 420 + var now = Date.now() 421 + var elapsed = prevDrawTime - firstDrawTime 422 + firstDrawTime = now - elapsed 423 + prevDrawTime = now 424 + animate() 425 + } 426 + } 427 + 428 + setup() 429 + }
+69
src/lib/visualizer/Visualizer.svelte
··· 1 + <script lang="ts"> 2 + import { start_visualizer } from './visualizer' 3 + import { ShaderToyLite } from './ShaderToyLite.js' 4 + import { onMount } from 'svelte' 5 + import { audioContext, mediaElementSource } from '$lib/player' 6 + 7 + const canvas_id = 'visualizer' 8 + 9 + onMount(() => { 10 + const a = ` 11 + #define WARP true 12 + #define BALLS 10. 13 + #define CONTRAST 3 14 + #define GLOW .1 15 + #define ORB_SIZE 0.492519 16 + #define PI 3.14159265359 17 + 18 + vec2 kale(vec2 uv, vec2 offset, float splits) { 19 + float angle = atan(uv.y, uv.x); 20 + angle = ((angle / PI) + 1.0) * 0.5; 21 + angle = mod(angle, 1.0 / splits) * splits; 22 + angle = -abs(2.0 * angle - 1.0) + 1.0; 23 + float y = length(uv); 24 + angle = angle * (y); 25 + return vec2(angle, y) - offset; 26 + } 27 + 28 + void mainImage (out vec4 fragColor, in vec2 fragCoord) { 29 + vec2 uv = 2. * fragCoord/iResolution.xy - 1.; 30 + uv.x *= iResolution.x / iResolution.y; 31 + uv *= 2.2; 32 + fragColor = vec4(0.); 33 + float dist = distance(uv, vec2(0.)); 34 + uv = WARP ? uv * kale(uv, vec2(0.), 2.) : uv; 35 + for (float i = 0.; i < BALLS; i++) { 36 + float t = iTime/2. - i * PI / BALLS * cos(iTime / max(i, 0.0001)); 37 + vec2 p = vec2(cos(t), sin(t)) / sin(i / BALLS * PI / dist + iTime); 38 + vec3 c = cos(vec3(0, 5, -5) * PI * 2. / PI + PI * (iTime / (i+1.) / 5.)) * (GLOW) + (GLOW); 39 + fragColor += vec4(vec3(dist * .35 / length(uv - p * ORB_SIZE) * c), 1.0); 40 + } 41 + fragColor.xyz = pow(fragColor.xyz, vec3(CONTRAST)); 42 + } 43 + ` 44 + const image = ` 45 + void mainImage( out vec4 fragColor, in vec2 fragCoord ) { 46 + vec2 uv = fragCoord.xy / iResolution.xy; 47 + vec4 col = texture(iChannel0, uv); 48 + fragColor = vec4(col.rgb, 1.); 49 + } 50 + ` 51 + const toy = new ShaderToyLite(canvas_id) 52 + toy.setCommon('') 53 + toy.setBufferA({ source: a }) 54 + toy.setImage({ source: image, iChannel0: 'A' }) 55 + toy.play() 56 + console.log('start') 57 + const visualizer = start_visualizer(audioContext, mediaElementSource, (info) => { 58 + console.log('update', info.stream, info.volume) 59 + }) 60 + 61 + return () => { 62 + visualizer.destroy() 63 + } 64 + }) 65 + </script> 66 + 67 + <div class="fixed top-0 left-0 flex h-screen w-screen items-center justify-center"> 68 + <canvas id={canvas_id} width="840" height="472"></canvas> 69 + </div>