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

난이도 선택 ui 9종 — 드래그 각도로 값이 바뀌는 계산까지

난이도 선택 ui 아홉 가지 가운데 하나는 손을 떼면 슬라이더 손잡이가 단계를 살짝 지나쳤다가 스프링 계산으로 되튕겨 멈추고, 하나는 배지 다섯 개의 색을 고정값이 아니라 단계 번호를 넣은 색상각 수식으로 계산하며, 하나는 반원 게이지 바늘을 끄는 손가락 각도를 값으로 바꿉니다.

아홉 항목 모두 click과 keydown을 기본으로 받고, 02·05·06·07·08 다섯 개는 이 둘만으로 동작합니다. 01 은 여기에 pointerenter/pointerleave를 더해 가리키는 동안만 미리 보여주고, 03·09 는 keydown 옆에 pointerdown·pointermove·pointerup으로 드래그를 받으며, 04 는 keydown·pointerdown 조합으로 클릭 위치를 즉시 값으로 바꿉니다. 아홉 항목 어디에도 Math.random은 없고, 그 대신 03·04·05·06 넷은 requestAnimationFrame 루프로 스프링·이징·보간을 매 프레임 계산하고 나머지 다섯은 CSS 트랜지션이나 스타일 값을 즉시 계산해 넣습니다.

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

값 하나만 고르면 끝나는 01 에서 시작해, 그림자 물리가 붙는 02, 스프링 물리가 붙는 03, 시각 효과가 붙는 04·05를 거쳐 관성 보간이 붙는 06, 색·자릿값을 수식으로 구하는 07·08, 마지막으로 드래그 각도 자체를 값으로 바꾸는 09 순서로 계산이 한 겹씩 늘어나도록 배치했습니다.

아홉 가지 난이도 UI — 계산 한 겹이 어디서 도는가

항목 누적면적 강도 평균 움직인 프레임
01 점 색 램프 3.11% 68.0 13/23
02 카드 그림자 8.92% 78.1 12/23
03 슬라이더 스프링 4.29% 108.3 13/23
04 막대 게이지 2.77% 77.6 12/23
05 아이콘 모프 2.07% 81.1 11/23
06 키보드 관성 5.87% 144.7 14/23
07 배지 색상각 4.04% 70.0 10/23
08 숫자 오도미터 0.69% 105.6 11/23
09 게이지 드래그 2.20% 116.6 9/23

누적면적은 02(8.92%)가 가장 넓고 06(5.87%)이 그다음이며, 08(0.69%)이 가장 좁습니다. 강도 평균은 06(144.7)이 다른 여덟 항목과 확연히 차이 나게 가장 높은데, 초점 고리가 매 프레임 좁히는 거리 계산을 23프레임 내내 이어가기 때문입니다. 반대로 01(68.0)이 가장 낮고, 움직인 프레임은 06(14/23)이 가장 많이 09(9)는 가장 적게 움직였습니다.

01 — 색상각 보간 한 겹

dotHue가 점 순서를 초록(132°)에서 빨강(0°) 사이 각도로 선형 보간해, 색을 다섯 개 미리 정해두지 않습니다.

02 — 그림자 물리 한 겹

shadowFor가 단계 번호에 비례해 그림자 y·blur·들뜬 높이 세 값을 동시에 구해, 상급 카드일수록 더 떠 보이게 계산합니다.

03 — 스프링 적분 한 겹

springTo가 force = -SPRING_K*(pos-target) - SPRING_D*vel를 매 프레임 오일러 적분해 손잡이가 단계를 살짝 지나쳤다 되튕겨 멈춥니다.

04 — 이징 한 겹

sweep이 easeOutCubic(경과비율)로 광택 띠의 가로 위치를 매 프레임 다시 구해 왼쪽에서 오른쪽으로 훑습니다.

05 — 기하 보간 한 겹

polygonRadius가 정다각형의 변 수와 각도로 그 방향 반지름을 구하고, buildPath가 그 반지름들을 이어 다음 단계 모양으로 자연스럽게 넘어갑니다.

06 — 지수 감쇠 한 겹

