GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

리퀴드 글라스 효과 9가지 — 굴절 계산으로 만드는 리퀴드 글라스 디자인

리퀴드 글라스 효과는 캔버스 픽셀을 다시 배치해 유리 표면처럼 배경을 휘게 만드는 굴절 계산이다. 03번 가장자리 색분리와 04번 탄성 복귀, 08번 눌림 두께 변화는 전부 같은 굴절 계산 하나에서 갈라져 나온 결과다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

같은 굴절 계산 하나가 아홉 갈래로 갈라지는 이유

아홉 개 모두 판 가장자리에서 배경 픽셀을 안쪽으로 당겨오는 boxEdgeSample 굴절 하나를 공유하지만, 그 위에 무엇을 더하느냐에 따라 결과가 완전히 달라진다 — 01은 굴절 자체만 보여주고, 02·05는 포인터 위치나 배경 밝기 같은 외부 값을 더하고, 03은 채널마다 표본 위치를 다르게 잡아 색을 갈라놓고, 04·07·08은 시간이 지나며 값이 줄거나 물결치게 하고, 06·09는 여러 층과 스크롤 값을 겹친다. 배경은 사진이 아니라 seedRand로 데모마다 고정 시드를 넣어 그린 절차적 무늬이고, 계산은 160×100(16,000픽셀) 오프스크린 캔버스에서 getImageData로 읽은 픽셀을 매 프레임 다시 배치해 putImageData로 그린 뒤 화면 크기로 확대해 보여준다. 아래 코드는 run/421/demo 원본 index.html에서 연속된 줄 그대로 옮겼다.

01가장자리만 배경을 휘게 하는 굴절 렌즈

판 안쪽은 배경이 그대로 비치고, 가장자리 쪽으로 갈수록 boxEdgeSample이 돌려주는 안쪽 방향 벡터만큼 배경 표본 좌표가 당겨져 두께가 있는 유리처럼 눌려 보인다. GLASS_REFRACTION_STRENGTH(기본 8)가 이 당김의 최대 크기를 정해, 판 자체는 가만히 있어도 뒤에서 배경만 좌우로 흐르며 가장자리 왜곡이 계속 눈에 띄게 유지된다.

가장자리만 배경을 휘게 하는 굴절 렌즈
새 창에서 열기

01 · 가장자리만 배경을 휘게 하는 굴절 렌즈 — 측정 누적면적 23.072% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GLASS_REFRACTION_STRENGTHboxEdgeSamplepanX
  var PANEL_X0 = 20, PANEL_Y0 = 16, PANEL_X1 = 140, PANEL_Y1 = 84;
  var EDGE_BAND = 16;
  var GLASS_REFRACTION_STRENGTH = 8;
  var SCENE_OFFSET_X = (SCENE_W - CALC_W) / 2;
  var PAN_AMP = 34;
  var PERIOD_MS = 2000;

  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 };
  }

  function panX(t) {
    return PAN_AMP * Math.sin(t * 2 * Math.PI / PERIOD_MS);
  }

(실측: 누적면적 23.1%·강도 평균 40.5·23/23프레임 움직임)

02포인터를 따라 움직이는 하이라이트 반사

포인터가 판 위 어디를 지나든 대각선 좌표 하나(d)로 환산해 그 값과 반사 중심(center) 사이 거리가 가까울수록 밝기를 더하는 방식으로, 반사 띠가 포인터를 그대로 쫓아온다. 포인터가 없을 때는 virtualCenter(t)가 대신 그 좌표를 시간에 따라 오가게 만들어 데모가 멈춰 보이지 않게 한다.

포인터를 따라 움직이는 하이라이트 반사
새 창에서 열기

02 · 포인터를 따라 움직이는 하이라이트 반사 — 측정 누적면적 39.642% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

