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

css 버튼 클릭 애니메이션 9가지 — 캔버스 계산판

버튼이 유리처럼 금 가 보이는 원인은 눌림 상태 값이 켜질 때 뻗는 균열 각도 값 하나다. 2번은 그 값으로 균열선을 그리고, 5번은 같은 값을 테두리를 도는 전류 굵기로 다시 쓰며, 8번은 눌린 좌표를 중심으로 같은 값만큼 화면을 밀어 물결로 되돌린다.

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

아홉 가지 버튼 클릭 애니메이션이 같은 계산 방식을 쓰는 이유

아래 아홉 개는 모두 <canvas> 위에서 CanvasRenderingContext2D로 직접 계산해 그립니다. 프레임마다 값을 갱신하는 루프는 전부 requestAnimationFrame으로 돌고, 입력은 click·keydown(Enter·Space)·pointerdown(좌표 포함)·resize 네 가지를 공통으로 받습니다. 자동 재생 구간은 Math.random()을 한 번도 쓰지 않고, 고정 시드 LCG(선형합동생성기)로 같은 순서의 난수를 매번 똑같이 재생합니다. 외부 라이브러리·WebGL·CDN 은 아홉 항목 전부 0개이고, 코드를 그대로 옮겨 붙이면 바로 동작합니다.

01클릭 좌표에서 번지는 잉크 확산 버튼

누른 좌표를 중심으로 반경 값이 프레임마다 늘어나고 알파 값이 함께 줄어드는 원형 번짐을 그려, 잉크가 종이에 스며들 듯 클릭 지점에서 퍼져 나간다.

클릭 좌표에서 번지는 잉크 확산 버튼
새 창에서 열기

01 · 클릭 좌표에서 번지는 잉크 확산 버튼 — 측정 누적면적 29.288% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

DIFFUSE_RATEinjectAtdiffuseStep
  function diffuseStep() {
    for (var y = 0; y < ROWS; y++) {
      for (var x = 0; x < COLS; x++) {
        var i = idx(x, y);
        var sum = 0, n = 0;
        if (x > 0) { sum += field[idx(x - 1, y)]; n++; }
        if (x < COLS - 1) { sum += field[idx(x + 1, y)]; n++; }
        if (y > 0) { sum += field[idx(x, y - 1)]; n++; }
        if (y < ROWS - 1) { sum += field[idx(x, y + 1)]; n++; }
        var avg = n ? sum / n : field[i];
        var v = field[i] + (avg - field[i]) * DIFFUSE_RATE;
        next[i] = v * DECAY_PER_FRAME;
      }
    }
    var tmp = field; field = next; next = tmp;
  }

DIFFUSE_RATE가 이웃 칸과 섞이는 비율을, DECAY_PER_FRAME이 매 프레임 옅어지는 속도를 정하며, 확인 버튼 클릭 피드백이나 폼 제출 버튼에 붙이면 어울립니다.

02누름 상태 값에 따라 갈라지는 균열선 버튼

눌림 상태 값이 켜지는 순간 중심에서 바깥으로 뻗는 균열 가지 몇 개를 각도 값으로 정해 그리고, 그 각도가 갈라지는 방향과 개수를 함께 결정한다.

누름 상태 값에 따라 갈라지는 균열선 버튼
새 창에서 열기

02 · 누름 상태 값에 따라 갈라지는 균열선 버튼 — 측정 누적면적 20.131% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

BTN_CANVAS_CRACK_BRANCH_COUNTgenBranchbuildTree
  function genBranch(x, y, angle, len, depth, t0, segs) {
    if (depth > CRACK_MAX_DEPTH || len < 5) return;
    var ang = angle + (rnd() - 0.5) * 0.35;
    var x2 = x + Math.cos(ang) * len;
    var y2 = y + Math.sin(ang) * len;
    var dur = 120 + len * 4;
    segs.push({ x1: x, y1: y, x2: x2, y2: y2, t0: t0, t1: t0 + dur, w: Math.max(1, 3 - depth) });
    var childT0 = t0 + dur * 0.6;
    var childA = [ang + CRACK_ANGLE_SPREAD * 0.5, ang - CRACK_ANGLE_SPREAD * 0.5];
    for (var i = 0; i < childA.length; i++) {
      genBranch(x2, y2, childA[i] + (rnd() - 0.5) * 0.3, len * CRACK_LEN_DECAY, depth + 1, childT0, segs);
    }
  }

  function buildTree(cx, cy) {
    var segs = [];
    for (var i = 0; i < BTN_CANVAS_CRACK_BRANCH_COUNT; i++) {
      var base = (Math.PI * 2 * i) / BTN_CANVAS_CRACK_BRANCH_COUNT;
      var ang = base + (rnd() - 0.5) * 0.5;
      genBranch(cx, cy, ang, CRACK_SEG_LEN, 0, 0, segs);
    }
    return segs;
  }

  var trees = [];