loop가 ringX += (targetX-ringX)*TRAIL_LERP로 남은 거리의 일부만 매 프레임 좁혀, 초점 고리가 목표를 관성으로 뒤따라옵니다.

07 — 색상각 수식 한 겹

badgeHue(i) = (BASE_HUE + i*HUE_SPACING) % 360로 배지 다섯 개의 색을 고정값이 아니라 수식으로 구해, 단계 수가 바뀌어도 간격이 다시 계산됩니다.

08 — 자릿값 한 겹

placeDigit(v, place)가 값을 10의 자리 거듭제곱으로 나눈 몫의 일의 자리를 구해, 그 값만큼 릴을 밀어 자리올림까지 자연히 표현합니다.

09 — 각도-값 매핑 한 겹

angleToValue가 포인터와 피벗 사이 각도를 atan2로 구해 -90~90도 구간을 0~4 값으로 매핑합니다.

패턴별 코드

항목마다 계산이 도는 함수만 뽑았습니다. 나머지 마크업·리스너는 각 데모의 코드 탭에서 그대로 볼 수 있습니다.

01색이 단계별로 번지는 점 난이도 선택기

점을 하나씩 가리키면 그 점까지 색이 초록에서 빨강 쪽으로 계산되어 채워지고, 실제로 고른 점까지만 그 색이 남습니다. 설문·퀴즈 앞에서 체감 난이도를 고르는 자리에 맞습니다.

  var HUE_LOW = 132, HUE_HIGH = 0, SAT = 46, LIGHT = 68;
  var root = document.getElementById('dr');
  var track = document.getElementById('drTrack');
  var fill = document.getElementById('drFill');
  var dots = Array.prototype.slice.call(track.querySelectorAll('.dr__dot'));
  var nameOut = document.getElementById('drName');
  var countOut = document.getElementById('drCount');
  var selected = 1;

  // 계산 겹: 점 순서(0~4)를 초록(132°)에서 빨강(0°)으로 이어지는 색상각 값으로 바꾼다
  function dotHue(i) { return HUE_LOW + (HUE_HIGH - HUE_LOW) * (i / (LEVELS.length - 1)); }
  function dotColor(i) { return 'hsl(' + dotHue(i).toFixed(1) + ', ' + SAT + '%, ' + LIGHT + '%)'; }

  function paintUpTo(i) {
    dots.forEach(function (d, idx) {
      d.style.background = idx <= i ? dotColor(idx) : '';
    });
    fill.style.width = ((i + 1) / LEVELS.length * 100) + '%';
    fill.style.background = dotColor(i);
  }
색이 단계별로 번지는 점 난이도 선택기
새 창에서 열기

01 · 색이 단계별로 번지는 점 난이도 선택기 — 측정 누적면적 3.112% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

dotHuedotColorpaintUpTo

02고를수록 그림자가 깊어지는 카드형 난이도 선택

카드를 고르면 그 카드의 그림자 높이와 번짐이 단계 번호에 비례해 계산되어 커지므로, 상급 카드일수록 더 떠 보입니다. 앱 시작 화면에서 상·중·하 난이도를 고르는 자리에 맞습니다.

  // 계산 겹: 카드 그림자의 y·blur·들뜬 높이를 고정값이 아니라 단계 번호(tier)에 비례해 구한다
  function shadowFor(tier) {
    var y = BASE_Y + tier * Y_STEP;
    var blur = BASE_BLUR + tier * BLUR_STEP;
    var lift = BASE_LIFT + tier * LIFT_STEP;
    return { y: y, blur: blur, lift: lift };
  }
  function paint() {
    cards.forEach(function (c, i) {
      var on = i === selected;
      c.setAttribute('aria-checked', on ? 'true' : 'false');
      c.setAttribute('tabindex', on ? '0' : '-1');
      if (on) {
        var s = shadowFor(i);
        c.style.boxShadow = '0 ' + s.y + 'px ' + s.blur + 'px rgba(23, 20, 26, .38)';
        c.style.transform = 'translateY(-' + s.lift + 'px)';
      } else {
        c.style.boxShadow = '';
        c.style.transform = '';
      }
    });
  }