virtualCenterpointerDiagHIGHLIGHT_BAND
  function renderFrame(t) {
    var center = pointerActive ? pointerDiag : virtualCenter(t);
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        var rgb;
        if (x >= PANEL_X0 && x <= PANEL_X1 && y >= PANEL_Y0 && y <= PANEL_Y1) {
          var e = boxEdgeSample(x, y, PANEL_X0, PANEL_Y0, PANEL_X1, PANEL_Y1, EDGE_BAND);
          var mag = e.f * GLASS_EDGE_BEND;
          rgb = sampleScene(x - e.nx * mag, y - e.ny * mag);
          var d = ((x - PANEL_X0) + (y - PANEL_Y0)) / DIAG_SPAN;
          var diff = (d - center) / HIGHLIGHT_BAND;
          var shine = Math.exp(-diff * diff) * HIGHLIGHT_MAX;
          rgb = [Math.min(255, rgb[0] + shine), Math.min(255, rgb[1] + shine), Math.min(255, rgb[2] + shine)];
        } else {
          rgb = sampleScene(x, y);
        }
        outBuf[idx] = rgb[0]; outBuf[idx + 1] = rgb[1]; outBuf[idx + 2] = rgb[2]; outBuf[idx + 3] = 255;
        idx += 4;
      }

(실측: 누적면적 39.6%·강도 평균 57.7·20/23프레임 움직임)

03가장자리에서 색이 갈라져 보이는 리퀴드 글라스 css 유리

빨강·초록·파랑 세 채널을 sampleChannel로 따로 뽑아 오는데, 가장자리로 갈수록 채널마다 표본 좌표를 다른 폭(chroma)만큼 어긋나게 잡아 색이 살짝씩 겹쳐 보이게 만든다. GLASS_CHROMA_SHIFT_PX(기본 1)가 이 어긋남의 기준 폭이고, 가장자리 굴절력(e.f)이 클수록 어긋남도 함께 커진다.

가장자리에 색이 갈라져 보이는 유리
새 창에서 열기

