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

룰렛 돌리기 — 튕겨 돌리고 마찰로 멈추는 돌림판 캔버스 9종

손가락으로 튕긴 만큼만 돌아갑니다. 각속도를 적분하고 마찰로 감쇠해 포인터 아래 칸을 계산으로 판정합니다. 가중치 확률·연속 뽑기·당첨 폭죽까지 — 캔버스 한 장, 코드는 비밀번호 zip으로 드립니다. 룰렛 돌리기를 브라우저에서 직접 만지는 아홉 개의 완성 판을 순서대로 조립합니다.

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

데모

아홉 판 전부 index.html 한 파일씩으로 열리자마자 스스로 돕니다. 계산 사슬은 하나입니다 — pointermove 로 받은 각도 차를 Math.atan2 로 각속도로 바꾸고, 지수이동평균(0.65/0.35)으로 다듬고, requestAnimationFrame 루프에서 매 프레임 마찰^dt 를 곱해 적분하다가, 각속도가 임계값 아래로 떨어지면 12시 고정 포인터 기준으로 정규화해 부채꼴 인덱스를 판정합니다. 사람이 드래그하면 자동 시연이 멈추고 같은 루프로 이어 돕니다.

실측 한눈에

항목 초점 누적면적(%) 면적최대(%) 강도 평균
01 부채꼴 바퀴 moveTo→arc→closePath · 2π/n 균등 23.344 22.728 55.9
02 튕김 각속도 atan2 · EMA 0.65/0.35 · ±24 clamp 23.217 22.120 52.4
03 마찰 감쇠 각도 += v·dt · v ×= 마찰^dt · 그래프 9.394 7.138 45.5
04 가중치 확률 누적 bounds · W=[5,3,2,1,3,2] 22.052 17.906 56.0
05 포인터 딸깍 몫 변화 감지 · 스프링 pk=340·pc=14 22.926 21.906 55.8
06 당첨 판정 norm(−π/2−각도) · aria-live 22.293 18.542 53.7
07 항목 편집기 measureText 이분 탐색 6~15px 21.903 11.780 62.0
08 연속 뽑기 splice 제거 · log.pop 되돌리기 21.825 15.651 50.6
09 히어로 씬 파티클 42개 · toDataURL 저장 22.094 20.947 60.1

낮은 누적면적이 정상인 이유

03 마찰 감쇠가 9.394% 로 유일하게 한 자릿수인 것은 결함이 아니라 구조입니다 — 화면 절반이 감속 곡선을 실시간으로 그리는 그래프 패널이라 회전판 자체의 면적이 좁고, 그래프의 짙은 배경 탓에 강도 평균 45.5 도 아홉 판 중 가장 어둡습니다. 08 연속 뽑기의 21.825% 는 당첨자가 옮겨 적히는 사이드 기록 판이 화면 폭을 나눠 가진 탓이고, 07 항목 편집기는 강도 평균 62.0 으로 가장 밝습니다. 나머지 일곱 판은 22~23% 대로 고르게 넓은 회전판을 차지합니다. 05 는 소리가 나는 판이지만 틱은 사람이 직접 돌리는 중에만 재생되므로 녹화·자동 시연에서는 무음인 것이 정상 동작입니다.

튕김은 왜 물리인가 — 각속도 적분과 마찰 감쇠