고를수록 그림자가 깊어지는 카드형 난이도 선택
새 창에서 열기

02 · 고를수록 그림자가 깊어지는 카드형 난이도 선택 — 측정 누적면적 8.917% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

shadowForpaintselect

03단계에서 튕기며 멈추는 난이도 슬라이더

손을 떼면 손잡이가 가장 가까운 단계를 살짝 지나쳤다가 스프링 계산으로 되튕겨 그 단계에 멈춥니다. 설정 화면에서 난이도를 손가락으로 끌어 정하는 자리에 맞습니다.

  // 계산 겹: 목표 단계까지 스프링-감쇠 방정식(가속도 = -k*(위치-목표) - d*속도)을 매 프레임 적분해
  // 단계를 살짝 지나쳤다가(오버슛) 되튕겨 정확히 그 단계에서 멈춘다
  function springTo(target) {
    cancelAnimationFrame(raf);
    if (reduce) { pos = target; paintAt(pos); return; }
    var vel = 0;
    function step() {
      var force = -SPRING_K * (pos - target) - SPRING_D * vel;
      vel += force * DT;
      pos += vel * DT;
      paintAt(pos);
      if (Math.abs(pos - target) > 0.15 || Math.abs(vel) > 0.15) raf = requestAnimationFrame(step);
      else { pos = target; paintAt(pos); }
    }
    raf = requestAnimationFrame(step);
  }
단계에서 튕기며 멈추는 난이도 슬라이더
새 창에서 열기

03 · 단계에서 튕기며 멈추는 난이도 슬라이더 — 측정 누적면적 4.291% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

springTotierPctpaintAt

04광택이 훑고 지나가는 막대 난이도 게이지

난이도가 바뀔 때마다 막대 안에서 밝은 띠 하나가 왼쪽에서 오른쪽으로 훑고 지나가는 위치를 매 프레임 계산해 그립니다. 대시보드에서 현재 난이도 수준을 한눈에 보여주는 자리에 맞습니다.

  function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }
  // 계산 겹: 채움이 바뀔 때마다 밝은 띠의 위치를 easeOutCubic(경과비율) 로 매 프레임 계산해 왼쪽에서 오른쪽으로 훑는다
  function sweep() {
    cancelAnimationFrame(raf);
    if (reduce) { gloss.style.opacity = '0'; return; }
    t0 = performance.now();
    gloss.style.opacity = '1';
    function step(now) {
      var t = Math.min(1, (now - t0) / SWEEP_MS);
      gloss.style.left = (easeOutCubic(t) * 100) + '%';
      if (t < 1) raf = requestAnimationFrame(step);
      else gloss.style.opacity = '0';
    }
    raf = requestAnimationFrame(step);
  }
  function paint() {
    fill.style.width = ((tier + 1) / N * 100) + '%';
    meter.setAttribute('aria-valuenow', String(tier));
    meter.setAttribute('aria-valuetext', LEVELS[tier]);
    name.textContent = LEVELS[tier];
  }
광택이 훑고 지나가는 막대 난이도 게이지
새 창에서 열기

04 · 광택이 훑고 지나가는 막대 난이도 게이지 — 측정 누적면적 2.773% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

easeOutCubicsweeppaint

05아이콘 모양이 단계마다 변형되는 난이도 선택

단계를 넘길 때마다 아이콘 윤곽선의 좌표가 다음 단계 모양의 좌표로 보간되어, 형태가 튀지 않고 하나에서 다른 하나로 이어집니다. 게임 설정 화면에서 단계별 상징 아이콘을 보여주는 자리에 맞습니다.

  // 계산 겹: 정다각형의 변 수(sides)와 각도(theta)로 그 방향의 반지름을 구하는 기하 공식.
  // theta 가 한 변의 중심에서 멀어질수록(꼭짓점에 가까울수록) 반지름이 길어진다
  function polygonRadius(theta, sides) {
    var seg = (2 * Math.PI) / sides;
    var a = ((theta % seg) + seg) % seg - seg / 2;
    return R * Math.cos(seg / 2) / Math.cos(a);
  }
  function angleAt(i) { return (i / POINTS) * Math.PI * 2 - Math.PI / 2; }
  function radiiFor(sides) {
    var out = [];
    for (var i = 0; i < POINTS; i++) out.push(polygonRadius(angleAt(i), sides));
    return out;
  }
  function buildPath(radii) {
    var d = '';
    for (var i = 0; i < POINTS; i++) {
      var a = angleAt(i);
      var x = CX + radii[i] * Math.cos(a);
      var y = CY + radii[i] * Math.sin(a);
      d += (i === 0 ? 'M' : 'L') + x.toFixed(2) + ',' + y.toFixed(2) + ' ';
    }
    return d + 'Z';
  }