03 · 가장자리에 색이 갈라져 보이는 유리 — 측정 누적면적 51.8% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GLASS_CHROMA_SHIFT_PXsampleChannelboxEdgeSample
  function renderFrame(t) {
    var pan = panX(t);
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        var baseSx = x + SCENE_OFFSET_X + pan;
        if (x >= PANEL_X0 && x <= PANEL_X1 && y >= PANEL_Y0 && y <= PANEL_Y1) {
          var e = boxEdgeSample(x, y, PANEL_X0, PANEL_Y0, PANEL_X1, PANEL_Y1, EDGE_BAND);
          var mag = e.f * GLASS_BASE_BEND;
          var chroma = e.f * GLASS_CHROMA_SHIFT_PX * 6;
          var rC = sampleChannel(baseSx - e.nx * (mag + chroma), y - e.ny * (mag + chroma), 0);
          var gC = sampleChannel(baseSx - e.nx * mag, y - e.ny * mag, 1);
          var bC = sampleChannel(baseSx - e.nx * (mag - chroma), y - e.ny * (mag - chroma), 2);
          outBuf[idx] = rC; outBuf[idx + 1] = gC; outBuf[idx + 2] = bC;
        } else {
          outBuf[idx] = sampleChannel(baseSx, y, 0);
          outBuf[idx + 1] = sampleChannel(baseSx, y, 1);
          outBuf[idx + 2] = sampleChannel(baseSx, y, 2);
        }
        outBuf[idx + 3] = 255;
        idx += 4;

(실측: 누적면적 51.8%·강도 평균 92.3·23/23프레임 움직임)

04당겼다 놓으면 스프링처럼 출렁이며 되돌아오는 유리

모서리를 당기고 있는 동안은 삼차 이징으로 목표 지점까지 늘어나다가, 손을 떼는 순간부터는 elasticSpring(rt)가 GLASS_ELASTIC_RETURN_MS를 시정수로 한 지수 감쇠와 GLASS_ELASTIC_OSC_MS를 진동 주기로 한 Math.cos 항을 곱해 값을 되돌린다. 단일 감쇠 곡선이 아니라 언더댐프 스프링이라, 손을 뗀 뒤 2~3회 오버슛하며 출렁이다가 0으로 잦아들고, 이 값을 키우면 오버슛 횟수가 아니라 잦아드는 데 걸리는 시간만 늘어난다.

당기면 늘어났다 탄성으로 되돌아오는 유리
새 창에서 열기

04 · 당기면 늘어났다 탄성으로 되돌아오는 유리 — 측정 누적면적 31.812% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GLASS_ELASTIC_RETURN_MScurrentOffsetvirtualOffset
  function elasticSpring(rt) {
    var decay = Math.exp(-rt / GLASS_ELASTIC_RETURN_MS);
    var osc = Math.cos((2 * Math.PI / GLASS_ELASTIC_OSC_MS) * rt);
    return decay * osc;
  }

  function virtualOffset(t) {
    var cyc = t % PERIOD_MS;
    if (cyc < DRAG_PHASE_MS) {
      var p = cyc / DRAG_PHASE_MS;
      var e = 1 - Math.pow(1 - p, 3);
      return { x: e * DRAG_MAX_X, y: e * DRAG_MAX_Y };
    }
    var rt = cyc - DRAG_PHASE_MS;
    var k = elasticSpring(rt);
    return { x: DRAG_MAX_X * k, y: DRAG_MAX_Y * k };
  }

(실측: 누적면적 31.8%·강도 평균 27.7·20/23프레임 움직임)

05배경 밝기에 맞춰 틴트가 옅어지고 짙어지는 유리

읽어 온 배경 픽셀의 밝기를 0.299R+0.587G+0.114B 공식으로 계산한 뒤, 128을 기준으로 밝으면 틴트를 빼고 어두우면 틴트를 더하는 식으로 색을 보정한다. 그 보정 폭을 시간에 따라 오가게 하는 triangleWave가 없으면 배경이 고정된 데모에서는 변화가 한 번에 그쳐, 두 상태를 자연스럽게 오가도록 이 값을 함께 둔다.

배경 밝기에 따라 틴트가 바뀌는 유리
새 창에서 열기

05 · 배경 밝기에 따라 틴트가 바뀌는 유리 — 측정 누적면적 74.724% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

TINT_STRENGTHtriangleWaveboxEdgeSample
  function renderFrame(t) {
    var pan = panX(t);
    var adaptivity = triangleWave(t, PERIOD_MS);
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        var baseSx = x + SCENE_OFFSET_X + pan;
        var rgb;
        if (x >= PANEL_X0 && x <= PANEL_X1 && y >= PANEL_Y0 && y <= PANEL_Y1) {
          var e = boxEdgeSample(x, y, PANEL_X0, PANEL_Y0, PANEL_X1, PANEL_Y1, EDGE_BAND);
          var bendMag = e.f * GLASS_EDGE_BEND;
          rgb = sampleScene(baseSx - e.nx * bendMag, y - e.ny * bendMag);
          var lum = 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2];
          var delta = (128 - lum) * TINT_STRENGTH * adaptivity * 0.5;
          rgb = [rgb[0] + delta, rgb[1] + delta * 0.94, rgb[2] + delta * 0.86];
          var shine = e.f * 18;
          rgb = [
            Math.max(0, Math.min(255, rgb[0] + shine)),
            Math.max(0, Math.min(255, rgb[1] + shine)),
            Math.max(0, Math.min(255, rgb[2] + shine))
          ];
        } else {
          rgb = sampleScene(baseSx, y);
        }

(실측: 누적면적 74.7%·강도 평균 30.6·23/23프레임 움직임)

06그리기 순서가 도는 만큼 깊이가 드러나는 패널 스택

세 패널의 굴절·흐림·그림자는 값이 고정돼 있지 않고, depthFor가 매 프레임 계산하는 깊이 인덱스(맨 앞 0 → 맨 뒤 2)에 비례해 정해진다 — 굴절은 BEND_PER_DEPTH만큼, 흐림은 BLUR_RADIUS_PER_DEPTH만큼 깊이가 깊어질수록 커지고 그림자 띠의 크기·짙기도 함께 깊어진다. stackOrder가 3구간 주기로 앞뒤 순서를 바꿀 때는 값이 순간 튀지 않도록 STACK_TRANSITION_MS 동안 이전 깊이에서 새 깊이로 서서히 넘어간다.

여러 겹 쌓일수록 깊이가 드러나는 유리판
새 창에서 열기

06 · 여러 겹 쌓일수록 깊이가 드러나는 유리판 — 측정 누적면적 37.909% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

stackOrderdarkenBandPANELS
  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;
  }

