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

css 오디오 플레이어 디자인 9종 — 계산으로 움직이는 플레이어 UI

css 오디오 플레이어 디자인 아홉 가지 가운데 하나는 여러 사인 곡선을 겹쳐 막대 높이를 미리 계산해 이퀄라이저처럼 흔들고, 하나는 각속도 값을 서서히 줄여 앨범판이 관성으로 멈추게 하며, 하나는 타임라인 값으로 가사 하이라이트 줄을 찾아 화면 가운데로 스크롤합니다.

아홉 개 모두 requestAnimationFrame 으로 매 프레임 값을 다시 계산해 화면에 그리고, <audio> 태그도 Web Audio API 도 fetch 도 쓰지 않습니다 — 진행률·이퀄라이저 막대·각속도·가사 타임라인은 전부 사인 합성이나 고정 시드 계산으로 만든 합성값이라, 실제 곡을 물리지 않아도 zip 을 풀자마자 그대로 움직입니다. 웹 오디오 플레이어를 처음부터 만들 때 이 계산 한 겹을 어디에 얹을지 감을 잡는 용도로 보면 됩니다. 라이브러리도 CDN 도 없이 순수 자바스크립트와 CSS/SCSS 만으로 짰습니다.

데모

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

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

아홉 개를 나열한 순서는 계산이 화면 어디를 움직이느냐를 기준으로 잡았습니다. 01·04 는 진행률이나 스크롤 위치 같은 하나의 값을 프레임마다 다시 재는 방식이고, 02·06·07·09 는 사인 곡선을 겹쳐 막대·점·링의 높이나 반지름을 합성하는 방식이며, 03 은 각속도 값을 감쇠시켜 관성을 흉내 내고, 05·08 은 포인터 위치나 재생 시각을 다른 항목들의 값과 비교해 순서·하이라이트를 다시 매깁니다. 음악 플레이어 디자인을 코드로 옮길 때 자주 놓치는 부분이 바로 이 계산 한 겹인데, 속성 트윈만 걸면 멈춘 듯 보이는 자리에 값 하나를 얹으면 살아 움직입니다.

01 진행률 컨트롤 바와 08 가사 스크롤은 다른 일곱보다 누적면적이 훨씬 낮습니다 — 둘 다 화면 대부분이 정적인 텍스트·트랙 구조라 원래 움직이는 픽셀이 적은 컴포넌트형이라서, 억지로 면적을 키우는 대신 진행률·타임라인 값이 정확히 반영되는지에 계산을 집중했습니다.

항목 누적면적 강도평균 움직인프레임
01 진행률 컨트롤 바 2.546% 154.8 13/23
02 합성 파형 이퀄라이저 14.217% 163.9 23/23
03 턴테이블 앨범 아트 7.679% 67.9 23/23
04 하단 고정 미니 바 30.593% 35.9 23/23
05 대기열 드래그 정렬 29.946% 46.7 23/23
06 원형 스펙트럼 링 8.062% 101.9 23/23
07 프리셋 이퀄라이저 슬라이더 9.767% 177.3 23/23
08 가사 스크롤 하이라이트 1.891% 82.4 14/23
09 플레이어 히어로 장면 39.284% 31.2 23/23

패턴별 코드

아래 아홉 개는 실제 데모 소스에서 값이 계산되는 함수만 그대로 옮긴 조각입니다. 상수와 그 옆 주석이 변형 표의 값과 같은 자리입니다.

01재생 진행률 값으로 그려지는 커스텀 컨트롤 바

재생 버튼을 누른 순간부터 경과한 시간을 값으로 환산해 진행바 손잡이 위치와 남은 시간 표기를 매 프레임 다시 계산해 그리는 커스텀 오디오 컨트롤입니다. PROGRESS_UPDATE_MS 주기마다 seekTo 가 값을 다시 밀어 넣고, fmt 는 초 단위 값을 분:초 문자열로 바꿉니다.

  function fmt(sec) {
    sec = Math.max(0, Math.round(sec));
    var m = Math.floor(sec / 60), s = sec % 60;
    return m + ':' + (s < 10 ? '0' : '') + s;
  }

  function render(elapsedS) {
    var p = Math.min(1, Math.max(0, elapsedS / TRACK_LENGTH_S));
    fill.style.transform = 'scaleX(' + p + ')';
    handle.style.transform = 'translateX(' + (p * trackW) + 'px)';
    elapsedEl.textContent = fmt(elapsedS);
    remainEl.textContent = '-' + fmt(TRACK_LENGTH_S - elapsedS);
    seek.setAttribute('aria-valuenow', String(Math.round(p * 100)));
  }

  function human() { if (isDemo) { root.classList.remove('is-demo'); isDemo = false; } }

  var state = { elapsedS: 0, playing: false, playStart: 0, lastRenderMs: -1 };

  function setPlaying(next) {
    state.playing = next;
    root.classList.toggle('is-playing', next);
    if (next) state.playStart = performance.now() - state.elapsedS * 1000;
  }