BTN_CANVAS_CRACK_BRANCH_COUNT가 1차 균열 가지 수를 정하고, 삭제나 위험 액션을 확인받는 버튼에 붙이면 클릭이 되돌릴 수 없다는 느낌을 줍니다.

03드래그 속도 값을 따라 흩어지는 잔불 트레일 버튼

포인터가 움직인 속도 값만큼 잔불 입자를 매 프레임 새로 만들고, 입자마다 자기 속도 값이 중력 값으로 서서히 줄어들며 흩어져 사라진다.

드래그 속도 값을 따라 흩어지는 잔불 트레일 버튼
새 창에서 열기

03 · 드래그 속도 값을 따라 흩어지는 잔불 트레일 버튼 — 측정 누적면적 28.606% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GRAVITYspawnEmbersstepParticles
  function spawnEmbers(x, y, speed, now) {
    var count = Math.max(1, Math.min(9, Math.round(speed / 26)));
    for (var i = 0; i < count; i++) {
      var ang = rnd() * Math.PI * 2;
      var spd = 24 + rnd() * 96;
      particles.push({
        x: x, y: y,
        vx: Math.cos(ang) * spd * 0.34,
        vy: Math.sin(ang) * spd * 0.34 - 34 - rnd() * 46,
        born: now,
        life: EMBER_LIFE_MIN + rnd() * EMBER_LIFE_SPAN,
        size: 2.6 + rnd() * 3.2
      });
    }
  }

GRAVITY 값이 입자가 아래로 처지는 속도를 정하고, 드래그형 재생·전송 버튼에 붙이면 놓는 순간의 관성이 느껴집니다.

04포인터 거리 값에 따라 뭉쳤다 퍼지는 리퀴드 메탈 버튼

버튼 표면에 흩어진 여러 방울마다 포인터까지의 거리 값을 계산해 세기 값으로 바꾸고, 그 세기 값들을 합친 장(場)이 문턱 값을 넘는 자리끼리 하나로 뭉쳐 보이게 그린다.

포인터 거리 값에 따라 뭉쳤다 퍼지는 리퀴드 메탈 버튼
새 창에서 열기

04 · 포인터 거리 값에 따라 뭉쳤다 퍼지는 리퀴드 메탈 버튼 — 측정 누적면적 51.592% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

fieldAtBLOB_COUNTbuildBlobs
  function fieldAt(px, py, now) {
    var burstT = now < burstUntil ? clamp01((burstUntil - now) / BURST_MS) : 0;
    var burstBoost = burstT * BURST_BOOST;
    var sum = 0;
    for (var i = 0; i < blobs.length; i++) {
      var b = blobs[i];
      var boost = burstBoost;
      if (pointer) {
        var dpx = pointer.x - b.x, dpy = pointer.y - b.y;
        var distToPointer = Math.sqrt(dpx * dpx + dpy * dpy);
        boost += POINTER_BOOST * Math.exp(-distToPointer / POINTER_FALLOFF);
      }
      var effR = b.r * (1 + boost);
      var dx = px - b.x, dy = py - b.y;
      var dist2 = dx * dx + dy * dy + 1;
      sum += (effR * effR) / dist2;
    }
    return sum;
  }

fieldAt은 방울마다 (반경²)/거리²를 더하는 메타볼 계산이고, 플로팅 액션 버튼이나 커서를 따라다니는 CTA에 붙이면 자연스럽습니다.

05호버 세기 값이 커질수록 굵어지는 전기 아크 테두리 버튼

버튼 테두리를 따라 구간마다 흔들림 값을 다르게 주고, 호버 세기 값이 오를수록 그 흔들림 폭과 선 굵기가 함께 커져 테두리 전체가 전류처럼 떨리게 그린다.

호버 세기 값이 커질수록 굵어지는 전기 아크 테두리 버튼
새 창에서 열기

05 · 호버 세기 값이 커질수록 굵어지는 전기 아크 테두리 버튼 — 측정 누적면적 23.585% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

