09Depth fog scene
Six ridge layers stand in distance order, and fog colour is mixed in exponentially by distance. The further a layer is, the less contrast it keeps and the slower it drifts, so six same-coloured shapes read as depth.
Best forQuiet distant background for a travel landing page
createLinearGradientfillRectglobalAlpha

<main class="stage">
<div class="df" id="df">
<p class="df__hint">
<svg class="df__ico" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M224 100a68 68 0 0 1-68 68H76a44 44 0 1 1 14.2-85.66v.11A68.06 68.06 0 0 1 224 100" opacity=".2"/><path d="M120 208H72a8 8 0 0 1 0-16h48a8 8 0 0 1 0 16m64-16h-24a8 8 0 0 0 0 16h24a8 8 0 0 0 0-16m-24 32h-56a8 8 0 0 0 0 16h56a8 8 0 0 0 0-16m72-124a76.08 76.08 0 0 1-76 76H76a52 52 0 0 1 0-104a53 53 0 0 1 8.92.76A76.08 76.08 0 0 1 232 100m-16 0a60.06 60.06 0 0 0-120-3.54a8 8 0 0 1-16-.92q.21-3.66.77-7.23A38 38 0 0 0 76 88a36 36 0 0 0 0 72h80a60.07 60.07 0 0 0 60-60"/></g></svg>
<span>Raise the fog density and the far ridges melt into the sky</span>
</p>
<div class="df__field">
<canvas class="df__cv" id="cv"></canvas>
</div>
<div class="df__bar">
<label class="df__dial">Fog density
<input type="range" id="fog" min="10" max="60" step="1" value="24" aria-label="Fog density">
<b class="df__val" id="fogVal">×2.4</b>
</label>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 깊이 안개 씬 — 움직임은 전부 캔버스 rAF 루프 안에 있다. CSS 는 무대·판·다이얼만 맡는다.
// @keyframes 를 두지 않는다 — CSS 애니메이션이 0개여야 렌더러가 실시간으로 24장을 찍는다(01·05 항목과 같은 방식).
// 캔버스가 쓰는 색은 커스텀 프로퍼티로 내려보낸다 — 값은 토큰 한 곳에만 둔다.
$duration: $dur-loop;
$easing: $ease-spring;
$color: $subject-ink;
@include stage($stage-yellow);
.df {
// 하늘 — 위가 노랑에 가깝고 지평선으로 갈수록 크림에 녹는다(안개의 목표색이기도 하다)
--sky-0: #{mix($stage-yellow, $subject-cream, 62%)};
--sky-1: #{mix($stage-yellow, $subject-cream, 24%)};
--sun-0: #{rgba($stage-orange, .32)};
--sun-1: #{rgba($stage-orange, 0)};
--bird: #{$subject-ink};
--ink: #{$subject-ink};
display: flex; flex-direction: column; gap: $sp-2;
width: 100%; max-width: 424px;
}
.df__hint {
display: flex; align-items: center; gap: $sp-1; margin: 0;
font-weight: 700; font-size: clamp(10px, 2.8vw, 12px); line-height: 1.35;
color: $subject-ink;
}
.df__ico { width: 16px; height: 16px; flex: 0 0 auto; color: $subject-orange-deep; }
.df__field {
position: relative; height: 184px;
border-radius: $r-card; overflow: hidden;
background: $subject-cream;
box-shadow: inset 0 0 0 1px rgba($subject-ink, .16);
}
.df__cv { display: block; width: 100%; height: 100%; }
.df__bar { display: flex; align-items: center; gap: $sp-3; }
.df__dial {
display: flex; align-items: center; gap: $sp-2;
font-weight: 700; font-size: 11px; color: $subject-ink;
}
.df__dial input { width: 116px; margin: 0; accent-color: $subject-blue; }
.df__val { color: $subject-ink; font-size: 11px; font-weight: 700; }
@media (max-width: 340px) {
.df { gap: $sp-1; }
.df__field { height: 104px; }
.df__dial { gap: $sp-1; }
.df__dial input { width: 68px; }
.df__hint { font-size: 10px; }
}
// reduce — 루프가 아예 안 돈다. 여섯 층 깊이가 다 보이는 정적 한 장이 남는다.
@media (prefers-reduced-motion: reduce) {
.df.is-still .df__val { opacity: .92 !important; }
}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);
}
.df {
--sky-0: rgb(100%, 87.8666666667%, 49.5921568627%);
--sky-1: rgb(100%, 93.3803921569%, 74.4784313725%);
--sun-0: rgba(255, 77, 31, 0.32);
--sun-1: rgba(255, 77, 31, 0);
--bird: #17141a;
--ink: #17141a;
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
max-width: 424px;
}
.df__hint {
display: flex;
align-items: center;
gap: 4px;
margin: 0;
font-weight: 700;
font-size: clamp(10px, 2.8vw, 12px);
line-height: 1.35;
color: #17141a;
}
.df__ico {
width: 16px;
height: 16px;
flex: 0 0 auto;
color: #c93000;
}
.df__field {
position: relative;
height: 184px;
border-radius: 16px;
overflow: hidden;
background: #fff7e6;
box-shadow: inset 0 0 0 1px rgba(23, 20, 26, 0.16);
}
.df__cv {
display: block;
width: 100%;
height: 100%;
}
.df__bar {
display: flex;
align-items: center;
gap: 12px;
}
.df__dial {
display: flex;
align-items: center;
gap: 8px;
font-weight: 700;
font-size: 11px;
color: #17141a;
}
.df__dial input {
width: 116px;
margin: 0;
accent-color: #2f6df6;
}
.df__val {
color: #17141a;
font-size: 11px;
font-weight: 700;
}
@media (max-width: 340px) {
.df {
gap: 4px;
}
.df__field {
height: 104px;
}
.df__dial {
gap: 4px;
}
.df__dial input {
width: 68px;
}
.df__hint {
font-size: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
.df.is-still .df__val {
opacity: 0.92 !important;
}
}// 깊이 안개 씬 — 능선 실루엣 여섯 겹을 거리순으로 뒤에서 앞으로 그린다. 각 능선은 시드 고정
// 노이즈로 미리 계산한 봉우리 점렬(Path2D)이고, 거리별 안개는 지수 혼합이다:
// 그 층이 남길 대비 t = exp(-거리/밀도). 멀수록 하늘색에 녹아 대비가 사라지고, 앞쪽은 어둡고
// 대비가 강하며 서로 다른 속도로 가로 흘러(시차) 같은 색 도형 여섯 개가 깊이로 읽힌다.
var root = document.getElementById('df');
var cv = document.getElementById('cv');
var ctx = cv.getContext('2d');
var fogIn = document.getElementById('fog');
var fogOut = document.getElementById('fogVal');
var css = getComputedStyle(root);
function v(n) { return css.getPropertyValue(n).trim(); }
var SKY0 = v('--sky-0'), SKY1 = v('--sky-1');
var SUN0 = v('--sun-0'), SUN1 = v('--sun-1');
var BIRD = v('--bird');
var INK = hex3(v('--ink'));
var SKY1C = hex3(SKY1); // 안개가 희석해 가는 목표색 — 지평선 근처 하늘색
// '#rrggbb' → [r, g, b]. 토큰 mix 값은 컴파일 타임에 hex 로 굳는다
function hex3(s) {
var m = /^#([0-9a-f]{6})$/i.exec(s);
return m ? [parseInt(s.substr(1, 2), 16), parseInt(s.substr(3, 2), 16), parseInt(s.substr(5, 2), 16)] : [0, 0, 0];
}
function mix3(a, b, t) {
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
}
function css3(c) {
return 'rgb(' + (c[0] | 0) + ',' + (c[1] | 0) + ',' + (c[2] | 0) + ')';
}
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var LAYERS = 6;
var density = 2.4; // Fog density(지수 혼합의 분모) — 슬라이더로 바뀐다
var STILL_T = 3; // reduce 정지판을 찍는 시각(초) — 새가 하늘에 떠 있는 순간
var raf = 0, W = 0, H = 0, dpr = 1;
var paths = []; // 능선별 Path2D — fit() 에서 한 번만 만든다
var meta = []; // 능선별 기준선·진폭·흐름 속도
// 시드 고정 난수와 능선 점렬 — 08 빛 산란과 같은 방식(조절점 사이 코사인 보간, 양 끝 이어짐)
function rng(seed) {
return function () { seed = (seed * 1664525 + 1013904223) >>> 0; return seed / 4294967296; };
}
function ridgePts(seed, bumps) {
var rr = rng(seed);
var cp = [];
for (var i = 0; i < bumps; i++) cp.push(rr());
cp.push(cp[0]);
var pts = [];
var n = bumps * 12;
for (var k = 0; k <= n; k++) {
var t = k / n * bumps;
var i0 = Math.floor(t), f = t - i0;
var s = (1 - Math.cos(f * Math.PI)) * 0.5;
pts.push(cp[i0] + (cp[i0 + 1] - cp[i0]) * s);
}
return pts;
}
var ridgeData = [];
for (var li = 0; li < LAYERS; li++) {
ridgeData.push(ridgePts(31 + li * 17, 3 + li)); // 앞으로 올수록 봉우리가 촘촘해진다
meta.push({
base: 0.40 + li * 0.082, // 능선 기준선(정규) — 뒤가 높고 앞이 낮게 내려온다
amp: 0.045 + li * 0.026, // 진폭 — 앞으로 올수록 산이 크다
speed: 4 + li * 6 // 흐름 속도(px/초) — 앞이 빠르다(시차)
});
}
// 새 두 마리 — 하늘을 가로지르는 작은 v 자. 날개각이 떨린다
var BIRDS = [
{ y: 0.15, speed: 0.11, phase: 0.1, size: 9 },
{ y: 0.23, speed: 0.07, phase: 0.62, size: 7 }
];
function fit() {
var r = cv.parentElement.getBoundingClientRect();
dpr = window.devicePixelRatio || 1;
W = r.width; H = r.height;
cv.width = Math.round(W * dpr);
cv.height = Math.round(H * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
paths = [];
for (var i = 0; i < LAYERS; i++) {
var pts = ridgeData[i], m = meta[i];
var p = new Path2D();
p.moveTo(-2, H + 2);
for (var k = 0; k < pts.length; k++) {
p.lineTo(k / (pts.length - 1) * W, H * (m.base - m.amp * pts[k]));
}
p.lineTo(W + 2, H + 2);
p.closePath();
paths.push(p);
}
}
function birds(sec) {
ctx.strokeStyle = BIRD;
ctx.lineCap = 'round';
ctx.globalAlpha = 0.8;
for (var i = 0; i < BIRDS.length; i++) {
var b = BIRDS[i];
var span = W + 80;
var x = -40 + ((sec * b.speed * W + b.phase * span) % span);
var y = H * (b.y + Math.sin(sec * 0.8 + b.phase * 6) * 0.02);
var wing = 0.18 + (Math.sin(sec * 6.5 + b.phase * 9) * 0.5 + 0.5) * 0.62; // 날개각(라디안, 0.18~0.8)
var dy = Math.sin(wing) * b.size;
var dx = Math.cos(wing) * b.size;
ctx.lineWidth = Math.max(1, b.size * 0.14);
ctx.beginPath();
ctx.moveTo(x - dx, y - dy);
ctx.quadraticCurveTo(x - dx * 0.3, y - dy * 0.2, x, y);
ctx.quadraticCurveTo(x + dx * 0.3, y - dy * 0.2, x + dx, y - dy);
ctx.stroke();
}
ctx.globalAlpha = 1;
}
function paint(sec) {
// 하늘 — 위가 진하고 지평선으로 갈수록 옅어지는 세로 그라디언트
var sky = ctx.createLinearGradient(0, 0, 0, H);
sky.addColorStop(0, SKY0);
sky.addColorStop(1, SKY1);
ctx.fillStyle = sky;
ctx.fillRect(0, 0, W, H);
// 옅은 해 — 거리 감각을 잡아 주는 반점 하나(빛줄기는 없다. 그건 08 의 소재다)
var sun = ctx.createRadialGradient(W * 0.74, H * 0.2, 1, W * 0.74, H * 0.2, H * 0.3);
sun.addColorStop(0, SUN0);
sun.addColorStop(1, SUN1);
ctx.fillStyle = sun;
ctx.fillRect(0, 0, W, H);
birds(sec);
// 능선 — 뒤에서 앞으로. 거리마다 안개 색을 지수로 섞는다: t = exp(-거리/밀도)
for (var i = 0; i < LAYERS; i++) {
var d = LAYERS - i; // 거리 — 앞층이 1
var t = Math.exp(-d / density); // 그 층이 남길 대비
var off = (sec * meta[i].speed) % W;
ctx.fillStyle = css3(mix3(SKY1C, INK, t));
ctx.save();
ctx.translate(-off, 0);
ctx.fill(paths[i]);
ctx.translate(W, 0);
ctx.fill(paths[i]);
ctx.restore();
}
}
function frame(now) {
paint(now / 1000);
raf = requestAnimationFrame(frame);
}
function setFog(val) {
density = Number(val) / 10;
fogOut.textContent = '×' + density.toFixed(1);
}
fogIn.addEventListener('input', function () { setFog(fogIn.value); if (reduce) paint(STILL_T); });
window.addEventListener('resize', function () { fit(); if (reduce) paint(STILL_T); });
fit();
setFog(fogIn.value);
if (reduce) {
// reduce — 루프를 시작하지 않고 여섯 층 깊이가 다 보이는 정적 한 장만 그린다(빈 화면이 아니다)
root.classList.add('is-still');
paint(STILL_T);
} else {
raf = requestAnimationFrame(frame);
}// DepthFog — vanilla 판(run/077/demo/09-fx-depth-fog/index.html) 의 React 포트.
// 능선 실루엣 여섯 겹을 거리순으로 세우고 거리마다 안개 색을 지수로 섞는다(t = exp(-거리/밀도)).
// 움직임은 캔버스 rAF 루프가 전부 그리므로 @keyframes 가 없다 — props 는 CSS 변수로 내려간다.
import { useEffect, useRef } from "react";
import type { CSSProperties } from "react";
import styles from "./DepthFog.module.scss";
export type DepthFogProps = {
duration?: string;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
// useRef 는 T | null 을 돌려준다. 처음부터 값이 있고 절대 비지 않는 상자는 이렇게 좁혀 쓴다
type Box<T> = { current: T };
// React 이벤트 제네릭이 없는 환경이라 필요한 것만 로컬로 선언한다
type RngEvt = { target: { value: string } };
const LAYERS = 6;
const STILL_T = 3; // reduce 정지판을 찍는 시각(초) — 새가 하늘에 떠 있는 순간
const FOG_LABEL = "안개 밀도";
const BASE_FOG = "24";
const BIRDS = [
{ y: 0.15, speed: 0.11, phase: 0.1, size: 9 },
{ y: 0.23, speed: 0.07, phase: 0.62, size: 7 },
];
type Size = { w: number; h: number; dpr: number };
type RGB = [number, number, number];
// 시드 고정 난수와 능선 점렬(정규화 0..1) — 조절점 사이 코사인 보간, 양 끝이 이어진다
function rng(seed: number): () => number {
return function (): number {
seed = (seed * 1664525 + 1013904223) >>> 0;
return seed / 4294967296;
};
}
function ridgePts(seed: number, bumps: number): number[] {
const rr = rng(seed);
const cp: number[] = [];
for (let i = 0; i < bumps; i++) cp.push(rr());
cp.push(cp[0]);
const pts: number[] = [];
const n = bumps * 12;
for (let k = 0; k <= n; k++) {
const t = k / n * bumps;
const i0 = Math.floor(t), f = t - i0;
const s = (1 - Math.cos(f * Math.PI)) * 0.5;
pts.push(cp[i0] + (cp[i0 + 1] - cp[i0]) * s);
}
return pts;
}
const RIDGE_DATA: number[][] = [];
const META: { base: number; amp: number; speed: number }[] = [];
for (let i = 0; i < LAYERS; i++) {
RIDGE_DATA.push(ridgePts(31 + i * 17, 3 + i)); // 앞으로 올수록 봉우리가 촘촘해진다
META.push({
base: 0.40 + i * 0.082, // 능선 기준선(정규) — 뒤가 높고 앞이 낮게 내려온다
amp: 0.045 + i * 0.026, // 진폭 — 앞으로 올수록 산이 크다
speed: 4 + i * 6, // 흐름 속도(px/초) — 앞이 빠르다(시차)
});
}
// '#rrggbb' → [r, g, b]. 토큰 mix 값은 컴파일 타임에 hex 로 굳는다
function hex3(s: string): RGB {
const m = /^#([0-9a-f]{6})$/i.exec(s);
return m ? [parseInt(s.substr(1, 2), 16), parseInt(s.substr(3, 2), 16), parseInt(s.substr(5, 2), 16)] : [0, 0, 0];
}
function mix3(a: RGB, b: RGB, t: number): RGB {
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
}
function css3(c: RGB): string {
return "rgb(" + (c[0] | 0) + "," + (c[1] | 0) + "," + (c[2] | 0) + ")";
}
export default function DepthFog({
duration = "2s", // vanilla $duration: $dur-loop
easing = "cubic-bezier(.2, .8, .2, 1)", // vanilla $easing: $ease-spring
color = "#17141a", // vanilla $color: $subject-ink
}: DepthFogProps) {
const rootRef = useRef<HTMLDivElement | null>(null);
const cvRef = useRef<HTMLCanvasElement | null>(null);
const ctxRef = useRef<CanvasRenderingContext2D | null>(null);
const fogOutRef = useRef<HTMLElement | null>(null);
const sizeRef = useRef({ w: 0, h: 0, dpr: 1 }) as Box<Size>;
const pathsRef = useRef<Path2D[]>([]) as Box<Path2D[]>;
const densityRef = useRef(2.4) as Box<number>;
const rafRef = useRef(0) as Box<number>;
const vars: Vars = { "--duration": duration, "--easing": easing, "--color": color };
function reduced(): boolean {
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}
function fit(): void {
const cv = cvRef.current;
const ctx = ctxRef.current;
if (!cv || !ctx || !cv.parentElement) return;
const r = cv.parentElement.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
const s = sizeRef.current;
s.w = r.width; s.h = r.height; s.dpr = dpr;
cv.width = Math.round(r.width * dpr);
cv.height = Math.round(r.height * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const paths: Path2D[] = [];
for (let i = 0; i < LAYERS; i++) {
const pts = RIDGE_DATA[i], m = META[i];
const p = new Path2D();
p.moveTo(-2, r.height + 2);
for (let k = 0; k < pts.length; k++) {
p.lineTo(k / (pts.length - 1) * r.width, r.height * (m.base - m.amp * pts[k]));
}
p.lineTo(r.width + 2, r.height + 2);
p.closePath();
paths.push(p);
}
pathsRef.current = paths;
}
// 새 두 마리 — 하늘을 가로지르는 작은 v 자. 날개각이 떨린다
function birds(ctx: CanvasRenderingContext2D, sec: number): void {
const s = sizeRef.current;
const cssv = (n: string) => getComputedStyle(rootRef.current as HTMLDivElement).getPropertyValue(n).trim();
ctx.strokeStyle = cssv("--bird");
ctx.lineCap = "round";
ctx.globalAlpha = 0.8;
for (const b of BIRDS) {
const span = s.w + 80;
const x = -40 + ((sec * b.speed * s.w + b.phase * span) % span);
const y = s.h * (b.y + Math.sin(sec * 0.8 + b.phase * 6) * 0.02);
const wing = 0.18 + (Math.sin(sec * 6.5 + b.phase * 9) * 0.5 + 0.5) * 0.62;
const dy = Math.sin(wing) * b.size;
const dx = Math.cos(wing) * b.size;
ctx.lineWidth = Math.max(1, b.size * 0.14);
ctx.beginPath();
ctx.moveTo(x - dx, y - dy);
ctx.quadraticCurveTo(x - dx * 0.3, y - dy * 0.2, x, y);
ctx.quadraticCurveTo(x + dx * 0.3, y - dy * 0.2, x + dx, y - dy);
ctx.stroke();
}
ctx.globalAlpha = 1;
}
function paint(sec: number): void {
const ctx = ctxRef.current;
const s = sizeRef.current;
if (!ctx || !s.w) return;
const cssv = (n: string) => getComputedStyle(rootRef.current as HTMLDivElement).getPropertyValue(n).trim();
const sky1c = hex3(cssv("--sky-1"));
const ink = hex3(cssv("--ink"));
// 하늘 — 위가 진하고 지평선으로 갈수록 옅어지는 세로 그라디언트
const sky = ctx.createLinearGradient(0, 0, 0, s.h);
sky.addColorStop(0, cssv("--sky-0"));
sky.addColorStop(1, cssv("--sky-1"));
ctx.fillStyle = sky;
ctx.fillRect(0, 0, s.w, s.h);
// 옅은 해 — 거리 감각을 잡아 주는 반점 하나(빛줄기는 없다. 그건 08 의 소재다)
const sun = ctx.createRadialGradient(s.w * 0.74, s.h * 0.2, 1, s.w * 0.74, s.h * 0.2, s.h * 0.3);
sun.addColorStop(0, cssv("--sun-0"));
sun.addColorStop(1, cssv("--sun-1"));
ctx.fillStyle = sun;
ctx.fillRect(0, 0, s.w, s.h);
birds(ctx, sec);
// 능선 — 뒤에서 앞으로. 거리마다 안개 색을 지수로 섞는다: t = exp(-거리/밀도)
for (let i = 0; i < LAYERS; i++) {
const d = LAYERS - i; // 거리 — 앞층이 1
const t = Math.exp(-d / densityRef.current); // 그 층이 남길 대비
const off = (sec * META[i].speed) % s.w;
ctx.fillStyle = css3(mix3(sky1c, ink, t));
ctx.save();
ctx.translate(-off, 0);
ctx.fill(pathsRef.current[i]);
ctx.translate(s.w, 0);
ctx.fill(pathsRef.current[i]);
ctx.restore();
}
}
function frame(now: number): void {
paint(now / 1000);
rafRef.current = requestAnimationFrame(frame);
}
function setFog(val: string): void {
densityRef.current = Number(val) / 10;
if (fogOutRef.current) fogOutRef.current.textContent = "×" + densityRef.current.toFixed(1);
}
function onRange(e: RngEvt): void {
setFog(e.target.value);
if (reduced()) paint(STILL_T);
}
function onResize(): void {
fit();
if (reduced()) paint(STILL_T);
}
useEffect(() => {
const cv = cvRef.current;
if (!cv) return;
ctxRef.current = cv.getContext("2d");
if (!ctxRef.current) return;
fit();
setFog(BASE_FOG);
if (reduced()) {
// reduce — 루프를 시작하지 않고 여섯 층 깊이가 다 보이는 정적 한 장만 그린다
rootRef.current?.classList.add("is-still");
paint(STILL_T);
} else {
rafRef.current = requestAnimationFrame(frame);
}
window.addEventListener("resize", onResize);
return () => {
window.removeEventListener("resize", onResize);
cancelAnimationFrame(rafRef.current);
};
}, []);
return (
<main className={styles.stage}>
<div className={styles.root} ref={rootRef} style={vars}>
<p className={styles.hint}>
<svg className={styles.ico} viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M224 100a68 68 0 0 1-68 68H76a44 44 0 1 1 14.2-85.66v.11A68.06 68.06 0 0 1 224 100" opacity=".2"/><path d="M120 208H72a8 8 0 0 1 0-16h48a8 8 0 0 1 0 16m64-16h-24a8 8 0 0 0 0 16h24a8 8 0 0 0 0-16m-24 32h-56a8 8 0 0 0 0 16h56a8 8 0 0 0 0-16m72-124a76.08 76.08 0 0 1-76 76H76a52 52 0 0 1 0-104a53 53 0 0 1 8.92.76A76.08 76.08 0 0 1 232 100m-16 0a60.06 60.06 0 0 0-120-3.54a8 8 0 0 1-16-.92q.21-3.66.77-7.23A38 38 0 0 0 76 88a36 36 0 0 0 0 72h80a60.07 60.07 0 0 0 60-60"/></g></svg>
<span>안개 밀도를 올리면 먼 능선이 하늘에 녹아 사라집니다</span>
</p>
<div className={styles.field}>
<canvas className={styles.cv} ref={cvRef}></canvas>
</div>
<div className={styles.bar}>
<label className={styles.dial}>{FOG_LABEL}
<input type="range" min="10" max="60" step="1" defaultValue={BASE_FOG} aria-label={FOG_LABEL} onChange={onRange}></input>
<b className={styles.val} ref={fogOutRef}>×2.4</b>
</label>
</div>
</div>
</main>
);
}09 Depth fog scene — Measured area 1.434% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · HTML5 Canvas Animation: 9 Warp & Droplet Effects
The post this demo lives in
HTML5 Canvas Animation: 9 Warp & Droplet EffectsHTML5 canvas animation bends pixels — a droplet swelling the text under it, a black hole pulling the screen in — no reto See all demos in this post
If you like this effect - similar demos
Creative UI03 A school of fish — schooling with a sense of depth9 Canvas Boids Algorithm Flocking Birds Simulation
Creative UI08 Strike flash — the whole scene cools off exponentially right after impact9 Lightning Effect Background Scenes, No WebGL
Creative UI06 Density cloud — visit counts piling into pixel-brightness fogLorenz Attractor: 9 Butterfly Chaos Demos