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

css 게이지 바 9종 — 손잡이가 계산으로 도는 원형 컨트롤

아홉 개의 원형 컨트롤 중 하나는 드래그 각도 값이 관성으로 감쇠하며 손잡이가 서서히 멈추고, 하나는 각 구간 값이 각도로 변환되어 css 게이지 바가 원을 그리며 채워지고, 하나는 스냅 판정 값에 따라 가장 가까운 구간으로 튕기듯 정렬됩니다.

트리거는 항목마다 다릅니다 — 01 은 열기 click·닫기 Escape keydown, 02 는 pointerdown/move/up 에 방향키 keydown, 03 은 클릭 없이 pointermove 와 pointerleave 만, 04 는 pointercancel 까지 받고, 05 는 pointerdown/move/up, 06 은 click 두 버튼, 07 은 pointerdown/up/leave 에 Enter·Space keydown, 08 은 pointerdown/move/up 에 방향키 keydown, 09 는 keydown 이 먼저고 pointerdown 이 초점을 옮깁니다. 자동 시연은 .is-demo 클래스가 붙어 있고 prefers-reduced-motion 이 거짓일 때만 돌다가 첫 사용자 입력에 풀리는데, 04·05·06·07 은 감쇠·정착·보간·스프링 계산 자체에 reduce 검사가 박혀 있어 자동 시연 조건과 무관하게 실사용 중에도 설정을 계속 지키고, 나머지 다섯은 실사용 전환을 css prefers-reduced-motion 블록이 대신 끊습니다. Math.random 은 한 곳도 쓰지 않아 같은 조작은 언제나 같은 결과를 냅니다.

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

아홉 개의 해부 — 어떤 계산이 원형 컨트롤을 움직이는가

아홉 컨트롤을 각각 24장으로 캡처해 세 지표로 비교했습니다 — 얼마나 넓은 화면이 바뀌었는지(누적면적), 프레임끼리 픽셀 차를 얼마나 셌는지(강도), 캡처 24장 중 실제로 움직임이 잡힌 장 수(움직인프레임)입니다.

항목 누적면적% 강도_평균 움직인프레임
01 11.25% 98.5 12/23
02 23.78% 120.2 23/23
03 11.20% 75.6 22/23
04 17.07% 55.4 17/23
05 1.66% 153.9 18/23
06 5.93% 181.2 23/23
07 18.51% 54.2 12/23
08 8.37% 88.8 23/23
09 2.96% 150.8 15/23

누적면적 1위는 02(23.78%)입니다 — 원판 전체가 돌며 여러 장르의 크기·선명도가 함께 바뀌니 픽셀이 바뀌는 폭이 가장 넓습니다. 꼴찌는 05(1.66%)로 손잡이가 눈금 사이 짧은 거리만 움직이기 때문이고, 그 앞의 09(2.96%)도 손잡이 하나가 원호를 따라 이동하는 좁은 변화입니다. 강도가 180 을 넘는 항목은 06(181.2) 하나뿐인데, 고리가 채워지는 순간 대비가 높은 픽셀 변화가 좁은 영역에 집중되어서입니다. 최저는 07(54.2)로 스프링으로 흔들리는 손잡이의 변화가 완만하기 때문이고, 움직인 프레임은 02·06·08 이 23/23 로 가장 많으며 01·07 이 12/23 로 가장 적습니다 — 이는 흠이 아니라 단계 사이 머무는 정지 구간의 길이가 다를 뿐입니다.

값을 구하는 경로로 묶으면 네 부류로 갈립니다.

좌표를 각도에서 만드는 01·08

01 은 항목 i 의 각도를 ARC_SPAN_DEG(200) 안에 균등 분할해 cos·sin 으로 좌표를 내고, 08 은 포인터 각도를 atan2 로 구해 0~1 로 정규화한 뒤 다시 cos·sin 으로 손잡이 좌표를 냅니다. 둘 다 "어디에 놓을지"를 각도에서 시작해 좌표로 끝내는 축입니다.

나머지 연산이 값을 고르는 02·03·09

02 는 회전각을 ((-rotation % 360) + 360) % 360 으로 정규화해 활성 인덱스를 구하고, 03 은 포인터 각도를 360/N 폭으로 나눠 구간을 판정하며, 09 는 값-각도 비율 stepPct 로 키보드 한 걸음의 크기를 정합니다. 돌아온 값을 0~N 안으로 접어 넣는 나머지가 공통 분모입니다.