재생 진행률 값으로 그려지는 커스텀 컨트롤 바
새 창에서 열기

01 · 재생 진행률 값으로 그려지는 커스텀 컨트롤 바 — 측정 누적면적 2.546% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

PROGRESS_UPDATE_MSseekTofmt

02합성 파형 값으로 흔들리는 이퀄라이저 막대

실제 오디오 분석 없이 사인 곡선 두 겹을 겹쳐 막대마다 다른 높이 값을 미리 계산해두고, 재생 중에는 그 값을 시간에 따라 흔들어 이퀄라이저처럼 보이게 합니다. 291 편 오디오 비주얼라이저처럼 AudioContext·AnalyserNode 로 실제 소리를 분석하는 것이 아니라, makeLcg 로 뽑은 위상·주파수를 synth 함수 하나로 합성한 장식용 값이라는 점이 다릅니다.

  function synth(bar, tSec) {
    var s1 = Math.sin(tSec * bar.f1 * Math.PI * 2 + bar.p1);
    var s2 = Math.sin(tSec * bar.f2 * Math.PI * 2 + bar.p2) * bar.w2;
    var v = (s1 + s2) / (1 + bar.w2); // -1..1 범위로 정규화
    return 0.5 + v * 0.5; // 0..1
  }

  function human() { if (isDemo) { root.classList.remove('is-demo'); isDemo = false; } }

  var playing = true, t0 = 0;

  function render(now, playingNow) {
    var tSec = (now - t0) / 1000;
    for (var i = 0; i < BAR_COUNT; i++) {
      var bar = bars[i];
      var h = playingNow ? synth(bar, tSec) : bar.idle;
      // 막대는 바닥이 기준점이라 아래쪽 원점으로 세로로만 자란다 — 중심 기준이 아닌 실제 앵커 지점
      bar.el.style.transform = 'scaleY(' + Math.max(0.06, h) + ')';
    }
  }

  function setPlaying(next) {
    playing = next;
    root.classList.toggle('is-playing', next);
  }
합성 파형 값으로 흔들리는 이퀄라이저 막대
새 창에서 열기

02 · 합성 파형 값으로 흔들리는 이퀄라이저 막대 — 측정 누적면적 14.217% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

makeLcgsynthBAR_COUNT

03각속도 값으로 도는 턴테이블 앨범 아트

재생 상태에 따라 각속도 값을 계산해 앨범 아트 원판과 톤암 각도를 매 프레임 회전시키고, 일시정지하면 SPIN_DECAY 로 그 값을 서서히 줄여 관성으로 천천히 멈추는 것처럼 보이게 합니다.

  var ARM_ANGLE_END = -34;   // 안쪽 홈(재생 끝 위치)

  var angle = 0, velocity = 0, progressS = 0, playing = true, demoPlaying = true, last = 0, driveStart = 0;

  function human() { if (isDemo) { root.classList.remove('is-demo'); isDemo = false; } }

  function setPlaying(next) {
    playing = next;
    root.classList.toggle('is-playing', next);
  }

  function step(effPlaying, dt) {
    if (effPlaying) {
      velocity += (TARGET_VELOCITY - velocity) * SPIN_UP_RATE;
      progressS = Math.min(PLAY_LENGTH_S, progressS + dt);
    } else {
      velocity *= SPIN_DECAY; // 매 프레임 각속도를 줄여 관성으로 멎는 것처럼 보이게 한다
    }
    angle = (angle + velocity * dt) % 360;
    disc.style.transform = 'rotate(' + angle.toFixed(2) + 'deg)';
    var t = progressS / PLAY_LENGTH_S;
    var armAngle = ARM_ANGLE_START + (ARM_ANGLE_END - ARM_ANGLE_START) * t;
    arm.style.transform = 'rotate(' + armAngle.toFixed(2) + 'deg)';
  }
각속도 값으로 도는 턴테이블 앨범 아트
새 창에서 열기