BTN_CANVAS_ARC_JITTER_MSARC_SEGMENTSstampGlow
    for (var gy = 0; gy < GLOW_ROWS; gy++) {
      for (var gx = 0; gx < GLOW_COLS; gx++) {
        var v = Math.min(1, field[gy * GLOW_COLS + gx] / 2.4);
        data[idx] = GLOW_COLOR[0]; data[idx + 1] = GLOW_COLOR[1]; data[idx + 2] = GLOW_COLOR[2];
        data[idx + 3] = (v * 255) | 0;
        idx += 4;
      }
    }
    sctx.putImageData(img, 0, 0);

    ctx.fillStyle = 'rgb(' + BG[0] + ',' + BG[1] + ',' + BG[2] + ')';
    ctx.fillRect(0, 0, W, H);
    ctx.imageSmoothingEnabled = true;
    ctx.drawImage(small, 0, 0, GLOW_COLS, GLOW_ROWS, 0, 0, W, H);

    var lineWidth = 1 + intensity * CRACK_VALUE_ECHO;
    ctx.beginPath();
    for (var q = 0; q < pts.length; q++) {
      if (q === 0) ctx.moveTo(pts[q].x, pts[q].y); else ctx.lineTo(pts[q].x, pts[q].y);
    }
    ctx.closePath();
    ctx.lineWidth = lineWidth;
    ctx.strokeStyle = 'rgba(' + GLOW_COLOR[0] + ',' + GLOW_COLOR[1] + ',' + GLOW_COLOR[2] + ',' + (0.55 + intensity * 0.45).toFixed(3) + ')';
    ctx.stroke();

선 굵기를 정하는 CRACK_VALUE_ECHO는 2번 항목의 균열 가지 개수와 같은 값 6을 그대로 물려받고, 강조 CTA나 위험·경고 버튼을 눈에 띄게 만들 때 씁니다.

06비활성 값이 켜지면 연기처럼 흩어지는 버튼

비활성 값이 켜지는 순간 버튼 표면을 작은 격자로 나눠 각 칸마다 흩어지는 방향 값과 사라지는 속도 값을 주고, 칸들이 그 값을 따라 위로 흩어지며 옅어진다.

비활성 값이 켜지면 연기처럼 흩어지는 버튼
새 창에서 열기

06 · 비활성 값이 켜지면 연기처럼 흩어지는 버튼 — 측정 누적면적 22.697% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ASSEMBLE_SPEEDroundRecttoggle
  function render(dt) {
    assembly += (target - assembly) * Math.min(1, dt * ASSEMBLE_SPEED);
    ctx.clearRect(0, 0, W, H);
    for (var i = 0; i < shards.length; i++) {
      var s = shards[i];
      var local = Math.max(0, Math.min(1, (assembly - s.delay) / (1 - s.delay)));
      var scatter = 1 - local;
      var px = s.ox + s.dirX * scatter * scatter;
      var py = s.oy + s.dirY * scatter * scatter - scatter * 18;
      var alpha = local;
      if (alpha <= 0.01) continue;
      ctx.save();
      ctx.globalAlpha = alpha;
      ctx.translate(px + s.w / 2, py + s.h / 2);
      ctx.rotate(s.spin * scatter);
      ctx.fillStyle = 'rgb(' + s.r + ',' + s.g + ',' + s.b + ')';
      ctx.fillRect(-s.w / 2, -s.h / 2, s.w, s.h);
      ctx.restore();
    }
  }

ASSEMBLE_SPEED가 칸이 도로 모이는 속도를 정해 토글을 되돌릴 때도 쓸 수 있고, 조건 미충족 버튼이나 재고 소진 버튼에 붙입니다.

07진행 값만큼 모래처럼 쌓이는 프로그레스 버튼

진행 값이 오를 때마다 버튼 안쪽 위에서 모래 알갱이를 하나씩 더 떨어뜨리고, 각 알갱이는 바닥 높이 값에 부딪힐 때까지 떨어지다 쌓여 진행률만큼 채운 것처럼 보이게 한다.

진행 값만큼 모래처럼 쌓이는 프로그레스 버튼
새 창에서 열기

07 · 진행 값만큼 모래처럼 쌓이는 프로그레스 버튼 — 측정 누적면적 21.685% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

