17Ripple

Ripple

GSAPCanvasInteractiveWave

Canvas-based ripple waves from click point. Multiple concentric rings expand outward like water ripples. Interactive and visually rich with glowing wave effects.

Quick Start

  • 📦npm install next-transition-router gsap
  • 🎯Track click position with useState
  • 🎨Canvas-based ripple waves with concentric rings
  • 🌊Multiple animated rings expand from click point

Installation

Terminal
1npm install next-transition-router gsap

Provider Setup

app/providers.tsx
1'use client'
2
3import { useRef, useState, useEffect } from 'react'
4import { TransitionRouter } from 'next-transition-router'
5import gsap from 'gsap'
6
7export 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 })
11
12 useEffect(() => {
13 const canvas = canvasRef.current
14 if (!canvas) return
15 const resize = () => {
16 canvas.width = window.innerWidth
17 canvas.height = window.innerHeight
18 }
19 resize()
20 window.addEventListener('resize', resize)
21 return () => window.removeEventListener('resize', resize)
22 }, [])
23
24 useEffect(() => {
25 const handleClick = (e: MouseEvent) => {
26 const x = (e.clientX / window.innerWidth) * 100
27 const y = (e.clientY / window.innerHeight) * 100
28 setClickPos({ x, y })
29 }
30 window.addEventListener('click', handleClick)
31 return () => window.removeEventListener('click', handleClick)
32 }, [])
33
34 const drawRipples = (progress: number, centerX: number, centerY: number) => {
35 const canvas = canvasRef.current
36 const ctx = canvas?.getContext('2d')
37 if (!canvas || !ctx) return
38
39 ctx.clearRect(0, 0, canvas.width, canvas.height)
40 if (progress <= 0) return
41
42 const cx = (centerX / 100) * canvas.width
43 const cy = (centerY / 100) * canvas.height
44 const maxRadius = Math.sqrt(canvas.width ** 2 + canvas.height ** 2)
45
46 // Draw multiple concentric ripple rings
47 for (let i = 0; i < 5; i++) {
48 const ringProgress = Math.max(0, progress - i * 0.12)
49 if (ringProgress <= 0) continue
50 const radius = ringProgress * maxRadius * 1.2
51 const alpha = Math.max(0, 0.3 - i * 0.05) * (1 - progress * 0.5)
52
53 // Ring stroke
54 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.5
58 ctx.stroke()
59 }
60
61 // Center fill that expands
62 const fillRadius = progress * maxRadius
63 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 }
68
69 return (
70 <>
71 <TransitionRouter
72 auto
73 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>
96
97 <canvas ref={canvasRef} style={{
98 position: 'fixed', inset: 0,
99 zIndex: 9999, pointerEvents: 'none',
100 }} />
101 </>
102 )
103}