CSS transition 의 감속 이징은 튕긴 세기를 반영하지 못합니다 — 미리 정해둔 곡선대로 도는 것이지 입력이 결과를 바꾸는 것이 아닙니다. 이 아홉 판은 매 프레임 실제로 계산합니다 — pointermove 이벤트가 잡힐 때마다 중심에서의 각도를 Math.atan2 로 재고 이전 각도와의 차(dA, ±π 경계는 TAU 로 보정)를 경과 시간으로 나누면 rad/s 각속도가 나오며, 순간값 대신 drag.v = 0.65 * drag.v + 0.35 * (dA / dt) 지수이동평균으로 다듬어 손 떨림이 회전에 그대로 묻지 않게 합니다. 손을 뗀 순간의 값이 초기 속도가 되어 ±24 로 clamp 뒤 루프에 들어가고, 루프에서는 각도 += 각속도 * dt; 각속도 *= Math.pow(마찰계수, dt) 를 반복해 지수곡선으로 느려지다가 절대값이 임계값(STOP 0.4~0.5) 아래로 떨어지면 끝납니다 — 프레임 수를 세어 멈추지 않습니다. dt 는 64ms 상한으로 clamp 하는데, 탭이 배경으로 가 프레임이 오래 끊기면 dt 가 커져 판이 한 번에 몇 바퀴를 건너뛰기 때문입니다. 세기를 바꾸면 회전 시간과 정지 각도가 실제로 달라지는 것이 물리와 애니메이션을 가르는 기준입니다.

당첨은 각도 계산이다 — 포인터 판정과 가중치

판이 멈췄을 때 당첨을 정하는 것은 화면 연출이 아니라 두 값입니다 — 정지 각도와 칸 경계. 06 의 idxAt 는 균등 분할에서 Math.floor(norm(-Math.PI / 2 - a) / SEG) % N 한 줄로 끝나고, 04·09 의 가중치 판은 같은 정규화 값을 칸 각도로 나누는 대신 누적 bounds 배열의 어느 구간에 떨어지는지 찾습니다. 어느 쪽이든 포인터가 12시에 고정되어 있다는 사실 하나에서 나오는 계산이므로, 칸을 늘리거나(07) 당첨자를 빼거나(08) 해서 배치가 바뀌면 판정식에 들어가는 분모와 경계만 새로 계산하면 됩니다 — 이것이 룰렛 돌리기 사이트를 만들 때 가장 많이 틀리는 지점으로, 화면에 보이는 각도와 판정에 쓰는 각도가 어긋나면 눈으로는 A 칸인데 결과가 B 로 뜹니다.

아홉 도구 단계별 코드

01부채꼴 바퀴 — 극좌표 호로 칸을 잇는 판때기

칸 하나는 moveTo(0, 0) → arc(0, 0, R, 시작각, 끝각) → closePath 의 판때기이고 n=8 칸을 45° 씩 이어 붙이면 돌림판 바닥이 완성됩니다. 색은 팔레트 배열을 순환시켜 이웃 칸이 같은 색으로 붙지 않게 하며, 자동 시연은 고정 튕김 세 값 [9.6, −7.4, 12.6] 을 순회해 매번 같은 회전을 재현합니다.

부채꼴 바퀴 — 극좌표 호로 칸을 잇는 판때기
새 창에서 열기

01 · 부채꼴 바퀴 — 극좌표 호로 칸을 잇는 판때기 — 측정 누적면적 23.344% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

arcbeginPathfillText

