Horizontal Bars
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 HorizontalFlowBarsProps {6backgroundColor?: string;7lineColor?: string;8barColor?: string;9lineWidth?: number;10removeWaveLine?: boolean;11animationSpeed?: number;12}1314// Add a hex to RGB conversion helper function at the top of the component15const hexToRgb = (hex: string): { r: number; g: number; b: number } => {16// Remove # if present17const cleanHex = hex.charAt(0) === '#' ? hex.substring(1) : hex;18// Parse hex values19const r = Number.parseInt(cleanHex.substring(0, 2), 16);20const g = Number.parseInt(cleanHex.substring(2, 4), 16);21const b = Number.parseInt(cleanHex.substring(4, 6), 16);22return { r, g, b };23};2425const HorizontalFlowBars = ({26backgroundColor = '#F0EEE6',27lineColor = '#444',28barColor = '#000000',29lineWidth = 1,30removeWaveLine = true,31animationSpeed = 0.0005,32}: HorizontalFlowBarsProps) => {33const canvasRef = useRef<HTMLCanvasElement>(null);34const timeRef = useRef<number>(0);35const animationFrameId = useRef<number | null>(null);36const mouseRef = useRef({ x: 0, y: 0, isDown: false });37const ripples = useRef<38Array<{ x: number; y: number; time: number; intensity: number }>39>([]);40const dprRef = useRef<number>(1);4142const noise = (x: number, y: number, t: number): number => {43const n =44Math.sin(x * 0.01 + t) * Math.cos(y * 0.01 + t) +45Math.sin(x * 0.015 - t) * Math.cos(y * 0.005 + t);46return (n + 1) / 2;47};4849const getMouseInfluence = (x: number, y: number): number => {50const dx = x - mouseRef.current.x;51const dy = y - mouseRef.current.y;52const distance = Math.sqrt(dx * dx + dy * dy);53const maxDistance = 200;54return Math.max(0, 1 - distance / maxDistance);55};5657const getRippleInfluence = (58x: number,59y: number,60currentTime: number61): number => {62let totalInfluence = 0;63ripples.current.forEach((ripple) => {64const age = currentTime - ripple.time;65const maxAge = 2000;66if (age < maxAge) {67const dx = x - ripple.x;68const dy = y - ripple.y;69const distance = Math.sqrt(dx * dx + dy * dy);70const rippleRadius = (age / maxAge) * 300;71const rippleWidth = 50;72if (Math.abs(distance - rippleRadius) < rippleWidth) {73const rippleStrength = (1 - age / maxAge) * ripple.intensity;74const proximityToRipple =751 - Math.abs(distance - rippleRadius) / rippleWidth;76totalInfluence += rippleStrength * proximityToRipple;77}78}79});80return Math.min(totalInfluence, 2);81};8283const resizeCanvas = useCallback(() => {84const canvas = canvasRef.current;85if (!canvas) return;8687const dpr = window.devicePixelRatio || 1;88dprRef.current = dpr;8990const displayWidth = window.innerWidth;91const displayHeight = window.innerHeight;9293// Set the actual size in memory (scaled up for high DPI)94canvas.width = displayWidth * dpr;95canvas.height = displayHeight * dpr;9697// Scale the canvas back down using CSS98canvas.style.width = displayWidth + 'px';99canvas.style.height = displayHeight + 'px';100101// Scale the drawing context so everything draws at the correct size102const ctx = canvas.getContext('2d');103if (ctx) {104ctx.scale(dpr, dpr);105}106}, []);107108const handleMouseMove = useCallback((e: MouseEvent) => {109const canvas = canvasRef.current;110if (!canvas) return;111112const rect = canvas.getBoundingClientRect();113mouseRef.current.x = e.clientX - rect.left;114mouseRef.current.y = e.clientY - rect.top;115}, []);116117const handleMouseDown = useCallback((e: MouseEvent) => {118mouseRef.current.isDown = true;119const canvas = canvasRef.current;120if (!canvas) return;121122const rect = canvas.getBoundingClientRect();123const x = e.clientX - rect.left;124const y = e.clientY - rect.top;125126ripples.current.push({127x,128y,129time: Date.now(),130intensity: 1.5,131});132133const now = Date.now();134ripples.current = ripples.current.filter(135(ripple) => now - ripple.time < 2000136);137}, []);138139const handleMouseUp = useCallback(() => {140mouseRef.current.isDown = false;141}, []);142143const animate = useCallback(() => {144const canvas = canvasRef.current;145if (!canvas) return;146147const ctx = canvas.getContext('2d');148if (!ctx) return;149150timeRef.current += animationSpeed;151const currentTime = Date.now();152153// Use CSS pixel dimensions for calculations154const canvasWidth = canvas.clientWidth;155const canvasHeight = canvas.clientHeight;156157const numLines = Math.floor(canvasWidth / 15);158const lineSpacing = canvasWidth / numLines;159160ctx.fillStyle = backgroundColor;161ctx.fillRect(0, 0, canvasWidth, canvasHeight);162163for (let i = 0; i < numLines; i++) {164const x = i * lineSpacing + lineSpacing / 2;165166// Draw vertical line with mouse influence167const mouseInfluence = getMouseInfluence(x, canvasHeight / 2);168const lineAlpha = Math.max(0.3, 0.3 + mouseInfluence * 0.7);169170ctx.beginPath();171const lineRgb = hexToRgb(lineColor);172ctx.strokeStyle = `rgba(${lineRgb.r}, ${lineRgb.g}, ${lineRgb.b}, ${lineAlpha})`;173ctx.lineWidth = lineWidth + mouseInfluence * 2;174ctx.moveTo(x, 0);175ctx.lineTo(x, canvasHeight);176ctx.stroke();177178for (let y = 0; y < canvasHeight; y += 8) {179const noiseVal = noise(x, y, timeRef.current);180const mouseInfl = getMouseInfluence(x, y);181const rippleInfl = getRippleInfluence(x, y, currentTime);182const totalInfluence = mouseInfl + rippleInfl;183184const threshold = Math.max(1850.2,1860.5 - mouseInfl * 0.2 - Math.abs(rippleInfl) * 0.1187);188189if (noiseVal > threshold) {190const barWidth = 2 + noiseVal * 3 + totalInfluence * 3;191const barHeight = 3 + noiseVal * 10 + totalInfluence * 5;192193const baseAnimation =194Math.sin(timeRef.current + x * 0.0375) * 20 * noiseVal;195const mouseAnimation = mouseRef.current.isDown196? Math.sin(timeRef.current * 3 + y * 0.01) * 10 * mouseInfl197: 0;198const rippleAnimation =199rippleInfl * Math.sin(timeRef.current * 2 + y * 0.02) * 15;200201const animatedY =202y + baseAnimation + mouseAnimation + rippleAnimation;203204// Color intensity based on influence205const intensity = Math.min(2061,207Math.max(0.7, 0.7 + totalInfluence * 0.3)208);209const barRgb = hexToRgb(barColor);210ctx.fillStyle = `rgba(${barRgb.r}, ${barRgb.g}, ${barRgb.b}, ${intensity})`;211212ctx.fillRect(213x - barWidth / 2,214animatedY - barHeight / 2,215barWidth,216barHeight217);218}219}220}221222// Draw ripple effects223if (!removeWaveLine) {224ripples.current.forEach((ripple) => {225const age = currentTime - ripple.time;226const maxAge = 2000;227if (age < maxAge) {228const progress = age / maxAge;229const radius = progress * 300;230const alpha = (1 - progress) * 0.3 * ripple.intensity;231232ctx.beginPath();233ctx.strokeStyle = `rgba(100, 100, 100, ${alpha})`;234ctx.lineWidth = 2;235ctx.arc(ripple.x, ripple.y, radius, 0, 2 * Math.PI);236ctx.stroke();237}238});239}240241animationFrameId.current = requestAnimationFrame(animate);242}, [243backgroundColor,244lineColor,245removeWaveLine,246barColor,247lineWidth,248animationSpeed,249]);250251useEffect(() => {252const canvas = canvasRef.current;253if (!canvas) return;254255resizeCanvas();256257const handleResize = () => resizeCanvas();258259window.addEventListener('resize', handleResize);260canvas.addEventListener('mousemove', handleMouseMove);261canvas.addEventListener('mousedown', handleMouseDown);262canvas.addEventListener('mouseup', handleMouseUp);263264animate();265266return () => {267window.removeEventListener('resize', handleResize);268canvas.removeEventListener('mousemove', handleMouseMove);269canvas.removeEventListener('mousedown', handleMouseDown);270canvas.removeEventListener('mouseup', handleMouseUp);271272if (animationFrameId.current) {273cancelAnimationFrame(animationFrameId.current);274animationFrameId.current = null;275}276timeRef.current = 0;277ripples.current = [];278};279}, [animate, resizeCanvas, handleMouseMove, handleMouseDown, handleMouseUp]);280281return (282<div283className='absolute inset-0 w-full h-full overflow-hidden'284style={{ backgroundColor }}285>286<canvas ref={canvasRef} className='block w-full h-full' />287</div>288);289};290291export default HorizontalFlowBars;292
Prop | Type | Default | Description |
---|---|---|---|
backgroundColor | string | '#F0EEE6' | Background color of the canvas. |
lineColor | string | '#444' | Color of the vertical lines. |
barColor | string | '#000000' | Color of the animated bars. |
lineWidth | number | 1 | Width of the vertical lines. |
animationSpeed | number | 0.0005 | Speed of the bar animation. |
removeWaveLine | boolean | true | Whether to remove the animated wave line (if true, the wave is not shown). |