아이콘 모양이 단계마다 변형되는 난이도 선택
새 창에서 열기

05 · 아이콘 모양이 단계마다 변형되는 난이도 선택 — 측정 누적면적 2.066% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

polygonRadiusbuildPathradiiFor

06초점 고리가 관성으로 따라오는 키보드 난이도 선택

화살표 키로 단계를 옮기면 초점 고리가 그 자리로 즉시 이동하지 않고, 매 프레임 남은 거리의 일부만 좁히는 계산으로 뒤따라와 멈춥니다. 키보드만으로 조작하는 접근성 설정의 단계 고르기 자리에 맞습니다.

  function measure(i) {
    var r = opts[i].getBoundingClientRect();
    var pr = rowEl.getBoundingClientRect();
    return { x: r.left - pr.left, w: r.width };
  }
  function paintRing(x, w) { ring.style.transform = 'translateX(' + x + 'px)'; ring.style.width = w + 'px'; }
  // 계산 겹: 매 프레임 남은 거리의 TRAIL_LERP 비율만 좁히는 지수 감쇠(lerp)로 고리가 목표를 뒤따라온다
  function loop() {
    ringX += (targetX - ringX) * TRAIL_LERP;
    ringW += (targetW - ringW) * TRAIL_LERP;
    paintRing(ringX, ringW);
    if (Math.abs(targetX - ringX) > 0.4 || Math.abs(targetW - ringW) > 0.4) raf = requestAnimationFrame(loop);
    else { ringX = targetX; ringW = targetW; paintRing(ringX, ringW); }
  }
  function moveRingTo(i) {
    var m = measure(i);
    targetX = m.x; targetW = m.w;
    cancelAnimationFrame(raf);
    if (reduce) { ringX = targetX; ringW = targetW; paintRing(ringX, ringW); return; }
    raf = requestAnimationFrame(loop);
  }
초점 고리가 관성으로 따라오는 키보드 난이도 선택
새 창에서 열기

06 · 초점 고리가 관성으로 따라오는 키보드 난이도 선택 — 측정 누적면적 5.865% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

measuremoveRingTopaintRing

07색이 수식으로 이어지는 배지형 난이도 선택창

배지 다섯 개가 고정된 다섯 색이 아니라 단계 번호를 색상각 계산식에 넣어 만든 색이라, 단계를 늘리면 배지 사이 색 간격도 다시 계산됩니다. 관리자 화면에서 난이도 등급을 태그처럼 붙이는 자리에 맞습니다.

  var BASE_HUE = 140, HUE_SPACING = 46, SAT = 45, LIGHT = 72;
  var root = document.getElementById('bhs');
  var badges = Array.prototype.slice.call(root.querySelectorAll('.bhs__badge'));
  var selected = 2;

  // 계산 겹: 배지 색을 고정 다섯 색이 아니라 단계 번호를 색상각 수식(기준 색상 + 번호*간격)에 넣어 구한다
  function badgeHue(i) { return (BASE_HUE + i * HUE_SPACING) % 360; }
  function badgeColor(i) { return 'hsl(' + badgeHue(i) + ', ' + SAT + '%, ' + LIGHT + '%)'; }
  function paint() {
    badges.forEach(function (b, i) {
      b.style.background = badgeColor(i);
      b.setAttribute('aria-checked', i === selected ? 'true' : 'false');
      b.setAttribute('tabindex', i === selected ? '0' : '-1');
    });
  }