hashNoisespawnGrainupdatePile
  function spawnGrain() {
    var col = Math.floor(rnd() * COLS);
    var size = 3 + rnd() * 3;
    grains.push({ x: BX + col * COL_W + COL_W / 2, y: SPAWN_Y, vy: 40 + rnd() * 30, size: size, col: col });
  }

  function updateGrains(dt) {
    if (grains.length > 90) grains.length = 90;
    for (var i = grains.length - 1; i >= 0; i--) {
      var g = grains[i];
      g.vy += GRAVITY * dt;
      g.y += g.vy * dt;
      var floorY = BY + BH - pileHeights[g.col];
      if (g.y + g.size / 2 >= floorY) grains.splice(i, 1);
    }
  }

  function updatePile(dt) {
    var targetHeight = targetFill * BH;
    for (var c = 0; c < COLS; c++) {
      var t = Math.max(0, Math.min(BH, targetHeight + colJitter[c]));
      pileHeights[c] += (t - pileHeights[c]) * Math.min(1, dt * EASE_RATE);
    }
  }

알갱이는 떨어지다 사라지고 기둥 높이만 남는 구조라 계산량이 늘지 않고, 업로드·다운로드 진행 버튼에 붙입니다.

08누른 좌표에서 픽셀을 밀어내는 굴절 리플 버튼

눌린 좌표에서 퍼지는 물결의 위상 값만큼 그 자리의 픽셀을 옆으로 밀어 다시 그리고, 물결 진폭 값이 프레임마다 줄어들며 화면이 원래 자리로 가라앉는다.

누른 좌표에서 픽셀을 밀어내는 굴절 리플 버튼
새 창에서 열기

08 · 누른 좌표에서 픽셀을 밀어내는 굴절 리플 버튼 — 측정 누적면적 17.142% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CRACK_VALUE_ECHOTEXTURE_CELLtoCanvasXY
      for (var y = minY; y <= maxY; y++) {
        for (var x = minX; x <= maxX; x++) {
          var dx = x - r.x, dy = y - r.y;
          var dist = Math.sqrt(dx * dx + dy * dy);
          var bandDist = dist - radius;
          if (bandDist > BAND_WIDTH || bandDist < -BAND_WIDTH) continue;
          var envelope = 1 - Math.abs(bandDist) / BAND_WIDTH;
          var phase = dist * FREQ - (elapsed / 1000) * ANG_SPEED;
          var shiftMag = CRACK_VALUE_ECHO * ampDecay * envelope * Math.sin(phase);
          var nx = dist > 0.0001 ? dx / dist : 0, ny = dist > 0.0001 ? dy / dist : 0;
          var sx = clampInt(Math.round(x + nx * shiftMag), 0, W - 1);
          var sy = clampInt(Math.round(y + ny * shiftMag), 0, H - 1);
          var srcIdx = (sy * W + sx) * 4, dstIdx = (y * W + x) * 4;
          outData[dstIdx] = baseData[srcIdx];
          outData[dstIdx + 1] = baseData[srcIdx + 1];
          outData[dstIdx + 2] = baseData[srcIdx + 2];
          outData[dstIdx + 3] = baseData[srcIdx + 3];
        }
      }

이 항목도 CRACK_VALUE_ECHO를 자기 소스 안에 따로 선언해 같은 값 6을 getImageData 방식의 픽셀 이동 폭으로 쓰고, 검색·확인 버튼의 눌림 피드백에 붙입니다.

09오류 값이 뒤집히면 정적처럼 흔들리는 노이즈 버튼

오류 값이 false 에서 true 로 뒤집히는 순간 버튼 표면 픽셀마다 밝기 값을 무작위로 다시 계산해 텔레비전 정적 같은 노이즈를 짧게 덮어씌운다.

오류 값이 뒤집히면 정적처럼 흔들리는 노이즈 버튼
새 창에서 열기

09 · 오류 값이 뒤집히면 정적처럼 흔들리는 노이즈 버튼 — 측정 누적면적 22.181% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

BTN_CANVAS_STATIC_BURST_MSdrawNoiseTICK_MS
  function render(now) {
    ctx.clearRect(0, 0, W, H);
    drawPanel(errorOn);

    if (burstT0 !== null) {
      var elapsed = now - burstT0;
      if (elapsed >= BTN_CANVAS_STATIC_BURST_MS) {
        burstT0 = null;
      } else {
        var envelope = 1 - elapsed / BTN_CANVAS_STATIC_BURST_MS;
        var tick = Math.floor(elapsed / TICK_MS);
        drawNoise(envelope, tick);
      }
    }
  }

  function trigger() {
    root.classList.remove('is-demo');
    errorOn = !errorOn;
    if (errorOn) burstT0 = performance.now();
    if (reduceMQ.matches) render(performance.now());
  }

