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

Better transitioning

Kasper (Oct 2, 2025, 11:46 PM +0200) 4450a142 d036ea0f

+69 -33
+69 -33
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 + import { quadInOut } from 'svelte/easing' 10 10 11 11 let { on_close }: { on_close: () => void } = $props() 12 12 ··· 18 18 let isTransitioning = $state(false) 19 19 let transitionCanvas: HTMLCanvasElement | undefined = $state() 20 20 const transition_canvas_id = 'visualizer-transition' 21 + let autoTransitionTimeout: ReturnType<typeof setTimeout> | undefined 22 + let pendingTransition: number | undefined 21 23 22 24 const shaders = [ 23 25 { ··· 115 117 } 116 118 ` 117 119 118 - function transitionToShader(newShaderIndex: number) { 119 - if (isTransitioning || currentShaderIndex === newShaderIndex) return 120 + onMount(() => { 121 + const a = shaders[0].shader 122 + toy = new ShaderToyLite(canvas_id) 123 + toy.setCommon('') 124 + toy.setBufferA({ source: a }) 125 + toy.setImage({ source: imageShader, iChannel0: 'A' }) 126 + toy.play() 127 + 128 + const visualizer = start_visualizer(audioContext, mediaElementSource, (info) => { 129 + toy?.setStream(info.stream) 130 + toy?.setVolume(info.volume) 131 + // Also update transition toy if it exists 132 + transitionToy?.setStream(info.stream) 133 + transitionToy?.setVolume(info.volume) 134 + }) 135 + 136 + return () => { 137 + visualizer.destroy() 138 + toy?.pause() 139 + transitionToy?.pause() 140 + clearTimeout(autoTransitionTimeout) 141 + } 142 + }) 143 + 144 + function transitionToShader(newShaderIndex: number, duration: number = 2000) { 145 + if (currentShaderIndex === newShaderIndex) return 146 + 147 + if (isTransitioning) { 148 + pendingTransition = newShaderIndex 149 + return 150 + } 120 151 121 152 console.log('Starting transition to:', newShaderIndex) 122 153 isTransitioning = true 123 154 155 + // Clear any pending auto transition 156 + clearTimeout(autoTransitionTimeout) 157 + 124 158 // Create transition toy 125 159 transitionToy = new ShaderToyLite(transition_canvas_id) 126 160 transitionToy.setCommon('') ··· 133 167 134 168 // Animate transition 135 169 let startTime = Date.now() 136 - const duration = 2000 137 170 138 171 function animateTransition() { 139 172 const elapsed = Date.now() - startTime ··· 149 182 } 150 183 151 184 if (linearProgress < 1) { 152 - // Use linear progress for timing check 153 185 requestAnimationFrame(animateTransition) 154 186 } else { 155 187 // Transition complete - clean up properly ··· 173 205 if (transitionCanvas) { 174 206 transitionCanvas.style.opacity = '0' 175 207 } 208 + 209 + // Schedule next auto transition 210 + scheduleAutoTransition() 211 + 212 + // Handle any pending transition 213 + if (pendingTransition !== undefined) { 214 + const nextIndex = pendingTransition 215 + pendingTransition = undefined 216 + transitionToShader(nextIndex, 500) 217 + } 176 218 } 177 219 } 178 220 179 221 requestAnimationFrame(animateTransition) 180 222 } 181 223 182 - onMount(() => { 183 - const a = shaders[0].shader 184 - toy = new ShaderToyLite(canvas_id) 185 - toy.setCommon('') 186 - toy.setBufferA({ source: a }) 187 - toy.setImage({ source: imageShader, iChannel0: 'A' }) 188 - toy.play() 224 + function scheduleAutoTransition() { 225 + clearTimeout(autoTransitionTimeout) 226 + autoTransitionTimeout = setTimeout(() => { 227 + if (!isTransitioning) { 228 + const nextIndex = (currentShaderIndex + 1) % shaders.length 229 + transitionToShader(nextIndex, 2000) 230 + } 231 + }, 1000) 232 + } 189 233 190 - const visualizer = start_visualizer(audioContext, mediaElementSource, (info) => { 191 - toy?.setStream(info.stream) 192 - toy?.setVolume(info.volume) 193 - // Also update transition toy if it exists 194 - transitionToy?.setStream(info.stream) 195 - transitionToy?.setVolume(info.volume) 196 - }) 197 - 198 - return () => { 199 - visualizer.destroy() 200 - toy?.pause() 201 - transitionToy?.pause() 202 - } 203 - }) 234 + scheduleAutoTransition() 204 235 205 236 let show_player = $state(false) 206 237 let hide_player_timeout: ReturnType<typeof setTimeout> | undefined ··· 232 263 on_close() 233 264 }} 234 265 onkeydown={(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, {})) { 240 - show_player_temporarily() 266 + if (check_modifiers(e, {})) { 267 + if (e.key === 'ArrowLeft') { 268 + transitionToShader((currentShaderIndex - 1 + shaders.length) % shaders.length, 500) 269 + } else if (e.key === 'ArrowRight') { 270 + transitionToShader((currentShaderIndex + 1) % shaders.length, 500) 271 + } else if (/^[0-9]$/.test(e.key)) { 272 + transitionToShader(Math.min(currentShaderIndex + 1, shaders.length - 1), 500) 273 + } else { 274 + show_player_temporarily() 275 + } 241 276 } 242 277 }} 243 278 > ··· 253 288 <!-- Always render transition canvas, but hidden --> 254 289 <canvas 255 290 bind:this={transitionCanvas} 256 - class="pointer-events-none fixed inset-0 opacity-0 mix-blend-screen" 291 + class="pointer-events-none fixed inset-0 mix-blend-screen" 257 292 id={transition_canvas_id} 258 293 width={window.innerWidth} 259 294 height={window.innerHeight} 260 295 onmousemove={show_player_temporarily} 296 + style:opacity="0" 261 297 ></canvas> 262 298 {#if show_player} 263 299 <!-- svelte-ignore a11y_no_static_element_interactions -->