색이 수식으로 이어지는 배지형 난이도 선택
새 창에서 열기

07 · 색이 수식으로 이어지는 배지형 난이도 선택 — 측정 누적면적 4.045% · 움직인 프레임 10/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

badgeHuebadgeColorpaint

08숫자가 계기판처럼 굴러가는 난이도 스테퍼

+/- 버튼을 누르면 숫자 한 자리가 위아래로 회전하며 다음 값으로 넘어가는 위치를 그 자리의 자릿값 계산으로 얻어, 자리올림이 생기면 왼쪽 자리도 함께 돕니다. 폼에서 문항 난이도 점수를 숫자로 직접 정하는 자리에 맞습니다.

  var MIN = 1, MAX = 12, DIGIT_H = 26;
  var root = document.getElementById('nso');
  var display = document.getElementById('nsoDisplay');
  var tensStrip = document.getElementById('nsoTens');
  var onesStrip = document.getElementById('nsoOnes');
  var out = document.getElementById('nsoOut');
  var value = 5;

  // 계산 겹: 값을 10^자리로 나눈 몫의 일의 자리(placeDigit)로 각 릴이 보여줄 숫자를 구하고,
  // 그 자릿값 * 칸 높이만큼 릴을 밀어 그 숫자가 창에 걸리게 한다 — 자리올림은 몫이 바뀌면서 자연히 생긴다
  function placeDigit(v, place) { return Math.floor(v / Math.pow(10, place)) % 10; }
  function paint() {
    var ones = placeDigit(value, 0), tens = placeDigit(value, 1);
    onesStrip.style.transform = 'translateY(-' + (ones * DIGIT_H) + 'px)';
    tensStrip.style.transform = 'translateY(-' + (tens * DIGIT_H) + 'px)';
    display.setAttribute('aria-valuenow', String(value));
    out.textContent = String(value);
  }
숫자가 계기판처럼 굴러가는 난이도 스테퍼
새 창에서 열기

08 · 숫자가 계기판처럼 굴러가는 난이도 스테퍼 — 측정 누적면적 0.688% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

placeDigitpaintset

09드래그 각도로 값이 정해지는 반원 난이도 게이지

바늘을 손가락으로 끌면 중심에서 손가락까지의 각도를 구해 그 각도를 난이도 값으로 바꾸고, 호 안의 구간 색은 그 값 범위에 맞춰 다시 그립니다. 설정 대시보드에서 손끝으로 난이도를 튜닝하는 자리에 맞습니다.

  // 계산 겹: 포인터 좌표와 피벗(바닥 중앙) 사이의 각도를 atan2 로 구해 -90~90도 구간을 0~4 값으로 매핑한다
  function angleToValue(clientX, clientY) {
    var r = face.getBoundingClientRect();
    var px = r.left + r.width / 2;
    var py = r.bottom;
    var dx = clientX - px;
    var dy = Math.min(clientY - py, -1); // 피벗 위쪽으로 강제해 좌측 경계에서 각도가 튀지 않게 한다
    var theta = Math.atan2(dy, dx) * 180 / Math.PI; // -180..0
    var t = (theta + 180) / 180;
    t = Math.max(0, Math.min(1, t));
    return t * 4;
  }
  function paint() {
    var deg = -90 + (value / 4) * 180;
    needle.style.transform = 'rotate(' + deg + 'deg)';
    var zi = Math.round(value);
    zones.forEach(function (z, i) { z.classList.toggle('is-active', i === zi); });
    face.setAttribute('aria-valuenow', String(zi));
    face.setAttribute('aria-valuetext', LEVELS[zi]);
    out.textContent = LEVELS[zi];
  }
드래그 각도로 값이 정해지는 반원 난이도 게이지
새 창에서 열기

09 · 드래그 각도로 값이 정해지는 반원 난이도 게이지 — 측정 누적면적 2.203% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

angleToValuezoneCenterDegpaint

값이 UI를 바꾸는 곳 — 단계 수·색상각 간격·스프링 감쇠