(실측: 누적면적 37.9%·강도 평균 30.8·23/23프레임 움직임)

07눌린 자리에서 굴절이 물결로 퍼지는 유리

눌린 좌표에서 거리가 멀어질수록 envelope가 Math.exp(-age/260)으로 잦아들고, 그 안에서 Math.sin(dist*0.3-age*0.02)가 만드는 물결(wave)이 가장자리 굴절 오프셋에 그대로 더해져 동심원이 밖으로 퍼지는 것처럼 보인다. 900ms 안에 새로 누르지 않으면 PRESS_SCHEDULE에 미리 정해 둔 시각이 대신 그 자리를 눌러 데모가 저절로 반복된다.

눌린 자리에서 표면 굴절이 물결로 퍼지는 유리
새 창에서 열기

07 · 눌린 자리에서 표면 굴절이 물결로 퍼지는 유리 — 측정 누적면적 26.01% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

activePressPRESS_SCHEDULERIPPLE_AMPLITUDE
          var e = boxEdgeSample(x, y, PANEL_X0, PANEL_Y0, PANEL_X1, PANEL_Y1, EDGE_BAND);
          var dx = x - press.x, dy = y - press.y;
          var dist = Math.sqrt(dx * dx + dy * dy) + 0.0001;
          var envelope = Math.exp(-age / 260) * Math.max(0, 1 - dist / 95);
          var wave = Math.sin(dist * 0.3 - age * 0.02) * envelope * RIPPLE_AMPLITUDE;
          var nx = dx / dist, ny = dy / dist;
          var offX = e.nx * (e.f * GLASS_EDGE_BEND) + nx * wave;
          var offY = e.ny * (e.f * GLASS_EDGE_BEND) + ny * wave;
          rgb = sampleScene(x - offX, y - offY);
          var shine = e.f * 16 + Math.max(0, wave) * 8;
          rgb = [Math.min(255, rgb[0] + shine), Math.min(255, rgb[1] + shine), Math.min(255, rgb[2] + shine)];

(실측: 누적면적 26.0%·강도 평균 22.2·23/23프레임 움직임)

08눌린 두께만큼 볼록함이 줄어드는 버튼

버튼이 눌리면 squeeze가 1에서 SQUEEZE_MIN까지 줄어 돔 굴절 폭(dome)과 테두리 림 밝기(rim)가 같은 비율로 함께 작아져 두께가 실제로 얇아진 것처럼 읽히고, 반대로 k(=1-squeeze)에 비례하는 바닥 그림자 반경(shadowMult)과 바깥쪽 haloMult 번짐은 눌릴수록 오히려 커진다. 900ms 넘게 조작이 없으면 virtualPressed가 대신 눌림 상태를 만들어 항상 무언가 움직이게 한다.

눌린 두께만큼 볼록함이 줄어드는 버튼
새 창에서 열기

08 · 눌린 두께만큼 볼록함이 줄어드는 버튼 — 측정 누적면적 14.814% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