BTN_CANVAS_STATIC_BURST_MS가 정적이 걷히는 데 걸리는 시간을 정하고, 오류 상태 버튼이나 재시도를 유도하는 버튼에 붙이면 눈에 띕니다.

값 하나가 02→05→08 로 전파되는 이유

02번 항목의 소스에는 BTN_CANVAS_CRACK_BRANCH_COUNT = 6이 있고, 이 값은 중심에서 뻗는 1차 균열 가지의 개수를 정합니다. 05번과 08번은 이 변수를 가져다 쓰는 것이 아니라 각자의 소스 안에 var CRACK_VALUE_ECHO = 6;을 따로 선언해 두고 있습니다 — 두 파일은 서로 다른 즉시실행함수(IIFE)라 이름이 같아도 실제로는 별개의 변수입니다. 05번과 08번 소스의 주석은 첫 줄이 "02번(btn-canvas-crack-shatter-press)의 균열 각도 값(BTN_CANVAS_CRACK_BRANCH_COUNT = 6)을" 으로 동일하지만, 둘째 줄은 각자의 용도를 따로 적어 05번은 "테두리를 도는 전류 굵기 계산에", 08번은 "화면을 밀어내는 픽셀 이동량에" 그대로 재사용한다고 구분해 둡니다. 05번은 이 값을 테두리 선 굵기(lineWidth = 1 + intensity * CRACK_VALUE_ECHO)로 쓰고, 08번은 같은 값을 픽셀을 밀어내는 굴절 폭(shiftMag = CRACK_VALUE_ECHO * ampDecay * envelope * Math.sin(phase))으로 씁니다. 전파되는 것은 변수 자체가 아니라 "여섯 갈래"라는 숫자와 그 뒤에 있는 개념이고, 세 항목의 소스 파일은 끝까지 서로 독립적입니다.

아홉 항목을 실측하면 어디가 가장 많이 움직였나

측정은 자동 재생 24프레임을 캡처해 냈고, 표의 움직인프레임은 그중 비교 가능한 23프레임 기준입니다.

항목 이름 누적면적% 강도평균 움직인프레임
01 잉크 확산 29.3 35.8 23/23
02 균열선 20.1 64.7 23/23
03 잔불 트레일 28.6 45.8 23/23
04 리퀴드 메탈 51.6 33.9 23/23
05 전기 아크 테두리 23.6 54.0 23/23
06 연기 분해 22.7 57.2 23/23
07 모래 채움 21.7 137.8 23/23
08 굴절 리플 17.1 37.4 20/23
09 노이즈 정적 22.2 61.5 23/23

04번 리퀴드 메탈이 누적면적 51.6% 로 가장 넓게 움직였고, 07번 모래 채움은 강도평균 137.8로 프레임 사이 픽셀 변화가 가장 큽니다. 08번 굴절 리플은 물결이 잦아드는 뒷부분 프레임에서 변화가 화면 대비 미세해 캡처마다 소폭 흔들리므로 움직인프레임을 파일 값 그대로 20/23으로 적었습니다.

어디에 붙이나

02번 균열선은 값을 눌렀을 때 되돌릴 수 없다는 인상을 주므로, 삭제나 결제 취소처럼 한 번 누르면 돌이키기 어려운 액션 확인 버튼에 어울립니다. 06번 연기 분해는 비활성 값이 켜지는 순간에만 반응하므로, 조건을 채우지 못해 눌러도 소용없는 버튼이나 재고가 없는 상품의 구매 버튼에 붙이면 상태 변화가 눈에 들어옵니다. 09번 노이즈 정적은 오류 값이 뒤집힐 때만 짧게 나타나므로, 폼 제출이 실패했을 때의 재시도 버튼처럼 평소에는 조용하다가 문제가 생겼을 때만 깜빡여야 하는 자리에 맞습니다.

값을 하나만 바꾸면 무엇이 달라지나

항목 값 이름 기존 → 변경 바뀌는 느낌
02 BTN_CANVAS_CRACK_BRANCH_COUNT 6 → 3 값을 낮추면 균열이 한두 갈래로 단순하게 뻗어야 하고, 값을 올리면 갈래가 여러 방향으로 갈라져 더 복잡한 금이 가는 것처럼 보여야 합니다.
05 BTN_CANVAS_ARC_JITTER_MS 70 → 30 값을 짧게 줄이면 아크선이 프레임마다 빠르게 떨려 지지직거리는 느낌이 강해지고, 값을 늘리면 떨림 간격이 넓어져 아크가 천천히 흔들리는 쪽에 가까워집니다.
08 BTN_CANVAS_RIPPLE_DECAY_MS 320 → 220 값을 줄이면 물결이 빨리 가라앉아 톡 치는 느낌만 남고, 값을 늘리면 물결이 오래 퍼지며 가장자리까지 여러 번 출렁여야 합니다.