01 의 HUE_LOW·HUE_HIGH(132°→0°)와 07 의 BASE_HUE·HUE_SPACING(140°에서 46°씩)은 같은 색상각 계산이라도 서로 다른 구간을 씁니다 — 01 은 초록에서 빨강으로 이어지는 연속 램프, 07 은 다섯 개 배지 사이 간격입니다. 03 의 SPRING_K·SPRING_D(210·18)는 손잡이 위치를 매 프레임 다시 계산하는 물리 상수이고, 06 의 TRAIL_LERP(0.18)는 남은 거리의 비율만 좁히는 감쇠 상수라 서로 다른 수식에 들어갑니다. 08 의 DIGIT_H(26)는 릴이 밀리는 픽셀 폭, 09 의 ZONE_DEG(36)는 반원을 5구역으로 나누는 각도 폭으로, 이 선택 UI 안에서도 항목마다 값이 바꾸는 대상이 다릅니다.

이웃 가지

세그먼트 컨트롤 선택 UI 9종 — 복붙하면 되는은 막대 아래 밑줄이 선택 위치로 슬라이드해 가는 데 초점이 있고, 이 편처럼 색상각·스프링·각도를 계산해 값을 구하는 항목은 없습니다. 별점 UI 9종 — 채움 값이 상태에 따라 바뀌는 방식은 채움 너비만 상태에 따라 바뀔 뿐 색상각이나 관성 계산은 쓰지 않습니다. 달력 선택 UI 9종 — 복붙하면 되는에는 03·08·09 와 겹쳐 보이는 슬라이더·스테퍼·드래그 패턴이 있지만 그쪽은 날짜·가격 값을 다루고, 이 편은 난이도 단계라는 맥락에서 같은 조작을 다시 얹었습니다.

어디서 깨지는가 — 접근성·reduced-motion·키보드 포커스

01 의 HUE_LOW·HUE_HIGH와 07 의 BASE_HUE·HUE_SPACING은 0~360° 색상각 도메인이고, 03 의 SPRING_K·SPRING_D는 픽셀에 가까운 위치·속도 도메인이며, 06 의 TRAIL_LERP는 getBoundingClientRect로 잰 실제 픽셀 거리 도메인, 09 의 angleToValue는 atan2가 뱉는 라디안을 0~4 값으로 다시 매핑한 도메인입니다. 한 항목의 상수를 다른 항목에 그대로 옮기면(예: 06 의 lerp 비율을 03 의 스프링 힘에 쓰는 식) 단위가 안 맞아 계산이 튀거나 멈추지 않습니다 — 변형은 반드시 그 항목의 수식 안에서만 바꿔야 합니다.

reduce 상태에서 각자 무엇을 멈추는가

아홉 항목 모두 prefers-reduced-motion: reduce를 감지하면 자동 시연 timer를 멈추고, SCSS 쪽에서도 해당 요소에 transition: none을 겁니다. 이 가운데 03·04·05·06 넷은 한 겹 더 나아가 springTo·sweep·morphTo·moveRingTo 안에서 reduce를 확인해 requestAnimationFrame 루프 자체를 건너뛰고 목표 값으로 바로 이동합니다. 즉 나머지 다섯은 애초에 rAF 루프가 없어 멈출 것이 트랜지션뿐이고, rAF 를 쓰는 넷만 "루프 생략"이라는 조건 분기가 하나 더 있는 셈입니다.

이 선택 화면을 그대로 옮기려면 아이디어 자체보다 이런 계산 겹을 그대로 가져가는 게 빠릅니다. 코드와 SCSS 를 그대로 담은 zip 은 비밀번호로 열리고, 비밀번호는 m4ceuurv 형식으로 안내됩니다.

키보드·스크린리더로 확인할 것

