Terminal
1npm install next-transition-router gsap
Canvas-based ripple waves from click point. Multiple concentric rings expand outward like water ripples. Interactive and visually rich with glowing wave effects.
npm install next-transition-router gsapuseState1npm install next-transition-router gsap
1'use client'23import { useRef, useState, useEffect } from 'react'4import { TransitionRouter } from 'next-transition-router'5import gsap from 'gsap'67export default function RippleProvider({ children }: { children: React.ReactNode }) {8 const wrapperRef = useRef<HTMLDivElement>(null)9 const canvasRef = useRef<HTMLCanvasElement>(null)10 const [clickPos, setClickPos] = useState({ x: 50, y: 50 })1112 useEffect(() => {13 const canvas = canvasRef.current14 if (!canvas) return15 const resize = () => {16 canvas.width = window.innerWidth17 canvas.height = window.innerHeight18 }19 resize()20 window.addEventListener('resize', resize)21 return () => window.removeEventListener('resize', resize)22 }, [])2324 useEffect(() => {25 const handleClick = (e: MouseEvent) => {26 const x = (e.clientX / window.innerWidth) * 10027 const y = (e.clientY / window.innerHeight) * 10028 setClickPos({ x, y })29 }30 window.addEventListener('click', handleClick)31 return () => window.removeEventListener('click', handleClick)32 }, [])3334 const drawRipples = (progress: number, centerX: number, centerY: number) => {35 const canvas = canvasRef.current36 const ctx = canvas?.getContext('2d')37 if (!canvas || !ctx) return3839 ctx.clearRect(0, 0, canvas.width, canvas.height)40 if (progress <= 0) return4142 const cx = (centerX / 100) * canvas.width43 const cy = (centerY / 100) * canvas.height44 const maxRadius = Math.sqrt(canvas.width ** 2 + canvas.height ** 2)4546 // Draw multiple concentric ripple rings47 for (let i = 0; i < 5; i++) {48 const ringProgress = Math.max(0, progress - i * 0.12)49 if (ringProgress <= 0) continue50 const radius = ringProgress * maxRadius * 1.251 const alpha = Math.max(0, 0.3 - i * 0.05) * (1 - progress * 0.5)5253 // Ring stroke54 ctx.beginPath()55 ctx.arc(cx, cy, radius, 0, Math.PI * 2)56 ctx.strokeStyle = `rgba(150, 150, 255, ${alpha})`57 ctx.lineWidth = 3 - i * 0.558 ctx.stroke()59 }6061 // Center fill that expands62 const fillRadius = progress * maxRadius63 ctx.beginPath()64 ctx.arc(cx, cy, fillRadius, 0, Math.PI * 2)65 ctx.fillStyle = `rgba(10, 10, 20, ${0.9 * progress})`66 ctx.fill()67 }6869 return (70 <>71 <TransitionRouter72 auto73 leave={(next) => {74 const tl = gsap.timeline({ onComplete: next })75 const anim = { progress: 0 }76 tl.to(anim, {77 progress: 1,78 duration: 0.6,79 ease: 'power2.out',80 onUpdate: () => drawRipples(anim.progress, clickPos.x, clickPos.y),81 })82 }}83 enter={(next) => {84 const tl = gsap.timeline({ onComplete: next })85 const anim = { progress: 1 }86 tl.to(anim, {87 progress: 0,88 duration: 0.5,89 ease: 'power2.in',90 onUpdate: () => drawRipples(anim.progress, clickPos.x, clickPos.y),91 })92 }}93 >94 <div ref={wrapperRef}>{children}</div>95 </TransitionRouter>9697 <canvas ref={canvasRef} style={{98 position: 'fixed', inset: 0,99 zIndex: 9999, pointerEvents: 'none',100 }} />101 </>102 )103}