Smooth Wavy Canvas
An interactive React component that adds a dynamic bubble effect, visually tracking cursor movement in real time.
An interactive React component that adds a dynamic bubble effect, visually tracking cursor movement in real time.
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. |