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

qr 코드 만료 카운트다운 css 애니메이션 9가지

qr 코드 만료 표시는 값 하나로 아홉 장면을 만든다. 스캔 실패 때문도 아니고 그냥 오래 떠 있어서도 아니다 — qr 코드 만료까지 남은시간 비율 값 하나가 사라져야 링이 좁아지고, 각도가 깎이고, 끝에 닿으면 흔들린다.

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

아래 QR 격자는 모두 고정 시드 의사난수로 찍은 장식 패턴이다. 실제 데이터를 인코딩하지 않으므로 카메라로 스캔해도 읽히지 않는다 — 칸이 드러나는 순서·링이 좁아지는 반경·자릿수가 구르는 시차처럼 "값 하나가 움직이는 방식"만 보여주는 목업이다. 아홉 개는 그 값을 어디에 대입하느냐로 나뉜다 — 01·06·09는 지연·진폭·인덱스 값으로 모듈이나 코드를 순서대로 바꾸고, 02·03·04·08은 거리·비율·각도 값으로 링이나 코너의 밝기·크기를 바꾸며, 05·07은 판정·전환 시점 값으로 스프링이나 굴림 애니메이션을 튕긴다.

01지연 값 하나로 위에서 아래로 번지는 모듈 리빌 QR

생성 진행 값이 0에서 1로 오르는 동안, 행(row) 값을 기준으로 삼은 스태거 지연에 시드 난수 지터를 더한 값 하나가 각 모듈이 드러나는 순서를 정한다. 결제 QR 코드 생성 화면에서 격자가 위에서 아래로 조립되듯 채워지는 느낌을 만든다.

진행 값만큼 모듈이 하나씩 차오르는 QR 코드
새 창에서 열기

01 · 진행 값만큼 모듈이 하나씩 차오르는 QR 코드 — 측정 누적면적 17.923% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