03 · 각속도 값으로 도는 턴테이블 앨범 아트 — 측정 누적면적 7.679% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SPIN_DECAYARM_ANGLE_STARTstep

04스크롤 값으로 접히는 하단 고정 미니 바

페이지 스크롤 위치 값을 기준으로 전체 플레이어와 하단 고정 미니 바 사이의 전환 시점을 계산해, MORPH_RANGE_PX 를 지나면 앨범 아트와 컨트롤이 작은 칩 형태로 줄어들며 화면 아래에 붙습니다.

  var MORPH_RANGE_PX = 56; // 완전 전환까지 걸리는 스크롤 거리
  var SCROLL_STEP_PX = 48; // 방향키 한 번의 스크롤 단위
  var programmatic = false; // 시연 루프가 scrollTop 을 밀 때 뜨는 scroll 이벤트를 실사용 스크롤과 구분한다

  function human() { if (isDemo) { root.classList.remove('is-demo'); isDemo = false; } }

  function recompute() {
    var threshold = full.offsetHeight; // 실제 카드 높이를 기준으로 전환 시점을 계산한다
    var y = scrollEl.scrollTop;
    var p = Math.min(1, Math.max(0, (y - threshold) / MORPH_RANGE_PX));
    mini.style.opacity = String(p);
    mini.style.transform = 'translateY(' + ((1 - p) * 16) + 'px) scale(' + (0.94 + p * 0.06) + ')';
    mini.style.pointerEvents = p > 0.5 ? 'auto' : 'none';
  }

  function onScroll() {
    if (programmatic) { programmatic = false; recompute(); return; }
    human(); recompute();
  }
스크롤 값으로 접히는 하단 고정 미니 바
새 창에서 열기

04 · 스크롤 값으로 접히는 하단 고정 미니 바 — 측정 누적면적 30.593% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

MORPH_RANGE_PXrecomputeprogrammatic

05순서 값을 다시 매기는 대기열 드래그 정렬

대기열 항목을 포인터로 끌어 놓는 위치의 세로 좌표를 다른 항목들의 위치 값과 비교해 slotOf 로 순서를 다시 매기고, 놓일 자리가 정해지면 그 사이 항목들이 SETTLE_RATE 비율만큼 목표 자리로 관성 있게 다가갑니다.

  var SETTLE_RATE = 0.22; // 목표 위치로 매 프레임 다가가는 비율 — 관성 lerp 한 겹
  var order = rows.map(function (_, i) { return i; }); // order[슬롯] = 행 인덱스
  var y = rows.map(function (_, i) { return slotOf(i) * ROW_H; }); // 각 행의 현재(보간 중) y값
  list.style.height = (rows.length * ROW_H) + 'px';

  function slotOf(rowIndex) { return order.indexOf(rowIndex); }

  function human() { if (isDemo) { root.classList.remove('is-demo'); isDemo = false; rows[DEMO_ROW].classList.remove('is-dragging'); } }

  var dragIndex = -1, startClientY = 0, startSlotY = 0, dragY = 0;

  function onPointerDown(e, i) {
    human();
    dragIndex = i;
    startClientY = e.clientY;
    startSlotY = slotOf(i) * ROW_H;
    dragY = startSlotY;
    rows[i].classList.add('is-dragging');
    rows[i].setPointerCapture(e.pointerId);
  }
순서 값을 다시 매기는 대기열 드래그 정렬
새 창에서 열기

05 · 순서 값을 다시 매기는 대기열 드래그 정렬 — 측정 누적면적 29.946% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SETTLE_RATEslotOfonPointerMove

06반지름 값으로 번지는 원형 스펙트럼 링

