[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.

Transition visualizers

Kasper (Oct 2, 2025, 11:46 PM +0200) d036ea0f 719de215

+177 -80
+177 -80
src/lib/visualizer/Visualizer.svelte
··· 6 6 import Player from '$components/Player.svelte' 7 7 import { fly } from 'svelte/transition' 8 8 import { check_modifiers } from '$lib/helpers' 9 + import { cubicInOut, quadInOut } from 'svelte/easing' 9 10 10 11 let { on_close }: { on_close: () => void } = $props() 11 12 12 13 let canvas: HTMLCanvasElement | undefined 13 14 const canvas_id = 'visualizer' 14 15 let toy: ShaderToyLite | undefined 16 + let currentShaderIndex = $state(0) 17 + let transitionToy: ShaderToyLite | undefined 18 + let isTransitioning = $state(false) 19 + let transitionCanvas: HTMLCanvasElement | undefined = $state() 20 + const transition_canvas_id = 'visualizer-transition' 15 21 16 - const shaders = { 17 - 'Color Warp by yozic': ` 18 - #define WARP true 19 - #define BALLS 10. 20 - #define CONTRAST 3 21 - #define GLOW .1 22 - #define ORB_SIZE 0.492519 23 - #define PI 3.14159265359 22 + const shaders = [ 23 + { 24 + name: 'Color Warp', 25 + author: 'yozic', 26 + shader: ` 27 + #define WARP true 28 + #define BALLS 10. 29 + #define CONTRAST 3 30 + #define GLOW .1 31 + #define ORB_SIZE 0.492519 32 + #define PI 3.14159265359 33 + 34 + vec2 kale(vec2 uv, vec2 offset, float splits) { 35 + float angle = atan(uv.y, uv.x); 36 + angle = ((angle / PI) + 1.0) * 0.5; 37 + angle = mod(angle, 1.0 / splits) * splits; 38 + angle = -abs(2.0 * angle - 1.0) + 1.0; 39 + float y = length(uv); 40 + angle = angle * (y); 41 + return vec2(angle, y) - offset; 42 + } 43 + 44 + void mainImage (out vec4 fragColor, in vec2 fragCoord) { 45 + vec2 uv = 2. * fragCoord/iResolution.xy - 1.; 46 + uv.x *= iResolution.x / iResolution.y; 47 + uv *= 2.2; 48 + fragColor = vec4(0.); 49 + float dist = distance(uv, vec2(0.)); 50 + uv = WARP ? uv * kale(uv, vec2(0.), 2.) : uv; 51 + for (float i = 0.; i < BALLS; i++) { 52 + float t = iStream/2. - i * PI / BALLS * cos(iStream / max(i, 0.0001)); 53 + vec2 p = vec2(cos(t), sin(t)) / sin(i / BALLS * PI / dist + iStream); 54 + vec3 c = iVolume * cos(vec3(0, 5, -5) * PI * 2. / PI + PI * (iStream / (i+1.) / 5.)) * (GLOW) + (GLOW); 55 + fragColor += vec4(iVolume * vec3(dist * .35 / length(uv - p * ORB_SIZE) * c), 1.0); 56 + } 57 + fragColor.xyz = pow(fragColor.xyz, vec3(CONTRAST)); 58 + } 59 + `, 60 + }, 61 + { 62 + name: 'Bars', 63 + author: 'yozic', 64 + shader: ` 65 + #define orbs 20. 66 + #define PI 3.14159265359 67 + #define zoom 24.950 68 + #define div 10.000 69 + #define div2 10.000 70 + #define radius 8.159 71 + #define orbSize 0.682 72 + #define colorShift 12.710 73 + #define contrast 0.902 74 + 75 + // Helper function to get normalized UV coordinates 76 + vec2 k_uv() { 77 + vec2 uv = 2. * gl_FragCoord.xy/iResolution.xy - 1.; 78 + uv.x *= iResolution.x / iResolution.y; 79 + return uv; 80 + } 24 81 25 - vec2 kale(vec2 uv, vec2 offset, float splits) { 26 - float angle = atan(uv.y, uv.x); 27 - angle = ((angle / PI) + 1.0) * 0.5; 28 - angle = mod(angle, 1.0 / splits) * splits; 29 - angle = -abs(2.0 * angle - 1.0) + 1.0; 30 - float y = length(uv); 31 - angle = angle * (y); 32 - return vec2(angle, y) - offset; 33 - } 82 + // Helper function to create orb effect 83 + vec4 k_orb(vec2 uv, float size, vec2 position, vec3 color, float contrast_val) { 84 + float d = length(uv - position); 85 + float intensity = size / (d + 0.001); 86 + intensity = pow(intensity, contrast_val); 87 + return vec4(color * intensity, 1.0); 88 + } 34 89 35 - void mainImage (out vec4 fragColor, in vec2 fragCoord) { 36 - vec2 uv = 2. * fragCoord/iResolution.xy - 1.; 37 - uv.x *= iResolution.x / iResolution.y; 38 - uv *= 2.2; 39 - fragColor = vec4(0.); 40 - float dist = distance(uv, vec2(0.)); 41 - uv = WARP ? uv * kale(uv, vec2(0.), 2.) : uv; 42 - for (float i = 0.; i < BALLS; i++) { 43 - float t = iStream/2. - i * PI / BALLS * cos(iStream / max(i, 0.0001)); 44 - vec2 p = vec2(cos(t), sin(t)) / sin(i / BALLS * PI / dist + iStream); 45 - vec3 c = iVolume * cos(vec3(0, 5, -5) * PI * 2. / PI + PI * (iStream / (i+1.) / 5.)) * (GLOW) + (GLOW); 46 - fragColor += vec4(iVolume * vec3(dist * .35 / length(uv - p * ORB_SIZE) * c), 1.0); 90 + void mainImage (out vec4 fragColor, in vec2 fragCoord) { 91 + vec2 uv = k_uv(); 92 + uv *= zoom; 93 + fragColor = vec4(0.); 94 + 95 + for (float i = 0.; i < orbs; i++) { 96 + uv.y -= cos((i+1.)*uv.y/div - iStream); 97 + uv.x += cos((i+1.)*uv.y/div2 - iStream/1.5); 98 + float t = i * PI / orbs; 99 + float x = radius * tan(t + iStream/2.); 100 + float y = radius * cos(t - iStream/2.2) * sin(t-iStream/3.); 101 + vec2 position = vec2(x, y); 102 + vec3 color = cos(vec3(-2, 0, -1) * PI * 2. / 3. + PI * (i / colorShift)) * 0.5 + 0.5; 103 + fragColor += k_orb(uv, iVolume * orbSize, position, color, contrast); 104 + } 47 105 } 48 - fragColor.xyz = pow(fragColor.xyz, vec3(CONTRAST)); 49 - } 50 - `, 51 - 'Bars by yozic': ` 52 - #define orbs 20. 53 - #define PI 3.14159265359 54 - #define zoom 24.950 55 - #define div 10.000 56 - #define div2 10.000 57 - #define radius 8.159 58 - #define orbSize 0.682 59 - #define colorShift 12.710 60 - #define contrast 0.902 106 + `, 107 + }, 108 + ] 109 + 110 + const imageShader = ` 111 + void mainImage( out vec4 fragColor, in vec2 fragCoord ) { 112 + vec2 uv = fragCoord.xy / iResolution.xy; 113 + vec4 col = texture(iChannel0, uv); 114 + fragColor = vec4(col.rgb, 1.); 115 + } 116 + ` 117 + 118 + function transitionToShader(newShaderIndex: number) { 119 + if (isTransitioning || currentShaderIndex === newShaderIndex) return 61 120 62 - // Helper function to get normalized UV coordinates 63 - vec2 k_uv() { 64 - vec2 uv = 2. * gl_FragCoord.xy/iResolution.xy - 1.; 65 - uv.x *= iResolution.x / iResolution.y; 66 - return uv; 67 - } 121 + console.log('Starting transition to:', newShaderIndex) 122 + isTransitioning = true 68 123 69 - // Helper function to create orb effect 70 - vec4 k_orb(vec2 uv, float size, vec2 position, vec3 color, float contrast_val) { 71 - float d = length(uv - position); 72 - float intensity = size / (d + 0.001); 73 - intensity = pow(intensity, contrast_val); 74 - return vec4(color * intensity, 1.0); 124 + // Create transition toy 125 + transitionToy = new ShaderToyLite(transition_canvas_id) 126 + transitionToy.setCommon('') 127 + transitionToy.setBufferA({ source: shaders[newShaderIndex].shader }) 128 + transitionToy.setImage({ 129 + source: imageShader, 130 + iChannel0: 'A', 131 + }) 132 + transitionToy.play() 133 + 134 + // Animate transition 135 + let startTime = Date.now() 136 + const duration = 2000 137 + 138 + function animateTransition() { 139 + const elapsed = Date.now() - startTime 140 + const linearProgress = Math.min(elapsed / duration, 1) 141 + const progress = quadInOut(linearProgress) 142 + 143 + // Fade out old canvas, fade in new canvas 144 + if (canvas) { 145 + canvas.style.opacity = (1 - progress).toString() 146 + } 147 + if (transitionCanvas) { 148 + transitionCanvas.style.opacity = progress.toString() 75 149 } 76 150 77 - void mainImage (out vec4 fragColor, in vec2 fragCoord) { 78 - vec2 uv = k_uv(); 79 - uv *= zoom; 80 - fragColor = vec4(0.); 81 - 82 - for (float i = 0.; i < orbs; i++) { 83 - uv.y -= cos((i+1.)*uv.y/div - iStream); 84 - uv.x += cos((i+1.)*uv.y/div2 - iStream/1.5); 85 - float t = i * PI / orbs; 86 - float x = radius * tan(t + iStream/2.); 87 - float y = radius * cos(t - iStream/2.2) * sin(t-iStream/3.); 88 - vec2 position = vec2(x, y); 89 - vec3 color = cos(vec3(-2, 0, -1) * PI * 2. / 3. + PI * (i / colorShift)) * 0.5 + 0.5; 90 - fragColor += k_orb(uv, iVolume * orbSize, position, color, contrast); 151 + if (linearProgress < 1) { 152 + // Use linear progress for timing check 153 + requestAnimationFrame(animateTransition) 154 + } else { 155 + // Transition complete - clean up properly 156 + toy?.pause() 157 + 158 + // Update the main toy with new shader 159 + toy?.setBufferA({ source: shaders[newShaderIndex].shader }) 160 + toy?.play() 161 + 162 + // Clean up transition toy 163 + transitionToy?.pause() 164 + transitionToy = undefined 165 + 166 + currentShaderIndex = newShaderIndex 167 + isTransitioning = false 168 + 169 + // Reset canvas opacities 170 + if (canvas) { 171 + canvas.style.opacity = '1' 172 + } 173 + if (transitionCanvas) { 174 + transitionCanvas.style.opacity = '0' 91 175 } 92 176 } 93 - `, 177 + } 178 + 179 + requestAnimationFrame(animateTransition) 94 180 } 95 181 96 182 onMount(() => { 97 - const a = shaders['Bars by yozic'] 98 - const image = ` 99 - void mainImage( out vec4 fragColor, in vec2 fragCoord ) { 100 - vec2 uv = fragCoord.xy / iResolution.xy; 101 - vec4 col = texture(iChannel0, uv); 102 - fragColor = vec4(col.rgb, 1.); 103 - } 104 - ` 183 + const a = shaders[0].shader 105 184 toy = new ShaderToyLite(canvas_id) 106 185 toy.setCommon('') 107 186 toy.setBufferA({ source: a }) 108 - toy.setImage({ source: image, iChannel0: 'A' }) 187 + toy.setImage({ source: imageShader, iChannel0: 'A' }) 109 188 toy.play() 189 + 110 190 const visualizer = start_visualizer(audioContext, mediaElementSource, (info) => { 111 191 toy?.setStream(info.stream) 112 192 toy?.setVolume(info.volume) 193 + // Also update transition toy if it exists 194 + transitionToy?.setStream(info.stream) 195 + transitionToy?.setVolume(info.volume) 113 196 }) 114 197 115 198 return () => { 116 199 visualizer.destroy() 117 200 toy?.pause() 201 + transitionToy?.pause() 118 202 } 119 203 }) 120 204 ··· 140 224 /> 141 225 142 226 <dialog 143 - class="overflow-hidden" 227 + class="max-h-screen max-w-screen outline-none" 144 228 {@attach (e) => { 145 229 e.showModal() 146 230 }} ··· 148 232 on_close() 149 233 }} 150 234 onkeydown={(e) => { 151 - if (check_modifiers(e, {})) { 235 + if (e.key === 'ArrowLeft') { 236 + transitionToShader((currentShaderIndex - 1 + shaders.length) % shaders.length) 237 + } else if (e.key === 'ArrowRight') { 238 + transitionToShader((currentShaderIndex + 1) % shaders.length) 239 + } else if (check_modifiers(e, {})) { 152 240 show_player_temporarily() 153 241 } 154 242 }} ··· 158 246 bind:this={canvas} 159 247 class="fixed inset-0" 160 248 id={canvas_id} 249 + width={window.innerWidth} 250 + height={window.innerHeight} 251 + onmousemove={show_player_temporarily} 252 + ></canvas> 253 + <!-- Always render transition canvas, but hidden --> 254 + <canvas 255 + bind:this={transitionCanvas} 256 + class="pointer-events-none fixed inset-0 opacity-0 mix-blend-screen" 257 + id={transition_canvas_id} 161 258 width={window.innerWidth} 162 259 height={window.innerHeight} 163 260 onmousemove={show_player_temporarily}