QR_MODULE_REVEAL_STAGGER_MSbuildModulesqrRand
  var jitterRand = qrRand(20260924);
  var order = [];
  for (var y2 = 0; y2 < SIZE; y2 += 1) {
    for (var x2 = 0; x2 < SIZE; x2 += 1) {
      var jitter = (jitterRand() - 0.5) * QR_MODULE_REVEAL_STAGGER_MS;
      order.push({ x: x2, y: y2, delay: y2 * QR_MODULE_REVEAL_STAGGER_MS + jitter });
    }
  }
  var maxDelay = 0;
  order.forEach(function (o) { if (o.delay > maxDelay) maxDelay = o.delay; });
  var SWEEP_END = maxDelay + REVEAL_MS;

  function draw(elapsed) {
    var cw = canvas.width, ch = canvas.height;
    ctx.clearRect(0, 0, cw, ch);
    var cell = Math.min(cw, ch) / SIZE;
    var ox = (cw - cell * SIZE) / 2;
    var oy = (ch - cell * SIZE) / 2;
    var revealedCount = 0;
    for (var i = 0; i < order.length; i += 1) {
      var o = order[i];
      if (!modules[o.y][o.x]) continue;
      var local = elapsed - o.delay;
      var p = local <= 0 ? 0 : (local >= REVEAL_MS ? 1 : local / REVEAL_MS);

측정: 누적면적 17.9%, 강도 평균 83.8, 9/23 프레임에서 움직였다.

02스캔선과 모서리 사이 거리 값이 코너를 밝히는 QR

스캔선이 위아래로 오가며 매 프레임 각 모서리까지 남은 거리 값을 재고, 그 값이 0에 가까울수록 코너 표시의 두께와 밝기가 커진다. 매장 결제 스캔 대기 화면에서 카메라가 곧 인식할 것 같은 신호를 준다.

코너 프레임이 깜빡이며 인식을 기다리는 QR 코드
새 창에서 열기

02 · 코너 프레임이 깜빡이며 인식을 기다리는 QR 코드 — 측정 누적면적 28.194% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SCF_CORNER_GLOW_RANGE_PXcornerGlowseedRand
  var rand = seedRand(407025);
  var dots = [];
  for (var y = 0; y < SIZE; y += 1) {
    for (var x = 0; x < SIZE; x += 1) {
      if (rand() < 0.4) dots.push({ x: x, y: y });
    }
  }

  function cornerGlow(dist) {
    // 계산 한 겹: 스캔선까지 거리 → 코너 발광 강도(0~1), 범위 밖이면 0
    var p = 1 - dist / SCF_CORNER_GLOW_RANGE_PX;
    return p < 0 ? 0 : (p > 1 ? 1 : p);
  }

  function draw(elapsed) {
    var cw = canvas.width, ch = canvas.height;
    ctx.clearRect(0, 0, cw, ch);

    var pad = 28;
    var boxX = pad, boxY = pad, boxW = cw - pad * 2, boxH = ch - pad * 2;

측정: 누적면적 28.2%, 강도 평균 62.6, 23/23 프레임에서 움직였다.

03남은시간 비율이 그대로 반경 배율이 되는 펄스 링 QR

루프 안에서 남은시간 비율 값이 0.4~1 사이 배율로 바뀌어 매 파동의 최대 반경을 정하므로, 남은시간이 줄수록 링이 더 좁게 퍼진다. 쿠폰 QR 코드의 만료 임박을 링 크기 하나로 보여준다.

남은시간 값만큼 좁아지는 펄스 링 QR 코드
새 창에서 열기

03 · 남은시간 값만큼 좁아지는 펄스 링 QR 코드 — 측정 누적면적 40.212% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

PULSE_RING_INTERVAL_MSringsForMAX_RADIUS
    // 계산 한 겹: 루프 안 남은 유효시간 비율이 곧 그대로 링의 최대 반경 스케일이 된다
    // (남은 시간이 줄수록 링이 더 작게 퍼진다 = 만료 임박 표시).
    var remain = 1 - (elapsed % LOOP_MS) / LOOP_MS;
    var radiusScale = 0.4 + 0.6 * remain;
    var strokeBase = Math.max(2.5, cw * 0.02);

    ctx.globalCompositeOperation = 'lighter';
    ringsFor(elapsed).forEach(function (p) {
      var r = p * MAX_RADIUS * radiusScale;
      var alpha = (1 - p) * 0.5;
      // 글로우 패스 — 굵고 옅은 띠를 먼저 얹어 화면에 퍼지는 면적을 넓힌다.
      ctx.strokeStyle = 'rgba(120,200,255,' + (alpha * 0.55).toFixed(3) + ')';
      ctx.lineWidth = strokeBase * 3.2;
      ctx.beginPath();
      ctx.arc(cx, cy, r, 0, Math.PI * 2);
      ctx.stroke();

측정: 누적면적 40.2%, 강도 평균 46.9, 23/23 프레임에서 움직였다.

04남은시간 값이 각도로 깎이는 만료 카운트다운 링

경과 비율 값이 0~360도 각도로 바뀌어 링을 시계 방향으로 깎아내고, 남은시간이 20% 아래로 내려가면 같은 urgency 값이 링 색·배경 톤·외곽 글로우를 한꺼번에 붉게 조정한다. 결제 QR 코드의 유효시간 카운트다운에 쓴다 — 데모의 30초는 시연 안에서만 빠르게 도는 가상 시계 배속을 곱한 결과다(뒤 "함정" 절에서 다룬다).

남은시간 값이 각도 값으로 깎이는 카운트다운 링
새 창에서 열기

04 · 남은시간 값이 각도 값으로 깎이는 카운트다운 링 — 측정 누적면적 61.73% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

QR_EXPIRE_DURATION_MSDEMO_ACCEL_FACTORurgency
    // 계산 한 겹: 경과 비율 → 각도(0~360) 로 링을 깎고, 만료 근접(20% 이하)일수록
    // 파랑→빨강으로 색을 보간한다. 같은 urgency 값이 배경 톤·링 외곽 글로우에도 반영돼
    // 화면 넓은 면이 함께 바뀐다.
    var ratio = Math.min(1, virtualElapsed / QR_EXPIRE_DURATION_MS);
    var remainRatio = 1 - ratio;
    var angle = ratio * 360;
    var urgency = remainRatio < 0.2 ? (0.2 - remainRatio) / 0.2 : 0;
    var r1 = lerp(120, 255, urgency), g1 = lerp(200, 90, urgency), b1 = lerp(255, 90, urgency);
    var ringColor = 'rgb(' + Math.round(r1) + ',' + Math.round(g1) + ',' + Math.round(b1) + ')';

    // 카드 배경 밝기 — urgency 가 오를수록 전체 캔버스가 옅은 붉은 톤으로 밝아진다.
    var bg = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.max(cw, ch) * 0.72);
    bg.addColorStop(0, 'rgba(' + Math.round(lerp(40, 120, urgency)) + ',' + Math.round(lerp(30, 26, urgency)) + ',' + Math.round(lerp(48, 30, urgency)) + ',1)');
    bg.addColorStop(1, 'rgba(18,20,30,1)');
    ctx.fillStyle = bg;
    ctx.fillRect(0, 0, cw, ch);

    var ringWidth = Math.max(6, cw * 0.034);

    // 링 외곽 글로우 — urgency 가 오를수록 더 크고 옅게 번진다.
    ctx.globalCompositeOperation = 'lighter';
    ctx.strokeStyle = 'rgba(' + Math.round(r1) + ',' + Math.round(g1) + ',' + Math.round(b1) + ',' + (0.16 + urgency * 0.22).toFixed(3) + ')';
    ctx.lineWidth = ringWidth * (2.4 + urgency * 1.6);
    ctx.beginPath();
    ctx.arc(cx, cy, radius, -Math.PI / 2, -Math.PI / 2 + (angle * Math.PI / 180));

측정: 누적면적 61.7%, 강도 평균 81.0, 23/23 프레임에서 움직였다.

05인식 판정 값이 스프링 곡선으로 튕기는 체크 QR

스캔 인식 시점부터 지난 시간을 감쇠 코사인 함수 하나에 넣어, 목표 크기를 살짝 넘었다가 되돌아오는 스프링 스케일 값을 만든다. 티켓 QR 코드가 인식되는 순간 체크 배지가 통통 튀며 완료를 알린다.

인식되는 순간 체크 모양으로 접히는 QR 코드
새 창에서 열기

05 · 인식되는 순간 체크 모양으로 접히는 QR 코드 — 측정 누적면적 6.892% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SUCCESS_CHECK_SPRING_OVERSHOOTspringScalePOP_START
  // 계산 한 겹: 감쇠 스프링 근사 — overshoot 비율만큼 목표를 넘었다가 되돌아오는
  // 단일 코사인 감쇠식으로 배지 스케일을 만든다 (linear tween 아님).
  function springScale(p, overshoot) {
    if (p <= 0) return 0;
    if (p >= 1) return 1;
    var decay = Math.exp(-4.5 * p);
    return 1 + overshoot * decay * Math.cos(p * Math.PI * 2.2);
  }

  function clamp01(v) { return v < 0 ? 0 : (v > 1 ? 1 : v); }

  function draw(elapsed) {
    var cw = canvas.width, ch = canvas.height;
    ctx.clearRect(0, 0, cw, ch);
    var cx = cw / 2, cy = ch / 2;

    var drawP = clamp01((elapsed - DRAW_START) / (DRAW_END - DRAW_START));
    var popP = clamp01((elapsed - POP_START) / (POP_END - POP_START));

측정: 누적면적 6.9%, 강도 평균 119.7, 14/23 프레임에서 움직였다.

06진행 값이 커질수록 흔들림이 잦아드는 생성 QR

생성 진행 값이 오를수록 지수 감쇠로 줄어드는 진폭 값 하나가 모듈마다 다른 위상·방향의 흔들림을 만들어, 노이즈처럼 떨리던 격자가 점점 자리를 잡는다. 신규 QR 코드 발급 애니메이션에 쓴다.

흔들림 폭이 줄며 자리 잡는 생성 중 QR 코드
새 창에서 열기

06 · 흔들림 폭이 줄며 자리 잡는 생성 중 QR 코드 — 측정 누적면적 10.958% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

QR_NOISE_SETTLE_AMPLITUDE_PXSETTLE_MSwobble
    // 계산 한 겹: 진폭이 시간에 따라 지수 감쇠 — 초기 QR_NOISE_SETTLE_AMPLITUDE_PX 에서
    // SETTLE_MS 동안 0으로 잦아들며, 모듈마다 다른 위상·방향으로 흔들려 자리를 잡는다.
    var settleP = Math.min(1, elapsed / SETTLE_MS);
    var amp = QR_NOISE_SETTLE_AMPLITUDE_PX * Math.exp(-4 * settleP);

    ctx.fillStyle = 'rgb(255,247,230)';
    modules.forEach(function (m) {
      var wobble = Math.sin(elapsed / 1000 * m.freq + m.phase) * amp;
      var jx = Math.cos(m.dir) * wobble;
      var jy = Math.sin(m.dir) * wobble;
      var s = cell * 0.68;
      var cx = ox + m.x * cell + cell / 2 + jx;
      var cy = oy + m.y * cell + cell / 2 + jy;
      ctx.globalAlpha = 0.55 + 0.45 * settleP;
      ctx.fillRect(cx - s / 2, cy - s / 2, s, s);
    });
    ctx.globalAlpha = 1;

    badge.textContent = settleP >= 1 ? '코드 생성 완료' : '코드 생성 중 ' + Math.round(settleP * 100) + '%';

측정: 누적면적 11.0%, 강도 평균 44.7, 23/23 프레임에서 움직였다.

07자릿수 지연 값이 시차를 두고 넘어가는 금액 패널

결제 금액이 바뀔 때마다 각 자릿수가 자리 순서만큼 지연 값을 받아, 그 값이 지난 자릿수부터 차례로 세로로 굴러 이전 숫자에서 새 숫자로 넘어간다. 가맹점 결제 금액 실시간 표시에 쓴다.

결제 금액 자릿수가 굴러 바뀌는 QR 코드 패널
새 창에서 열기

07 · 결제 금액 자릿수가 굴러 바뀌는 QR 코드 패널 — 측정 누적면적 11.454% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

AMOUNT_ROLL_DIGIT_STAGGER_MSeaseOutBackROLL_MS
  function easeOutBack(t) {
    var c1 = 1.4, c3 = c1 + 1;
    return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
  }

  function progressFor(elapsed, segStart, delay) {
    var local = elapsed - segStart - delay;
    if (local <= 0) return 0;
    if (local >= ROLL_MS) return 1;
    return local / ROLL_MS;
  }

  function render(elapsed) {
    var anyMoving = false;

    // 계산 한 겹: 두 번째 구간(감소 방향)이 시작됐는지는 컬럼별 지연 없이 구간 전체로 판정한다 —
    // 첫 구간 꼬리와 겹치지 않도록 SEG2_START 를 첫 구간 최대 종료(ROLL_END)보다 뒤에 둔다.
    var seg2Active = elapsed >= SEG2_START;

    cols.forEach(function (c) {

측정: 누적면적 11.5%, 강도 평균 74.0, 20/23 프레임에서 움직였다.

08남은시간 값이 0에 닿는 순간 커지는 흔들림 QR

남은시간 비율이 0에 닿는 순간부터 지난 시간을 감쇠 사인파 하나에 넣어 흔들림 값을 만들고, 같은 값의 크기를 그림자 번짐·배경 톤에도 그대로 퍼뜨린다. 만료된 결제 QR 코드에서 재발급을 유도할 때 쓴다.

남은시간 값이 0에 닿으면 흔들리는 만료 QR 코드
새 창에서 열기

08 · 남은시간 값이 0에 닿으면 흔들리는 만료 QR 코드 — 측정 누적면적 32.133% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

EXPIRED_SHAKE_AMPLITUDE_PXshakeOffsetSHAKE_FREQ
  // 계산 한 겹: 진동수(SHAKE_FREQ)와 지수 감쇠를 곱한 감쇠 사인파로 흔들림을 만든다.
  function shakeOffset(t) {
    if (t < 0 || t > SHAKE_MS) return 0;
    var p = t / SHAKE_MS;
    return Math.sin(p * Math.PI * 2 * SHAKE_FREQ) * EXPIRED_SHAKE_AMPLITUDE_PX * Math.exp(-3.2 * p);
  }

  // 자동 재생에서 만료 구간 판정: 첫 만료(EXPIRE_AT~RETRY_AT) · 두 번째 만료(EXPIRE_AT2~루프 끝) 두 번
  function autoExpired(elapsed) {
    return (elapsed >= EXPIRE_AT && elapsed < RETRY_AT) || elapsed >= EXPIRE_AT2;
  }
  function autoExpireStart(elapsed) {
    return elapsed < RETRY_AT ? EXPIRE_AT : EXPIRE_AT2;
  }

  var manualRetryAt = -1;

  function render(elapsed) {
    var expired = manualRetryAt < 0 && autoExpired(elapsed);
    var t = manualRetryAt >= 0 ? elapsed - manualRetryAt : elapsed - autoExpireStart(elapsed);
    var offset = expired ? shakeOffset(t) : 0;
    panel.style.transform = offset ? 'translateX(' + offset.toFixed(2) + 'px)' : 'none';

측정: 누적면적 32.1%, 강도 평균 65.1, 23/23 프레임에서 움직였다.

09인덱스 값이 거리로 바뀌어 코드가 넘어가는 캐러셀

경과 시간을 한 단계 지속시간으로 나눈 몫이 인덱스 값이 되고, 각 코드와 그 인덱스 사이 거리 값 하나가 원근 스케일·투명도·좌우 이동량을 한꺼번에 정한다. 멤버십 앱에서 쿠폰·포인트·결제 코드를 옆으로 넘기는 화면에 쓴다.

여러 코드가 인덱스 값을 따라 넘어가는 QR 캐러셀
새 창에서 열기

09 · 여러 코드가 인덱스 값을 따라 넘어가는 QR 캐러셀 — 측정 누적면적 18.378% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

MULTI_CODE_CAROUSEL_STEP_MSeaseInOutgrids
  function draw(elapsed) {
    lastElapsed = elapsed;
    ctx.clearRect(0, 0, cw, ch);

    var raw = elapsed / MULTI_CODE_CAROUSEL_STEP_MS;
    var index = Math.floor(raw) % COUNT;
    var localT = raw - Math.floor(raw);
    var eased = easeInOut(localT);
    var pos = index + eased;

    for (var i = 0; i < COUNT; i += 1) {
      // 계산 한 겹: 현재 위치와의 거리 → 원근 스케일·투명도·좌우 이동량을 함께 구한다.
      var diff = i - pos;
      while (diff > COUNT / 2) diff -= COUNT;
      while (diff < -COUNT / 2) diff += COUNT;
      var dist = Math.abs(diff);
      if (dist > 1.8) continue;
      var scale = Math.max(0.35, 1 - dist * 0.42);
      var alpha = Math.max(0, 1 - dist * 0.55);
      var cx = cw / 2 + diff * stepX;

      var box = boxBase * scale;
      ctx.globalAlpha = alpha;
      ctx.fillStyle = 'rgba(20,26,38,.92)';
      ctx.fillRect(cx - box / 2, cy - box / 2, box, box);

측정: 누적면적 18.4%, 강도 평균 118.8, 23/23 프레임에서 움직였다.

css 로딩바 애니메이션 아홉 개가 남은시간 값 하나로 갈라지는 세 갈래

아홉 개 전부 같은 축 하나에서 출발한다 — 생성 진행 값이거나 남은시간 비율 값이다. 이 값이 어디에 대입되느냐로 장면이 갈린다. 01·06은 지연·진폭처럼 모듈 하나하나의 등장 시점에 대입하고, 02·03·04·08은 거리·비율·각도로 바꿔 링이나 코너의 크기·색·흔들림에 대입하며, 07·09는 자리·인덱스 값으로 전환 그 자체(자릿수가 구르는 순서, 코드가 넘어가는 순서)를 대입한다. 05는 예외로 판정 시점(인식 순간) 값 하나를 스프링 곡선에 넣어 튕긴다. 캔버스 2D API로 매 프레임 좌표·반경·각도를 직접 계산해 그리므로, 속성을 트윈만 하는 것과 달리 값이 바뀌는 그 순간 화면 전체의 밝기·두께·위치가 같이 갱신된다 — MDN Canvas API 문서에 이 렌더링 방식의 기본기가 정리돼 있다.

9개 패턴이 화면을 실제로 얼마나 바꿨는지 — 프레임별 실측

렌더 화면을 24장 찍어 바로 앞 장과 비교한 수치다. 움직인프레임 칸은 그 24장을 잇는 23번의 비교 가운데 실제로 화면이 달라진 횟수를 센다.

항목 누적면적% 강도평균 움직인프레임
01 모듈 리빌 17.9 83.8 9/23
02 코너 프레임 28.2 62.6 23/23
03 펄스 링 만료 40.2 46.9 23/23
04 만료 카운트다운 링 61.7 81.0 23/23
05 스캔 성공 체크 6.9 119.7 14/23
06 생성 노이즈 정착 11.0 44.7 23/23
07 금액 롤 스왑 11.5 74.0 20/23
08 만료 흔들림 재시도 32.1 65.1 23/23
09 멀티 코드 캐러셀 18.4 118.8 23/23

04가 61.7%로 가장 넓게 움직인 이유는 전체 화면(배경·링·외곽 글로우)이 urgency 값 하나로 함께 바뀌기 때문이고, 05가 6.9%로 가장 좁은 이유는 체크 배지 하나만 짧게 튕기는 동작이라서다 — 항목 성격이 다르면 면적이 좁은 쪽이 오히려 정상이라 억지로 면적을 키우지 않았다.

결제·쿠폰·티켓 화면에 이 아홉 개를 놓는 자리

결제 QR 코드 생성·검증 흐름에서는 01(생성) → 04(유효시간) → 08(만료) 순서로 이어 붙이면 같은 화면 하나가 생성부터 재발급까지 상태를 전부 보여준다. 매장 결제 스캔 대기 화면에는 02를, 인식 완료에는 05를 붙이는 식으로 진행 상태마다 화면을 새로 그리지 않고 같은 캔버스 위에서 값만 갈아 끼울 수 있다. 매장 대기 화면에 흔한 css 로딩 스피너 하나로 뭉뚱그리지 않고 02처럼 상태를 보여주는 코너 표시를 놓으면 대기 이유가 더 분명해지고, 정적인 로딩바 디자인 대신 QR 자체가 진행·만료 상태를 보여주면 사용자가 곁의 문구를 따로 읽지 않아도 된다. 09는 멤버십 앱처럼 코드 여러 개를 한 화면에서 오가야 하는 곳에 붙인다.

시연 배속을 지우지 않으면 깨지는 함정

04의 가상 시계 배속을 실서비스에 그대로 옮기면

04의 QR_EXPIRE_DURATION_MS 는 30000(30초)으로 그대로 두지만, 데모는 DEMO_ACCEL_FACTOR(15배) 를 곱한 가상 시계로 짧은 측정 창 안에 전체 카운트다운을 압축해서 보여준다. 이 배속은 root.classList.contains('is-demo') 로 감지한 시연 전용 클래스에만 걸리므로, 코드를 그대로 복사하되 is-demo 클래스를 붙인 채로 실서비스에 올리면 30초짜리 만료가 순식간에 끝나 버린다. 값을 실사용에 옮길 때는 이 클래스를 빼서 배속 자체를 제거해야 한다.

저사양 기기에서 캔버스 프레임이 밀리면

아홉 개 전부 매 프레임 clearRect 로 캔버스를 비우고 다시 그리므로, 저사양 기기에서 프레임이 밀리면 04의 색 보간이나 08의 흔들림처럼 값이 크게 바뀌는 항목일수록 뚝뚝 끊겨 보인다. requestAnimationFrame 지원이 불안정한 낡은 환경에서는 재생을 직접 멈춰 마지막 프레임만 정적으로 보여주는 편이 낫다.

직접 코드를 열어 이 값이 어디까지 퍼지는지 대조해 보고 싶다면 wkt849wy으로 아홉 개 데모의 HTML·SCSS·JS 원본을 그대로 받아 확인하면 된다.

값 하나만 바꿔보는 변형 세 가지

01의 QR_MODULE_REVEAL_STAGGER_MS 는 모듈이 드러나는 행 간 지연 값이다 — 18ms일 때는 모듈이 거의 한꺼번에 쏟아지듯 채워지지만, 36ms로 늘리면 한 칸씩 눈에 보이게 천천히 채워진다. 04의 QR_EXPIRE_DURATION_MS 는 만료까지 걸리는 지속시간 값으로, 30000ms에서 15000ms로 줄이면 카운트다운 링이 훨씬 빠르게 깎여 나가 다급한 인상을 준다. 06의 QR_NOISE_SETTLE_AMPLITUDE_PX 는 생성 중 흔들림의 최대 진폭 값이며, 6px에서 2px로 줄이면 흔들림이 약해져 처음부터 거의 안정된 것처럼 보인다.

항목 상수 전 → 후
01 QR_MODULE_REVEAL_STAGGER_MS 18ms → 36ms
04 QR_EXPIRE_DURATION_MS 30000ms → 15000ms
06 QR_NOISE_SETTLE_AMPLITUDE_PX 6px → 2px

접근성 — reduced-motion 값 하나로 아홉 개가 멈추는 법

아홉 개 전부 window.matchMedia('(prefers-reduced-motion: reduce)').matches 값을 시작할 때 한 번 읽어, 참이면 requestAnimationFrame 루프 자체를 돌리지 않고 각 데모를 완결된 정지 프레임 하나로 고정한다. 01은 전체 스윕이 끝난 상태, 04는 남은시간이 다 닳아 정적으로 붉게 멈춘 상태, 09는 첫 코드에 고정된 상태처럼 항목마다 그 장면이 "다 끝났을 때"의 모습으로 멈춘다. reduced-motion 이 켜진 화면에서는 재생 버튼 대신 정지된 QR과 상태 문구만 남고, 캔버스 배경 톤을 살짝 낮춰 정적인 화면임을 알리는 항목도 있다. MDN prefers-reduced-motion 문서에 이 미디어 쿼리의 기준이 정리돼 있다.

395·399·403편과 무엇이 다른가?

캔버스 로딩 애니메이션 효과 9가지 — 코드 그대로도 캔버스 2D 로 직접 그리지만 물리 시뮬레이션(중력·충돌) 값이 축이고, 이 편은 시간·거리·인덱스 같은 상태값이 축이라 물리 연산이 없다. css 로딩바 애니메이션 9가지 — 3D 계산형 로더는 정점·면 값을 3D 좌표로 투영하는 계산이 중심이지만, 이 편은 평면 캔버스 위에서 반경·각도·거리 같은 2D 값만 계산한다. AI 로딩 애니메이션 9가지 — 값 하나로 도는 처리중 표시는 대기 시간이라는 불확실한 값을 다루는 반면, 이 편의 04·08은 정해진 만료 시각까지 정확히 줄어드는 값을 다룬다는 점이 다르다.

qr 코드 만료 카운트다운을 적용하기 전에 자주 나오는 질문

이 QR 격자가 실제 QR 코드로 스캔되나요?

아니요. 격자는 고정 시드로 찍은 의사난수 장식 패턴이고 실제 데이터를 인코딩하지 않는다. 화면에 QR 모양의 움직임을 보여주는 목업이므로 카메라로 스캔해도 읽히지 않는다.

04의 30초 만료가 데모에서는 왜 훨씬 빨리 끝나나요?

데모는 30초 만료를 짧은 측정 창 안에서 보여주려고 DEMO_ACCEL_FACTOR(15배) 가상 시계 배속을 곱해서 재생한다. QR_EXPIRE_DURATION_MS 상수 자체는 30000(30초) 그대로이고, 실서비스에서는 이 배속을 곱하는 is-demo 클래스를 빼면 원래 속도로 돌아간다.

로딩 스피너 디자인 대신 이 아홉 개를 그대로 붙여도 되나요?

그대로 붙여도 된다. 아홉 개 전부 requestAnimationFrame 과 캔버스 2D API 만으로 동작해 외부 라이브러리가 필요 없고, zip 안 HTML·SCSS·JS 코드를 그대로 옮기면 된다.

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

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