UI Components Medium
Warp Background
A canvas-based warped grid mesh background with flowing sine-wave distortions that animate over time, creating an organic warped-space effect.
Open in Lab
MCP
css javascript vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
min-height: 100vh;
background: #000;
overflow: hidden;
}
.warp-wrapper {
position: relative;
width: 100%;
height: 100vh;
display: grid;
place-items: center;
background: #0a0a0a;
overflow: hidden;
}
#warp-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
.warp-overlay {
position: absolute;
inset: 0;
background: radial-gradient(
ellipse 50% 50% at 50% 50%,
transparent 20%,
rgba(10, 10, 10, 0.5) 60%,
#0a0a0a 85%
);
pointer-events: none;
}
.warp-content {
position: relative;
z-index: 10;
text-align: center;
color: #f1f5f9;
pointer-events: none;
}
.warp-title {
font-size: clamp(2rem, 5vw, 3.5rem);
font-weight: 800;
letter-spacing: -0.03em;
background: linear-gradient(135deg, #fde68a 0%, #f59e0b 40%, #d946ef 80%, #8b5cf6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
}
.warp-subtitle {
font-size: clamp(0.875rem, 2vw, 1.125rem);
color: rgba(148, 163, 184, 0.8);
font-weight: 400;
}// Warp Background — canvas animation drawing a warped grid mesh with sine-wave distortions
(function () {
"use strict";
const canvas = document.getElementById("warp-canvas");
if (!canvas) return;
const ctx = canvas.getContext("2d");
// Configuration
const GRID_COLS = 40;
const GRID_ROWS = 30;
const SPEED = 0.0008;
const AMPLITUDE_X = 30;
const AMPLITUDE_Y = 25;
const FREQUENCY = 0.06;
// Colors
const COLOR_R = 139;
const COLOR_G = 92;
const COLOR_B = 246;
let dpr = 1;
let width = 0;
let height = 0;
let time = 0;
let animId;
function resize() {
dpr = window.devicePixelRatio || 1;
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = width + "px";
canvas.style.height = height + "px";
}
function getWarpedPoint(col, row, t) {
const baseX = (col / GRID_COLS) * width;
const baseY = (row / GRID_ROWS) * height;
// Layer 1: Primary wave
const dx1 = Math.sin(baseY * FREQUENCY + t * 1.3) * AMPLITUDE_X;
const dy1 = Math.cos(baseX * FREQUENCY + t * 1.1) * AMPLITUDE_Y;
// Layer 2: Secondary smaller wave
const dx2 =
Math.sin(baseX * FREQUENCY * 1.5 + baseY * FREQUENCY * 0.5 + t * 0.7) * AMPLITUDE_X * 0.5;
const dy2 =
Math.cos(baseY * FREQUENCY * 1.3 + baseX * FREQUENCY * 0.4 + t * 0.9) * AMPLITUDE_Y * 0.5;
// Layer 3: Micro turbulence
const dx3 = Math.sin(baseX * FREQUENCY * 3 + t * 2.1) * AMPLITUDE_X * 0.15;
const dy3 = Math.cos(baseY * FREQUENCY * 2.8 + t * 1.8) * AMPLITUDE_Y * 0.15;
return {
x: baseX + dx1 + dx2 + dx3,
y: baseY + dy1 + dy2 + dy3,
displacement: Math.sqrt((dx1 + dx2) * (dx1 + dx2) + (dy1 + dy2) * (dy1 + dy2)),
};
}
function draw(timestamp) {
time = timestamp * SPEED;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, width, height);
// Build the warped grid points
const points = [];
for (let r = 0; r <= GRID_ROWS; r++) {
points[r] = [];
for (let c = 0; c <= GRID_COLS; c++) {
points[r][c] = getWarpedPoint(c, r, time);
}
}
// Draw horizontal lines
for (let r = 0; r <= GRID_ROWS; r++) {
ctx.beginPath();
for (let c = 0; c <= GRID_COLS; c++) {
const pt = points[r][c];
const alpha = 0.04 + (pt.displacement / (AMPLITUDE_X + AMPLITUDE_Y)) * 0.18;
if (c === 0) {
ctx.moveTo(pt.x, pt.y);
} else {
ctx.lineTo(pt.x, pt.y);
}
}
// Use average displacement for line alpha
const avgDisp = points[r].reduce((sum, p) => sum + p.displacement, 0) / points[r].length;
const lineAlpha = 0.03 + (avgDisp / (AMPLITUDE_X + AMPLITUDE_Y)) * 0.15;
ctx.strokeStyle = `rgba(${COLOR_R}, ${COLOR_G}, ${COLOR_B}, ${lineAlpha.toFixed(3)})`;
ctx.lineWidth = 0.8;
ctx.stroke();
}
// Draw vertical lines
for (let c = 0; c <= GRID_COLS; c++) {
ctx.beginPath();
for (let r = 0; r <= GRID_ROWS; r++) {
const pt = points[r][c];
if (r === 0) {
ctx.moveTo(pt.x, pt.y);
} else {
ctx.lineTo(pt.x, pt.y);
}
}
let totalDisp = 0;
for (let r = 0; r <= GRID_ROWS; r++) totalDisp += points[r][c].displacement;
const avgDisp = totalDisp / (GRID_ROWS + 1);
const lineAlpha = 0.03 + (avgDisp / (AMPLITUDE_X + AMPLITUDE_Y)) * 0.15;
ctx.strokeStyle = `rgba(${COLOR_R}, ${COLOR_G}, ${COLOR_B}, ${lineAlpha.toFixed(3)})`;
ctx.lineWidth = 0.8;
ctx.stroke();
}
// Draw glow nodes at intersections with high displacement
for (let r = 0; r <= GRID_ROWS; r += 2) {
for (let c = 0; c <= GRID_COLS; c += 2) {
const pt = points[r][c];
const normalizedDisp = pt.displacement / (AMPLITUDE_X + AMPLITUDE_Y);
if (normalizedDisp > 0.3) {
const dotAlpha = (normalizedDisp - 0.3) * 0.6;
const dotRadius = 1 + normalizedDisp * 2;
ctx.beginPath();
ctx.arc(pt.x, pt.y, dotRadius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${COLOR_R}, ${COLOR_G}, ${COLOR_B}, ${dotAlpha.toFixed(3)})`;
ctx.fill();
// Outer glow
ctx.beginPath();
ctx.arc(pt.x, pt.y, dotRadius * 3, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${COLOR_R}, ${COLOR_G}, ${COLOR_B}, ${(dotAlpha * 0.15).toFixed(3)})`;
ctx.fill();
}
}
}
animId = requestAnimationFrame(draw);
}
resize();
animId = requestAnimationFrame(draw);
let resizeTimer;
window.addEventListener("resize", () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(resize, 150);
});
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Warp Background</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="warp-wrapper">
<canvas id="warp-canvas"></canvas>
<div class="warp-overlay"></div>
<div class="warp-content">
<h1 class="warp-title">Warp Background</h1>
<p class="warp-subtitle">Flowing mesh distortions powered by layered sine waves</p>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useEffect, useRef, useCallback } from "react";
interface WarpBackgroundProps {
gridCols?: number;
gridRows?: number;
speed?: number;
amplitudeX?: number;
amplitudeY?: number;
frequency?: number;
color?: [number, number, number];
className?: string;
}
export function WarpBackground({
gridCols = 40,
gridRows = 30,
speed = 0.0008,
amplitudeX = 30,
amplitudeY = 25,
frequency = 0.06,
color = [139, 92, 246],
className = "",
}: WarpBackgroundProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const animRef = useRef<number>(0);
const sizeRef = useRef({ width: 0, height: 0, dpr: 1 });
const resize = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const parent = canvas.parentElement;
if (!parent) return;
const dpr = window.devicePixelRatio || 1;
const w = parent.clientWidth;
const h = parent.clientHeight;
canvas.width = w * dpr;
canvas.height = h * dpr;
canvas.style.width = w + "px";
canvas.style.height = h + "px";
sizeRef.current = { width: w, height: h, dpr };
}, []);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
resize();
const [cR, cG, cB] = color;
function getWarpedPoint(col: number, row: number, t: number) {
const { width, height } = sizeRef.current;
const baseX = (col / gridCols) * width;
const baseY = (row / gridRows) * height;
const dx1 = Math.sin(baseY * frequency + t * 1.3) * amplitudeX;
const dy1 = Math.cos(baseX * frequency + t * 1.1) * amplitudeY;
const dx2 =
Math.sin(baseX * frequency * 1.5 + baseY * frequency * 0.5 + t * 0.7) * amplitudeX * 0.5;
const dy2 =
Math.cos(baseY * frequency * 1.3 + baseX * frequency * 0.4 + t * 0.9) * amplitudeY * 0.5;
const dx3 = Math.sin(baseX * frequency * 3 + t * 2.1) * amplitudeX * 0.15;
const dy3 = Math.cos(baseY * frequency * 2.8 + t * 1.8) * amplitudeY * 0.15;
return {
x: baseX + dx1 + dx2 + dx3,
y: baseY + dy1 + dy2 + dy3,
displacement: Math.sqrt((dx1 + dx2) ** 2 + (dy1 + dy2) ** 2),
};
}
function draw(timestamp: number) {
const time = timestamp * speed;
const { width, height, dpr } = sizeRef.current;
ctx!.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx!.clearRect(0, 0, width, height);
// Build warped points
const points: { x: number; y: number; displacement: number }[][] = [];
for (let r = 0; r <= gridRows; r++) {
points[r] = [];
for (let c = 0; c <= gridCols; c++) {
points[r][c] = getWarpedPoint(c, r, time);
}
}
const maxAmp = amplitudeX + amplitudeY;
// Horizontal lines
for (let r = 0; r <= gridRows; r++) {
ctx!.beginPath();
for (let c = 0; c <= gridCols; c++) {
const pt = points[r][c];
if (c === 0) ctx!.moveTo(pt.x, pt.y);
else ctx!.lineTo(pt.x, pt.y);
}
const avg = points[r].reduce((s, p) => s + p.displacement, 0) / points[r].length;
const alpha = 0.03 + (avg / maxAmp) * 0.15;
ctx!.strokeStyle = `rgba(${cR}, ${cG}, ${cB}, ${alpha.toFixed(3)})`;
ctx!.lineWidth = 0.8;
ctx!.stroke();
}
// Vertical lines
for (let c = 0; c <= gridCols; c++) {
ctx!.beginPath();
let totalDisp = 0;
for (let r = 0; r <= gridRows; r++) {
const pt = points[r][c];
if (r === 0) ctx!.moveTo(pt.x, pt.y);
else ctx!.lineTo(pt.x, pt.y);
totalDisp += pt.displacement;
}
const avg = totalDisp / (gridRows + 1);
const alpha = 0.03 + (avg / maxAmp) * 0.15;
ctx!.strokeStyle = `rgba(${cR}, ${cG}, ${cB}, ${alpha.toFixed(3)})`;
ctx!.lineWidth = 0.8;
ctx!.stroke();
}
// Glow nodes
for (let r = 0; r <= gridRows; r += 2) {
for (let c = 0; c <= gridCols; c += 2) {
const pt = points[r][c];
const norm = pt.displacement / maxAmp;
if (norm > 0.3) {
const dotAlpha = (norm - 0.3) * 0.6;
const dotRadius = 1 + norm * 2;
ctx!.beginPath();
ctx!.arc(pt.x, pt.y, dotRadius, 0, Math.PI * 2);
ctx!.fillStyle = `rgba(${cR}, ${cG}, ${cB}, ${dotAlpha.toFixed(3)})`;
ctx!.fill();
ctx!.beginPath();
ctx!.arc(pt.x, pt.y, dotRadius * 3, 0, Math.PI * 2);
ctx!.fillStyle = `rgba(${cR}, ${cG}, ${cB}, ${(dotAlpha * 0.15).toFixed(3)})`;
ctx!.fill();
}
}
}
animRef.current = requestAnimationFrame(draw);
}
animRef.current = requestAnimationFrame(draw);
let resizeTimer: ReturnType<typeof setTimeout>;
const handleResize = () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(resize, 150);
};
window.addEventListener("resize", handleResize);
return () => {
cancelAnimationFrame(animRef.current);
clearTimeout(resizeTimer);
window.removeEventListener("resize", handleResize);
};
}, [gridCols, gridRows, speed, amplitudeX, amplitudeY, frequency, color, resize]);
return (
<div className={className} style={{ position: "absolute", inset: 0, overflow: "hidden" }}>
<canvas
ref={canvasRef}
style={{ position: "absolute", inset: 0, width: "100%", height: "100%", display: "block" }}
/>
<div
style={{
position: "absolute",
inset: 0,
background:
"radial-gradient(ellipse 50% 50% at 50% 50%, transparent 20%, rgba(10,10,10,0.5) 60%, #0a0a0a 85%)",
pointerEvents: "none",
}}
/>
</div>
);
}
// Demo usage
export default function WarpBackgroundDemo() {
return (
<div
style={{
width: "100vw",
height: "100vh",
background: "#0a0a0a",
display: "grid",
placeItems: "center",
position: "relative",
}}
>
<WarpBackground
gridCols={40}
gridRows={30}
speed={0.0008}
amplitudeX={30}
amplitudeY={25}
color={[139, 92, 246]}
/>
<div
style={{
position: "relative",
zIndex: 10,
textAlign: "center",
pointerEvents: "none",
}}
>
<h1
style={{
fontSize: "clamp(2rem, 5vw, 3.5rem)",
fontWeight: 800,
letterSpacing: "-0.03em",
background:
"linear-gradient(135deg, #fde68a 0%, #f59e0b 40%, #d946ef 80%, #8b5cf6 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
marginBottom: "0.5rem",
fontFamily: "system-ui, -apple-system, sans-serif",
}}
>
Warp Background
</h1>
<p
style={{
fontSize: "clamp(0.875rem, 2vw, 1.125rem)",
color: "rgba(148, 163, 184, 0.8)",
fontFamily: "system-ui, -apple-system, sans-serif",
}}
>
Flowing mesh distortions powered by layered sine waves
</p>
</div>
</div>
);
}<script setup>
import { ref, onMounted, onUnmounted } from "vue";
const props = defineProps({
gridCols: { type: Number, default: 40 },
gridRows: { type: Number, default: 30 },
speed: { type: Number, default: 0.0008 },
amplitudeX: { type: Number, default: 30 },
amplitudeY: { type: Number, default: 25 },
frequency: { type: Number, default: 0.06 },
color: { type: Array, default: () => [139, 92, 246] },
});
const canvasEl = ref(null);
let animId = 0;
let sizeState = { width: 0, height: 0, dpr: 1 };
let resizeTimer;
function resize() {
const canvas = canvasEl.value;
if (!canvas) return;
const parent = canvas.parentElement;
if (!parent) return;
const dpr = window.devicePixelRatio || 1;
const w = parent.clientWidth;
const h = parent.clientHeight;
canvas.width = w * dpr;
canvas.height = h * dpr;
canvas.style.width = w + "px";
canvas.style.height = h + "px";
sizeState = { width: w, height: h, dpr };
}
function getWarpedPoint(col, row, t) {
const { width, height } = sizeState;
const baseX = (col / props.gridCols) * width;
const baseY = (row / props.gridRows) * height;
const dx1 = Math.sin(baseY * props.frequency + t * 1.3) * props.amplitudeX;
const dy1 = Math.cos(baseX * props.frequency + t * 1.1) * props.amplitudeY;
const dx2 =
Math.sin(baseX * props.frequency * 1.5 + baseY * props.frequency * 0.5 + t * 0.7) *
props.amplitudeX *
0.5;
const dy2 =
Math.cos(baseY * props.frequency * 1.3 + baseX * props.frequency * 0.4 + t * 0.9) *
props.amplitudeY *
0.5;
const dx3 = Math.sin(baseX * props.frequency * 3 + t * 2.1) * props.amplitudeX * 0.15;
const dy3 = Math.cos(baseY * props.frequency * 2.8 + t * 1.8) * props.amplitudeY * 0.15;
return {
x: baseX + dx1 + dx2 + dx3,
y: baseY + dy1 + dy2 + dy3,
displacement: Math.sqrt((dx1 + dx2) ** 2 + (dy1 + dy2) ** 2),
};
}
onMounted(() => {
const canvas = canvasEl.value;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
resize();
const [cR, cG, cB] = props.color;
function draw(timestamp) {
const time = timestamp * props.speed;
const { width, height, dpr } = sizeState;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, width, height);
const points = [];
for (let r = 0; r <= props.gridRows; r++) {
points[r] = [];
for (let c = 0; c <= props.gridCols; c++) {
points[r][c] = getWarpedPoint(c, r, time);
}
}
const maxAmp = props.amplitudeX + props.amplitudeY;
for (let r = 0; r <= props.gridRows; r++) {
ctx.beginPath();
for (let c = 0; c <= props.gridCols; c++) {
const pt = points[r][c];
if (c === 0) ctx.moveTo(pt.x, pt.y);
else ctx.lineTo(pt.x, pt.y);
}
const avg = points[r].reduce((s, p) => s + p.displacement, 0) / points[r].length;
const alpha = 0.03 + (avg / maxAmp) * 0.15;
ctx.strokeStyle = `rgba(${cR}, ${cG}, ${cB}, ${alpha.toFixed(3)})`;
ctx.lineWidth = 0.8;
ctx.stroke();
}
for (let c = 0; c <= props.gridCols; c++) {
ctx.beginPath();
let totalDisp = 0;
for (let r = 0; r <= props.gridRows; r++) {
const pt = points[r][c];
if (r === 0) ctx.moveTo(pt.x, pt.y);
else ctx.lineTo(pt.x, pt.y);
totalDisp += pt.displacement;
}
const avg = totalDisp / (props.gridRows + 1);
const alpha = 0.03 + (avg / maxAmp) * 0.15;
ctx.strokeStyle = `rgba(${cR}, ${cG}, ${cB}, ${alpha.toFixed(3)})`;
ctx.lineWidth = 0.8;
ctx.stroke();
}
for (let r = 0; r <= props.gridRows; r += 2) {
for (let c = 0; c <= props.gridCols; c += 2) {
const pt = points[r][c];
const norm = pt.displacement / maxAmp;
if (norm > 0.3) {
const dotAlpha = (norm - 0.3) * 0.6;
const dotRadius = 1 + norm * 2;
ctx.beginPath();
ctx.arc(pt.x, pt.y, dotRadius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${cR}, ${cG}, ${cB}, ${dotAlpha.toFixed(3)})`;
ctx.fill();
ctx.beginPath();
ctx.arc(pt.x, pt.y, dotRadius * 3, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${cR}, ${cG}, ${cB}, ${(dotAlpha * 0.15).toFixed(3)})`;
ctx.fill();
}
}
}
animId = requestAnimationFrame(draw);
}
animId = requestAnimationFrame(draw);
const handleResize = () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(resize, 150);
};
window.addEventListener("resize", handleResize);
// Store cleanup for onUnmounted
window.__warpCleanup = () => {
cancelAnimationFrame(animId);
clearTimeout(resizeTimer);
window.removeEventListener("resize", handleResize);
};
});
onUnmounted(() => {
if (window.__warpCleanup) {
window.__warpCleanup();
delete window.__warpCleanup;
}
});
</script>
<template>
<div
style="width: 100vw; height: 100vh; background: #0a0a0a; display: grid; place-items: center; position: relative;"
>
<div style="position: relative; width: 100%; height: 100%; overflow: hidden;">
<canvas
ref="canvasEl"
style="position: absolute; inset: 0; width: 100%; height: 100%; display: block;"
/>
<div
style="position: absolute; inset: 0; background: radial-gradient(ellipse 50% 50% at 50% 50%, transparent 20%, rgba(10,10,10,0.5) 60%, #0a0a0a 85%); pointer-events: none;"
/>
</div>
<div
style="position: absolute; z-index: 10; text-align: center; pointer-events: none;"
>
<h1
style="font-size: clamp(2rem, 5vw, 3.5rem); font-weight: 800; letter-spacing: -0.03em; background: linear-gradient(135deg, #fde68a 0%, #f59e0b 40%, #d946ef 80%, #8b5cf6 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; margin-bottom: 0.5rem; font-family: system-ui, -apple-system, sans-serif;"
>
Warp Background
</h1>
<p
style="font-size: clamp(0.875rem, 2vw, 1.125rem); color: rgba(148, 163, 184, 0.8); font-family: system-ui, -apple-system, sans-serif;"
>
Flowing mesh distortions powered by layered sine waves
</p>
</div>
</div>
</template><script>
import { onMount, onDestroy } from "svelte";
export let gridCols = 40;
export let gridRows = 30;
export let speed = 0.0008;
export let amplitudeX = 30;
export let amplitudeY = 25;
export let frequency = 0.06;
export let color = [139, 92, 246];
let canvasEl;
let animId = 0;
let sizeState = { width: 0, height: 0, dpr: 1 };
let resizeTimer;
function resize() {
if (!canvasEl) return;
const parent = canvasEl.parentElement;
if (!parent) return;
const dpr = window.devicePixelRatio || 1;
const w = parent.clientWidth;
const h = parent.clientHeight;
canvasEl.width = w * dpr;
canvasEl.height = h * dpr;
canvasEl.style.width = w + "px";
canvasEl.style.height = h + "px";
sizeState = { width: w, height: h, dpr };
}
function getWarpedPoint(col, row, t) {
const { width, height } = sizeState;
const baseX = (col / gridCols) * width;
const baseY = (row / gridRows) * height;
const dx1 = Math.sin(baseY * frequency + t * 1.3) * amplitudeX;
const dy1 = Math.cos(baseX * frequency + t * 1.1) * amplitudeY;
const dx2 =
Math.sin(baseX * frequency * 1.5 + baseY * frequency * 0.5 + t * 0.7) * amplitudeX * 0.5;
const dy2 =
Math.cos(baseY * frequency * 1.3 + baseX * frequency * 0.4 + t * 0.9) * amplitudeY * 0.5;
const dx3 = Math.sin(baseX * frequency * 3 + t * 2.1) * amplitudeX * 0.15;
const dy3 = Math.cos(baseY * frequency * 2.8 + t * 1.8) * amplitudeY * 0.15;
return {
x: baseX + dx1 + dx2 + dx3,
y: baseY + dy1 + dy2 + dy3,
displacement: Math.sqrt((dx1 + dx2) ** 2 + (dy1 + dy2) ** 2),
};
}
onMount(() => {
const ctx = canvasEl.getContext("2d");
if (!ctx) return;
resize();
const [cR, cG, cB] = color;
function draw(timestamp) {
const time = timestamp * speed;
const { width, height, dpr } = sizeState;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, width, height);
const points = [];
for (let r = 0; r <= gridRows; r++) {
points[r] = [];
for (let c = 0; c <= gridCols; c++) {
points[r][c] = getWarpedPoint(c, r, time);
}
}
const maxAmp = amplitudeX + amplitudeY;
for (let r = 0; r <= gridRows; r++) {
ctx.beginPath();
for (let c = 0; c <= gridCols; c++) {
const pt = points[r][c];
if (c === 0) ctx.moveTo(pt.x, pt.y);
else ctx.lineTo(pt.x, pt.y);
}
const avg = points[r].reduce((s, p) => s + p.displacement, 0) / points[r].length;
const alpha = 0.03 + (avg / maxAmp) * 0.15;
ctx.strokeStyle = `rgba(${cR}, ${cG}, ${cB}, ${alpha.toFixed(3)})`;
ctx.lineWidth = 0.8;
ctx.stroke();
}
for (let c = 0; c <= gridCols; c++) {
ctx.beginPath();
let totalDisp = 0;
for (let r = 0; r <= gridRows; r++) {
const pt = points[r][c];
if (r === 0) ctx.moveTo(pt.x, pt.y);
else ctx.lineTo(pt.x, pt.y);
totalDisp += pt.displacement;
}
const avg = totalDisp / (gridRows + 1);
const alpha = 0.03 + (avg / maxAmp) * 0.15;
ctx.strokeStyle = `rgba(${cR}, ${cG}, ${cB}, ${alpha.toFixed(3)})`;
ctx.lineWidth = 0.8;
ctx.stroke();
}
for (let r = 0; r <= gridRows; r += 2) {
for (let c = 0; c <= gridCols; c += 2) {
const pt = points[r][c];
const norm = pt.displacement / maxAmp;
if (norm > 0.3) {
const dotAlpha = (norm - 0.3) * 0.6;
const dotRadius = 1 + norm * 2;
ctx.beginPath();
ctx.arc(pt.x, pt.y, dotRadius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${cR}, ${cG}, ${cB}, ${dotAlpha.toFixed(3)})`;
ctx.fill();
ctx.beginPath();
ctx.arc(pt.x, pt.y, dotRadius * 3, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${cR}, ${cG}, ${cB}, ${(dotAlpha * 0.15).toFixed(3)})`;
ctx.fill();
}
}
}
animId = requestAnimationFrame(draw);
}
animId = requestAnimationFrame(draw);
const handleResize = () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(resize, 150);
};
window.addEventListener("resize", handleResize);
return () => {
cancelAnimationFrame(animId);
clearTimeout(resizeTimer);
window.removeEventListener("resize", handleResize);
};
});
onDestroy(() => {
cancelAnimationFrame(animId);
clearTimeout(resizeTimer);
});
</script>
<div
style="width: 100vw; height: 100vh; background: #0a0a0a; display: grid; place-items: center; position: relative;"
>
<div style="position: relative; width: 100%; height: 100%; overflow: hidden;">
<canvas
bind:this={canvasEl}
style="position: absolute; inset: 0; width: 100%; height: 100%; display: block;"
/>
<div
style="position: absolute; inset: 0; background: radial-gradient(ellipse 50% 50% at 50% 50%, transparent 20%, rgba(10,10,10,0.5) 60%, #0a0a0a 85%); pointer-events: none;"
/>
</div>
<div
style="position: absolute; z-index: 10; text-align: center; pointer-events: none;"
>
<h1
style="font-size: clamp(2rem, 5vw, 3.5rem); font-weight: 800; letter-spacing: -0.03em; background: linear-gradient(135deg, #fde68a 0%, #f59e0b 40%, #d946ef 80%, #8b5cf6 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; margin-bottom: 0.5rem; font-family: system-ui, -apple-system, sans-serif;"
>
Warp Background
</h1>
<p
style="font-size: clamp(0.875rem, 2vw, 1.125rem); color: rgba(148, 163, 184, 0.8); font-family: system-ui, -apple-system, sans-serif;"
>
Flowing mesh distortions powered by layered sine waves
</p>
</div>
</div>Warp Background
A mesmerizing canvas-based warped mesh that flows and distorts using layered sine-wave transformations. The grid lines bend and ripple organically, creating a living, breathing background.
How it works
- A grid mesh is drawn on an HTML Canvas with configurable rows and columns
- Each vertex is displaced by multiple layered sine/cosine functions with different frequencies and phases
- The phase advances each frame, creating continuous flowing motion
- Line color and opacity are modulated by displacement magnitude for depth
Customization
GRID_COLS/GRID_ROWScontrol mesh densityAMPLITUDEsets how far vertices warp from their originFREQUENCYandSPEEDcontrol wave tightness and animation paceLINE_COLORsets the base color of the mesh lines- Multiple wave layers can be added or removed for complexity
When to use it
- Full-screen animated backgrounds
- Hero sections for creative/tech sites
- Behind-content ambient effects
- Music visualizer or generative art displays