02Background Blurring Out of Focus by Simulated Distance
Each shape's depth value is converted into a blur radius and re-applied through filter blur in real time, so the farther a shape sits from the focal plane the more its edges spread.
Best forIntro-section background with a camera-focus feel
blurForDepthDEPTH_BLUR_MAX_PXfocalDepthAt

<main class="stage">
<div class="d3 is-demo" id="d3Root" tabindex="0">
<canvas class="d3__canvas" id="d3Canvas" width="480" height="300"></canvas>
<p class="d3__caption">Each shape's depth value is converted into a blur radius and re-applied through filter blur in real time, so the farther a shape sits from the focal plane the more its edges spread</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
@include stage($stage-yellow);
.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-yellow;
outline: none;
cursor: crosshair;
}
.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: pointer; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
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: #ffd23f;
outline: none;
cursor: crosshair;
}
.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: pointer;
}
}(function () {
// 기법: 원마다 가상 거리값(distancePx)을 갖고, 매 프레임 focalDepth(초점면 거리, 시간에 따라
// 오간다)와의 차이를 blurForDepth 로 흐림 반경(px)으로 바꿔 ctx.filter = blur(...) 를 다시
// 세팅한 뒤 그린다 — 초점면에 가까운 원은 선명하게, 멀어질수록 DEPTH_BLUR_MAX_PX 까지 크게 번진다.
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 DEPTH_BLUR_MAX_PX = 14;
var FOCAL_CYCLE_MS = 4200;
var FOCAL_RANGE_NEAR = 30, FOCAL_RANGE_FAR = 160;
var BOKEH_COUNT = 14;
function seedRand(seed) {
var s = seed >>> 0;
return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
var rnd = seedRand(429021);
var palette = ['#ffe6b3', '#ffd23f', '#ff9ebb', '#6ec8ff', '#4cd4a6'];
var circles = [];
for (var i = 0; i < BOKEH_COUNT; i += 1) {
circles.push({
distancePx: 20 + rnd() * 150,
baseX: 14 + rnd() * (CALC_W - 28),
baseY: 12 + rnd() * (CALC_H - 24),
size: 5 + rnd() * 11,
phase: rnd() * Math.PI * 2,
driftAmp: 3 + rnd() * 5,
color: palette[i % palette.length]
});
}
function focalDepthAt(t) {
var f = (Math.sin(t / FOCAL_CYCLE_MS * Math.PI * 2) + 1) / 2;
return FOCAL_RANGE_NEAR + f * (FOCAL_RANGE_FAR - FOCAL_RANGE_NEAR);
}
function blurForDepth(distancePx, focalDepth) {
var diff = Math.abs(distancePx - focalDepth);
return Math.max(0, Math.min(DEPTH_BLUR_MAX_PX, diff / 9));
}
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;
var focalDepth = focalDepthAt(nowMs);
ctx.clearRect(0, 0, cw, ch);
ctx.globalCompositeOperation = 'lighter';
var order = circles.slice().sort(function (a, b) { return b.distancePx - a.distancePx; });
for (var i = 0; i < order.length; i += 1) {
var c = order[i];
var blurPx = blurForDepth(c.distancePx, focalDepth);
var alpha = Math.max(0.3, Math.min(0.95, 1 - c.distancePx / 210));
var driftX = Math.sin(nowMs * 0.0006 + c.phase) * c.driftAmp;
var driftY = Math.cos(nowMs * 0.0005 + c.phase) * c.driftAmp * 0.6;
var dx = (c.baseX + driftX) * sx, dy = (c.baseY + driftY) * sy;
var dr = c.size * savg;
ctx.filter = blurPx > 0.15 ? 'blur(' + (blurPx * savg).toFixed(1) + 'px)' : 'none';
var g = ctx.createRadialGradient(dx, dy, 0, dx, dy, dr);
g.addColorStop(0, c.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.filter = 'none';
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 focalNudge = 0;
wrap.addEventListener('pointermove', function (e) {
var r = wrap.getBoundingClientRect();
focalNudge = ((e.clientX - r.left) / r.width - 0.5) * 2;
if (reduce) drawScene(elapsed + focalNudge * FOCAL_CYCLE_MS * 0.25);
});
wrap.addEventListener('keydown', function (e) {
var moved = true;
if (e.key === 'ArrowLeft') focalNudge = Math.max(-1, focalNudge - 0.12);
else if (e.key === 'ArrowRight') focalNudge = Math.min(1, focalNudge + 0.12);
else moved = false;
if (moved) {
e.preventDefault();
if (reduce) drawScene(elapsed + focalNudge * FOCAL_CYCLE_MS * 0.25);
}
});
var warmSteps = reduce ? 160 : 120;
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 + focalNudge * FOCAL_CYCLE_MS * 0.25);
}
requestAnimationFrame(tick);
}
fit();
window.addEventListener('resize', fit);
drawScene(elapsed);
requestAnimationFrame(tick);
})();import { useEffect, useRef } from "react";
import styles from "./Depth3dBokehBlurFalloff.module.scss";
type Bokeh = {
distancePx: number;
baseX: number;
baseY: number;
size: number;
phase: number;
driftAmp: number;
color: string;
};
export default function Depth3dBokehBlurFalloff() {
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 DEPTH_BLUR_MAX_PX = 14;
const FOCAL_CYCLE_MS = 4200;
const FOCAL_RANGE_NEAR = 30, FOCAL_RANGE_FAR = 160;
const BOKEH_COUNT = 14;
function seedRand(seed: number) {
let s = seed >>> 0;
return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
const rnd = seedRand(429021);
const palette = ["#ffe6b3", "#ffd23f", "#ff9ebb", "#6ec8ff", "#4cd4a6"];
const circles: Bokeh[] = [];
for (let i = 0; i < BOKEH_COUNT; i += 1) {
circles.push({
distancePx: 20 + rnd() * 150,
baseX: 14 + rnd() * (CALC_W - 28),
baseY: 12 + rnd() * (CALC_H - 24),
size: 5 + rnd() * 11,
phase: rnd() * Math.PI * 2,
driftAmp: 3 + rnd() * 5,
color: palette[i % palette.length]
});
}
function focalDepthAt(t: number) {
const f = (Math.sin(t / FOCAL_CYCLE_MS * Math.PI * 2) + 1) / 2;
return FOCAL_RANGE_NEAR + f * (FOCAL_RANGE_FAR - FOCAL_RANGE_NEAR);
}
function blurForDepth(distancePx: number, focalDepth: number) {
const diff = Math.abs(distancePx - focalDepth);
return Math.max(0, Math.min(DEPTH_BLUR_MAX_PX, diff / 9));
}
let elapsed = 0;
let cw = 480, ch = 300;
let focalNudge = 0;
function drawScene(nowMs: number) {
const sx = cw / CALC_W, sy = ch / CALC_H;
const savg = (sx + sy) / 2;
const focalDepth = focalDepthAt(nowMs);
ctx!.clearRect(0, 0, cw, ch);
ctx!.globalCompositeOperation = "lighter";
const order = circles.slice().sort((a, b) => b.distancePx - a.distancePx);
for (let i = 0; i < order.length; i += 1) {
const c = order[i];
const blurPx = blurForDepth(c.distancePx, focalDepth);
const alpha = Math.max(0.3, Math.min(0.95, 1 - c.distancePx / 210));
const driftX = Math.sin(nowMs * 0.0006 + c.phase) * c.driftAmp;
const driftY = Math.cos(nowMs * 0.0005 + c.phase) * c.driftAmp * 0.6;
const dx = (c.baseX + driftX) * sx, dy = (c.baseY + driftY) * sy;
const dr = c.size * savg;
ctx!.filter = blurPx > 0.15 ? "blur(" + (blurPx * savg).toFixed(1) + "px)" : "none";
const g = ctx!.createRadialGradient(dx, dy, 0, dx, dy, dr);
g.addColorStop(0, c.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!.filter = "none";
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");
function onPointerMove(e: PointerEvent) {
const r = wrap.getBoundingClientRect();
focalNudge = ((e.clientX - r.left) / r.width - 0.5) * 2;
if (reduce) drawScene(elapsed + focalNudge * FOCAL_CYCLE_MS * 0.25);
}
function onKeydown(e: KeyboardEvent) {
let moved = true;
if (e.key === "ArrowLeft") focalNudge = Math.max(-1, focalNudge - 0.12);
else if (e.key === "ArrowRight") focalNudge = Math.min(1, focalNudge + 0.12);
else moved = false;
if (moved) {
e.preventDefault();
if (reduce) drawScene(elapsed + focalNudge * FOCAL_CYCLE_MS * 0.25);
}
}
wrap.addEventListener("pointermove", onPointerMove);
wrap.addEventListener("keydown", onKeydown);
const warmSteps = reduce ? 160 : 120;
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 + focalNudge * FOCAL_CYCLE_MS * 0.25);
}
raf = requestAnimationFrame(tick);
}
fit();
window.addEventListener("resize", fit);
drawScene(elapsed);
raf = requestAnimationFrame(tick);
return () => {
cancelAnimationFrame(raf);
window.removeEventListener("resize", fit);
wrap.removeEventListener("pointermove", onPointerMove);
wrap.removeEventListener("keydown", onKeydown);
};
}, []);
return (
<main className={styles.stage}>
<div className={`${styles.d3} is-demo`} ref={rootRef} tabIndex={0}>
<canvas className={styles.d3__canvas} ref={canvasRef} width={480} height={300} />
<p className={styles.d3__caption}>각 도형의 거리값을 흐림 반경으로 변환해 실시간으로 filter blur 값을 다시 계산해, 초점면에서 멀어질수록 도형 윤곽이 더 크게 번진다</p>
</div>
</main>
);
}02 Background Blurring Out of Focus by Simulated Distance — Measured area 16.966% · moving frames 15/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
Interactions08 A Volley Where the Sky Tints at Each Burst and the Skyline Lights Up by DistanceCSS Fireworks: 9 Bursts, Trails and Reflections
Interactions07 Step-back slide out9 Breadcrumb UI Patterns That Survive Deep Paths
Interactions04 Menu tiles and the sold-out cellKiosk UI Design: 9 Finger-Sized Order Screens