01·02·05·06·07 다섯은 role="radiogroup" 컨테이너 안에 role="radio" 항목을 두고 aria-checked로 선택 상태를 알립니다. 03·04·09 는 role="slider"에 aria-valuemin·aria-valuemax·aria-valuenow·aria-valuetext를 붙이고, 08 은 role="spinbutton"에 aria-valuemin·aria-valuemax·aria-valuenow만 둡니다. 01·02·05·07 은 화살표 키로 초점을 옮길 때 focus({ preventScroll: true })를 두 곳(좌·우 방향)에서 호출하는데, 06 은 여기에 Home·End 키까지 더해 다섯 곳에서 같은 호출을 해 어느 경로로 초점이 옮겨가도 화면이 갑자기 스크롤되지 않게 막습니다. radiogroup 과 slider 각각의 키보드 규약은 MDN radiogroup 역할과 MDN slider 역할 문서에 정리돼 있습니다.

변형 세 가지와 접근성

변형 값 느낌
스프링 감쇠값 높여 오버슛 줄이기 03 SPRING_D 18 → 26 같은 SPRING_K=210 대비 진동이 더 빨리 잦아들어, 손잡이가 단계를 지나치는 정도가 줄고 더 차분하게 멈춘다.
배지 색상각 간격 넓히기 07 HUE_SPACING 46 → 64 badgeHue(i) 계산값이 서로 더 멀어져 배지 다섯 개의 색이 뚜렷하게 갈린다.
포커스 고리 관성 lerp 값 낮추기 06 TRAIL_LERP 0.18 → 0.10 초점 고리가 매 프레임 좁히는 거리 비율이 줄어 목표 단계까지 더 천천히, 더 부드럽게 따라간다.

세 변형 모두 상수 하나만 바뀔 뿐, click·keydown·pointerdown 리스너 구성이나 role="radiogroup"·role="slider"·aria-valuenow 흐름은 손대지 않습니다. 스크린리더가 읽는 값의 형식도 수정 전후로 달라지지 않으므로, 어떤 값을 골라도 접근성 지원 범위는 그대로 유지됩니다.

난이도 고르기 ui 예제는 어디서 보나?

이 편 위쪽 3×3 격자가 그 예제이고, 항목마다 이어지는 코드 펜스를 열면 계산이 도는 함수만 따로 읽을 수 있습니다. 값 하나만 고르는 선택 흐름을 더 보려면 세그먼트 컨트롤 선택 UI 9종을, 채움 값이 상태로 바뀌는 쪽을 보려면 별점 UI 9종을 함께 열어보면 됩니다.

FAQ

이 선택 화면의 단계 개수를 5개가 아니라 3개로 줄이려면 어떻게 하나요?

01·03·04·05·07·09 여섯 개는 LEVELS 배열 길이를 5에서 3으로 줄이면 dotHue·springTo가 쓰는 단계 수·radiiFor·badgeHue·zoneCenterDeg가 모두 새 길이를 기준으로 다시 계산됩니다. 06 은 LEVELS 배열이 아니라 마크업에 놓인 옵션 버튼(.kal__opt) 개수 자체를 5에서 3으로 줄이면 measure·moveRingTo가 그 목록 길이를 기준으로 다시 계산됩니다. 02 는 이미 LEVELS가 세 개(쉬움·보통·어려움)뿐이고, 08 은 숫자 스테퍼라 LEVELS 대신 MIN·MAX 값 자체를 바꾸면 됩니다.

이 선택 화면을 게임이 아닌 설문·폼에도 쓸 수 있나요?

네, 됩니다. 아홉 항목 모두 LEVELS 라벨을 "쉬움·보통·어려움"처럼 중립으로 잡아 게임 브랜드나 캐릭터 이름을 넣지 않았습니다. 02 는 이미 앱 시작 화면의 상·중·하를, 03 은 설정 화면의 드래그 슬라이더를 예로 들었듯, 라벨 문자열만 바꾸면 설문·체크아웃 폼에도 그대로 옮겨 쓸 수 있습니다.

reduced-motion을 켠 화면에서도 아홉 가지 티어 선택 UI가 전부 되나요?

네, 됩니다. reduce 가 막는 건 자동 시연 timer와 트랜지션·rAF 루프뿐이고, 클릭·키보드·드래그로 값을 바꾸는 동작 자체는 아홉 항목 모두 그대로 동작합니다. 03·04·05·06 은 스프링·이징·보간 계산까지 건너뛰고 목표값으로 즉시 이동해 오히려 더 단순하게 반응합니다.

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

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