03Gradient Mesh Blobs Drifting at Different Depth Layers
Several blurred gradient blobs are drawn on top of each other with different movement periods, so the near layer is larger and faster while the far layer stays small and slow.
Best forSoft background for a SaaS landing hero
blobPositionAtMESH_DRIFT_PERIOD_BASE_MSMESH_LAYER_COUNT

<main class="stage">
<div class="d3 is-demo" id="d3Root">
<canvas class="d3__canvas" id="d3Canvas" width="480" height="300"></canvas>
<p class="d3__caption">Several blurred gradient blobs are drawn on top of each other with different movement periods, so the near layer is larger and faster while the far layer stays small and slow</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
@include stage($stage-orange);
.d3 {
position: relative;
width: min(92vw, calc(160vh - 12.8vw));
aspect-ratio: 480 / 300;
margin: 0 auto;
border-radius: $r-card;
overflow: hidden;
box-shadow: $shadow-raised;
background: $stage-orange;
}
.d3__canvas {
display: block;
width: 100%;
height: 100%;
}
.d3__caption {
position: absolute;
left: $sp-3;
bottom: $sp-3;
max-width: 78%;
margin: 0;
padding: $sp-1 $sp-3;
border-radius: $r-pill;
background: rgba(255, 255, 255, .85);
color: $subject-ink;
font-weight: 500;
font-size: 12px;
line-height: 1.4;
pointer-events: none;
}
@media (max-width: 340px) {
.d3__caption { font-size: 10px; padding: $sp-1 $sp-2; }
}
@media (prefers-reduced-motion: reduce) {
.d3 { cursor: default; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.d3 {
position: relative;
width: min(92vw, 160vh - 12.8vw);
aspect-ratio: 480/300;
margin: 0 auto;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
background: #ff4d1f;
}
.d3__canvas {
display: block;
width: 100%;
height: 100%;
}
.d3__caption {
position: absolute;
left: 12px;
bottom: 12px;
max-width: 78%;
margin: 0;
padding: 4px 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.85);
color: #17141a;
font-weight: 500;
font-size: 12px;
line-height: 1.4;
pointer-events: none;
}
@media (max-width: 340px) {
.d3__caption {
font-size: 10px;
padding: 4px 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.d3 {
cursor: default;
}
}(function () {
// 기법: meshLayers 배열의 층마다 이동 주기(period)·반경·이동 폭을 다르게 줘 Lissajous 곡선으로
// 흐르게 한다 — 가까운 층(인덱스 0)은 MESH_DRIFT_PERIOD_BASE_MS 그대로 짧게·크게·빠르게, 먼 층일수록
// 주기를 늘리고 반경·이동 폭을 줄여 작고 느리게 흐르는 것처럼 보이게 한다.
var root = document.getElementById('d3Root');
var canvas = document.getElementById('d3Canvas');
var ctx = canvas.getContext('2d');
var wrap = canvas.parentElement;
var CALC_W = 160, CALC_H = 100;
var MESH_DRIFT_PERIOD_BASE_MS = 3200;
var MESH_LAYER_COUNT = 5;
function seedRand(seed) {
var s = seed >>> 0;
return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
var rnd = seedRand(429031);
var palette = ['#ff9ebb', '#ffb37a', '#ffd23f', '#ff8a5c', '#ffcf99'];
var meshLayers = [];
for (var i = 0; i < MESH_LAYER_COUNT; i += 1) {
var depthIndex = i;
meshLayers.push({
depthIndex: depthIndex,
period: MESH_DRIFT_PERIOD_BASE_MS * (1 + depthIndex * 0.62),
centerX: 20 + rnd() * (CALC_W - 40),
centerY: 18 + rnd() * (CALC_H - 36),
reachPx: Math.max(10, 42 - depthIndex * 6),
radius: Math.max(14, 46 - depthIndex * 7),
phaseX: rnd() * Math.PI * 2,
phaseY: rnd() * Math.PI * 2,
color: palette[i % palette.length]
});
}
meshLayers.sort(function (a, b) { return b.depthIndex - a.depthIndex; });
function blobPositionAt(layer, t) {
var ax = Math.sin(t / layer.period * Math.PI * 2 + layer.phaseX) * layer.reachPx;
var ay = Math.cos(t / layer.period * Math.PI * 2.3 + layer.phaseY) * layer.reachPx * 0.7;
return { x: layer.centerX + ax, y: layer.centerY + ay };
}
var elapsed = 0;
var cw = 480, ch = 300;
function drawScene(nowMs) {
var sx = cw / CALC_W, sy = ch / CALC_H;
var savg = (sx + sy) / 2;
ctx.clearRect(0, 0, cw, ch);
ctx.globalCompositeOperation = 'lighter';
for (var i = 0; i < meshLayers.length; i += 1) {
var layer = meshLayers[i];
var pos = blobPositionAt(layer, nowMs);
var alpha = Math.max(0.3, 0.72 - layer.depthIndex * 0.1);
var dx = pos.x * sx, dy = pos.y * sy, dr = layer.radius * savg;
var g = ctx.createRadialGradient(dx, dy, 0, dx, dy, dr);
g.addColorStop(0, layer.color);
g.addColorStop(1, 'rgba(0,0,0,0)');
ctx.globalAlpha = alpha;
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(dx, dy, dr, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = 'source-over';
}
function fit() {
var r = wrap.getBoundingClientRect();
cw = Math.max(1, Math.round(r.width));
ch = Math.max(1, Math.round(r.height));
canvas.width = cw; canvas.height = ch;
drawScene(elapsed);
}
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) root.classList.remove('is-demo');
var warmSteps = reduce ? 0 : 90;
for (var w = 0; w < warmSteps; w += 1) elapsed += 20;
var lastTs = null;
function tick(ts) {
if (lastTs === null) lastTs = ts;
var dt = Math.min(ts - lastTs, 48);
lastTs = ts;
if (!reduce) {
elapsed += dt;
drawScene(elapsed);
}
requestAnimationFrame(tick);
}
fit();
window.addEventListener('resize', fit);
drawScene(elapsed);
requestAnimationFrame(tick);
})();import { useEffect, useRef } from "react";
import styles from "./Depth3dGradientMeshDrift.module.scss";
type MeshLayer = {
depthIndex: number;
period: number;
centerX: number;
centerY: number;
reachPx: number;
radius: number;
phaseX: number;
phaseY: number;
color: string;
};
export default function Depth3dGradientMeshDrift() {
const rootRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const root = rootRef.current;
const canvas = canvasRef.current;
if (!root || !canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const wrap = canvas.parentElement as HTMLElement;
const CALC_W = 160, CALC_H = 100;
const MESH_DRIFT_PERIOD_BASE_MS = 3200;
const MESH_LAYER_COUNT = 5;
function seedRand(seed: number) {
let s = seed >>> 0;
return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
const rnd = seedRand(429031);
const palette = ["#ff9ebb", "#ffb37a", "#ffd23f", "#ff8a5c", "#ffcf99"];
const meshLayers: MeshLayer[] = [];
for (let i = 0; i < MESH_LAYER_COUNT; i += 1) {
const depthIndex = i;
meshLayers.push({
depthIndex,
period: MESH_DRIFT_PERIOD_BASE_MS * (1 + depthIndex * 0.62),
centerX: 20 + rnd() * (CALC_W - 40),
centerY: 18 + rnd() * (CALC_H - 36),
reachPx: Math.max(10, 42 - depthIndex * 6),
radius: Math.max(14, 46 - depthIndex * 7),
phaseX: rnd() * Math.PI * 2,
phaseY: rnd() * Math.PI * 2,
color: palette[i % palette.length]
});
}
meshLayers.sort((a, b) => b.depthIndex - a.depthIndex);
function blobPositionAt(layer: MeshLayer, t: number) {
const ax = Math.sin(t / layer.period * Math.PI * 2 + layer.phaseX) * layer.reachPx;
const ay = Math.cos(t / layer.period * Math.PI * 2.3 + layer.phaseY) * layer.reachPx * 0.7;
return { x: layer.centerX + ax, y: layer.centerY + ay };
}
let elapsed = 0;
let cw = 480, ch = 300;
function drawScene(nowMs: number) {
const sx = cw / CALC_W, sy = ch / CALC_H;
const savg = (sx + sy) / 2;
ctx!.clearRect(0, 0, cw, ch);
ctx!.globalCompositeOperation = "lighter";
for (let i = 0; i < meshLayers.length; i += 1) {
const layer = meshLayers[i];
const pos = blobPositionAt(layer, nowMs);
const alpha = Math.max(0.3, 0.72 - layer.depthIndex * 0.1);
const dx = pos.x * sx, dy = pos.y * sy, dr = layer.radius * savg;
const g = ctx!.createRadialGradient(dx, dy, 0, dx, dy, dr);
g.addColorStop(0, layer.color);
g.addColorStop(1, "rgba(0,0,0,0)");
ctx!.globalAlpha = alpha;
ctx!.fillStyle = g;
ctx!.beginPath();
ctx!.arc(dx, dy, dr, 0, Math.PI * 2);
ctx!.fill();
}
ctx!.globalAlpha = 1;
ctx!.globalCompositeOperation = "source-over";
}
function fit() {
const r = wrap.getBoundingClientRect();
cw = Math.max(1, Math.round(r.width));
ch = Math.max(1, Math.round(r.height));
canvas!.width = cw; canvas!.height = ch;
drawScene(elapsed);
}
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) root.classList.remove("is-demo");
const warmSteps = reduce ? 0 : 90;
for (let w = 0; w < warmSteps; w += 1) elapsed += 20;
let lastTs: number | null = null;
let raf = 0;
function tick(ts: number) {
if (lastTs === null) lastTs = ts;
const dt = Math.min(ts - lastTs, 48);
lastTs = ts;
if (!reduce) {
elapsed += dt;
drawScene(elapsed);
}
raf = requestAnimationFrame(tick);
}
fit();
window.addEventListener("resize", fit);
drawScene(elapsed);
raf = requestAnimationFrame(tick);
return () => {
cancelAnimationFrame(raf);
window.removeEventListener("resize", fit);
};
}, []);
return (
<main className={styles.stage}>
<div className={`${styles.d3} is-demo`} ref={rootRef}>
<canvas className={styles.d3__canvas} ref={canvasRef} width={480} height={300} />
<p className={styles.d3__caption}>여러 겹의 흐린 그라데이션 덩어리에 서로 다른 이동 주기를 줘 겹쳐 그리고, 가까운 층일수록 더 크고 빠르게, 먼 층일수록 작고 느리게 흐른다</p>
</div>
</main>
);
}03 Gradient Mesh Blobs Drifting at Different Depth Layers — Measured area 52.471% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · 9 CSS 3D Background Animation Depth Layers
The post this demo lives in
9 CSS 3D Background Animation Depth LayersThis css 3d background animation set has nine layers below. Layer 02 loses focus with distance, layer 07 loses shadow st See all demos in this post
If you like this effect - similar demos
Interactions03 A Flock of Bubbles Drifting on Buoyancy, Air Drag and a Noise WindBubble Animation CSS: 9 Soap-Film Scenes That Pop
Interactions02 Card stack that shifts by depth3D Hover Effect CSS: 9 Pointer-Following Scenes
Interactions02 A background that follows — three parallax layersMouse Follow Effect CSS — 9 Cursor Chasers