wantPressedSQUEEZE_MINvirtualPressed
    var pressY = BTN_CY + (1 - squeeze) * 3;
    var bandStart = BAND_BASE - k * BAND_K;
    var shadowMult = SHADOW_MULT_BASE + k * SHADOW_MULT_K;
    var haloMult = HALO_MULT_BASE + k * HALO_MULT_K;
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        var rgb;
        var nx = (x - BTN_CX) / BTN_HW;
        var ny = (y - pressY) / BTN_HH;
        var r2 = nx * nx + ny * ny;
        if (r2 <= 1) {
          var dome = (1 - r2) * BULGE_STRENGTH * squeeze;
          var sx = x - nx * BTN_HW * dome * 0.5;
          var sy = y - ny * BTN_HH * dome * 0.5;
          rgb = sampleScene(sx, sy);
          var rNorm = Math.sqrt(r2);
          var rim = Math.max(0, (rNorm - bandStart) / (1 - bandStart)) * RIM_MAX * squeeze;
          var lift = (1 - r2) * LIFT_MAX * squeeze;
          rgb = [
            Math.min(255, rgb[0] + rim + lift),
            Math.min(255, rgb[1] + rim + lift),
            Math.min(255, rgb[2] + rim + lift)
          ];

(실측: 누적면적 14.8%·강도 평균 29.7·23/23프레임 움직임)

09스크롤 값에 반응하는 상단 유리 내비게이션 장면

wantProgress가 돌려주는 진행값(0~1)이 커질수록 굴절 폭(bendMag)과 서리 낀 밝기(frost)가 함께 늘어나, 내용 위를 더 지날수록 상단 막대가 두꺼운 유리처럼 보인다. 마우스 휠과 방향키·PageUp·PageDown 입력이 이 진행값을 직접 바꾸고, 2.5초 넘게 조작이 없을 때만 triangleWave가 대신 값을 오가게 한다.

스크롤 값에 반응하는 상단 유리 내비게이션 장면
새 창에서 열기

09 · 스크롤 값에 반응하는 상단 유리 내비게이션 장면 — 측정 누적면적 74.716% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

wantProgressEXTRA_BENDtriangleWave
  function renderFrame(t, progress) {
    var panY = progress * PAN_RANGE;
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        var rgb;
        if (y >= NAV_Y0 && y <= NAV_Y1) {
          var e = boxEdgeSample(x, y, NAV_Y0 - 999, NAV_Y0, NAV_Y0 + 1998, NAV_Y1, EDGE_BAND);
          var bendMag = (BASE_BEND + EXTRA_BEND * progress) * (e.f || 1) * 0.6 + BASE_BEND * 0.4;
          var ripple = Math.sin(x * 0.16 - t * 0.006) * RIPPLE_AMP * progress;
          rgb = sampleScene(x, (y + panY) - e.ny * bendMag - ripple);
          var frost = 60 * progress;
          rgb = [
            Math.min(255, rgb[0] * (1 - progress * 0.25) + frost),
            Math.min(255, rgb[1] * (1 - progress * 0.25) + frost),
            Math.min(255, rgb[2] * (1 - progress * 0.25) + frost)
          ];

(실측: 누적면적 74.7%·강도 평균 45.4·23/23프레임 움직임)

픽셀 단위로 잰 아홉 굴절 계산의 면적과 강도 수치

NN 이름 누적면적 % 강도 평균 x/23
01 가장자리 굴절 렌즈 23.1 40.5 23/23
02 하이라이트 반사 39.6 57.7 20/23
03 색분리 가장자리 51.8 92.3 23/23
04 탄성 복귀 31.8 27.7 20/23
05 적응 틴트 74.7 30.6 23/23
06 패널 스택 깊이 37.9 30.8 23/23
07 눌림 물결 26.0 22.2 23/23
08 눌림 두께 스퀴즈 14.8 29.7 23/23
09 히어로 내비 장면 74.7 45.4 23/23

03이 강도 평균 92.3으로 가장 세게 픽셀을 바꾸는 이유는 세 채널을 서로 다른 좌표에서 따로 읽어 오는 만큼 원본과의 색 차이가 한 픽셀 안에서도 크게 벌어지기 때문이고, 08이 누적면적 14.8%로 가장 작은 것은 버튼 반경 안쪽만 계산 대상이라 화면 전체에서 차지하는 비중 자체가 작기 때문이다.

GLASS_REFRACTION_STRENGTH 값 하나가 리퀴드 글라스 디자인의 두께를 가르는 자리

굴절 폭·색분리 폭·복귀 시간, 이 세 값만 바꿔도 아홉 계산의 인상이 갈라진다. GLASS_REFRACTION_STRENGTH는 가장자리 배경이 얼마나 안쪽으로 눌려 들어가는지를, GLASS_CHROMA_SHIFT_PX는 채널별 표본 좌표가 얼마나 벌어지는지를, GLASS_ELASTIC_RETURN_MS는 감쇠 곡선이 0에 닿기까지 걸리는 시간을 정한다. 세 값 모두 픽셀 좌표 오프셋에 곱해지는 배수이지 색상표나 블러 반경이 아니어서, 값을 조금만 올려도 화면 전체 픽셀이 다시 계산되는 굴절의 세기 자체가 바뀐다.

리퀴드 글라스 카드를 실무 화면 어디에 쓰는가

정적인 유리 질감 대신 실제로 픽셀이 움직이는 굴절이 필요한 자리는 뒤에 콘텐츠가 계속 바뀌는 화면이다 — 히어로 섹션 유리 카드(01)나 제품 소개 패널(02)처럼 배경이 스크롤되거나 슬라이드되는 곳, 다크·라이트를 오가는 내비게이션 바(05)나 스크롤에 반응하는 고정 헤더(09)처럼 뒤에 깔리는 배경 밝기가 페이지마다 달라지는 곳이 특히 그렇다. 리퀴드 글래스 만들기를 드래그 가능한 위젯 창(04)이나 카드 여러 장을 겹친 갤러리(06), 결제·제출 버튼(07·08)에 적용하면 클릭·드래그마다 값이 실제로 다시 계산되므로, 배경이 고정된 정적 목업보다 상호작용을 눈으로 확인시키는 데 유리하다.

동작 줄임 설정과 저사양 기기에서 리퀴드 글라스가 깨지는 지점

prefers-reduced-motion: reduce가 걸리면 아홉 개 모두 requestAnimationFrame 루프 자체가 시작되지 않아 화면이 첫 프레임에서 그대로 굳지만, 07·08·09는 포인터·키보드·휠 이벤트가 들어올 때마다 draw를 그 순간에 한 번 더 불러 정지 화면에서도 눌림이나 스크롤 값 변화를 볼 수 있다. 지금은 160×100(16,000픽셀) 버퍼라 getImageData·putImageData 왕복이 가볍지만, 리퀴드 글래스 디자인 만들기를 실제 화면 해상도 그대로 올리면 픽셀 수가 수십 배로 뛰어 저사양 기기에서는 한 프레임 계산이 16ms를 넘어 끊기기 쉽다. 그리고 굴절은 뒤에 놓인 배경 픽셀을 당겨 오는 계산이라, 배경이 단색이거나 아예 없는 자리에 얹으면 좌표만 바뀔 뿐 눈에 보이는 왜곡이 거의 없다.

아홉 개의 완성된 소스와 측정값은 글 아래 암호 zip 안에 index.html·style.scss 그대로 들어 있다. 여는 암호는 k28ehrdh이고, 세 로케일 판의 받기 단추가 모두 이 한 암호를 공유한다.

굴절 강도 값 세 가지 변형과 reduce·키보드·글자 대비 접근성

이름 값 느낌
굴절 강도 값 조정 01 GLASS_REFRACTION_STRENGTH 8 → 28 값을 낮추면 판 안쪽 배경이 거의 그대로 비쳐 두께감이 사라지고, 값을 올리면 가장자리 배경이 눈에 띄게 눌려 들어가 훨씬 두꺼운 유리로 읽힌다.
색분리 폭 값 조정 03 GLASS_CHROMA_SHIFT_PX 1 → 4 값을 낮추면 가장자리 색 경계가 또렷하게 남고, 값을 올리면 빨강·초록·파랑 표본 좌표가 크게 벌어져 겹친 색이 확연히 갈라져 보인다.
탄성 복귀 시간 값 조정 04 GLASS_ELASTIC_RETURN_MS 180 → 520 값을 낮추면 손을 뗀 즉시 스프링이 빨리 잦아들어 오버슛이 금방 사라지고, 값을 올리면 같은 스프링이 훨씬 느리게 잦아들어 출렁이는 시간이 길게 늘어난다.

아홉 데모 모두 .lg__caption이 어두운 알약형 배경(rgba(23,20,26,.68)) 위에 밝은 글자색을 올려 두어, 뒤에서 배경이 계속 바뀌어도 캡션 글자 대비는 굴절 계산과 무관하게 고정된다. 키보드로는 07·08이 Enter·Space로 눌림을 일으키고, 09는 방향키·PageUp·PageDown이 마우스 휠과 똑같이 진행값을 움직여 스크롤 없이도 굴절 변화를 볼 수 있다. .is-demo 클래스는 reduce가 켜지면 함께 빠져, 정지된 화면에 커서 유도 스타일만 남는 일을 막는다.

글래스모피즘과 리퀴드 글라스는 같은 흐림 효과인가?

CSS backdrop-filter로 만드는 글래스모피즘은 뒤 배경을 흐리고 반투명하게 겹치는 필터 한 겹으로 끝나, 배경 좌표 자체는 그대로 두고 색만 섞는다. 여기 아홉 개는 getImageData로 픽셀을 직접 읽어 좌표를 옮긴 뒤 다시 그리는 굴절이라, 흐림 대신 배경이 실제로 휘어 보이는 렌즈 효과에 가깝다. 같은 픽셀 재배치를 정지된 장면 전체로 넓힌 결과는 캔버스 특수효과 9종에서, 사인파 여러 겹으로 화면 색 자체를 다시 칠하는 계산은 플라즈마 레인보우 필드 장면 9종에서 확인할 수 있다.

굴절 계산을 처음 붙일 때 자주 걸리는 질문

리퀴드 글라스 css 코드를 라이브러리 없이 그대로 복사해서 쓸 수 있나요?

그렇다. 아홉 개 모두 WebGL이나 외부 라이브러리 없이 순수 캔버스 2D getImageData·putImageData만으로 짜여 있어, zip 안 index.html을 열면 별도 설치 없이 그대로 동작한다. 다만 판 크기나 위치를 바꾸려면 PANEL_X0~PANEL_Y1 같은 좌표 상수를 함께 조정해야 굴절 계산 범위가 어긋나지 않는다.

09번 내비게이션 장면은 실제 스크롤 이벤트에 반응하나요?

데모 안에서는 페이지 스크롤이 아니라 패널 위에서 발생하는 wheel 이벤트와 방향키가 진행값을 직접 바꾼다. 실제 페이지 스크롤에 붙이려면 wantProgress에 넘기는 값을 window.scrollY를 문서 높이로 나눈 비율로 바꿔 주면 같은 굴절·서리 계산이 그대로 작동한다.

04번 탄성 복귀는 여러 번 출렁이며 움직이나요?

그렇다. elasticSpring(rt)가 Math.exp(-rt/GLASS_ELASTIC_RETURN_MS) 감쇠와 Math.cos(2π·rt/GLASS_ELASTIC_OSC_MS) 진동을 곱해, 손을 뗀 뒤 2~3회 오버슛하며 출렁이다가 0으로 잦아든다. GLASS_ELASTIC_RETURN_MS를 키우면 오버슛 횟수가 아니라 잦아드는 데 걸리는 시간만 늘어난다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.