판을 그리는 열다섯 줄 — arc 하나가 칸 하나

    var cx = w / 2, cy = h / 2, R = Math.min(w, h) / 2 - 8;
    ctx.save();
    ctx.translate(cx, cy);
    ctx.rotate(ang);
    for (var k = 0; k < N; k++) {
      var a0 = k * SEG, a1 = a0 + SEG;
      ctx.beginPath();
      ctx.moveTo(0, 0);
      ctx.arc(0, 0, R, a0, a1);
      ctx.closePath();
      ctx.fillStyle = PAL[k];
      ctx.fill();
      ctx.strokeStyle = 'rgba(23,20,26,.22)';
      ctx.lineWidth = 1;
      ctx.stroke();

누적면적 23.344%(면적최대 22.728%)로 아홉 판 중 가장 넓게 화면을 차지하는 판때기 바닥이며, ang 하나만 바꿔 다시 그리면 판 전체가 따라 돕니다.

02튕김 각속도 — 포인터 이동을 회전 속도로 바꾸기

pointermove 가 발생할 때마다 중심에서의 각도를 Math.atan2 로 재는 angleAt 이 입력의 전부이고, 이전 각도와의 차에 경과 시간을 나눈 값을 지수이동평균으로 다듬으면 rad/s 각속도가 나옵니다. 손을 뗀 순간 값을 ±24 로 clamp 해 초기 속도로 쓰고 세기를 5단계 바로 표시합니다.

튕김 각속도 — 포인터 이동을 회전 속도로 바꾸기
새 창에서 열기

02 · 튕김 각속도 — 포인터 이동을 회전 속도로 바꾸기 — 측정 누적면적 23.217% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

pointermovepointerdownMath.atan2

각도 차를 각속도로 — EMA 0.65/0.35

  field.addEventListener('pointermove', function (e) {
    if (!drag.on) return;
    var a = angleAt(e), now = performance.now();
    var dA = a - drag.lastA;
    if (dA > Math.PI) dA -= TAU;
    if (dA < -Math.PI) dA += TAU;
    ang += dA;
    var dt = Math.max(1, now - drag.lastT) / 1000;
    drag.v = 0.65 * drag.v + 0.35 * (dA / dt);
    drag.lastA = a; drag.lastT = now;
    velEl.textContent = (drag.v).toFixed(1);
    idle = 1.2;
  });

dA > Math.PI 검사 두 줄이 ±π 를 넘는 경계(예: −179° → +179°)에서 한 바퀴 오차가 생기는 것을 TAU 보정으로 막으며, 0.65/0.35 비율이 순간 노이즈를 누르고도 방향 전환을 놓치지 않는 절충입니다.

03마찰 감쇠 — 매 프레임 적분해 스스로 멈추는 판

requestAnimationFrame 루프에서 dt 를 재고 각도 += 각속도·dt, 각속도 *= Math.pow(마찰계수, dt) 를 적분하면 튕김 직후엔 빠르다가 지수곡선으로 느려집니다. 우측 그래프가 그 적분을 실시간으로 그려 판의 각속도와 그래프 기울기가 같음을 눈으로 확인할 수 있고, 마찰 계수 3단계(0.08·0.18·0.38) 버튼으로 같은 튕김(16 rad/s)의 감속 곡선을 비교합니다.

마찰 감쇠 — 매 프레임 적분해 스스로 멈추는 판
새 창에서 열기

03 · 마찰 감쇠 — 매 프레임 적분해 스스로 멈추는 판 — 측정 누적면적 9.394% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

requestAnimationFrameperformance.now

적분의 두 줄과 정지 판정

    var dt = Math.min(64, now - last) / 1000; last = now;
    if (!drag.on) {
      if (angularVelocity !== 0) {
        ang += angularVelocity * dt;
        var before = Math.abs(angularVelocity);
        angularVelocity *= Math.pow(COEFS[ci], dt);
        t0 += dt;
        if (Math.abs(angularVelocity) < STOP) {
          angularVelocity = 0; settled = true; settleAt = t0;
          secEl.textContent = settleAt.toFixed(1);
          hist.push({ t: t0, v: 0 });
        } else {
          hist.push({ t: t0, v: Math.abs(angularVelocity) });
        }

Math.min(64, now - last) 가 탭 전환으로 프레임이 끊겨도 dt 가 64ms 를 넘지 않게 막으며, 정지는 프레임 수가 아니라 각속도 절대값이 STOP 아래로 떨어지는 순간 판정됩니다.

04가중치 확률 — 칸 각도를 무게로 나눠 공정하게 뽑기

모든 칸을 같은 각도로 그리면 칸 수만큼 균등 확률이지만, 항목마다 무게 W=[5,3,2,1,3,2] 를 두면 시작각·끝각을 누적 비율로 잡아 각도 자체가 확률이 됩니다. 자동 시연은 mulberry32 시드 332004 고정 난수로 튕김 세기를 흔들어 뽑아도 매번 같은 순서로 재현합니다.

가중치 확률 — 칸 각도를 무게로 나눠 공정하게 뽑기
새 창에서 열기

04 · 가중치 확률 — 칸 각도를 무게로 나눠 공정하게 뽑기 — 측정 누적면적 22.052% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

Math.randomfillTextarc

bounds 배열 — 무게를 각도 경계로 굽고 그 경계로 되짚어 읽는다

  var W = [5, 3, 2, 1, 3, 2];
  var TOT = W.reduce(function (a, b) { return a + b; }, 0);
  var bounds = [], acc = 0;
  for (var q = 0; q < W.length; q++) { bounds.push(acc / TOT); acc += W[q]; }
  bounds.push(1);

  function norm(a) { a %= TAU; return a < 0 ? a + TAU : a; }
  function idxAt(a) {
    var u = norm(-Math.PI / 2 - a);
    for (var k = 0; k < W.length; k++) if (u >= bounds[k] && u < bounds[k + 1]) return k;
    return W.length - 1;
  }

누적 비율 0 → 5/16 → 8/16 → 10/16 → 11/16 → 14/16 → 1 이 각 칸의 시작각·끝각이 되므로 화면에 보이는 부채꼴 넓이가 곧 당첨 확률이고, idxAt 은 정지 각도가 그 비율 구간 어디에 떨어지는지 순서대로 찾아 인덱스를 돌려줄 뿐이니 결과를 조작할 자리가 없습니다.

05포인터 딸깍 — 칸 경계를 지날 때마다 튕기는 소리와 흔들림

회전 각도를 칸 경계 각도(SEG)로 나눈 몫이 바뀌는 프레임을 tickCheck 로 감지하면 포인터가 핀을 지나간 순간이고, 그 프레임에 OscillatorNode 삼각파 1750Hz 짧은 틱이 납니다. 포인터 변위는 스프링 적분(a = −pk·x − pc·v, pk=340·pc=14)으로 튕겼다 되돌아오며, 소리는 사람이 직접 돌릴 때만 재생됩니다.

포인터 딸깍 — 칸 경계를 지날 때마다 튕기는 소리와 흔들림
새 창에서 열기

05 · 포인터 딸깍 — 칸 경계를 지날 때마다 튕기는 소리와 흔들림 — 측정 누적면적 22.926% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

requestAnimationFrameOscillatorNoderotate

몫이 바뀌면 핀 1개 통과 — 틱과 스프링 속도

  function tickCheck() {
    var q = Math.floor(ang / SEG);
    if (q !== lastQ) {
      var d = q - lastQ;
      ticks += Math.abs(d);
      pV += (d > 0 ? 1 : -1) * Math.abs(vel || drag.v || 6) * 26;
      if (drag.on) tickSound();        // 소리는 사람 손으로 돌릴 때만
      ticksEl.textContent = String(ticks);
      lastQ = q;
    }
  }

drag.on 조건이 없으면 자동 반복 시연과 gif·mp4 녹화에도 소리가 실리는데 무음 매체가 소리 있는 척하는 어색한 결과가 되므로, tickSound 는 사람 손이 판을 붙잡고 있는 동안에만 불립니다.

06당첨 판정 — 정지 각도를 부채꼴 인덱스로 환산하기

포인터는 12시 방향(−π/2)에 고정이므로 정지 각도를 그 기준으로 정규화한 뒤 칸 각도로 나누면 당첨 인덱스가 나옵니다. 결과는 aria-live="polite" 로 읽어주고 판정·표시·재도전(round 카운터)을 한 흐름으로 묶어 화면을 켠 채로 몇 번이든 돌립니다.

당첨 판정 — 정지 각도를 부채꼴 인덱스로 환산하기
새 창에서 열기

06 · 당첨 판정 — 정지 각도를 부채꼴 인덱스로 환산하기 — 측정 누적면적 22.293% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

Math.atan2aria-livefillText

판정은 한 줄 — norm 과 나눗셈, 그리고 그 값을 읽어 알리는 judge

  function norm(a) { a %= TAU; return a < 0 ? a + TAU : a; }
  function idxAt(a) { return Math.floor(norm(-Math.PI / 2 - a) / SEG) % N; }

  function judge() {
    var deg = Math.round(norm(ang) * 180 / Math.PI);
    restEl.textContent = String(deg);
    var idx = idxAt(ang);
    lastWin = idx;
    winEl.textContent = String(idx + 1) + '번';
    nameEl.textContent = NAMES[idx];
    liveEl.textContent = idx + 1 + '번 ' + NAMES[idx];
  }

norm 이 음수 각도를 0~TAU 로 접고 -Math.PI/2 를 빼 12시 포인터를 기준으로 삼은 뒤 칸 각도로 나눠 몫을 취하는 것이 판정 전부이며, judge 는 그 몫 하나로 화면 표시 세 곳과 liveEl(aria-live) 을 동시에 채울 뿐이니 임의로 칸을 정하면 계산이 아니라 연출이 됩니다.

07항목 편집기 — 이름 줄과 칸 각도가 함께 다시 계산되는 판

이름을 치면 measureText 로 폭을 재고 칸 호 길이(R·seg·0.78)에 맞게 글자 크기를 이분 탐색(7회 반복, 6~15px)으로 줄입니다. 항목이 늘고 줄 때마다(기본 4개, 최대 7개) 각도 분할(TAU/n)과 배치를 전부 다시 계산해 판이 편집 내용을 그대로 따라갑니다.

항목 편집기 — 이름 줄과 칸 각도가 함께 다시 계산되는 판
새 창에서 열기

07 · 항목 편집기 — 이름 줄과 칸 각도가 함께 다시 계산되는 판 — 측정 누적면적 21.903% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

fillTextmeasureTextchange

fitFont — 7번 재면서 경계를 좁히는 이분 탐색

  function fitFont(text, arcLen) {
    var lo = 6, hi = 15, best = 6;
    for (var it = 0; it < 7; it++) {
      var mid = (lo + hi) / 2;
      ctx.font = '700 ' + mid + 'px sans-serif';
      if (ctx.measureText(text).width <= arcLen) { best = mid; lo = mid; } else { hi = mid; }
    }
    return best;
  }

글자 크기 후보 9단계를 7번의 measureText 로 좁히는 계산이고, 폭이 호 길이 안에 들면 답을 저장하고 하한을 올리고, 넘으면 상한을 내려 15px 한도 안에서 가장 큰 들어가는 크기를 돌려줍니다.

08연속 뽑기 — 당첨 칸을 빼고 도는 탈락 모드

뽑힌 항목을 alive 배열에서 splice 로 빼고 log 에 쌓아 남은 항목으로 각도를 재분배하면 발표자 뽑기·빙고 뽑기가 됩니다. 되돌리기는 log.pop 한 값을 alive 로 다시 push 해 직전 뽑기를 취소합니다.

연속 뽑기 — 당첨 칸을 빼고 도는 탈락 모드
새 창에서 열기

08 · 연속 뽑기 — 당첨 칸을 빼고 도는 탈락 모드 — 측정 누적면적 21.825% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

fillRectarcchange

확정과 되돌리기 — splice·push·pop 세 동작

  function commit() {
    if (previewIdx < 0 || !alive.length) return;
    var winner = alive.splice(previewIdx, 1)[0];
    log.push(winner);
    previewIdx = -1; settle = -1; pulse = -1;
    renderLog();
  }

  btnEl.addEventListener('click', function () {
    if (!log.length) return;
    var back = log.pop();
    alive.push(back);

빼낸 인덱스를 log 가 순서대로 기억하므로 되돌리기는 마지막 값 하나를 되살리는 pop 과 push 로 끝나며, 항목 수가 바뀌면 다음 회전의 각도 분할이 남은 수로 다시 계산됩니다.

09히어로 씬 — 폭죽 터지고 결과를 저장하는 완성판

정지 판정 직후 mulberry32 시드 90210 고정 난수로 파티클 42개를 방사형으로 쏘고(중력 220·감쇠 0.75/s), 당첨명이 올라온 뒤 판 전체를 toDataURL('image/png') 로 내보냅니다. 마찰·틱(스프링)·가중치·탈락 모드를 한 화면에 얹은 완성판입니다.

히어로 씬 — 폭죽 터지고 결과를 저장하는 완성판
새 창에서 열기

09 · 히어로 씬 — 폭죽 터지고 결과를 저장하는 완성판 — 측정 누적면적 22.094% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

requestAnimationFramearctoDataURL

spawnBurst — 42개를 방사형으로 쏘고 중력으로 떨어뜨린다

  function spawnBurst(cx, cy) {
    for (var i = 0; i < 42; i++) {
      var a = rnd() * TAU, sp = 60 + rnd() * 170;
      burst.push({
        x: cx, y: cy,
        vx: Math.cos(a) * sp, vy: Math.sin(a) * sp - 60,
        rot: rnd() * TAU, vr: (rnd() - 0.5) * 10,
        s: 3 + rnd() * 4,
        col: PAL[Math.floor(rnd() * PAL.length)],
        life: 1
      });
    }
  }
    var dt = Math.min(64, now - last) / 1000; last = now;
    // 포인터 스프링 적분
    var acc = -pk * pX - pc * pV;
    pV += acc * dt; pX += pV * dt;
    // 파티클 물리 — 중력 + 감쇠
    for (var i = 0; i < burst.length; i++) {
      var P = burst[i];
      P.vy += 220 * dt;
      P.x += P.vx * dt; P.y += P.vy * dt;
      P.rot += P.vr * dt;
      P.life -= dt * 0.75;
    }

각 파티클이 방향·속력·회전을 시드 고정 난수로 받아 중력 220 을 적분하며 떨어지는 것이 폭죽의 전부이고, rnd 가 mulberry32(90210) 이므로 녹화와 실제 사용이 같은 불꽃을 재현합니다.

감쇠 계산 없이 이징으로 흉내 내면 깨지는 함정 두 가지

dt 를 clamp 없이 쓰면 판이 몇 바퀴를 건너뛴다

탭을 배경으로 보내면 requestAnimationFrame 이 프레임을 쌓아두므로 다음 dt 가 수 초가 될 수 있고, clamp 없이 ang += vel * dt 를 쓰면 판이 한 번에 몇 바퀴를 돌아 정지 각도가 완전히 달라집니다. 아홉 판 전부 첫 줄에서 Math.min(64, now - last) / 1000 로 상한을 두며, 사람 입력과 자동 시연이 다른 함수를 도는 이중 경로를 만들면 데모와 실제 튕김이 다른 결과를 내므로 둘은 같은 적분 루프를 지나야 합니다. 이 아홉 판의 원본 index.html 그대로를 묶은 암호 zip 의 비밀번호는 sgvkvgus 이고, 풀면 본문의 조각들과 줄 단위로 나란히 대조해 보실 수 있습니다.

정지를 이징으로 흉내 내면 세기가 죽는다

CSS transition 의 cubic-bezier 감속을 돌려 판을 멈추면 튕김 세기가 결과에 영향을 주지 못합니다 — 같은 곡선으로만 멈추니 위에서 아래로 튕기나 살살 굴리나 같은 회전이 나옵니다. 각속도 *= Math.pow(마찰계수, dt) 를 매 프레임 실제로 곱해야 세기가 회전 시간과 정지 각도에 그대로 이어지고, 그래야 손맛이 삽니다.

마찰 계수 하나로 손맛이 달라지는 변형과 접근성 처리

값 하나를 바꾼 세 변형

이름 바꾼 값 느낌
마찰 계수 03 마찰 0.38 → 0.08 마찰이 낮으면 판이 오래 도는 대신 기다림이 길어지고, 높으면 등골이 시원할 만큼 확 멈추지만 튕김의 맛이 줄어든다
칸 수 01 칸 8 → 18 칸이 많아지면 틱 소리가 촘촘해져 속도감이 살지만 글자가 작아져 한눈에 읽히는 이름의 크기가 희생된다
틱 흔들림 05 포인터 진폭 4px → 7px 진폭을 키우면 실물 핀에 걸리는 촉감이 커지지만 지나치면 판정 각도와 포인터가 어긋나 보여 계산이 틀린 듯 느껴진다

동작을 줄인 사용자에게 남기는 장면

아홉 판 모두 시작 시 prefers-reduced-motion: reduce 를 검사해 켜져 있으면 루프를 돌리지 않고 결과가 이미 나온 정지 장면 한 장을 고정합니다 — 03 은 계수 0.18 로 시뮬레이션한 감속 곡선 전체를 미리 계산해 그린 그래프를, 09 는 판정과 폭죽이 끝난 상태에 흩어진 파티클 한 장을 보이므로 빈 캔버스가 아니라 결과가 남습니다.

스크린리더와 키보드로 만지는 아홉 판

판마다 role="application" 과 aria-label 이 붙고 포인터 입력은 setPointerCapture 로 캔버스 밖으로 나가도 놓치지 않으며(try/catch 로 감싼다), 당첨 결과는 aria-live="polite" 로 읽어줍니다 — 06 은 정지 각도를 도 단위로 함께 알려 계산이 눈에 보이는 판입니다.

어디에 붙이나 — 발표 순서 뽑기와 경품 추첨 화면

교실이나 사내 발표에서 순서를 정할 때는 08 연속 뽑기가 맞습니다 — 한 번 뽑힌 이름이 배열에서 빠지고 되돌리기도 가능해서, 발표자가 바뀌어도 같은 사람이 두 번 걸리지 않습니다. 사은품이나 경품을 나눌 때는 09 히어로 씬처럼 당첨 순간 폭죽이 터지고 결과 이미지를 저장할 수 있는 판이 화면 캡처 없이도 인증샷이 됩니다.

이름이나 상품명을 행사 직전에 바꿔 붙이는 자리라면 07 항목 편집기가 핵심입니다 — 줄 수가 늘거나 글자가 길어져도 칸 각도와 글자 크기가 함께 다시 계산되므로, 참석자 명단만 새로 넣으면 판이 저절로 맞춰집니다.

돌림판을 실제로 붙이기 전에 자주 나오는 질문

돌림판 만들기를 처음 시작해도 이 아홉 판으로 따라갈 수 있나요?

핵심은 01 의 부채꼴 그리기와 02 의 각속도 계산, 03 의 마찰 적분 세 동작입니다. zip 안에는 아홉 판을 같은 전제로 다시 짜라고 시키는 지시 문서가 들어 있어 코드 도구에 붙여 넣으면 같은 화면부터 시작하고, 그다음 마찰 계수와 칸 수를 하나씩 바꿔 보는 순서가 가장 빠릅니다.

랜덤 뽑기 돌림판의 당첨은 조작 없이 정말 공정한가요?

04 가 가중치를 두는 판조차 각도 자체가 확률입니다 — 시작각·끝각을 무게의 누적 비율로 나눠 그리므로 화면에 보이는 부채꼴 넓이가 곧 당첨 확률이고, 결과는 멈춘 뒤 그 각도로 판정합니다. 시드 고정 난수는 자동 시연의 재현을 위한 것이지 확률을 바꾸지 않습니다.

소리나 진동 설정 없이 발표 현장에서 쓸 수 있나요?

05 의 틱 소리는 사람이 직접 돌릴 때만 나므로 자동 반복·녹화 환경에서는 무음이고, 06 은 소리 없이 판정 결과를 화면과 스크린리더로 알립니다. 수업 발표 순서 뽑기에는 08 연속 뽑기가 당첨자를 빼고 도는 구조라 한 사람이 두 번 뽑히지 않게 해줍니다.

더 읽을거리

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

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