06여러 겹 쌓일수록 깊이가 드러나는 유리판
판이 하나씩 뒤로 겹쳐질 때마다 굴절과 그림자 양이 함께 늘어나 뒤쪽 판일수록 더 흐리고 깊어 보인다.
쓰임카드 여러 장 겹친 갤러리
stackOrderdarkenBandPANELS

<main class="stage">
<div class="lg is-demo" id="lgRoot" tabindex="0">
<canvas class="lg__canvas" id="lgCanvas" width="480" height="300"></canvas>
<p class="lg__caption">깊이가 다른 유리 판 세 장이 각자 다른 리듬으로 떠오르며 앞뒤 순서가 바뀐다</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
@include stage($stage-yellow);
.lg {
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;
outline: none;
}
.lg__canvas {
display: block;
width: 100%;
height: 100%;
}
.lg__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(23, 20, 26, .68);
color: $stage-paper;
font-weight: 500;
font-size: 12px;
line-height: 1.4;
pointer-events: none;
}
@media (max-width: 340px) {
.lg__caption { font-size: 10px; padding: $sp-1 $sp-2; }
}
@media (prefers-reduced-motion: reduce) {
.lg { cursor: default; }
}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);
}
.lg {
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);
outline: none;
}
.lg__canvas {
display: block;
width: 100%;
height: 100%;
}
.lg__caption {
position: absolute;
left: 12px;
bottom: 12px;
max-width: 78%;
margin: 0;
padding: 4px 12px;
border-radius: 999px;
background: rgba(23, 20, 26, 0.68);
color: #ffffff;
font-weight: 500;
font-size: 12px;
line-height: 1.4;
pointer-events: none;
}
@media (max-width: 340px) {
.lg__caption {
font-size: 10px;
padding: 4px 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.lg {
cursor: default;
}
}(function () {
// 기법: 판 세 장을 서로 다른 진폭·주기로 위아래로 띄우고(깊이감), 절반 주기마다
// 그리는 순서(앞뒤)를 바꾼다. depthFor 로 각 판의 스택 깊이(앞 0→뒤 n)를 구해
// 굴절(bend)·그림자(shadow)·흐림(blur, 굴절 샘플 반경)이 깊이에 비례해 늘어나게
// 하고, 순서가 바뀔 때는 STACK_TRANSITION_MS 동안 이전 깊이에서 새 깊이로 lerp 한다.
var root = document.getElementById('lgRoot');
var canvas = document.getElementById('lgCanvas');
var ctx = canvas.getContext('2d');
var wrap = canvas.parentElement;
function seedRand(seed) {
var s = seed >>> 0;
return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
var CALC_W = 160, CALC_H = 100;
var scene = document.createElement('canvas');
scene.width = CALC_W; scene.height = CALC_H;
(function drawScene() {
var pctx = scene.getContext('2d');
var rand = seedRand(421061);
var sky = pctx.createLinearGradient(0, 0, 0, CALC_H);
sky.addColorStop(0, '#fff0e6');
sky.addColorStop(1, '#e8f6ff');
pctx.fillStyle = sky;
pctx.fillRect(0, 0, CALC_W, CALC_H);
var cols = ['#b9a0ff', '#6ec8ff', '#4cd4a6', '#ff9ebb'];
for (var i = 0; i < cols.length; i += 1) {
pctx.beginPath();
pctx.arc(CALC_W * (0.12 + i * 0.26), CALC_H * (0.25 + (i % 2) * 0.45), CALC_H * 0.24, 0, Math.PI * 2);
pctx.fillStyle = cols[i];
pctx.globalAlpha = 0.45;
pctx.fill();
}
pctx.globalAlpha = 1;
for (var k = 0; k < 46; k += 1) {
var px = rand() * CALC_W, py = rand() * CALC_H, r = rand() * 1 + 0.3;
pctx.beginPath();
pctx.arc(px, py, r, 0, Math.PI * 2);
pctx.fillStyle = 'rgba(255,255,255,' + (0.1 + rand() * 0.18) + ')';
pctx.fill();
}
}());
var sceneCtx = scene.getContext('2d');
var sceneImg = sceneCtx.getImageData(0, 0, CALC_W, CALC_H);
var sceneBuf = sceneImg.data;
function sampleScene(sx, sy) {
sx = sx < 0 ? 0 : (sx > CALC_W - 1 ? CALC_W - 1 : sx);
sy = sy < 0 ? 0 : (sy > CALC_H - 1 ? CALC_H - 1 : sy);
var i = (Math.round(sy) * CALC_W + Math.round(sx)) * 4;
return [sceneBuf[i], sceneBuf[i + 1], sceneBuf[i + 2]];
}
var EDGE_BAND = 10;
var PERIOD_MS = 2000;
var SLOT_MS = PERIOD_MS / 3;
var STACK_TRANSITION_MS = 220;
var BEND_PER_DEPTH = 3;
var BLUR_RADIUS_PER_DEPTH = 1.4;
var SHADOW_BASE_SIZE = 5;
var SHADOW_SIZE_PER_DEPTH = 2.5;
var SHADOW_BASE_ALPHA = 0.14;
var SHADOW_ALPHA_PER_DEPTH = 0.1;
var PANELS = [
{ x0: 24, y0: 20, x1: 84, y1: 62, amp: 8, freq: 1, phase: 0, bend: 5, shine: 20 },
{ x0: 58, y0: 34, x1: 118, y1: 76, amp: 6, freq: 1.4, phase: 1.4, bend: 4, shine: 16 },
{ x0: 90, y0: 24, x1: 148, y1: 66, amp: 10, freq: 0.8, phase: 3.0, bend: 6, shine: 22 }
];
function boxEdgeSample(x, y, x0, y0, x1, y1, band) {
var dl = x - x0, dr = x1 - x, dt = y - y0, db = y1 - y;
var dmin = Math.min(dl, dr, dt, db);
if (dmin >= band || dmin < 0) return { f: 0, nx: 0, ny: 0 };
var nx = 0, ny = 0;
if (dl < band) nx -= (band - dl);
if (dr < band) nx += (band - dr);
if (dt < band) ny -= (band - dt);
if (db < band) ny += (band - db);
var len = Math.sqrt(nx * nx + ny * ny);
if (len > 0.0001) { nx /= len; ny /= len; }
return { f: Math.max(0, (band - dmin) / band), nx: nx, ny: ny };
}
var out = document.createElement('canvas');
out.width = CALC_W; out.height = CALC_H;
var outCtx = out.getContext('2d');
var outImg = outCtx.createImageData(CALC_W, CALC_H);
var outBuf = outImg.data;
function orderForSlot(slot) {
slot = ((slot % 3) + 3) % 3;
if (slot === 0) return [0, 1, 2];
if (slot === 1) return [1, 2, 0];
return [2, 0, 1];
}
function stackOrder(t) {
return orderForSlot(Math.floor((t % PERIOD_MS) / SLOT_MS));
}
function depthFor(panelIdx, t) {
var cyc = t % PERIOD_MS;
var slot = Math.floor(cyc / SLOT_MS);
var tSlot = cyc - slot * SLOT_MS;
var curOrder = orderForSlot(slot);
var prevOrder = orderForSlot(slot - 1);
var curDepth = (PANELS.length - 1) - curOrder.indexOf(panelIdx);
var prevDepth = (PANELS.length - 1) - prevOrder.indexOf(panelIdx);
var blend = Math.min(1, tSlot / STACK_TRANSITION_MS);
var e = 1 - Math.pow(1 - blend, 2);
return prevDepth + (curDepth - prevDepth) * e;
}
function darkenBand(x0, y0, x1, y1, bandH, alpha) {
for (var y = Math.max(0, Math.round(y1)); y < Math.min(CALC_H, y1 + bandH); y += 1) {
var f = 1 - (y - y1) / bandH;
for (var x = Math.max(0, Math.round(x0 - 3)); x < Math.min(CALC_W, x1 + 3); x += 1) {
var i = (y * CALC_W + x) * 4;
outBuf[i] = outBuf[i] * (1 - alpha * f);
outBuf[i + 1] = outBuf[i + 1] * (1 - alpha * f);
outBuf[i + 2] = outBuf[i + 2] * (1 - alpha * f);
}
}
}
function renderFrame(t) {
var idx = 0;
for (var y = 0; y < CALC_H; y += 1) {
for (var x = 0; x < CALC_W; x += 1) {
var rgb = sampleScene(x, y);
outBuf[idx] = rgb[0]; outBuf[idx + 1] = rgb[1]; outBuf[idx + 2] = rgb[2]; outBuf[idx + 3] = 255;
idx += 4;
}
}
var order = stackOrder(t);
for (var oi = 0; oi < order.length; oi += 1) {
var panelIdx = order[oi];
var p = PANELS[panelIdx];
var depth = depthFor(panelIdx, t);
var bend = p.bend + depth * BEND_PER_DEPTH;
var blurR = depth * BLUR_RADIUS_PER_DEPTH;
var shadowSize = SHADOW_BASE_SIZE + depth * SHADOW_SIZE_PER_DEPTH;
var shadowAlpha = SHADOW_BASE_ALPHA + depth * SHADOW_ALPHA_PER_DEPTH;
var bobY = p.amp * Math.sin(t * 2 * Math.PI / PERIOD_MS * p.freq + p.phase);
var x0 = p.x0, x1 = p.x1, y0 = p.y0 + bobY, y1 = p.y1 + bobY;
darkenBand(x0, y0, x1, y1, shadowSize, shadowAlpha);
for (var yy = Math.max(0, Math.round(y0)); yy < Math.min(CALC_H, y1); yy += 1) {
for (var xx = Math.max(0, Math.round(x0)); xx < Math.min(CALC_W, x1); xx += 1) {
var e = boxEdgeSample(xx, yy, x0, y0, x1, y1, EDGE_BAND);
var mag = e.f * bend;
var sxx = xx - e.nx * mag, syy = (yy - bobY) - e.ny * mag;
var c;
if (blurR > 0.05) {
var c0 = sampleScene(sxx, syy);
var c1 = sampleScene(sxx - blurR, syy);
var c2 = sampleScene(sxx + blurR, syy);
var c3 = sampleScene(sxx, syy - blurR);
var c4 = sampleScene(sxx, syy + blurR);
c = [
(c0[0] + c1[0] + c2[0] + c3[0] + c4[0]) / 5,
(c0[1] + c1[1] + c2[1] + c3[1] + c4[1]) / 5,
(c0[2] + c1[2] + c2[2] + c3[2] + c4[2]) / 5
];
} else {
c = sampleScene(sxx, syy);
}
var s = e.f * p.shine;
var i2 = (yy * CALC_W + xx) * 4;
outBuf[i2] = Math.min(255, c[0] + s);
outBuf[i2 + 1] = Math.min(255, c[1] + s);
outBuf[i2 + 2] = Math.min(255, c[2] + s);
}
}
}
}
var cw = 480, ch = 300;
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;
draw(lastElapsed);
}
var lastElapsed = 0;
function draw(t) {
lastElapsed = t;
renderFrame(t);
outCtx.putImageData(outImg, 0, 0);
ctx.clearRect(0, 0, cw, ch);
ctx.drawImage(out, 0, 0, CALC_W, CALC_H, 0, 0, cw, ch);
}
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) { root.classList.remove('is-demo'); }
var lastTs = null;
var elapsed = 0;
function tick(ts) {
if (lastTs === null) lastTs = ts;
var dt = ts - lastTs;
lastTs = ts;
elapsed += dt;
draw(elapsed);
requestAnimationFrame(tick);
}
fit();
window.addEventListener('resize', fit);
draw(elapsed);
if (!reduce) requestAnimationFrame(tick);
})();// PanelStackDepth — vanilla 판(run/421/demo/06-liquid-glass-panel-stack-depth/index.html) 의 React 포트.
// 유리 판 세 장이 각자 다른 진폭·주파수·위상으로 위아래로 떠오르고, 일정 구간마다
// 그리는 순서(앞뒤 겹침)가 rotate 된다. depthFor 로 각 판의 스택 깊이(앞 0→뒤 n)를 구해
// 굴절(bend)·그림자(shadow)·흐림(blur, 굴절 샘플 반경)이 깊이에 비례해 늘어나게 하고,
// 순서가 바뀌는 구간에서는 STACK_TRANSITION_MS 동안 이전 깊이에서 새 깊이로 lerp 한다.
import { useEffect, useRef } from "react";
import styles from "./PanelStackDepth.module.scss";
type Box<T> = { current: T };
type Edge = { f: number; nx: number; ny: number };
type Panel = { x0: number; y0: number; x1: number; y1: number; amp: number; freq: number; phase: number; bend: number; shine: number };
const CALC_W = 160, CALC_H = 100;
const EDGE_BAND = 10;
const PERIOD_MS = 2000;
const SLOT_MS = PERIOD_MS / 3;
const STACK_TRANSITION_MS = 220;
const BEND_PER_DEPTH = 3;
const BLUR_RADIUS_PER_DEPTH = 1.4;
const SHADOW_BASE_SIZE = 5;
const SHADOW_SIZE_PER_DEPTH = 2.5;
const SHADOW_BASE_ALPHA = 0.14;
const SHADOW_ALPHA_PER_DEPTH = 0.1;
const PANELS: Panel[] = [
{ x0: 24, y0: 20, x1: 84, y1: 62, amp: 8, freq: 1, phase: 0, bend: 5, shine: 20 },
{ x0: 58, y0: 34, x1: 118, y1: 76, amp: 6, freq: 1.4, phase: 1.4, bend: 4, shine: 16 },
{ x0: 90, y0: 24, x1: 148, y1: 66, amp: 10, freq: 0.8, phase: 3.0, bend: 6, shine: 22 },
];
interface State {
ctx: CanvasRenderingContext2D | null;
wrap: HTMLElement | null;
sceneBuf: Uint8ClampedArray | null;
outCtx: CanvasRenderingContext2D | null;
out: HTMLCanvasElement | null;
outImg: ImageData | null;
outBuf: Uint8ClampedArray | null;
cw: number; ch: number;
lastElapsed: number;
lastTs: number | null;
elapsed: number;
raf: number;
reduce: boolean;
}
function seedRand(seed: number): () => number {
let s = seed >>> 0;
return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
function boxEdgeSample(x: number, y: number, x0: number, y0: number, x1: number, y1: number, band: number): Edge {
const dl = x - x0, dr = x1 - x, dt = y - y0, db = y1 - y;
const dmin = Math.min(dl, dr, dt, db);
if (dmin >= band || dmin < 0) return { f: 0, nx: 0, ny: 0 };
let nx = 0, ny = 0;
if (dl < band) nx -= (band - dl);
if (dr < band) nx += (band - dr);
if (dt < band) ny -= (band - dt);
if (db < band) ny += (band - db);
const len = Math.sqrt(nx * nx + ny * ny);
if (len > 0.0001) { nx /= len; ny /= len; }
return { f: Math.max(0, (band - dmin) / band), nx, ny };
}
function orderForSlot(slot: number): number[] {
const s = ((slot % 3) + 3) % 3;
if (s === 0) return [0, 1, 2];
if (s === 1) return [1, 2, 0];
return [2, 0, 1];
}
function stackOrder(t: number): number[] {
return orderForSlot(Math.floor((t % PERIOD_MS) / SLOT_MS));
}
function depthFor(panelIdx: number, t: number): number {
const cyc = t % PERIOD_MS;
const slot = Math.floor(cyc / SLOT_MS);
const tSlot = cyc - slot * SLOT_MS;
const curOrder = orderForSlot(slot);
const prevOrder = orderForSlot(slot - 1);
const curDepth = (PANELS.length - 1) - curOrder.indexOf(panelIdx);
const prevDepth = (PANELS.length - 1) - prevOrder.indexOf(panelIdx);
const blend = Math.min(1, tSlot / STACK_TRANSITION_MS);
const e = 1 - Math.pow(1 - blend, 2);
return prevDepth + (curDepth - prevDepth) * e;
}
function darkenBand(outBuf: Uint8ClampedArray, x0: number, y1: number, x1: number, bandH: number, alpha: number): void {
const yMax = Math.min(CALC_H - 1, Math.ceil(y1 + bandH));
for (let y = Math.max(0, Math.floor(y1)); y <= yMax; y += 1) {
const f = Math.max(0, 1 - (y - y1) / bandH);
const mul = 1 - alpha * f;
const xLo = Math.max(0, Math.floor(x0 - 3));
const xHi = Math.min(CALC_W - 1, Math.ceil(x1 + 3));
for (let x = xLo; x <= xHi; x += 1) {
const i = (y * CALC_W + x) * 4;
outBuf[i] = outBuf[i] * mul;
outBuf[i + 1] = outBuf[i + 1] * mul;
outBuf[i + 2] = outBuf[i + 2] * mul;
}
}
}
export default function PanelStackDepth() {
const rootRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const st = useRef({
ctx: null, wrap: null, sceneBuf: null, outCtx: null, out: null, outImg: null, outBuf: null,
cw: 480, ch: 300, lastElapsed: 0, lastTs: null, elapsed: 0, raf: 0, reduce: false,
}) as Box<State>;
function sampleScene(sx: number, sy: number): number[] {
const buf = st.current.sceneBuf!;
const cx = sx < 0 ? 0 : sx > CALC_W - 1 ? CALC_W - 1 : sx;
const cy = sy < 0 ? 0 : sy > CALC_H - 1 ? CALC_H - 1 : sy;
const i = (Math.round(cy) * CALC_W + Math.round(cx)) * 4;
return [buf[i], buf[i + 1], buf[i + 2]];
}
function renderFrame(t: number): void {
const s = st.current;
const outBuf = s.outBuf!;
outBuf.set(s.sceneBuf!);
const order = stackOrder(t);
for (const pi of order) {
const p = PANELS[pi];
const depth = depthFor(pi, t);
const bend = p.bend + depth * BEND_PER_DEPTH;
const blurR = depth * BLUR_RADIUS_PER_DEPTH;
const shadowSize = SHADOW_BASE_SIZE + depth * SHADOW_SIZE_PER_DEPTH;
const shadowAlpha = SHADOW_BASE_ALPHA + depth * SHADOW_ALPHA_PER_DEPTH;
const bobY = p.amp * Math.sin(((t * 2 * Math.PI) / PERIOD_MS) * p.freq + p.phase);
const rx0 = p.x0, ry0 = p.y0 + bobY, rx1 = p.x1, ry1 = p.y1 + bobY;
darkenBand(outBuf, rx0, ry1, rx1, shadowSize, shadowAlpha);
const yLo = Math.max(0, Math.floor(ry0)), yHi = Math.min(CALC_H - 1, Math.ceil(ry1));
const xLo = Math.max(0, Math.floor(rx0)), xHi = Math.min(CALC_W - 1, Math.ceil(rx1));
for (let y = yLo; y <= yHi; y += 1) {
for (let x = xLo; x <= xHi; x += 1) {
const e = boxEdgeSample(x, y, rx0, ry0, rx1, ry1, EDGE_BAND);
const mag = e.f * bend;
const sx = x - e.nx * mag;
const sy = (y - bobY) - e.ny * mag;
let rgb: number[];
if (blurR > 0.05) {
const c0 = sampleScene(sx, sy);
const c1 = sampleScene(sx - blurR, sy);
const c2 = sampleScene(sx + blurR, sy);
const c3 = sampleScene(sx, sy - blurR);
const c4 = sampleScene(sx, sy + blurR);
rgb = [
(c0[0] + c1[0] + c2[0] + c3[0] + c4[0]) / 5,
(c0[1] + c1[1] + c2[1] + c3[1] + c4[1]) / 5,
(c0[2] + c1[2] + c2[2] + c3[2] + c4[2]) / 5,
];
} else {
rgb = sampleScene(sx, sy);
}
const sh = e.f * p.shine;
const i = (y * CALC_W + x) * 4;
outBuf[i] = Math.min(255, rgb[0] + sh);
outBuf[i + 1] = Math.min(255, rgb[1] + sh);
outBuf[i + 2] = Math.min(255, rgb[2] + sh);
outBuf[i + 3] = 255;
}
}
}
}
function draw(t: number): void {
const s = st.current;
s.lastElapsed = t;
renderFrame(t);
s.outCtx!.putImageData(s.outImg!, 0, 0);
s.ctx!.clearRect(0, 0, s.cw, s.ch);
s.ctx!.drawImage(s.out!, 0, 0, CALC_W, CALC_H, 0, 0, s.cw, s.ch);
}
function fit(): void {
const s = st.current;
const canvas = canvasRef.current;
if (!s.wrap || !canvas) return;
const r = s.wrap.getBoundingClientRect();
s.cw = Math.max(1, Math.round(r.width));
s.ch = Math.max(1, Math.round(r.height));
canvas.width = s.cw; canvas.height = s.ch;
draw(s.lastElapsed);
}
function tick(ts: number): void {
const s = st.current;
if (s.lastTs === null) s.lastTs = ts;
const dt = ts - s.lastTs;
s.lastTs = ts;
s.elapsed += dt;
draw(s.elapsed);
s.raf = requestAnimationFrame(tick);
}
useEffect(() => {
const root = rootRef.current;
const canvas = canvasRef.current;
if (!root || !canvas) return;
const ctx = canvas.getContext("2d");
const wrap = canvas.parentElement;
if (!ctx || !wrap) return;
const scene = document.createElement("canvas");
scene.width = CALC_W; scene.height = CALC_H;
const pctx = scene.getContext("2d")!;
const rand = seedRand(421061);
const sky = pctx.createLinearGradient(0, 0, CALC_W, CALC_H);
sky.addColorStop(0, "#fff0e6");
sky.addColorStop(1, "#e8f6ff");
pctx.fillStyle = sky;
pctx.fillRect(0, 0, CALC_W, CALC_H);
const cols = ["#6ec8ff", "#ffd23f", "#ff9ebb", "#b9a0ff"];
for (let i = 0; i < cols.length; i += 1) {
pctx.beginPath();
pctx.arc(CALC_W * (0.16 + i * 0.24), CALC_H * (0.3 + (i % 2) * 0.34), CALC_H * 0.24, 0, Math.PI * 2);
pctx.fillStyle = cols[i];
pctx.globalAlpha = 0.45;
pctx.fill();
}
pctx.globalAlpha = 1;
for (let k = 0; k < 46; k += 1) {
const px = rand() * CALC_W, py = rand() * CALC_H, r = rand() * 1 + 0.3;
pctx.beginPath();
pctx.arc(px, py, r, 0, Math.PI * 2);
pctx.fillStyle = "rgba(255,255,255," + (0.08 + rand() * 0.18) + ")";
pctx.fill();
}
const sceneImg = pctx.getImageData(0, 0, CALC_W, CALC_H);
const out = document.createElement("canvas");
out.width = CALC_W; out.height = CALC_H;
const outCtx = out.getContext("2d")!;
const outImg = outCtx.createImageData(CALC_W, CALC_H);
const s = st.current;
s.ctx = ctx; s.wrap = wrap; s.sceneBuf = sceneImg.data;
s.outCtx = outCtx; s.out = out; s.outImg = outImg; s.outBuf = outImg.data;
s.reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (s.reduce) { root.classList.remove("is-demo"); }
fit();
window.addEventListener("resize", fit);
draw(s.elapsed);
if (!s.reduce) s.raf = requestAnimationFrame(tick);
return () => {
window.removeEventListener("resize", fit);
cancelAnimationFrame(st.current.raf);
};
}, []);
return (
<main className={styles.stage}>
<div className={`${styles.lg} is-demo`} ref={rootRef} tabIndex={0}>
<canvas className={styles.lg__canvas} ref={canvasRef} width={480} height={300} />
<p className={styles.lg__caption}>깊이가 다른 유리 판 세 장이 각자 다른 리듬으로 떠오르며 앞뒤 순서가 바뀐다</p>
</div>
</main>
);
}06 여러 겹 쌓일수록 깊이가 드러나는 유리판 — 측정 누적면적 37.909% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 리퀴드 글라스 효과 9가지 — 굴절 계산으로 만드는 리퀴드 글라스 디자인
이 데모가 속한 글
리퀴드 글라스 효과 9가지 — 굴절 계산으로 만드는 리퀴드 글라스 디자인리퀴드 글라스 효과는 캔버스 픽셀을 다시 배치해 유리 표면처럼 배경을 휘게 만드는 굴절 계산이다. 03번 가장자리 색분리와 04번 탄성 복귀, 08번 눌림 두께 변화는 전부 같은 굴절 계산 하나에서 갈라져 나온 결과 이 글의 데모 전부 보기


