Magnetic Fluid Grid
🔵 InteractiveA highly responsive minimalist geometric grid that warps, bends, and reacts under the user's cursor using custom spring-mass physics and high-performance distance calculation.
Responsive Horizontal Leaderboard
Slot ID: ca-pub-detail-below-preview
Interactive Settings
Medium Rectangle Block
Slot ID: ca-pub-detail-after-customization
Quick Copy Actions
Download File Packages
Keyboard Shortcuts
Responsive Horizontal Leaderboard
Slot ID: ca-pub-detail-before-related
More in Interactive & Cursor-Driven
Loading Canvas Shader...
Semantic Warp Lens
A gorgeous, high-fidelity monospaced text grid that warps, rotates, and inverts colors like an optical lens centered directly on the user's cursor.
Loading Canvas Shader...
Ethereal Smoke Trails
An ultra-responsive interactive canvas trail that renders smooth, glowing fluid-like ribbons accompanied by drifting and slowly dissolving micro-smoke particles.
Loading Canvas Shader...
Kinetic Lissajous Matrix
A mathematically precise, symmetrical geometric mandala that seamlessly morphs and folds into itself in response to your cursor coordinates.
Magnetic Fluid Grid - HTML5 Canvas & JavaScript Interactive & Cursor-Driven background for React & Tailwind
Integrate the Magnetic Fluid Grid directly into your website. This asset is rendered using HTML5 Canvas 2D render loop. It is optimized for zero layout-shifts and runs with high-performance hardware-accelerated processing.
⚡ Performance Specifications
- Render Mode: INTERACTIVE (HTML5 Canvas & JavaScript)
- Fluidity: Locked at 60fps dynamic loop
- Bundle Footprint: Zero external NPM dependencies
- SEO Indexing status: 100% Crawlable static semantic HTML
🛠️ Integration Capabilities
Our templates expose inline design tokens like --color-1 and --color-2 for infinite color palettes. This template is designed to fit inside hero elements, full-screen landing pages, and interactive presentation cards.
Technical Code Reference & Syntaxes for Googlebot & Crawlers
The snippets below display the direct, unminified source code utilized for rendering this background.
HTML & Inline CSS Snippet (Vanilla)
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Magnetic Fluid Grid</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #09090b;
}
.magnetic-grid-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #08090c;
overflow: hidden;
}
#canvas-magnetic-fluid-grid {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
display: block;
}
</style>
</head>
<body>
<div class="magnetic-grid-container">
<canvas id="canvas-magnetic-fluid-grid"></canvas>
</div>
<script>
(function() {
const canvas = document.getElementById('canvas-magnetic-fluid-grid');
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
let cols = 0;
let rows = 0;
let nodes = [];
const spacing = 42;
const springK = 0.045;
const friction = 0.83;
const repulsionRadius = 160;
const repulsionStrength = 4.2;
let mouseX = -1000;
let mouseY = -1000;
let targetMouseX = -1000;
let targetMouseY = -1000;
function initNodes() {
const width = canvas.width;
const height = canvas.height;
cols = Math.ceil(width / spacing) + 1;
rows = Math.ceil(height / spacing) + 1;
nodes = [];
const offsetX = (width - (cols - 1) * spacing) / 2;
const offsetY = (height - (rows - 1) * spacing) / 2;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const ox = offsetX + c * spacing;
const oy = offsetY + r * spacing;
nodes.push({
x: ox,
y: oy,
ox: ox,
oy: oy,
vx: 0,
vy: 0,
disp: 0,
col: c,
row: r
});
}
}
}
function resize() {
const rect = canvas.parentNode ? canvas.parentNode.getBoundingClientRect() : null;
const width = rect ? rect.width : window.innerWidth;
const height = rect ? rect.height : window.innerHeight;
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
initNodes();
}
}
function onMouseMove(e) {
const rect = canvas.getBoundingClientRect();
targetMouseX = e.clientX - rect.left;
targetMouseY = e.clientY - rect.top;
}
function onMouseLeave() {
targetMouseX = -1000;
targetMouseY = -1000;
}
function onTouchMove(e) {
if (e.touches.length > 0) {
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
targetMouseX = touch.clientX - rect.left;
targetMouseY = touch.clientY - rect.top;
}
}
function onTouchEnd() {
targetMouseX = -1000;
targetMouseY = -1000;
}
window.addEventListener('resize', resize);
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseleave', onMouseLeave);
window.addEventListener('touchmove', onTouchMove, { passive: true });
window.addEventListener('touchstart', onTouchMove, { passive: true });
window.addEventListener('touchend', onTouchEnd);
window.addEventListener('touchcancel', onTouchEnd);
let observer;
if (canvas.parentNode) {
observer = new MutationObserver(resize);
observer.observe(canvas.parentNode, { attributes: true });
}
resize();
function cleanup() {
window.removeEventListener('resize', resize);
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseleave', onMouseLeave);
window.removeEventListener('touchmove', onTouchMove);
window.removeEventListener('touchstart', onTouchMove);
window.removeEventListener('touchend', onTouchEnd);
window.removeEventListener('touchcancel', onTouchEnd);
if (observer) {
observer.disconnect();
}
}
function update() {
if (targetMouseX === -1000) {
mouseX = -1000;
mouseY = -1000;
} else {
if (mouseX === -1000) {
mouseX = targetMouseX;
mouseY = targetMouseY;
} else {
mouseX += (targetMouseX - mouseX) * 0.16;
mouseY += (targetMouseY - mouseY) * 0.16;
}
}
const rSq = repulsionRadius * repulsionRadius;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const ax = (node.ox - node.x) * springK;
const ay = (node.oy - node.y) * springK;
node.vx += ax;
node.vy += ay;
if (mouseX !== -1000) {
const dx = node.x - mouseX;
const dy = node.y - mouseY;
const distSq = dx * dx + dy * dy;
if (distSq < rSq) {
const dist = Math.sqrt(distSq) || 0.001;
const force = (repulsionRadius - dist) / repulsionRadius;
const push = force * force * force * repulsionStrength;
node.vx += (dx / dist) * push;
node.vy += (dy / dist) * push;
}
}
node.vx *= friction;
node.vy *= friction;
node.x += node.vx;
node.y += node.vy;
node.disp = Math.hypot(node.x - node.ox, node.y - node.oy);
}
}
function draw() {
ctx.fillStyle = '#08090b';
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (mouseX !== -1000) {
ctx.beginPath();
const spotlight = ctx.createRadialGradient(mouseX, mouseY, 0, mouseX, mouseY, repulsionRadius * 1.8);
spotlight.addColorStop(0, 'rgba(14, 165, 233, 0.08)');
spotlight.addColorStop(0.5, 'rgba(99, 102, 241, 0.02)');
spotlight.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = spotlight;
ctx.arc(mouseX, mouseY, repulsionRadius * 1.8, 0, Math.PI * 2);
ctx.fill();
}
const inactiveLinesPath = new Path2D();
const activeLines = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const idx = r * cols + c;
const node = nodes[idx];
if (c < cols - 1) {
const rightNode = nodes[idx + 1];
if (node.disp < 0.25 && rightNode.disp < 0.25) {
inactiveLinesPath.moveTo(node.ox, node.oy);
inactiveLinesPath.lineTo(rightNode.ox, rightNode.oy);
} else {
activeLines.push([node, rightNode]);
}
}
if (r < rows - 1) {
const bottomNode = nodes[idx + cols];
if (node.disp < 0.25 && bottomNode.disp < 0.25) {
inactiveLinesPath.moveTo(node.ox, node.oy);
inactiveLinesPath.lineTo(bottomNode.ox, bottomNode.oy);
} else {
activeLines.push([node, bottomNode]);
}
}
}
}
ctx.strokeStyle = 'rgba(71, 85, 105, 0.09)';
ctx.lineWidth = 1;
ctx.stroke(inactiveLinesPath);
for (let i = 0; i < activeLines.length; i++) {
const [n1, n2] = activeLines[i];
const avgDisp = (n1.disp + n2.disp) / 2;
const t = Math.min(avgDisp / 48, 1.0);
ctx.beginPath();
ctx.moveTo(n1.x, n1.y);
ctx.lineTo(n2.x, n2.y);
const r_val = Math.floor(71 + t * (14 - 71));
const g_val = Math.floor(85 + t * (165 - 85));
const b_val = Math.floor(105 + t * (233 - 105));
const alpha = 0.09 + t * 0.42;
ctx.strokeStyle = "rgba(" + r_val + ", " + g_val + ", " + b_val + ", " + alpha + ")";
ctx.lineWidth = 1 + t * 1.5;
ctx.stroke();
}
const inactiveNodesPath = new Path2D();
const activeNodes = [];
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node.disp < 0.25) {
inactiveNodesPath.rect(node.ox - 1, node.oy - 1, 2, 2);
} else {
activeNodes.push(node);
}
}
ctx.fillStyle = 'rgba(100, 116, 139, 0.15)';
ctx.fill(inactiveNodesPath);
for (let i = 0; i < activeNodes.length; i++) {
const node = activeNodes[i];
const t = Math.min(node.disp / 48, 1.0);
ctx.beginPath();
ctx.arc(node.x, node.y, 1.5 + t * 2, 0, Math.PI * 2);
ctx.fillStyle = "rgba(56, 189, 248, " + (0.25 + t * 0.75) + ")";
ctx.fill();
if (t > 0.35) {
ctx.beginPath();
ctx.arc(node.x, node.y, 3 + t * 6, 0, Math.PI * 2);
ctx.strokeStyle = "rgba(56, 189, 248, " + ((t - 0.35) * 0.22) + ")";
ctx.lineWidth = 1;
ctx.stroke();
}
}
}
function render() {
if (!canvas.isConnected) {
cleanup();
return;
}
update();
draw();
requestAnimationFrame(render);
}
requestAnimationFrame(render);
})();
</script>
<script>
// Throttled interactive cursor coordinates mapping
(function() {
document.documentElement.style.setProperty('--mouse-x', '0');
document.documentElement.style.setProperty('--mouse-y', '0');
let targetX = 0, targetY = 0;
let currentX = 0, currentY = 0;
window.addEventListener('mousemove', (e) => {
targetX = (e.clientX / window.innerWidth) - 0.5;
targetY = (e.clientY / window.innerHeight) - 0.5;
});
window.addEventListener('mouseleave', () => {
targetX = 0;
targetY = 0;
});
function update() {
currentX += (targetX - currentX) * 0.08;
currentY += (targetY - currentY) * 0.08;
document.documentElement.style.setProperty('--mouse-x', currentX.toFixed(4));
document.documentElement.style.setProperty('--mouse-y', currentY.toFixed(4));
requestAnimationFrame(update);
}
requestAnimationFrame(update);
})();
</script>
</body>
</html>React Component Wrapper (TSX)
import React, { useEffect, useRef } from 'react';
export default function MagneticFluidGridBackground() {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let targetX = 0, targetY = 0;
let currentX = 0, currentY = 0;
let frameId: number;
const handleMouseMove = (e: MouseEvent) => {
targetX = (e.clientX / window.innerWidth) - 0.5;
targetY = (e.clientY / window.innerHeight) - 0.5;
};
const handleMouseLeave = () => {
targetX = 0;
targetY = 0;
};
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseleave', handleMouseLeave);
const updateCoordinates = () => {
currentX += (targetX - currentX) * 0.08;
currentY += (targetY - currentY) * 0.08;
if (containerRef.current) {
containerRef.current.style.setProperty('--mouse-x', currentX.toFixed(4));
containerRef.current.style.setProperty('--mouse-y', currentY.toFixed(4));
}
frameId = requestAnimationFrame(updateCoordinates);
};
frameId = requestAnimationFrame(updateCoordinates);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseleave', handleMouseLeave);
cancelAnimationFrame(frameId);
};
}, []);
return (
<div
ref={containerRef}
style={{ width: '100%', height: '100%', position: 'relative', overflow: 'hidden' }}
>
<style dangerouslySetInnerHTML={{ __html: `
.magnetic-grid-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #08090c;
overflow: hidden;
}
#canvas-magnetic-fluid-grid {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
display: block;
}
` }} />
{/* HTML Structure */}
<div
style={{ width: '100%', height: '100%' }}
dangerouslySetInnerHTML={{ __html: `
<div class="magnetic-grid-container">
<canvas id="canvas-magnetic-fluid-grid"></canvas>
</div>
<script>
(function() {
const canvas = document.getElementById('canvas-magnetic-fluid-grid');
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
let cols = 0;
let rows = 0;
let nodes = [];
const spacing = 42;
const springK = 0.045;
const friction = 0.83;
const repulsionRadius = 160;
const repulsionStrength = 4.2;
let mouseX = -1000;
let mouseY = -1000;
let targetMouseX = -1000;
let targetMouseY = -1000;
function initNodes() {
const width = canvas.width;
const height = canvas.height;
cols = Math.ceil(width / spacing) + 1;
rows = Math.ceil(height / spacing) + 1;
nodes = [];
const offsetX = (width - (cols - 1) * spacing) / 2;
const offsetY = (height - (rows - 1) * spacing) / 2;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const ox = offsetX + c * spacing;
const oy = offsetY + r * spacing;
nodes.push({
x: ox,
y: oy,
ox: ox,
oy: oy,
vx: 0,
vy: 0,
disp: 0,
col: c,
row: r
});
}
}
}
function resize() {
const rect = canvas.parentNode ? canvas.parentNode.getBoundingClientRect() : null;
const width = rect ? rect.width : window.innerWidth;
const height = rect ? rect.height : window.innerHeight;
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
initNodes();
}
}
function onMouseMove(e) {
const rect = canvas.getBoundingClientRect();
targetMouseX = e.clientX - rect.left;
targetMouseY = e.clientY - rect.top;
}
function onMouseLeave() {
targetMouseX = -1000;
targetMouseY = -1000;
}
function onTouchMove(e) {
if (e.touches.length > 0) {
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
targetMouseX = touch.clientX - rect.left;
targetMouseY = touch.clientY - rect.top;
}
}
function onTouchEnd() {
targetMouseX = -1000;
targetMouseY = -1000;
}
window.addEventListener('resize', resize);
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseleave', onMouseLeave);
window.addEventListener('touchmove', onTouchMove, { passive: true });
window.addEventListener('touchstart', onTouchMove, { passive: true });
window.addEventListener('touchend', onTouchEnd);
window.addEventListener('touchcancel', onTouchEnd);
let observer;
if (canvas.parentNode) {
observer = new MutationObserver(resize);
observer.observe(canvas.parentNode, { attributes: true });
}
resize();
function cleanup() {
window.removeEventListener('resize', resize);
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseleave', onMouseLeave);
window.removeEventListener('touchmove', onTouchMove);
window.removeEventListener('touchstart', onTouchMove);
window.removeEventListener('touchend', onTouchEnd);
window.removeEventListener('touchcancel', onTouchEnd);
if (observer) {
observer.disconnect();
}
}
function update() {
if (targetMouseX === -1000) {
mouseX = -1000;
mouseY = -1000;
} else {
if (mouseX === -1000) {
mouseX = targetMouseX;
mouseY = targetMouseY;
} else {
mouseX += (targetMouseX - mouseX) * 0.16;
mouseY += (targetMouseY - mouseY) * 0.16;
}
}
const rSq = repulsionRadius * repulsionRadius;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const ax = (node.ox - node.x) * springK;
const ay = (node.oy - node.y) * springK;
node.vx += ax;
node.vy += ay;
if (mouseX !== -1000) {
const dx = node.x - mouseX;
const dy = node.y - mouseY;
const distSq = dx * dx + dy * dy;
if (distSq < rSq) {
const dist = Math.sqrt(distSq) || 0.001;
const force = (repulsionRadius - dist) / repulsionRadius;
const push = force * force * force * repulsionStrength;
node.vx += (dx / dist) * push;
node.vy += (dy / dist) * push;
}
}
node.vx *= friction;
node.vy *= friction;
node.x += node.vx;
node.y += node.vy;
node.disp = Math.hypot(node.x - node.ox, node.y - node.oy);
}
}
function draw() {
ctx.fillStyle = '#08090b';
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (mouseX !== -1000) {
ctx.beginPath();
const spotlight = ctx.createRadialGradient(mouseX, mouseY, 0, mouseX, mouseY, repulsionRadius * 1.8);
spotlight.addColorStop(0, 'rgba(14, 165, 233, 0.08)');
spotlight.addColorStop(0.5, 'rgba(99, 102, 241, 0.02)');
spotlight.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = spotlight;
ctx.arc(mouseX, mouseY, repulsionRadius * 1.8, 0, Math.PI * 2);
ctx.fill();
}
const inactiveLinesPath = new Path2D();
const activeLines = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const idx = r * cols + c;
const node = nodes[idx];
if (c < cols - 1) {
const rightNode = nodes[idx + 1];
if (node.disp < 0.25 && rightNode.disp < 0.25) {
inactiveLinesPath.moveTo(node.ox, node.oy);
inactiveLinesPath.lineTo(rightNode.ox, rightNode.oy);
} else {
activeLines.push([node, rightNode]);
}
}
if (r < rows - 1) {
const bottomNode = nodes[idx + cols];
if (node.disp < 0.25 && bottomNode.disp < 0.25) {
inactiveLinesPath.moveTo(node.ox, node.oy);
inactiveLinesPath.lineTo(bottomNode.ox, bottomNode.oy);
} else {
activeLines.push([node, bottomNode]);
}
}
}
}
ctx.strokeStyle = 'rgba(71, 85, 105, 0.09)';
ctx.lineWidth = 1;
ctx.stroke(inactiveLinesPath);
for (let i = 0; i < activeLines.length; i++) {
const [n1, n2] = activeLines[i];
const avgDisp = (n1.disp + n2.disp) / 2;
const t = Math.min(avgDisp / 48, 1.0);
ctx.beginPath();
ctx.moveTo(n1.x, n1.y);
ctx.lineTo(n2.x, n2.y);
const r_val = Math.floor(71 + t * (14 - 71));
const g_val = Math.floor(85 + t * (165 - 85));
const b_val = Math.floor(105 + t * (233 - 105));
const alpha = 0.09 + t * 0.42;
ctx.strokeStyle = "rgba(" + r_val + ", " + g_val + ", " + b_val + ", " + alpha + ")";
ctx.lineWidth = 1 + t * 1.5;
ctx.stroke();
}
const inactiveNodesPath = new Path2D();
const activeNodes = [];
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node.disp < 0.25) {
inactiveNodesPath.rect(node.ox - 1, node.oy - 1, 2, 2);
} else {
activeNodes.push(node);
}
}
ctx.fillStyle = 'rgba(100, 116, 139, 0.15)';
ctx.fill(inactiveNodesPath);
for (let i = 0; i < activeNodes.length; i++) {
const node = activeNodes[i];
const t = Math.min(node.disp / 48, 1.0);
ctx.beginPath();
ctx.arc(node.x, node.y, 1.5 + t * 2, 0, Math.PI * 2);
ctx.fillStyle = "rgba(56, 189, 248, " + (0.25 + t * 0.75) + ")";
ctx.fill();
if (t > 0.35) {
ctx.beginPath();
ctx.arc(node.x, node.y, 3 + t * 6, 0, Math.PI * 2);
ctx.strokeStyle = "rgba(56, 189, 248, " + ((t - 0.35) * 0.22) + ")";
ctx.lineWidth = 1;
ctx.stroke();
}
}
}
function render() {
if (!canvas.isConnected) {
cleanup();
return;
}
update();
draw();
requestAnimationFrame(render);
}
requestAnimationFrame(render);
})();
</script>
` }}
/>
</div>
);
}Next.js App Router Component (use client)
'use client';
import React, { useEffect, useRef } from 'react';
export default function MagneticFluidGridBackground() {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let targetX = 0, targetY = 0;
let currentX = 0, currentY = 0;
let frameId: number;
const handleMouseMove = (e: MouseEvent) => {
targetX = (e.clientX / window.innerWidth) - 0.5;
targetY = (e.clientY / window.innerHeight) - 0.5;
};
const handleMouseLeave = () => {
targetX = 0;
targetY = 0;
};
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseleave', handleMouseLeave);
const updateCoordinates = () => {
currentX += (targetX - currentX) * 0.08;
currentY += (targetY - currentY) * 0.08;
if (containerRef.current) {
containerRef.current.style.setProperty('--mouse-x', currentX.toFixed(4));
containerRef.current.style.setProperty('--mouse-y', currentY.toFixed(4));
}
frameId = requestAnimationFrame(updateCoordinates);
};
frameId = requestAnimationFrame(updateCoordinates);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseleave', handleMouseLeave);
cancelAnimationFrame(frameId);
};
}, []);
return (
<div
ref={containerRef}
className="w-full h-full relative overflow-hidden"
>
<style dangerouslySetInnerHTML={{ __html: `
.magnetic-grid-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #08090c;
overflow: hidden;
}
#canvas-magnetic-fluid-grid {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
display: block;
}
` }} />
{/* HTML Structure */}
<div
className="w-full h-full"
dangerouslySetInnerHTML={{ __html: `
<div class="magnetic-grid-container">
<canvas id="canvas-magnetic-fluid-grid"></canvas>
</div>
<script>
(function() {
const canvas = document.getElementById('canvas-magnetic-fluid-grid');
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
let cols = 0;
let rows = 0;
let nodes = [];
const spacing = 42;
const springK = 0.045;
const friction = 0.83;
const repulsionRadius = 160;
const repulsionStrength = 4.2;
let mouseX = -1000;
let mouseY = -1000;
let targetMouseX = -1000;
let targetMouseY = -1000;
function initNodes() {
const width = canvas.width;
const height = canvas.height;
cols = Math.ceil(width / spacing) + 1;
rows = Math.ceil(height / spacing) + 1;
nodes = [];
const offsetX = (width - (cols - 1) * spacing) / 2;
const offsetY = (height - (rows - 1) * spacing) / 2;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const ox = offsetX + c * spacing;
const oy = offsetY + r * spacing;
nodes.push({
x: ox,
y: oy,
ox: ox,
oy: oy,
vx: 0,
vy: 0,
disp: 0,
col: c,
row: r
});
}
}
}
function resize() {
const rect = canvas.parentNode ? canvas.parentNode.getBoundingClientRect() : null;
const width = rect ? rect.width : window.innerWidth;
const height = rect ? rect.height : window.innerHeight;
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
initNodes();
}
}
function onMouseMove(e) {
const rect = canvas.getBoundingClientRect();
targetMouseX = e.clientX - rect.left;
targetMouseY = e.clientY - rect.top;
}
function onMouseLeave() {
targetMouseX = -1000;
targetMouseY = -1000;
}
function onTouchMove(e) {
if (e.touches.length > 0) {
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
targetMouseX = touch.clientX - rect.left;
targetMouseY = touch.clientY - rect.top;
}
}
function onTouchEnd() {
targetMouseX = -1000;
targetMouseY = -1000;
}
window.addEventListener('resize', resize);
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseleave', onMouseLeave);
window.addEventListener('touchmove', onTouchMove, { passive: true });
window.addEventListener('touchstart', onTouchMove, { passive: true });
window.addEventListener('touchend', onTouchEnd);
window.addEventListener('touchcancel', onTouchEnd);
let observer;
if (canvas.parentNode) {
observer = new MutationObserver(resize);
observer.observe(canvas.parentNode, { attributes: true });
}
resize();
function cleanup() {
window.removeEventListener('resize', resize);
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseleave', onMouseLeave);
window.removeEventListener('touchmove', onTouchMove);
window.removeEventListener('touchstart', onTouchMove);
window.removeEventListener('touchend', onTouchEnd);
window.removeEventListener('touchcancel', onTouchEnd);
if (observer) {
observer.disconnect();
}
}
function update() {
if (targetMouseX === -1000) {
mouseX = -1000;
mouseY = -1000;
} else {
if (mouseX === -1000) {
mouseX = targetMouseX;
mouseY = targetMouseY;
} else {
mouseX += (targetMouseX - mouseX) * 0.16;
mouseY += (targetMouseY - mouseY) * 0.16;
}
}
const rSq = repulsionRadius * repulsionRadius;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const ax = (node.ox - node.x) * springK;
const ay = (node.oy - node.y) * springK;
node.vx += ax;
node.vy += ay;
if (mouseX !== -1000) {
const dx = node.x - mouseX;
const dy = node.y - mouseY;
const distSq = dx * dx + dy * dy;
if (distSq < rSq) {
const dist = Math.sqrt(distSq) || 0.001;
const force = (repulsionRadius - dist) / repulsionRadius;
const push = force * force * force * repulsionStrength;
node.vx += (dx / dist) * push;
node.vy += (dy / dist) * push;
}
}
node.vx *= friction;
node.vy *= friction;
node.x += node.vx;
node.y += node.vy;
node.disp = Math.hypot(node.x - node.ox, node.y - node.oy);
}
}
function draw() {
ctx.fillStyle = '#08090b';
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (mouseX !== -1000) {
ctx.beginPath();
const spotlight = ctx.createRadialGradient(mouseX, mouseY, 0, mouseX, mouseY, repulsionRadius * 1.8);
spotlight.addColorStop(0, 'rgba(14, 165, 233, 0.08)');
spotlight.addColorStop(0.5, 'rgba(99, 102, 241, 0.02)');
spotlight.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = spotlight;
ctx.arc(mouseX, mouseY, repulsionRadius * 1.8, 0, Math.PI * 2);
ctx.fill();
}
const inactiveLinesPath = new Path2D();
const activeLines = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const idx = r * cols + c;
const node = nodes[idx];
if (c < cols - 1) {
const rightNode = nodes[idx + 1];
if (node.disp < 0.25 && rightNode.disp < 0.25) {
inactiveLinesPath.moveTo(node.ox, node.oy);
inactiveLinesPath.lineTo(rightNode.ox, rightNode.oy);
} else {
activeLines.push([node, rightNode]);
}
}
if (r < rows - 1) {
const bottomNode = nodes[idx + cols];
if (node.disp < 0.25 && bottomNode.disp < 0.25) {
inactiveLinesPath.moveTo(node.ox, node.oy);
inactiveLinesPath.lineTo(bottomNode.ox, bottomNode.oy);
} else {
activeLines.push([node, bottomNode]);
}
}
}
}
ctx.strokeStyle = 'rgba(71, 85, 105, 0.09)';
ctx.lineWidth = 1;
ctx.stroke(inactiveLinesPath);
for (let i = 0; i < activeLines.length; i++) {
const [n1, n2] = activeLines[i];
const avgDisp = (n1.disp + n2.disp) / 2;
const t = Math.min(avgDisp / 48, 1.0);
ctx.beginPath();
ctx.moveTo(n1.x, n1.y);
ctx.lineTo(n2.x, n2.y);
const r_val = Math.floor(71 + t * (14 - 71));
const g_val = Math.floor(85 + t * (165 - 85));
const b_val = Math.floor(105 + t * (233 - 105));
const alpha = 0.09 + t * 0.42;
ctx.strokeStyle = "rgba(" + r_val + ", " + g_val + ", " + b_val + ", " + alpha + ")";
ctx.lineWidth = 1 + t * 1.5;
ctx.stroke();
}
const inactiveNodesPath = new Path2D();
const activeNodes = [];
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node.disp < 0.25) {
inactiveNodesPath.rect(node.ox - 1, node.oy - 1, 2, 2);
} else {
activeNodes.push(node);
}
}
ctx.fillStyle = 'rgba(100, 116, 139, 0.15)';
ctx.fill(inactiveNodesPath);
for (let i = 0; i < activeNodes.length; i++) {
const node = activeNodes[i];
const t = Math.min(node.disp / 48, 1.0);
ctx.beginPath();
ctx.arc(node.x, node.y, 1.5 + t * 2, 0, Math.PI * 2);
ctx.fillStyle = "rgba(56, 189, 248, " + (0.25 + t * 0.75) + ")";
ctx.fill();
if (t > 0.35) {
ctx.beginPath();
ctx.arc(node.x, node.y, 3 + t * 6, 0, Math.PI * 2);
ctx.strokeStyle = "rgba(56, 189, 248, " + ((t - 0.35) * 0.22) + ")";
ctx.lineWidth = 1;
ctx.stroke();
}
}
}
function render() {
if (!canvas.isConnected) {
cleanup();
return;
}
update();
draw();
requestAnimationFrame(render);
}
requestAnimationFrame(render);
})();
</script>
` }}
/>
</div>
);
}Raw CSS Stylesheet Snippet
.magnetic-grid-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #08090c;
overflow: hidden;
}
#canvas-magnetic-fluid-grid {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
display: block;
}