손을 뗀 뒤를 계산하는 04·05·07

04 는 놓은 순간의 속도에 매 프레임 DRAG_DECAY(0.92) 를 곱해 줄이고, 05 는 눈금까지의 거리를 재 easeOutBack 곡선으로 정착하며, 07 은 목표와 현재의 차에 스프링 상수를 곱해 힘으로 반영합니다. 드래그가 끝난 뒤에도 프레임마다 다시 도는 계산이라는 점이 같습니다.

표시 값을 끊어 보이게 하는 06

06 은 quantizedDeg() 가 퍼센트를 각도로 바꾼 뒤 GAUGE_STEP_DEG(6) 배수로 끊어 내리고, 채움 속도는 shown += (value - shown) * 0.16 보간으로 따로 움직입니다. 각도 해상도와 속도를 두 겹으로 분리해 어느 한쪽을 바꿔도 다른 쪽이 그대로 남는 축입니다.

패턴별 코드

01방사형 메뉴 — 중심에서 부채꼴로 펼쳐지는 원형 메뉴

항목 i 의 각도를 -90 - ARC_SPAN_DEG/2 + i * (ARC_SPAN_DEG/(개수-1)) 로 구해 위쪽을 중심으로 200 도 부채꼴을 균등 분할하고, RADIUS(96px) 만큼 떨어진 좌표를 cos·sin 으로 계산합니다. 좌표는 열림 여부와 무관하게 한 번만 구해지고 열림은 --stagger 지연만 차이를 냅니다.

  // 계산 겹: 극좌표 → 데카르트 좌표. 항목 i 는 위쪽(-90도)을 중심으로 ARC_SPAN_DEG 를 균등 분할한
  // 각도에서 RADIUS 만큼 떨어진 자리를 차지한다. 이 좌표는 열림 여부와 무관하게 한 번만 계산한다.
  var items = ACTIONS.map(function (label, i) {
    var li = document.createElement('li');
    li.className = 'rm__item';
    li.setAttribute('role', 'none');
    var btn = document.createElement('button');
    btn.type = 'button';
    btn.className = 'rm__itemBtn';
    btn.setAttribute('role', 'menuitem');
    btn.tabIndex = -1;
    btn.textContent = label;
    var angleDeg = -90 - ARC_SPAN_DEG / 2 + i * (ARC_SPAN_DEG / (ACTIONS.length - 1));
    var rad = angleDeg * Math.PI / 180;
    var x = Math.round(Math.cos(rad) * RADIUS);
    var y = Math.round(Math.sin(rad) * RADIUS);
    li.style.setProperty('--x', x);
    li.style.setProperty('--y', y);
    li.style.setProperty('--stagger', i);
    li.appendChild(btn);
    list.appendChild(li);
방사형 메뉴 — 중심에서 부채꼴로 펼쳐지는 원형 메뉴
새 창에서 열기

01 · 방사형 메뉴 — 중심에서 부채꼴로 펼쳐지는 원형 메뉴 — 측정 누적면적 11.25% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ARC_SPAN_DEGRADIUStranslate

02장르 선택 휠 — 카테고리를 원판처럼 돌려 고르는 방식

드래그로 쌓인 회전각을 ((-rotation % 360) + 360) % 360 나머지로 정규화하고 STEP_DEG(=360/7) 단위로 나눠 지금 중앙을 지나는 인덱스를 구합니다. 각 장르의 색은 i * HUE_STEP_DEG 를 --hue 로 넘겨 색상환 각도로 그대로 매핑되고, 손을 떼면 가장 가까운 STEP_DEG 배수로 붙어 한 장르가 언제나 정중앙에 옵니다.

  function apply() {
    ring.style.setProperty('--rotation', rotation);
    var idx = activeIndex();
    lis.forEach(function (li, i) { li.classList.toggle('gw__slot--active', i === idx); });
    picked.textContent = '선택: ' + GENRES[idx];
  }

  wheel.addEventListener('pointerdown', function (e) {
    human();
    dragging = true;
    startX = e.clientX;
    startRotation = rotation;
    wheel.setPointerCapture(e.pointerId);
  });
  wheel.addEventListener('pointermove', function (e) {
    if (!dragging) return;
    rotation = startRotation + (e.clientX - startX) * 0.6;
    apply();
  });
  wheel.addEventListener('pointerup', function () {
    dragging = false;
    // 계산 겹: 놓은 각도를 가장 가까운 STEP_DEG 배수로 붙여 항상 한 장르가 정중앙에 오게 한다.
    rotation = Math.round(rotation / STEP_DEG) * STEP_DEG;
    apply();
  });
장르 선택 휠 — 카테고리를 원판처럼 돌려 고르는 방식
새 창에서 열기

02 · 장르 선택 휠 — 카테고리를 원판처럼 돌려 고르는 방식 — 측정 누적면적 23.781% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

activeIndexSTEP_DEGhsl(

03구간 호버 링 — 마우스가 지나는 구간만 도드라지는 원형 띠

포인터 좌표를 Math.atan2 로 링 중심 기준 각도로 바꾸고 12시를 0 도로 맞춘 뒤, segmentIndexAt() 의 나머지 연산으로 360/N 폭의 어느 구간인지 판정합니다. 클릭이나 드래그 없이 포인터가 지나가는 것만으로 반응하고 pointerleave 로 해제됩니다.

  // 계산 겹: 포인터 좌표를 링 중심 기준 각도로 바꾼 뒤(atan2), 어느 구간(360/N 폭)에 속하는지 판정한다.
  function angleAt(clientX, clientY) {
    var r = ring.getBoundingClientRect();
    var cx = r.left + r.width / 2, cy = r.top + r.height / 2;
    var deg = Math.atan2(clientY - cy, clientX - cx) * 180 / Math.PI + 90; // 12시를 0도로
    return ((deg % 360) + 360) % 360;
  }
  function segmentIndexAt(deg) {
    return Math.floor(deg / (360 / N)) % N;
  }
구간 호버 링 — 마우스가 지나는 구간만 도드라지는 원형 띠
새 창에서 열기

03 · 구간 호버 링 — 마우스가 지나는 구간만 도드라지는 원형 띠 — 측정 누적면적 11.2% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

segmentIndexAtMath.atan2conic-gradient(

04드래그 관성 손잡이 — 손을 떼도 서서히 멈추는 회전 손잡이

드래그 중엔 포인터 각도의 변화량이 그대로 프레임당 속도로 기록되고, 손을 떼면 decayStep() 이 매 프레임 velocity *= DRAG_DECAY(0.92) 로 속도를 줄이며 각도에 계속 더합니다. MIN_VELOCITY(0.15) 아래로 떨어지면 정지로 보는데, 놓은 속도가 세면 셀수록 더 오래 도는 물리감이 여기서 나옵니다.

  // 계산 겹: 관성 감쇠. 놓은 순간의 속도가 매 프레임 DRAG_DECAY 배로 줄며 각도에 계속 더해진다.
  // reduce 에서는(실사용 포함) 감쇠 구간을 건너뛰고 즉시 멈춘다 — human() 뒤에도 이 분기는 그대로 적용된다.
  function decayStep() {
    if (dragging) return;
    if (reduce) { velocity = 0; return; }
    if (Math.abs(velocity) < MIN_VELOCITY) { velocity = 0; return; }
    velocity *= DRAG_DECAY;
    angle += velocity;
  }
드래그 관성 손잡이 — 손을 떼도 서서히 멈추는 회전 손잡이
새 창에서 열기

04 · 드래그 관성 손잡이 — 손을 떼도 서서히 멈추는 회전 손잡이 — 측정 누적면적 17.067% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

decayStepDRAG_DECAYMath.atan2

05스냅 선택 다이얼 — 가까운 눈금으로 튕기듯 정렬되는 방식

손을 뗀 각도와 NOTCH_DEG 간격 눈금 사이의 최단 각거리를 재고, 그 거리가 SNAP_THRESHOLD_DEG(20) 이내면 그 눈금으로 정착을 시작합니다. 정착은 easeOutBack 곡선 — 목표를 살짝 넘겼다 되돌아오는 튕김 — 이 SETTLE_MS(260ms) 동안 이어지는데, 이 부호 반전 곡선은 cubic-bezier 로는 만들 수 없어 자바스크립트로 계산합니다.

  // 계산 겹: 놓은 각도와 가장 가까운 눈금(N 등분) 사이의 최단 각거리를 구해, 문턱 안이면 그 눈금으로 스냅한다.
  function nearestNotch(deg) {
    var idx = Math.round(deg / NOTCH_DEG) % N;
    var notchDeg = idx * NOTCH_DEG;
    var dist = Math.abs(((deg - notchDeg + 540) % 360) - 180);
    return { idx: idx, notchDeg: notchDeg, dist: dist };
  }

  function apply(a) {
    ring.style.setProperty('--angle', a);
  }

  function startSettle(target) {
    settling = true;
    settleFrom = angle;
    settleTo = target;
    settleStart = performance.now();
  }

  function easeOvershoot(t) {
    // 목표를 살짝 넘겼다 되돌아오는 튕김(easeOutBack) — cubic-bezier 로는 못 만드는 부호 반전 곡선
    var c1 = 1.70158, c3 = c1 + 1;
    var t1 = t - 1;
    return 1 + c3 * t1 * t1 * t1 + c1 * t1 * t1;
  }
스냅 선택 다이얼 — 가까운 눈금으로 튕기듯 정렬되는 방식
새 창에서 열기

05 · 스냅 선택 다이얼 — 가까운 눈금으로 튕기듯 정렬되는 방식 — 측정 누적면적 1.661% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

nearestNotchSNAP_THRESHOLD_DEGNOTCH_DEG

06원형 게이지 바 — 값에 따라 각도로 채워지는 원형 진행률 표시

퍼센트를 각도로 바꾼 뒤 quantizedDeg() 가 GAUGE_STEP_DEG(6) 배수로 끊어 내려 계단형으로 채웁니다. 채워지는 속도는 별개의 shown += (value - shown) * 0.16 보간과 LOOP_MS 가 정하므로, GAUGE_STEP_DEG 를 바꿔도 속도는 그대로입니다 — 이 두 계산의 분리가 이 항목의 핵심입니다. 원형 프로그레스바 css 로 흔히 검색되는 conic-gradient 채움을 값 계산과 함께 쓰고 싶을 때 붙여 넣는 자리입니다.

  // 계산 겹: 퍼센트를 각도로 바꾼 뒤 GAUGE_STEP_DEG 배수로 끊어 내림한다.
  // 매끈한 360*value/100 대신 계단형으로 보여 "단계형" 게이지가 된다.
  function quantizedDeg(v) {
    var raw = Math.min(100, Math.max(0, v)) * 3.6;
    return Math.floor(raw / GAUGE_STEP_DEG) * GAUGE_STEP_DEG;
  }

  function apply() {
    ring.style.setProperty('--deg', quantizedDeg(shown));
    pctEl.textContent = Math.round(shown) + '%';
  }
원형 게이지 바 — 값에 따라 각도로 채워지는 원형 진행률 표시
새 창에서 열기

06 · 원형 게이지 바 — 값에 따라 각도로 채워지는 원형 진행률 표시 — 측정 누적면적 5.931% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

quantizedDegGAUGE_STEP_DEGconic-gradient(

07물리형 노브 다이얼 — 손잡이 자체가 눌리고 튀는 질감의 다이얼

매 프레임 목표까지의 거리에 SPRING_K(0.18) 를 곱해 힘을 만들고, 누적된 속도에 SPRING_DAMP(0.72) 를 곱해 흔들림을 잦아들게 합니다. 단순 보간과 달리 목표를 넘겼다 되돌아오는 튕김이 생겨, 누르면 0.86 으로 가라앉고 떼면 1.12 를 지나 1 로 돌아오는 무게감이 나옵니다.

  // 계산 겹: 감쇠 스프링. 매 프레임 목표까지의 거리에 SPRING_K 를 곱해 가속하고,
  // 누적 속도에 SPRING_DAMP 를 곱해 서서히 잦아들게 한다(단순 lerp 와 달리 목표를 넘겨 튕긴다).
  function springStep() {
    if (reduce) { scaleCur = scaleTarget; scaleVel = 0; return; }
    var force = (scaleTarget - scaleCur) * SPRING_K;
    scaleVel = (scaleVel + force) * SPRING_DAMP;
    scaleCur += scaleVel;
  }
물리형 노브 다이얼 — 손잡이 자체가 눌리고 튀는 질감의 다이얼
새 창에서 열기

07 · 물리형 노브 다이얼 — 손잡이 자체가 눌리고 튀는 질감의 다이얼 — 측정 누적면적 18.514% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

springStepSPRING_KSPRING_DAMP

08원호 무드 슬라이더 — 반원을 따라 강도를 고르는 손잡이

포인터 좌표를 atan2 로 반원 각도(-180~0)로 바꾼 뒤 0~1 로 정규화한 t 하나가 좌표·색상·라벨을 동시에 구동합니다. t 로 다시 각도를 내 cos·sin 으로 손잡이 좌표를 계산하고, --hue 는 t * 130, 라벨 인덱스는 floor(t * N) — 값 하나가 세 출력을 함께 움직여 어긋나지 않습니다.

  // 계산 겹: 포인터 좌표를 atan2 로 반원(180~360deg, 왼쪽 끝~오른쪽 끝) 각도로 바꾸고 0~1 로 정규화한다.
  function angleToT(clientX, clientY) {
    var r = arc.getBoundingClientRect();
    var cx = r.left + r.width / 2, cy = r.bottom;
    var deg = Math.atan2(clientY - cy, clientX - cx) * 180 / Math.PI; // -180 ~ 0 (위쪽 반원)
    var clamped = Math.min(0, Math.max(-180, deg));
    return 1 - (clamped + 180) / 180;
  }

  function apply() {
    var deg = -180 + t * 180;
    var x = Math.cos(deg * Math.PI / 180);
    var y = Math.sin(deg * Math.PI / 180);
    handle.style.setProperty('--x', x.toFixed(4));
    handle.style.setProperty('--y', y.toFixed(4));
    handle.style.setProperty('--hue', Math.round(t * 130));
    var idx = Math.min(N - 1, Math.floor(t * N));
    labelEl.textContent = MOODS[idx];
  }
원호 무드 슬라이더 — 반원을 따라 강도를 고르는 손잡이
새 창에서 열기

08 · 원호 무드 슬라이더 — 반원을 따라 강도를 고르는 손잡이 — 측정 누적면적 8.369% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

angleToTMath.atan2hsl(

09키보드 조작 원형 컨트롤 — 화살표 키로 눈금을 옮기는 접근성 다이얼

valueToDeg() 가 0~100 값을 RANGE_DEG(300) 범위의 각도로 바꾸고, 화살표 키 한 번은 STEP_DEG(12)/RANGE_DEG 비율만큼 값을 옮깁니다. 요소는 role="slider" 에 aria-valuemin·aria-valuemax·aria-valuenow·aria-valuetext 를 갖추고 있어 값이 바뀔 때마다 스크린리더가 현재 값을 읽습니다.

  // 계산 겹: 값(%) 을 각도로 바꾼다. RANGE_DEG 범위 안에서 0%→시작각, 100%→끝각.
  function valueToDeg(v) {
    return -RANGE_DEG / 2 + (Math.min(100, Math.max(0, v)) / 100) * RANGE_DEG;
  }

  function apply() {
    handle.style.setProperty('--deg', valueToDeg(value));
    handle.setAttribute('aria-valuenow', String(Math.round(value)));
    handle.setAttribute('aria-valuetext', Math.round(value) + '%');
  }

  function setValue(v) { value = Math.min(100, Math.max(0, v)); apply(); }

  // 계산 겹: 키 입력 한 번 = STEP_DEG 만큼의 각도 = RANGE_DEG 대비 그 비율만큼의 퍼센트.
  var stepPct = (STEP_DEG / RANGE_DEG) * 100;

  handle.addEventListener('keydown', function (e) {
    if (e.key === 'ArrowRight' || e.key === 'ArrowUp') { e.preventDefault(); human(); setValue(value + stepPct); }
    else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') { e.preventDefault(); human(); setValue(value - stepPct); }
    else if (e.key === 'Home') { e.preventDefault(); human(); setValue(0); }
    else if (e.key === 'End') { e.preventDefault(); human(); setValue(100); }
  });
키보드 조작 원형 컨트롤 — 화살표 키로 눈금을 옮기는 접근성 다이얼
새 창에서 열기

09 · 키보드 조작 원형 컨트롤 — 화살표 키로 눈금을 옮기는 접근성 다이얼 — 측정 누적면적 2.96% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

valueToDegSTEP_DEGaria-valuenow

값이 손잡이를 바꾸는 곳 — 각도·관성·스냅·초점 기준

한 걸음을 정하는 상수들

아홉 개의 성질은 이름 붙은 상수값 하나하나가 정합니다 — 01 의 ARC_SPAN_DEG(200)·RADIUS(96), 02 의 STEP_DEG(=360/7)·HUE_STEP_DEG(=360/7), 04 의 DRAG_DECAY(0.92)·MIN_VELOCITY(0.15), 05 의 NOTCH_DEG·SNAP_THRESHOLD_DEG(20)·SETTLE_MS(260), 06 의 GAUGE_STEP_DEG(6), 07 의 SPRING_K(0.18)·SPRING_DAMP(0.72), 09 의 STEP_DEG(12)·RANGE_DEG(300) 입니다. css 도넛 차트처럼 눈으로 보이는 결과가 비슷해도 값을 손대는 자리는 이 상수들이고, 채움 속도처럼 상수가 아닌 값(06 의 0.16 보간 계수)은 소스 안에 자리를 따로 두고 있습니다.

이웃 가지 — 원형 컨트롤 축의 다른 편

값이 고리를 채우는 모양만 필요하면 원형 프로그레스바 CSS 9종 — 값만 바꾸면 되는가 값 표시에 더 가깝고, 이 편은 사람이 손으로 조작하는 컨트롤을 다룹니다. 손잡이를 밀어 멈추는 물리는 룰렛 돌리기 — 튕겨 돌리고 마찰로 멈추는 돌림판 캔버스 9종이 마찰 감쇠 축에서 다루는데, 이 편의 04 관성은 사용자가 돌린 방향과 속도를 그대로 잇는 점에서 갈립니다. 직선 축의 대응물은 css input range 슬라이더 — 관성·스냅 계산을 얹은 커스텀 9종입니다. 각도 계산의 근거는 conic-gradient 문서와 Pointer events 문서를 따릅니다.

어디서 깨지는가 — 터치·키보드 대응, 폴백, reduced-motion

원형 컨트롤이 실제로 깨지는 지점은 화면이 아니라 입력에서 먼저 옵니다. 드래그를 쓰는 다섯 항목 중 04 만 pointercancel 을 받는데, 터치 기기에서 브라우저가 제스처를 가로채면 pointerup 없이 pointercancel 이 오므로 이 이벤트를 놓치면 손잡이가 드래그 상태에 걸린 채 남습니다. 03 은 pointermove 호버만 쓰므로 터치 기기에서는 탭이 pointermove 를 한 번 내는 정도로만 반응합니다 — 데스크톱 마우스용 패턴으로 보는 것이 맞습니다. 02·08 은 setPointerCapture 로 포인터를 붙잡아 요소 밖으로 나가도 드래그가 이어지고, 09 는 pointerdown 이 값을 바꾸는 게 아니라 초점을 옮기는 데만 써서 조작은 전부 키보드가 담당합니다. 각도를 그대로 클래스 토글로만 쓰면 여러 바퀴를 돌았을 때 02 의 활성 인덱스가 어긋나는데, ((-rotation % 360) + 360) % 360 정규화가 그 어긋남을 막는 겹입니다.

아홉 컨트롤의 html·scss·js 소스 전체는 ydmj5nha 압축 비밀번호로 받아 상수를 직접 바꿔가며 열어볼 수 있습니다.

reduced-motion 이 남기는 것

reduce 설정이 켜지면 자동 시연 대신 정지 상태가 남는데, 04 는 decayStep() 안에서, 05 는 SETTLE_MS 계산에서, 06 은 보간 계수에서, 07 은 springStep() 안에서 reduce 를 자동 시연 조건(isDemo) 과 무관하게 검사해 사람이 조작하기 시작한 뒤에도 계속 지켜집니다 — 04 는 velocity 를 0 으로 놓아 관성 없이 즉시 멈추고, 05 는 SETTLE_MS 가 0 이 되어 튕김 없이 눈금에 바로 앉으며, 06 은 보간 계수가 1 이 되어 클릭 즉시 목표 각도를 보여 주고, 07 은 springStep() 이 현재값을 목표에 바로 붙입니다. 01·02·03·08·09 는 reduce 검사가 자동 시연 루프 안에만 있어 실사용 전환은 css prefers-reduced-motion 블록이 대신 끊는데, 01 은 펼쳐진 메뉴를, 09 는 40% 상태를 시연 화면에서 고정해 보여 줍니다.

한 화면에 원형 다이얼 아홉을 몰아 넣으면 requestAnimationFrame 루프가 동시에 아홉 개 도는 셈이라 저사양 기기에서는 프레임이 밀릴 수 있습니다 — 실서비스에 붙일 때는 필요한 컨트롤 하나만 남기는 편이 안전합니다.

변형 세 가지와 접근성

변형 값 느낌
드래그 관성 길게 남기기 04 DRAG_DECAY 0.92 → 0.98 감쇠율을 1에 더 가깝게 올리면 decayStep() 에서 손을 뗀 뒤 속도에 매 프레임 곱해지는 값이 커져(velocity *= DRAG_DECAY) 회전 속도가 더 천천히 줄어들고 관성이 더 오래 남는다 — 손잡이가 미끄러지듯 멀리 간다
스냅 판정 범위 좁히기 05 SNAP_THRESHOLD_DEG 20 → 10 판정 범위를 좁히면 nearestNotch() 가 재는 거리(n.dist)가 이 값 이하일 때만 스냅하므로 눈금에 더 가까이 다가가야만 스냅이 걸려 놓는 위치에 덜 관대해진다 — 정밀 조작 느낌이 강해진다
게이지 채움을 더 매끈하게 06 GAUGE_STEP_DEG 6 → 3 GAUGE_STEP_DEG 는 quantizedDeg() 가 표시 각도를 끊어 내리는 단위일 뿐 채워지는 속도(0.16 보간 계수·LOOP_MS)와는 무관하다 — 낮추면 계단처럼 보이던 채움 단위가 더 촘촘해져 같은 속도로도 더 매끈하게 보인다

접근성 면에서 09 는 role="slider" 와 aria-valuenow·aria-valuetext 로 값이 스크린리더에 읽히고 Home·End 키로 양 끝에 바로 갑니다. 01 은 role="menu"·menuitem 에 aria-expanded 로 열림을 알리고 닫힌 동안 항목의 tabIndex 를 -1 로 둬 탭 순서를 침범하지 않으며 Escape 로 닫힙니다. 02 는 role="listbox" 로 방향키를 받고, 04·05·07·08 은 aria-label 이 붙은 실제 button 이라 Enter·Space 로 그대로 작동합니다. 03 의 구간 조각과 05 의 눈금은 장식이라 aria-hidden 으로 숨겼고, 06 은 고리를 role="img" 로 라벨과 함께 읽히게 합니다. reduce 를 켠 환경에서는 앞 절의 정지 상태가 애니메이션 없이 그대로 남습니다.

FAQ

css 프로그레스바 애니메이션 효과를 이 게이지에 그대로 얹을 수 있나요?

06 이 바로 그 조합입니다 — 채움은 conic-gradient 로 그리고 값이 다가가는 속도는 0.16 보간 계수가 정하며, 표시 각도는 GAUGE_STEP_DEG 배수로 끊어 계단형으로 보입니다. 나머지 여덟은 값을 사람 조작으로 바꾸는 컨트롤이라, 진행률이 저절로 오르는 화면이 필요하면 06 만 떼어 쓰는 편이 맞습니다.

원형 다이얼을 키보드만으로 조작할 수 있나요?

09 가 그 역할 전용입니다 — 화살표 키 한 번이 STEP_DEG(12)/RANGE_DEG(300) 비율만큼 값을 옮기고 Home·End 로 양 끝에 갑니다. 그 외에 01·02·07·08 도 keydown 을 받아 Escape·방향키·Enter·Space 로 조작되고, 09 가 아니면 초점과 값 읽기는 각자의 role 과 aria 속성 수준에서 제공됩니다.

아홉 항목 중 화면을 가장 크게 바꾸는 건 어느 쪽인가요?

번지는 폭으로는 누적면적 23.78% 의 02 가, 픽셀 차의 셈으로는 강도 181.2 의 06 이 가장 큽니다 — 02 는 원판 전체가 돌고 06 은 고리 채움의 대비가 좁은 영역에 집중되기 때문입니다. 계산량 자체는 모두 가볍지만, 한 페이지에 아홉을 다 넣으면 루프가 겹쳐 저사양 기기에서 프레임이 밀릴 수 있으니 필요한 것만 남기는 것이 실서비스의 정답입니다.

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

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