Smooth Wavy Canvas
An interactive React component that creates beautiful flowing wave animations on canvas with customizable colors and smooth transitions.
An interactive React component that creates beautiful flowing wave animations on canvas with customizable colors and smooth transitions.
1"use client"23import { useEffect, useRef, useCallback } from "react"45interface SmoothWavyCanvasProps {6backgroundColor?: string7primaryColor?: string8secondaryColor?: string9accentColor?: string10lineOpacity?: number11animationSpeed?: number12}1314const SmoothWavyCanvas = ({15backgroundColor = "#F8F6F0",16primaryColor = "45, 45, 45",17secondaryColor = "80, 80, 80",18accentColor = "120, 120, 120",19lineOpacity = 1,20animationSpeed = 0.004,21}: SmoothWavyCanvasProps) => {22const canvasRef = useRef<HTMLCanvasElement>(null)23const requestIdRef = useRef<number | null>(null)24const timeRef = useRef<number>(0)25const mouseRef = useRef({ x: 0, y: 0, isDown: false })26const energyFields = useRef<Array<{ x: number; y: number; time: number; intensity: number }>>([])2728const getMouseInfluence = (x: number, y: number): number => {29const dx = x - mouseRef.current.x30const dy = y - mouseRef.current.y31const distance = Math.sqrt(dx * dx + dy * dy)32const maxDistance = 20033return Math.max(0, 1 - distance / maxDistance)34}3536const getEnergyFieldInfluence = (37x: number,38y: number,39currentTime: number,40): { intensity: number; direction: number } => {41let totalIntensity = 042let totalDirectionX = 043let totalDirectionY = 04445energyFields.current.forEach((field) => {46const age = currentTime - field.time47const maxAge = 40004849if (age < maxAge) {50const dx = x - field.x51const dy = y - field.y52const distance = Math.sqrt(dx * dx + dy * dy)53const fieldRadius = (age / maxAge) * 30054const fieldWidth = 1005556if (Math.abs(distance - fieldRadius) < fieldWidth) {57const fieldStrength = (1 - age / maxAge) * field.intensity58const proximityToField = 1 - Math.abs(distance - fieldRadius) / fieldWidth59const influence = fieldStrength * proximityToField * 0.6 // Reduced intensity6061totalIntensity += influence62if (distance > 0) {63totalDirectionX += (dx / distance) * influence64totalDirectionY += (dy / distance) * influence65}66}67}68})6970const direction = Math.atan2(totalDirectionY, totalDirectionX)71return { intensity: Math.min(totalIntensity, 1), direction } // Capped at 1 instead of 272}7374const resizeCanvas = useCallback(() => {75const canvas = canvasRef.current76if (!canvas) return77canvas.width = window.innerWidth78canvas.height = window.innerHeight79}, [])8081const handleMouseMove = useCallback((e: MouseEvent) => {82const canvas = canvasRef.current83if (!canvas) return8485const rect = canvas.getBoundingClientRect()86mouseRef.current.x = e.clientX - rect.left87mouseRef.current.y = e.clientY - rect.top88}, [])8990const handleMouseDown = useCallback((e: MouseEvent) => {91mouseRef.current.isDown = true92// Removed click effects - no more energy fields created93}, [])9495const handleMouseUp = useCallback(() => {96mouseRef.current.isDown = false97}, [])9899const animate = useCallback(() => {100const canvas = canvasRef.current101if (!canvas) return102103const ctx = canvas.getContext("2d")104if (!ctx) return105106const currentTime = Date.now()107timeRef.current += animationSpeed108109const width = canvas.width110const height = canvas.height111112// Clear with clean background113ctx.fillStyle = backgroundColor114ctx.fillRect(0, 0, width, height)115116// Primary horizontal flowing lines117const numPrimaryLines = 35118119for (let i = 0; i < numPrimaryLines; i++) {120const yPos = (i / numPrimaryLines) * height121const mouseInfl = getMouseInfluence(width / 2, yPos)122const { intensity: fieldIntensity, direction: fieldDirection } = getEnergyFieldInfluence(123width / 2,124yPos,125currentTime,126)127128const amplitude = 45 + 25 * Math.sin(timeRef.current * 0.25 + i * 0.15) + mouseInfl * 25129const frequency = 0.006 + 0.002 * Math.sin(timeRef.current * 0.12 + i * 0.08) + mouseInfl * 0.001130const speed = timeRef.current * (0.6 + 0.3 * Math.sin(i * 0.12)) + mouseInfl * timeRef.current * 0.3131const thickness = 0.6 + 0.4 * Math.sin(timeRef.current + i * 0.25) + mouseInfl * 0.8132const opacity =133(0.12 + 0.08 * Math.abs(Math.sin(timeRef.current * 0.3 + i * 0.18)) + mouseInfl * 0.15) *134lineOpacity135136ctx.beginPath()137ctx.lineWidth = thickness138ctx.strokeStyle = `rgba(${primaryColor}, ${opacity})`139140for (let x = 0; x < width; x += 2) {141const localMouseInfl = getMouseInfluence(x, yPos)142143const y =144yPos +145amplitude * Math.sin(x * frequency + speed) +146localMouseInfl * Math.sin(timeRef.current * 2 + x * 0.008) * 15147148if (x === 0) {149ctx.moveTo(x, y)150} else {151ctx.lineTo(x, y)152}153}154155ctx.stroke()156}157158// Secondary vertical flowing lines159const numSecondaryLines = 25160161for (let i = 0; i < numSecondaryLines; i++) {162const xPos = (i / numSecondaryLines) * width163const mouseInfl = getMouseInfluence(xPos, height / 2)164const { intensity: fieldIntensity, direction: fieldDirection } = getEnergyFieldInfluence(165xPos,166height / 2,167currentTime,168)169170const amplitude = 40 + 20 * Math.sin(timeRef.current * 0.18 + i * 0.14) + mouseInfl * 20171const frequency = 0.007 + 0.003 * Math.cos(timeRef.current * 0.14 + i * 0.09) + mouseInfl * 0.002172const speed = timeRef.current * (0.5 + 0.25 * Math.cos(i * 0.16)) + mouseInfl * timeRef.current * 0.25173const thickness = 0.5 + 0.3 * Math.sin(timeRef.current + i * 0.35) + mouseInfl * 0.7174const opacity =175(0.1 + 0.06 * Math.abs(Math.sin(timeRef.current * 0.28 + i * 0.2)) + mouseInfl * 0.12) *176lineOpacity177178ctx.beginPath()179ctx.lineWidth = thickness180ctx.strokeStyle = `rgba(${secondaryColor}, ${opacity})`181182for (let y = 0; y < height; y += 2) {183const localMouseInfl = getMouseInfluence(xPos, y)184185const x =186xPos +187amplitude * Math.sin(y * frequency + speed) +188localMouseInfl * Math.sin(timeRef.current * 2 + y * 0.008) * 12189190if (y === 0) {191ctx.moveTo(x, y)192} else {193ctx.lineTo(x, y)194}195}196197ctx.stroke()198}199200// Accent diagonal flowing lines201const numAccentLines = 15202203for (let i = 0; i < numAccentLines; i++) {204const offset = (i / numAccentLines) * width * 1.5 - width * 0.25205const amplitude = 30 + 15 * Math.cos(timeRef.current * 0.22 + i * 0.12)206const frequency = 0.01 + 0.004 * Math.sin(timeRef.current * 0.16 + i * 0.1)207const phase = timeRef.current * (0.4 + 0.2 * Math.sin(i * 0.13))208const thickness = 0.4 + 0.25 * Math.sin(timeRef.current + i * 0.28)209const opacity = (0.06 + 0.04 * Math.abs(Math.sin(timeRef.current * 0.24 + i * 0.15))) * lineOpacity210211ctx.beginPath()212ctx.lineWidth = thickness213ctx.strokeStyle = `rgba(${accentColor}, ${opacity})`214215const steps = 100216for (let j = 0; j <= steps; j++) {217const progress = j / steps218const baseX = offset + progress * width219const baseY = progress * height + amplitude * Math.sin(progress * 6 + phase)220221const mouseInfl = getMouseInfluence(baseX, baseY)222223const x =224baseX +225mouseInfl * Math.sin(timeRef.current * 1.5 + progress * 6) * 8226const y =227baseY +228mouseInfl * Math.cos(timeRef.current * 1.5 + progress * 6) * 8229230if (j === 0) {231ctx.moveTo(x, y)232} else {233ctx.lineTo(x, y)234}235}236237ctx.stroke()238}239240// No energy field effects - removed completely241242requestIdRef.current = requestAnimationFrame(animate)243}, [backgroundColor, primaryColor, secondaryColor, accentColor, lineOpacity, animationSpeed])244245useEffect(() => {246const canvas = canvasRef.current247if (!canvas) return248249resizeCanvas()250251const handleResize = () => resizeCanvas()252window.addEventListener("resize", handleResize)253canvas.addEventListener("mousemove", handleMouseMove)254canvas.addEventListener("mousedown", handleMouseDown)255canvas.addEventListener("mouseup", handleMouseUp)256257animate()258259return () => {260window.removeEventListener("resize", handleResize)261canvas.removeEventListener("mousemove", handleMouseMove)262canvas.removeEventListener("mousedown", handleMouseDown)263canvas.removeEventListener("mouseup", handleMouseUp)264265if (requestIdRef.current) {266cancelAnimationFrame(requestIdRef.current)267requestIdRef.current = null268}269270timeRef.current = 0271energyFields.current = []272}273}, [animate, resizeCanvas, handleMouseMove, handleMouseDown, handleMouseUp])274275return (276<div className="absolute inset-0 w-full h-full overflow-hidden" style={{ backgroundColor }}>277<canvas ref={canvasRef} className="block w-full h-full" />278</div>279)280}281282export default SmoothWavyCanvas
| Prop | Type | Default | Description |
|---|---|---|---|
backgroundColor | string | '#F8F6F0' | Background color of the canvas. |
primaryColor | string | '45, 45, 45' | RGB value for the primary wavy line. |
secondaryColor | string | '80, 80, 80' | RGB value for the secondary wavy line. |
accentColor | string | '120, 120, 120' | RGB value for the accent wavy line. |
lineOpacity | number | 1 | Opacity level for all wavy lines. |
animationSpeed | number | 0.004 | Speed of the wave animation. |