Docs
Canvas Cursor Effect
An interactive React component that adds a dynamic bubble effect, visually tracking cursor movement in real time.
Installation
Install dependencies
npm install framer-motion lucide-react
Run the following command
It will create a new file canvas-cursor-effect.tsx
inside the components/mage-ui/cursor-effects
directory.
mkdir -p components/mage-ui/cursor-effects && touch components/mage-ui/cursor-effects/canvas-cursor-effect.tsx
Paste the code
Open the newly created file and paste the following code:
'use client';
import { useEffect, useRef } from 'react';
const useCanvasCursor = () => {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
// Skip if canvas ref not available
if (!canvasRef.current) return;
// Create a proper type for the context with running property
type ExtendedCanvasContext = CanvasRenderingContext2D & {
running?: boolean;
frame?: number;
};
let ctx: ExtendedCanvasContext | null = null;
let f: any;
let e = 0;
let pos: { x: number; y: number } = { x: 0, y: 0 };
let lines: any[] = [];
const E = {
debug: true,
friction: 0.5,
trails: 20,
size: 50,
dampening: 0.25,
tension: 0.98,
};
// Use a constructor function pattern that works with TypeScript
function Oscillator(this: any, options: any = {}) {
this.phase = options.phase || 0;
this.offset = options.offset || 0;
this.frequency = options.frequency || 0.001;
this.amplitude = options.amplitude || 1;
}
Oscillator.prototype.update = function () {
this.phase += this.frequency;
e = this.offset + Math.sin(this.phase) * this.amplitude;
return e;
};
Oscillator.prototype.value = function () {
return e;
};
function Node(this: any) {
this.x = 0;
this.y = 0;
this.vy = 0;
this.vx = 0;
}
function Line(this: any, options: any = {}) {
this.spring = (options.spring || 0.45) + (0.1 * Math.random() - 0.02);
this.friction = E.friction + (0.01 * Math.random() - 0.002);
this.nodes = [];
for (let i = 0; i < E.size; i++) {
const node = new (Node as any)();
node.x = pos.x;
node.y = pos.y;
this.nodes.push(node);
}
}
Line.prototype.update = function () {
let spring = this.spring;
let node = this.nodes[0];
node.vx += (pos.x - node.x) * spring;
node.vy += (pos.y - node.y) * spring;
for (let i = 0, len = this.nodes.length; i < len; i++) {
node = this.nodes[i];
if (i > 0) {
const prev = this.nodes[i - 1];
node.vx += (prev.x - node.x) * spring;
node.vy += (prev.y - node.y) * spring;
node.vx += prev.vx * E.dampening;
node.vy += prev.vy * E.dampening;
}
node.vx *= this.friction;
node.vy *= this.friction;
node.x += node.vx;
node.y += node.vy;
spring *= E.tension;
}
};
Line.prototype.draw = function () {
if (!ctx) return;
let x = this.nodes[0].x;
let y = this.nodes[0].y;
let curNode, nextNode;
ctx.beginPath();
ctx.moveTo(x, y);
for (let i = 1, len = this.nodes.length - 2; i < len; i++) {
curNode = this.nodes[i];
nextNode = this.nodes[i + 1];
x = 0.5 * (curNode.x + nextNode.x);
y = 0.5 * (curNode.y + nextNode.y);
ctx.quadraticCurveTo(curNode.x, curNode.y, x, y);
}
// Make sure we have at least 2 nodes before trying to draw the last segment
if (this.nodes.length > 2) {
const i = this.nodes.length - 2;
curNode = this.nodes[i];
nextNode = this.nodes[i + 1];
ctx.quadraticCurveTo(curNode.x, curNode.y, nextNode.x, nextNode.y);
}
ctx.stroke();
ctx.closePath();
};
function createLines() {
lines = [];
for (let i = 0; i < E.trails; i++) {
lines.push(new (Line as any)({ spring: 0.4 + (i / E.trails) * 0.025 }));
}
}
function handlePointerMove(e: MouseEvent | TouchEvent) {
if ('touches' in e && e.touches.length > 0) {
pos.x = e.touches[0].pageX;
pos.y = e.touches[0].pageY;
} else if (!('touches' in e)) {
pos.x = e.clientX;
pos.y = e.clientY;
}
// Only prevent default if it's safe to do so (not passive)
if (e.cancelable) {
e.preventDefault();
}
}
function handleTouchStart(e: TouchEvent) {
if (e.touches.length === 1) {
pos.x = e.touches[0].pageX;
pos.y = e.touches[0].pageY;
}
}
function render() {
if (!ctx || ctx.running === false) return;
ctx.globalCompositeOperation = 'source-over';
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.globalCompositeOperation = 'lighter';
try {
const hue = Math.round(f.update());
ctx.strokeStyle = `hsla(${hue},50%,50%,0.2)`;
ctx.lineWidth = 1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
line.update();
line.draw();
}
if (ctx.frame !== undefined) {
ctx.frame++;
}
window.requestAnimationFrame(render);
} catch (error) {
console.error("Error in render loop:", error);
// Attempt to recover
if (ctx) ctx.running = false;
}
}
function resizeCanvas() {
if (!canvasRef.current) return;
canvasRef.current.width = window.innerWidth;
canvasRef.current.height = window.innerHeight;
}
function onMousemove(e: MouseEvent | TouchEvent) {
document.removeEventListener('mousemove', onMousemove);
document.removeEventListener('touchstart', onMousemove);
document.addEventListener('mousemove', handlePointerMove);
document.addEventListener('touchmove', handlePointerMove, { passive: true });
document.addEventListener('touchstart', handleTouchStart);
handlePointerMove(e);
createLines();
render();
}
function initCanvas() {
const canvas = canvasRef.current;
if (!canvas) return;
ctx = canvas.getContext('2d') as ExtendedCanvasContext;
if (!ctx) return;
ctx.running = true;
ctx.frame = 1;
// Create oscillator
const OscillatorConstructor = Oscillator as unknown as new (options: any) => any;
f = new OscillatorConstructor({
phase: Math.random() * 2 * Math.PI,
amplitude: 85,
frequency: 0.0015,
offset: 285,
});
// Set up event listeners
document.addEventListener('mousemove', onMousemove);
document.addEventListener('touchstart', onMousemove);
window.addEventListener('resize', resizeCanvas);
window.addEventListener('focus', handleFocus);
window.addEventListener('blur', handleBlur);
resizeCanvas();
// Initial position
pos = {
x: window.innerWidth / 2,
y: window.innerHeight / 2,
};
// Start right away
createLines();
render();
}
// Separate functions for event listeners to allow proper cleanup
const handleFocus = () => {
if (ctx && ctx.running === false) {
ctx.running = true;
render();
}
};
const handleBlur = () => {
if (ctx) ctx.running = true;
};
// Initialize the canvas
initCanvas();
// Cleanup function
return () => {
if (ctx) ctx.running = false;
document.removeEventListener('mousemove', onMousemove);
document.removeEventListener('touchstart', onMousemove);
document.removeEventListener('mousemove', handlePointerMove);
document.removeEventListener('touchmove', handlePointerMove);
document.removeEventListener('touchstart', handleTouchStart);
window.removeEventListener('resize', resizeCanvas);
window.removeEventListener('focus', handleFocus);
window.removeEventListener('blur', handleBlur);
};
}, []);
return canvasRef;
};
const CanvasCursor = () => {
const canvasRef = useCanvasCursor();
return (
<canvas
ref={canvasRef}
id="canvas"
className=" pointer-events-none inset-0 w-full h-screen z-50"
/>
);
};
export default CanvasCursor;