Flowing Ribbons Effect
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 FlowingRibbonsProps {6backgroundColor?: string;7lineColor?: string;8animationSpeed?: number;9removeWaveLine?: boolean;10}1112const FlowingRibbons = ({13backgroundColor = '#F0EEE6',14lineColor = '#777777',15animationSpeed = 0.3,16removeWaveLine = true,17}: FlowingRibbonsProps) => {18const canvasRef = useRef<HTMLCanvasElement>(null);19const timeRef = useRef<number>(0);20const animationFrameId = useRef<number | null>(null);21const mouseRef = useRef({ x: 0, y: 0, isDown: false });22const waveDisturbances = useRef<23Array<{ x: number; y: number; time: number; intensity: number }>24>([]);25const dprRef = useRef<number>(1);2627const getMouseInfluence = (x: number, y: number): number => {28const dx = x - mouseRef.current.x;29const dy = y - mouseRef.current.y;30const distance = Math.sqrt(dx * dx + dy * dy);31const maxDistance = 200;32return Math.max(0, 1 - distance / maxDistance);33};3435const getWaveDisturbance = (36x: number,37y: number,38currentTime: number39): number => {40let totalDisturbance = 0;4142waveDisturbances.current.forEach((disturbance) => {43const age = currentTime - disturbance.time;44const maxAge = 3000;45if (age < maxAge) {46const dx = x - disturbance.x;47const dy = y - disturbance.y;48const distance = Math.sqrt(dx * dx + dy * dy);49const waveRadius = (age / maxAge) * 400;50const waveWidth = 80;51if (Math.abs(distance - waveRadius) < waveWidth) {52const waveStrength = (1 - age / maxAge) * disturbance.intensity;53const proximityToWave =541 - Math.abs(distance - waveRadius) / waveWidth;55totalDisturbance +=56waveStrength *57proximityToWave *58Math.sin((distance - waveRadius) * 0.1);59}60}61});6263return totalDisturbance;64};6566const deform = (67x: number,68y: number,69t: number,70progress: number71): { offsetX: number; offsetY: number } => {72const mouseInfluence = getMouseInfluence(x, y);73const disturbance = getWaveDisturbance(x, y, Date.now());7475const wave1 = Math.sin(progress * Math.PI * 4 + t * 0.01) * 30;76const wave2 = Math.sin(progress * Math.PI * 7 - t * 0.008) * 15;77const harmonic = Math.sin(x * 0.02 + y * 0.015 + t * 0.005) * 10;7879const mouseWave =80mouseInfluence * Math.sin(t * 0.02 + progress * Math.PI * 2) * 20;81const disturbanceWave =82disturbance * Math.sin(t * 0.015 + progress * Math.PI * 3) * 25;8384return {85offsetX: wave1 + harmonic + mouseWave + disturbanceWave,86offsetY: wave2 + mouseWave * 0.5 + disturbanceWave * 0.7,87};88};8990const resizeCanvas = useCallback(() => {91const canvas = canvasRef.current;92if (!canvas) return;9394const dpr = window.devicePixelRatio || 1;95dprRef.current = dpr;9697const rect = canvas.parentElement?.getBoundingClientRect();98const displayWidth = rect?.width || window.innerWidth;99const displayHeight = rect?.height || window.innerHeight;100101canvas.width = displayWidth * dpr;102canvas.height = displayHeight * dpr;103104canvas.style.width = `${displayWidth}px`;105canvas.style.height = `${displayHeight}px`;106107const ctx = canvas.getContext('2d');108if (ctx) {109ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transform110ctx.scale(dpr, dpr);111}112}, []);113114const handleMouseMove = useCallback((e: MouseEvent) => {115const canvas = canvasRef.current;116if (!canvas) return;117118const rect = canvas.getBoundingClientRect();119mouseRef.current.x = e.clientX - rect.left;120mouseRef.current.y = e.clientY - rect.top;121}, []);122123const handleMouseDown = useCallback((e: MouseEvent) => {124mouseRef.current.isDown = true;125const canvas = canvasRef.current;126if (!canvas) return;127128const rect = canvas.getBoundingClientRect();129const x = e.clientX - rect.left;130const y = e.clientY - rect.top;131132waveDisturbances.current.push({133x,134y,135time: Date.now(),136intensity: 2,137});138139const now = Date.now();140waveDisturbances.current = waveDisturbances.current.filter(141(disturbance) => now - disturbance.time < 3000142);143}, []);144145const handleMouseUp = useCallback(() => {146mouseRef.current.isDown = false;147}, []);148149const animate = useCallback(() => {150const canvas = canvasRef.current;151if (!canvas) return;152153const ctx = canvas.getContext('2d');154if (!ctx) return;155156const currentTime = Date.now();157timeRef.current += animationSpeed;158159// Use CSS pixel dimensions for calculations160const width = canvas.clientWidth;161const height = canvas.clientHeight;162163const gridDensity = 80;164const ribbonWidth = width * 0.85;165const ribbonOffset = (width - ribbonWidth) / 2;166167ctx.fillStyle = backgroundColor;168ctx.fillRect(0, 0, width, height);169170ctx.strokeStyle = lineColor;171ctx.lineWidth = 0.5;172173// Draw vertical lines174for (let i = 0; i < gridDensity; i++) {175const x = ribbonOffset + (i / gridDensity) * ribbonWidth;176177ctx.beginPath();178for (let j = 0; j <= gridDensity; j++) {179const progress = (j / gridDensity) * 1.2 - 0.1;180const y = progress * height;181182const { offsetX, offsetY } = deform(x, y, timeRef.current, progress);183184const finalX = x + offsetX;185const finalY = y + offsetY;186187if (j === 0) {188ctx.moveTo(finalX, finalY);189} else {190ctx.lineTo(finalX, finalY);191}192}193ctx.stroke();194}195196// Draw horizontal lines197for (let j = 0; j < gridDensity; j++) {198const progress = (j / gridDensity) * 1.2 - 0.1;199const y = progress * height;200201ctx.beginPath();202for (let i = 0; i <= gridDensity; i++) {203const x = ribbonOffset + (i / gridDensity) * ribbonWidth;204205const { offsetX, offsetY } = deform(x, y, timeRef.current, progress);206207const finalX = x + offsetX;208const finalY = y + offsetY;209210if (i === 0) {211ctx.moveTo(finalX, finalY);212} else {213ctx.lineTo(finalX, finalY);214}215}216ctx.stroke();217}218219// Draw wave disturbance effects220if (!removeWaveLine) {221waveDisturbances.current.forEach((disturbance) => {222const age = currentTime - disturbance.time;223const maxAge = 3000;224if (age < maxAge) {225const progress = age / maxAge;226const radius = progress * 400;227const alpha = (1 - progress) * 0.2 * disturbance.intensity;228229ctx.beginPath();230ctx.strokeStyle = `rgba(100, 100, 100, ${alpha})`;231ctx.lineWidth = 2;232ctx.arc(disturbance.x, disturbance.y, radius, 0, 2 * Math.PI);233ctx.stroke();234235ctx.strokeStyle = lineColor;236ctx.lineWidth = 0.5;237}238});239}240241animationFrameId.current = requestAnimationFrame(animate);242}, [removeWaveLine, backgroundColor, lineColor, animationSpeed]);243244useEffect(() => {245const canvas = canvasRef.current;246if (!canvas) return;247248resizeCanvas();249250const handleResize = () => resizeCanvas();251252window.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 (animationFrameId.current) {266cancelAnimationFrame(animationFrameId.current);267animationFrameId.current = null;268}269timeRef.current = 0;270waveDisturbances.current = [];271};272}, [animate, resizeCanvas, handleMouseMove, handleMouseDown, handleMouseUp]);273274return (275<div276className='absolute inset-0 w-full h-full overflow-hidden'277style={{ backgroundColor }}278>279<canvas ref={canvasRef} className='block w-full h-full' />280</div>281);282};283284export default FlowingRibbons;285
Prop | Type | Default | Description |
---|---|---|---|
backgroundColor | string | '#F0EEE6' | Background color of the canvas. |
lineColor | string | '#777777' | Color of the flowing ribbon lines. |
animationSpeed | number | 0.3 | Speed of the ribbon flow animation. |
removeWaveLine | boolean | true | Whether to remove the animated wave line (if true, the wave is not shown). |