diff --git a/src/App.vue b/src/App.vue index a77f038..1000ea5 100644 --- a/src/App.vue +++ b/src/App.vue @@ -112,14 +112,83 @@ function switchView(view: 'editor' | 'manager' | 'presets') { currentView.value = view } -function cycleBackground() { - const currentIndex = bgModes.indexOf(currentBgMode.value) - const nextIndex = (currentIndex + 1) % bgModes.length - const nextMode = bgModes[nextIndex] - if (nextMode) { - currentBgMode.value = nextMode - localStorage.setItem('bg.mode', currentBgMode.value) +function cycleBackground(event?: MouseEvent) { + const isAppearanceTransition = 'startViewTransition' in document + && !window.matchMedia('(prefers-reduced-motion: reduce)').matches + && event instanceof MouseEvent + + const updateState = () => { + const currentIndex = bgModes.indexOf(currentBgMode.value) + const nextIndex = (currentIndex + 1) % bgModes.length + const nextMode = bgModes[nextIndex] + if (nextMode) { + currentBgMode.value = nextMode + localStorage.setItem('bg.mode', currentBgMode.value) + } } + + if (!isAppearanceTransition) { + updateState() + return + } + + const x = event.clientX + const y = event.clientY + + const transition = document.startViewTransition(async () => { + updateState() + await nextTick() + }) + + transition.ready.then(() => { + const effects = ['circle', 'vertical', 'horizontal', 'diamond'] + const effect = effects[Math.floor(Math.random() * effects.length)] + + let clipPath: string[] = [] + + if (effect === 'circle') { + const endRadius = Math.hypot( + Math.max(x, innerWidth - x), + Math.max(y, innerHeight - y) + ) + clipPath = [ + `circle(0px at ${x}px ${y}px)`, + `circle(${endRadius}px at ${x}px ${y}px)` + ] + } else if (effect === 'vertical') { + clipPath = [ + 'inset(0 0 100% 0)', + 'inset(0 0 0 0)' + ] + } else if (effect === 'horizontal') { + clipPath = [ + 'inset(0 100% 0 0)', + 'inset(0 0 0 0)' + ] + } else if (effect === 'diamond') { + const endRadius = Math.hypot( + Math.max(x, innerWidth - x), + Math.max(y, innerHeight - y) + ) * 1.5 // Multiply to ensure coverage for diamond shape + + clipPath = [ + `polygon(${x}px ${y}px, ${x}px ${y}px, ${x}px ${y}px, ${x}px ${y}px)`, + `polygon(${x}px ${y - endRadius}px, ${x + endRadius}px ${y}px, ${x}px ${y + endRadius}px, ${x - endRadius}px ${y}px)` + ] + } + + document.documentElement.animate( + { + clipPath: clipPath, + }, + { + duration: 1500, + easing: 'cubic-bezier(0.4, 0, 0.2, 1)', + pseudoElement: '::view-transition-new(root)', + fill: 'forwards', + } + ) + }) } const bgModeLabel = computed(() => { diff --git a/src/components/Background/BackgroundCanvas.vue b/src/components/Background/BackgroundCanvas.vue index a881982..31d649c 100644 --- a/src/components/Background/BackgroundCanvas.vue +++ b/src/components/Background/BackgroundCanvas.vue @@ -168,6 +168,7 @@ onMounted(() => { }) window.addEventListener('mousemove', onMouseMove, { passive: true }) document.addEventListener('visibilitychange', onVisibilityChange) + draw() // Initial draw to ensure no flicker raf = requestAnimationFrame(loop) }) diff --git a/src/components/Background/GradientBackground.vue b/src/components/Background/GradientBackground.vue index 571679b..9309165 100644 --- a/src/components/Background/GradientBackground.vue +++ b/src/components/Background/GradientBackground.vue @@ -185,6 +185,7 @@ onMounted(() => { initMesh() window.addEventListener('resize', onResize) window.addEventListener('mousemove', onMouseMove) + draw() // Initial draw raf = requestAnimationFrame(loop) }) diff --git a/src/components/Background/GridBackground.vue b/src/components/Background/GridBackground.vue index 4ca434a..108898c 100644 --- a/src/components/Background/GridBackground.vue +++ b/src/components/Background/GridBackground.vue @@ -132,6 +132,7 @@ onMounted(() => { resize() window.addEventListener('resize', resize) window.addEventListener('mousemove', onMouseMove) + draw() // Initial draw raf = requestAnimationFrame(loop) })