어디서 깨지는가

가장 흔한 함정은 리사이즈입니다. 캔버스의 width·height를 CSS 픽셀로만 바꾸고 실제 픽셀 버퍼를 다시 만들지 않으면 04번 리퀴드 메탈의 필드 계산이 좌표와 어긋나 방울이 버튼 바깥에서 뭉칩니다. 두 번째는 08번처럼 getImageData로 픽셀을 옮겨 쓰는 항목인데, 소스·대상 인덱스를 캔버스 폭이 아니라 버튼 요소 폭으로 계산하면 물결이 대각선으로 밀려 깨진 것처럼 보입니다. xbg2dm72 로 여는 zip 안의 원본 파일에는 이 두 계산이 리사이즈 리스너 안에서 함께 다시 맞춰지도록 되어 있으니, 값만 옮기지 말고 그 리스너까지 그대로 옮겨야 합니다. 세 번째 함정은 05번·08번의 CRACK_VALUE_ECHO를 하나의 전역 변수로 착각해 한쪽만 고치는 경우인데, 두 값은 파일이 다른 독립 변수라 반드시 양쪽 소스를 각각 고쳐야 합니다.

접근성은 어떻게 처리했나

아홉 항목 모두 prefers-reduced-motion을 확인해, 줄임 설정이 켜져 있으면 반복 루프를 아예 시작하지 않고 정지 프레임 한 장만 그립니다. 그 상태에서도 click·keydown·pointerdown 이 들어오면 그때마다 render()를 한 번씩 직접 불러 결과 값은 그대로 눈에 보이게 했습니다. 키보드 조작은 Enter·Space 에만 반응하도록 e.key를 검사하고 preventDefault()로 스크롤이 딸려 움직이지 않게 막았으며, 버튼 요소에는 aria-pressed를 눌림 상태 값과 함께 갱신해 스크린리더가 캔버스 그림과 별개로 상태를 읽게 했습니다. 이런 처리 덕분에 버튼 인터랙션은 시각 효과를 끄더라도 기능은 그대로 남습니다.

077·357·352 와 무엇이 다른가

077번 html5 canvas 애니메이션 9종은 버튼이 아니라 화면 전체를 채우는 장면 효과를 다루고, 이번 9종은 그 계산 방식을 버튼 한 개 크기로 줄여 클릭·눌림 이벤트에 물린 것이 다릅니다. 357번 버튼 눌림 효과 UI 9종은 눌림 값이 스프링처럼 되돌아오는 물리 트윈 위주라 CSS transform 계산이 중심이고, 이번 9종은 픽셀 단위로 값을 직접 계산해 그린다는 점에서 결이 다릅니다. 352번 CTA 버튼 디자인 9종은 그림자·막대 같은 레이어 속성을 움직이는 css 버튼 클릭 애니메이션 쪽이고, 이번 9종은 같은 자리에서 균열·잔불·노이즈처럼 캔버스가 직접 픽셀을 다시 그린다는 점이 갈립니다.

FAQ

왜 라이브러리 없이 캔버스로 직접 계산하나요?

방울이 뭉치는 거리 계산이나 균열이 갈라지는 각도 계산은 CSS 속성 트윈만으로는 표현하기 어렵고, 캔버스에서 프레임마다 좌표를 직접 계산해야 결과가 정확합니다. 라이브러리 없이 짜 두면 복붙한 뒤 바로 값을 읽고 고칠 수 있습니다.

리듀스 모션 환경에서는 어떻게 동작하나요?

반복 루프는 멈추고 정지 프레임 한 장만 남지만, 클릭이나 키보드 입력이 들어올 때마다 그 입력에 맞는 결과 프레임을 한 번씩 다시 그려서 상태 변화는 계속 확인할 수 있습니다.

버튼 인터랙션에 어떤 항목을 먼저 적용하면 좋을까요?

확인이 꼭 필요한 액션에는 02번 균열선을, 상태가 자주 바뀌는 버튼에는 06번 연기 분해나 09번 노이즈 정적을 먼저 붙여 보는 것을 권합니다. 세 항목 모두 값 하나만 바꿔도 느낌이 크게 달라지는 버튼 인터랙션 항목입니다.

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

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