앨범 아트 둘레를 감싸는 여러 개의 점마다 합성 진폭 값으로 중심에서의 반지름을 계산해, 재생 중에는 그 반지름이 시간에 따라 출렁이며 원형 링이 맥동하듯 커졌다 줄었다 합니다. 02 항목과 같은 사인 합성 방식이지만 여기서는 막대 높이가 아니라 RANGE_R 폭 안의 반지름 값을 움직입니다 — 291 편의 AnalyserNode 분석과 달리 이쪽도 실제 소리를 읽지 않습니다.

  function synth(pt, tSec) {
    var s1 = Math.sin(tSec * pt.f1 * Math.PI * 2 + pt.p1);
    var s2 = Math.sin(tSec * pt.f2 * Math.PI * 2 + pt.p2) * pt.w2;
    var v = (s1 + s2) / (1 + pt.w2); // -1..1
    return 0.5 + v * 0.5; // 0..1
  }

  function human() { if (isDemo) { root.classList.remove('is-demo'); isDemo = false; } }

  var playing = true, t0 = 0;
  var ringColor = '#3a3a3a';

  function draw(now, playingNow) {
    var w = canvas.width, h = canvas.height;
    var cx = w / 2, cy = h / 2;
    var tSec = (now - t0) / 1000;
    ctx.clearRect(0, 0, w, h);

    // 점마다 반지름 값을 먼저 계산해 아래 면·틱이 같은 값을 공유한다
    var vals = [];
    for (var i = 0; i < POINT_COUNT; i++) {
      vals.push(playingNow ? synth(points[i], tSec) : points[i].idle);
    }
반지름 값으로 번지는 원형 스펙트럼 링
새 창에서 열기

06 · 반지름 값으로 번지는 원형 스펙트럼 링 — 측정 누적면적 8.062% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

makeLcgsynthRANGE_R

07슬라이더 값이 막대 반응폭을 정하는 프리셋 이퀄라이저

저음·중음·고음 세 슬라이더 값을 각각 계산해 대응하는 막대 그룹의 최대 흔들림 폭을 정하고, 슬라이더를 옮기면 BAND_RESPONSE_RANGE 안에서 그 그룹의 막대들만 새로 계산된 폭으로 다시 흔들립니다. html 오디오 플레이어 디자인에서 음질 패널을 붙일 때 참고할 만한 계산 구조입니다.

  function synth(bar, tSec) {
    var s1 = Math.sin(tSec * bar.f1 * Math.PI * 2 + bar.p1);
    var s2 = Math.sin(tSec * bar.f2 * Math.PI * 2 + bar.p2) * bar.w2;
    return (s1 + s2) / (1 + bar.w2); // -1..1
  }

  function human() { if (isDemo) { root.classList.remove('is-demo'); isDemo = false; } }

  var playing = true, t0 = 0;

  function render(now) {
    var tSec = (now - t0) / 1000;
    var effPlaying = isDemo ? true : playing;
    for (var b = 0; b < BAND_COUNT; b++) {
      var norm = Number(sliders[b].value) / 100; // 0..1
      var amp = norm * BAND_RESPONSE_RANGE;
      var bars = bands[b];
      for (var i = 0; i < bars.length; i++) {
        var bar = bars[i];
        var sway = effPlaying ? synth(bar, tSec) : 0;
        var h = bar.base + sway * amp * 0.5;
        h = Math.max(0.06, Math.min(1, h));
        // 막대는 바닥이 기준점이라 아래쪽 원점으로 세로로만 자란다 — 중심 기준이 아닌 실제 앵커 지점
        bar.el.style.transform = 'scaleY(' + h + ')';
      }
슬라이더 값이 막대 반응폭을 정하는 프리셋 이퀄라이저
새 창에서 열기

07 · 슬라이더 값이 막대 반응폭을 정하는 프리셋 이퀄라이저 — 측정 누적면적 9.767% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

BAND_RESPONSE_RANGEsynthmakeLcg

08타임라인 값으로 하이라이트가 옮겨가는 가사 스크롤

가짜 재생 타임라인 값을 기준으로 각 가사 줄에 미리 정해둔 시작 시각을 activeIndexOf 로 비교해 지금 하이라이트할 줄을 찾고, 그 줄이 항상 화면 가운데 오도록 목록 스크롤 위치를 함께 계산해 옮깁니다. LINE_TIMES 배열에 줄마다 다른 시작 시각을 넣으면 직접 타이밍을 바꿔볼 수 있습니다.

  function activeIndexOf(progressS) {
    var idx = 0;
    for (var i = 0; i < LINE_TIMES.length; i++) { if (LINE_TIMES[i] <= progressS) idx = i; }
    return idx;
  }

  function seekToLine(i) {
    i = Math.max(0, Math.min(LINE_COUNT - 1, i));
    playedMs = (LINE_TIMES[i] / SPEED) * 1000;
  }

  function render() {
    var progressS = (playedMs / 1000) * SPEED % PLAYBACK_LENGTH_S;
    var activeIndex = activeIndexOf(progressS);
    var target = activeIndex * LINE_H - (VIEWPORT_H / 2 - LINE_H / 2);
    var maxOffset = Math.max(0, LINE_COUNT * LINE_H - VIEWPORT_H);
    target = Math.max(0, Math.min(maxOffset, target));
    if (currentOffset == null) currentOffset = target;
    currentOffset += (target - currentOffset) * 0.22; // 타임라인이 옮겨간 자리로 관성 있게 다가간다
    list.style.transform = 'translateY(' + (-currentOffset).toFixed(2) + 'px)';
타임라인 값으로 하이라이트가 옮겨가는 가사 스크롤
새 창에서 열기

08 · 타임라인 값으로 하이라이트가 옮겨가는 가사 스크롤 — 측정 누적면적 1.891% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

activeIndexOfseekToLineLINE_TIMES

09블러 배경과 링이 한 화면에서 맞물리는 플레이어 히어로

앨범 아트 색상 값을 뽑아 흐릿하게 확대한 배경 위에 턴테이블·파형 막대·원형 링 세 계산 레이어를 한 좌표계로 겹쳐, bandValue 하나가 저음·중음·고음 세 값을 계산하고 그 값이 drawRing 을 포함한 세 레이어를 동시에 움직이는 히어로 장면을 완성합니다.

  function bandValue(def, tSec) {
    var s1 = Math.sin(tSec * def.f1 * Math.PI * 2 + def.p1);
    var s2 = Math.sin(tSec * def.f2 * Math.PI * 2 + def.p2) * def.w2;
    var v = (s1 + s2) / (1 + def.w2); // -1..1
    return 0.5 + v * 0.5; // 0..1
  }

  var RING_POINTS = 32;
  var ringPts = [];
  for (var i = 0; i < RING_POINTS; i++) {
    ringPts.push({ f: 2 + rng() * 1.4, p: rng() * Math.PI * 2, w: 0.35 + rng() * 0.3 });
  }

  var BAR_COUNT = 14;
  var bars = [];
  for (var i = 0; i < BAR_COUNT; i++) {
    var el = document.createElement('div');
    el.className = 'hero__bar';
    barsEl.appendChild(el);
    bars.push({ el: el, f: 3 + rng() * 1.6, p: rng() * Math.PI * 2, base: 0.18 + rng() * 0.1 });
  }

  function human() { if (isDemo) { root.classList.remove('is-demo'); isDemo = false; } }

  var playing = true, t0 = 0;
블러 배경과 링이 한 화면에서 맞물리는 플레이어 히어로
새 창에서 열기

09 · 블러 배경과 링이 한 화면에서 맞물리는 플레이어 히어로 — 측정 누적면적 39.284% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

bandValuedrawRingRING_POINTS

값이 화면을 바꾸는 곳 — 진행률·각속도·타임라인

아홉 개 항목이 각각 어디에 상수 하나를 심어 뒀는지 모아 보면 계산이 화면을 바꾸는 지점이 뚜렷해집니다. 01 의 PROGRESS_UPDATE_MS 는 진행바가 몇 밀리초마다 다시 계산될지 정하고, 03 의 SPIN_DECAY 는 앨범판이 멈추는 데 걸리는 시간을, 07 의 BAND_RESPONSE_RANGE 는 슬라이더를 옮겼을 때 막대 그룹이 흔들리는 폭을 정합니다. 셋 다 상수 한 줄만 고치면 재계산되는 값의 크기가 그대로 바뀌어 화면에 나타납니다.

입력 리스너도 항목마다 필요한 것만 듣습니다. 01 은 click·keydown·resize, 02·03·06·08·09 는 click·keydown, 04 는 scroll·resize 까지, 05 는 keydown·pointerdown·pointermove·pointerup, 07 은 click·input·keydown 을 듣습니다. 값 하나를 재계산하는 함수가 이 리스너들 뒤에서 매번 다시 불립니다.

이웃 가지

어디서 깨지는가 — 접근성·reduced-motion·저사양

아홉 개가 공통으로 겪는 함정은 값이 재생 상태와 어긋나는 순간입니다. 01 은 트랙을 클릭하거나 방향키로 seekTo 를 불러 진행률 값을 옮길 때 state.playing 이 켜져 있으면 playStart 값도 같은 순간 다시 계산합니다 — 이 재계산 없이 elapsedS 만 바꾸면 다음 프레임에 경과 시간이 옮기기 전 위치를 기준으로 다시 흘러 손잡이가 순간 되돌아가는 함정에 빠집니다. 03 은 SPIN_DECAY 로 각속도를 줄이는 도중 재생 버튼을 다시 누르면 감쇠 중이던 값과 새로 올라가는 목표 속도가 한 프레임 안에서 충돌해 각도가 순간 튈 수 있어, 값을 덮어쓰기 전에 항상 현재 각속도를 기준으로 다시 계산합니다.

키보드·스크린리더

재생 버튼은 aria-label="재생" 으로 고정돼 있고 아이콘만 is-playing 클래스로 재생/일시정지 상태에 따라 바뀌며, 01 의 진행바에는 role="slider" 와 aria-valuenow 가 붙어 방향키로도 값을 옮길 수 있습니다. 08 가사 목록은 aria-selected 로 지금 하이라이트된 줄을 알리므로 스크린리더가 값 변화를 텍스트로도 따라갈 수 있습니다.

prefers-reduced-motion 이 켜져 있으면 아홉 개 모두 자동 진행을 멈추는 같은 규칙을 따릅니다. 01 은 자동 진행 루프(driveTick)를 시작하지 않고 정지 스냅샷 하나만 그린 뒤 click·keydown 리스너만 그대로 남기고, 나머지 여덟 개도 자동으로 값을 흔드는 루프만 꺼서 클릭이나 Enter 를 누르면 그 자리에서 즉시 다음 상태로 계산해 보여줍니다. 239r99kf 으로 받은 zip 을 그대로 열어 이 분기를 보면, 자동으로 도는 부분만 꺼질 뿐 값을 계산하는 함수 자체는 그대로 남는다는 걸 확인할 수 있습니다.

변형 세 가지와 접근성

이름 값 느낌
재생 진행률 갱신 주기 값 조정 01 PROGRESS_UPDATE_MS 200ms → 80ms 진행바 손잡이 위치가 더 자주 다시 계산되어 움직임이 매끄러워지는 쪽에 가까워집니다
턴테이블 관성 감쇠 값 조정 03 SPIN_DECAY 0.965 → 0.985 일시정지 후 앨범판이 멈추기까지 걸리는 시간이 길어지는 쪽에 가까워집니다
이퀄라이저 슬라이더 반응폭 값 조정 07 BAND_RESPONSE_RANGE 1.6 → 2.4 슬라이더를 움직였을 때 해당 막대 그룹이 흔들리는 높이 차이가 더 커지는 쪽에 가까워집니다

세 변형 모두 리스너와 접근성 분기는 건드리지 않습니다. reduce 분기와 aria-* 갱신 코드는 상수 값과 무관하게 그대로 동작하므로, 값만 바꾼 사본을 만들어도 저사양 기기나 동작 줄이기 설정에서 깨지지 않습니다.

css 오디오 플레이어 디자인 예제는 어디서 보나?

캔버스에 원을 그리는 방식은 MDN 의 CanvasRenderingContext2D 와 arc 문서에, 프레임을 언제 다시 그릴지는 requestAnimationFrame 문서에, 포인터로 대기열을 끄는 방식은 Pointer Events 문서에 정리되어 있습니다. 위 아홉 개는 이 API 들과 사인 합성 계산만으로 만든 결과이니, 표준 동작을 확인하면서 압축을 풀어 코드를 그대로 옮겨 써도 됩니다.

FAQ

이 아홉 개 오디오 플레이어 애니메이션은 실제 소리 없이도 항상 같은 모양으로 나오나요?

그렇습니다. 진행률·각속도·타임라인은 재생 경과 시간을 기준으로 계산되고, 02·06·07·09 의 파형·링·막대는 Math.random 대신 선형합동생성기(LCG)로 뽑은 값을 사인 곡선에 넣어 만들기 때문에 새로고침을 몇 번 해도 같은 결이 반복됩니다.

html 오디오 플레이어 디자인 예제를 실제 음원 파일에 연결해 써도 되나요?

가능합니다. 지금은 <audio> 태그 없이 경과 시간 값만으로 진행률·파형·각속도를 계산하므로, 실제 파일을 붙이려면 <audio> 요소를 두고 timeupdate 이벤트에서 나온 currentTime 값을 이 편의 render·step 함수가 받던 값 자리에 그대로 넣으면 됩니다. 화면을 그리는 계산 자체는 손댈 필요가 없습니다.

느린 기기나 동작 줄이기 설정에서는 어떻게 보이나요?

prefers-reduced-motion 이 켜져 있으면 자동으로 도는 시연 루프만 멈추고 정지 스냅샷 하나를 보여줍니다. 클릭이나 Enter 를 누르면 그 자리에서 즉시 다음 상태로 값을 계산해 반영하므로, 계산 자체는 그대로 남고 자동으로 흔들리는 부분만 꺼집니다.

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

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