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

게이지 바 9종 — 게임처럼 채워지는 아홉 가지 방식

게이지 바는 값이 오르내릴 때마다 채워진 폭이 바뀌는 막대인데, 아홉 개 중 하나는 값이 줄면 막대가 먼저 좁아지고 그 뒤로 붉은 잔상이 서서히 따라 줄어들며, 하나는 값이 상한을 넘으면 초과분이 반투명한 겹으로 얹히고, 또 하나는 보조 값이 상한에 닿으면 막대의 불투명도 값이

아래 아홉 개는 모두 캐릭터·게임 이름 없이 체력(HP)·기력·충전량 같은 일반적인 상태 값 하나를 시뮬레이션한 것입니다. 실제 게임 UI를 그대로 베낀 것이 아니라, 게이지 바 UI 를 만들 때 값이 바뀌는 계산 방식 자체를 아홉 갈래로 나눠 본 것에 가깝습니다.

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

아홉 개의 해부 — 어떤 값이 언제 바뀌는가

아홉 개 전부 자동 재생 중이던 .is-demo 클래스는 사람이 버튼을 누르거나 키보드로 Enter·Space 를 입력하는 순간 human() 함수에서 한 번 지워지고, 그 뒤로는 클릭·키보드 입력(06 만 포인터 누름·뗌·이탈·취소와 키다운·키업)에 반응하는 requestAnimationFrame 루프가 값을 그립니다. 아홉 개 모두 무작위 값이 아니라 정해 둔 배열(EVENTS·JOLTS·HIT_LIST)의 값을 순서대로 읽는 시드 방식이라, 같은 편을 두 번 열어도 같은 순서로 재생됩니다.

항목 누적면적 % 강도 평균 움직인 프레임
01 피해 흡수 전환 2.85 97.7 21/23
02 회복 틱 발광 전환 5.43 39.9 23/23
03 조각 칸 균열 전환 1.92 71.8 14/23
04 초과 회복 겹막 전환 2.87 99.7 13/23
05 위험 구간 박동 전환 1.99 75.4 7/23
06 충전 유지 방출 전환 3.33 76.6 14/23
07 다중 레인 대비 전환 2.98 127.9 23/23
08 기절 게이지 붕괴 전환 2.88 83.1 13/23
09 모션 감소 즉시 전환 1.27 136.1 13/23

09 는 게이지 바 UI 아홉 개 중 유일한 컴포넌트 편이라 누적면적이 가장 낮은데, 이건 정상입니다 — 09 가 재는 축은 화면을 얼마나 채웠는지가 아니라 나머지 여덟 개의 애니메이션을 얼마나 정교하게 대신 끊어내는지에 있기 때문입니다.

매 프레임 지수 감쇠 계산으로 값을 되돌리는 값 — 01·02·04

01·02·04 는 전부 Math.exp(-k * dt) 형태의 지수 감쇠로 값을 되돌립니다. 01 은 applyHit() 로 본값(mainHP)을 깎은 뒤 잔상 값(trailHP)이 LERP_RATE 로 뒤늦게 따라가고, 동시에 FLASH_TAU 로 플래시 불투명도가 가라앉습니다. 02 는 TICK_INTERVAL 마다 tick() 이 값을 올리면서 GLOW_TAU 로 그 순간의 밝기를 감쇠시키고, 04 는 값이 HP_MAX+OVERHEAL_CAP 을 넘었을 때만 DECAY_RATE 로 초과분을 서서히 깎습니다.

경과 시간과 정해둔 구간 길이의 비율로 값을 만드는 값 — 03·08

03·08 은 감쇠 대신 경과 시간을 정해둔 구간 길이로 나눈 비율(선형)로 값을 만듭니다. 03 은 stateFor() 가 깨진 시각으로부터 지난 시간을 CRACK_DUR·STAGGER 로 나눠 균열 두 겹(c1·c2)과 채움 값을 계산하고, 08 은 snapOpacity() 가 SPIKE_DUR 동안은 1을 유지하다 DROP_DUR 구간에서 선형으로 0까지 떨어뜨린 뒤, RECOVER_DUR 구간에서 다시 선형으로 채웁니다.

경계 값을 넘었는지에 따라 사인 주기로 흔드는 값 — 05

05 는 값이 THRESHOLD 밑으로 내려가야만 계산이 시작됩니다. pulseFor() 가 THRESHOLD 와 현재 값의 차이로 비율(ratio)을 구하고, 그 비율로 박동 주기(PERIOD_MAX~PERIOD_MIN)와 진폭을 매 프레임 다시 정한 뒤 사인 함수 하나로 밝기와 글로우를 같이 흔듭니다.

감쇠 계수와 코사인 진동을 함께 계산해 튕기는 값 — 06

06 은 아홉 개 중 계산이 가장 많은 항목입니다. burstAt() 이 방출 경과 시간을 RELEASE_ZETA 로 감쇠시킨 진폭과 코사인 값을 곱해, 방출 표시(burst)가 목표 지점을 한 번에 지나쳤다가 되돌아오는 위치(pos)를 매 프레임 다시 계산합니다. 이건 CHARGE_RATE 로 누르는 동안 값을 채우는 계산과는 완전히 분리된 별도의 방출 전용 계산입니다.

실측 좌표와 사인 값으로 여러 레인을 따로 움직이는 값 — 07

07 은 valuesAt() 이 레인마다 다른 주파수(freq)·진폭(amp)·위상(phase)을 가진 사인 값으로 각 막대를 따로 움직이고, 클릭마다 JOLTS 배열의 값을 레인별 오프셋에 더해 순위를 흔듭니다. 여기에 더해 왕관(crown) 요소의 이동 거리는 고정 px 이 아니라 getBoundingClientRect() 로 실제 행 사이 간격을 잰 값(rowStep)을 씁니다.

09 는 위 다섯 갈래와 성격이 다릅니다. 앞선 여덟 개 각각의 계산을 스스로 가진 것이 아니라, 왼쪽 판(fillNormal)은 01과 같은 지수 감쇠 잔상 계산을 그대로 돌리고 오른쪽 판(fillReduced)은 계산 없이 같은 값을 바로 대입만 해, 계산이 있는 막대와 없는 막대를 한 화면에 나란히 두고 대조해 보여주는 역할만 합니다.

패턴별 코드

01피해 흡수 전환 — 값이 줄어들면 막대가 먼저 좁아지고 그 뒤를 붉은 잔상이 서서히 따라가며 좁아지는 방식

값이 줄어드는 순간 막대 폭이 먼저 좁아지고, 그 차이만큼 남은 붉은 폭이 뒤늦게 서서히 줄어듭니다. applyHit() 이 본값만 즉시 깎고, 잔상 값은 매 프레임 LERP_RATE 로 뒤따라가며 FLASH_TAU 로 플래시가 가라앉습니다. 전투 화면의 체력 표시처럼 순간 피해를 강조하는 상태 막대에 씁니다.

    if (isDemo) {
      if (elapsed >= CYCLE) {
        cycleStart = ts;
        elapsed = 0;
        appliedIdx = 0;
        mainHP = HP_MAX;
        trailHP = HP_MAX;
        flashStart = -9999;
      }
      while (appliedIdx < EVENTS.length && elapsed >= EVENTS[appliedIdx][0]) {
        applyHit(EVENTS[appliedIdx][1], ts);
        appliedIdx++;
      }
    }

    // 계산 겹: 잔상 지수 평활 — dt(ms) 에 독립적으로 같은 반감기를 유지
    trailHP += (mainHP - trailHP) * (1 - Math.exp(-LERP_RATE * dt));
    var flash = Math.max(0, Math.exp(-(ts - flashStart) / FLASH_TAU));
    flashEl.style.opacity = flash < 0.02 ? '0' : flash.toFixed(3);

    render();
    requestAnimationFrame(loop);
피해 흡수 전환 — 값이 줄어들면 막대가 먼저 좁아지고 그 뒤를 붉은 잔상이 서서히 따라가며 좁아지는 방식
새 창에서 열기

01 · 피해 흡수 전환 — 값이 줄어들면 막대가 먼저 좁아지고 그 뒤를 붉은 잔상이 서서히 따라가며 좁아지는 방식 — 측정 누적면적 2.853% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

applyHit()LERP_RATEFLASH_TAU

02회복 틱 발광 전환 — 값이 일정 간격으로 조금씩 올라갈 때마다 막대 끝에 빛이 번쩍이는 방식

값이 짧은 간격으로 한 칸씩 올라갈 때마다 막대 끝의 밝기 값이 순간 커졌다가 가라앉습니다. TICK_INTERVAL 마다 tick() 이 값을 한 단계 올리고, 그 순간부터 GLOW_TAU 로 밝기가 지수 감쇠합니다. 자동 회복 상태의 체력·기력 표시나 대기 중 자원 충전 막대에 어울립니다.

  function loop(ts) {
    if (!cycleStart) cycleStart = ts;
    var elapsed = ts - cycleStart;

    if (isDemo) {
      if (elapsed >= CYCLE) {
        cycleStart = ts; elapsed = 0; value = START; lastTickIdx = -1;
      }
      var idx = Math.floor(elapsed / TICK_INTERVAL);
      if (idx > lastTickIdx && idx < TICK_COUNT) {
        lastTickIdx = idx;
        tick(ts);
      }
    }

    var glow = Math.max(0, Math.exp(-(ts - glowStart) / GLOW_TAU));
    render(glow);
    requestAnimationFrame(loop);
  }
회복 틱 발광 전환 — 값이 일정 간격으로 조금씩 올라갈 때마다 막대 끝에 빛이 번쩍이는 방식
새 창에서 열기

02 · 회복 틱 발광 전환 — 값이 일정 간격으로 조금씩 올라갈 때마다 막대 끝에 빛이 번쩍이는 방식 — 측정 누적면적 5.433% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

tick()GLOW_TAUTICK_INTERVAL

03조각 칸 균열 전환 — 막대가 여러 칸으로 나뉘어 있고 값이 한 칸 밑으로 내려가면 그 칸에 균열 모양이 남는 방식

값이 칸 하나를 넘어 내려가는 순간 그 칸의 갈라짐 정도 값이 커지며 다음 칸부터 채움이 사라집니다. stateFor() 가 각 칸이 깨진 시각으로부터 지난 시간을 CRACK_DUR·STAGGER 로 나눠 균열 두 겹과 채움 값을 각각 계산합니다. 보스 체력의 단계 구간 표시나 목숨 칸 표시에 씁니다.

  function stateFor(pipIdx, ts) {
    var eventIndex = PIPS - 1 - pipIdx;
    if (eventIndex < 0 || eventIndex >= appliedIdx) {
      return { c1: 0, c2: 0, fill: 1 };
    }
    var t0 = eventTs[eventIndex];
    var dt = Math.max(0, ts - t0);
    var c1 = Math.min(1, dt / (CRACK_DUR * 0.45));
    var c2 = Math.min(1, Math.max(0, dt - STAGGER) / (CRACK_DUR * 0.45));
    var fill = 1 - Math.min(1, Math.max(0, dt - STAGGER) / (CRACK_DUR - STAGGER));
    return { c1: c1, c2: c2, fill: fill };
  }
조각 칸 균열 전환 — 막대가 여러 칸으로 나뉘어 있고 값이 한 칸 밑으로 내려가면 그 칸에 균열 모양이 남는 방식
새 창에서 열기

03 · 조각 칸 균열 전환 — 막대가 여러 칸으로 나뉘어 있고 값이 한 칸 밑으로 내려가면 그 칸에 균열 모양이 남는 방식 — 측정 누적면적 1.917% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

stateFor()CRACK_DURSTAGGER

04초과 회복 겹막 전환 — 값이 상한을 넘으면 그 초과분이 막대 위에 반투명한 겹으로 얹히는 방식

값이 상한 값을 넘어서면 그 차이만큼의 폭이 반투명 겹으로 기존 막대 위에 얹혀 나타납니다. 값은 HP_MAX+OVERHEAL_CAP 까지만 오를 수 있고, 상한을 넘긴 뒤에는 DECAY_RATE 로 초과분만 서서히 깎입니다. 보호막이 덧씌워진 체력 표시나 한도를 넘긴 자원 막대에 맞습니다.

  function loop(ts) {
    if (!cycleStart) { cycleStart = ts; lastTs = ts; }
    var dt = ts - lastTs;
    lastTs = ts;
    var elapsed = ts - cycleStart;

    if (isDemo) {
      if (elapsed >= EVENTS[EVENTS.length - 1][0] + 700) {
        cycleStart = ts; elapsed = 0; appliedIdx = 0; value = START;
      }
      while (appliedIdx < EVENTS.length && elapsed >= EVENTS[appliedIdx][0]) {
        value = Math.max(0, Math.min(HP_MAX + OVERHEAL_CAP, value + EVENTS[appliedIdx][1]));
        appliedIdx++;
      }
    }

    if (value > HP_MAX) {
      value = HP_MAX + (value - HP_MAX) * Math.exp(-DECAY_RATE * dt);
    }

    render();
    requestAnimationFrame(loop);
  }
초과 회복 겹막 전환 — 값이 상한을 넘으면 그 초과분이 막대 위에 반투명한 겹으로 얹히는 방식
새 창에서 열기

04 · 초과 회복 겹막 전환 — 값이 상한을 넘으면 그 초과분이 막대 위에 반투명한 겹으로 얹히는 방식 — 측정 누적면적 2.868% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

render()OVERHEAL_CAPDECAY_RATE

05위험 구간 박동 전환 — 값이 정해둔 낮은 구간에 들어가면 막대 전체가 일정한 리듬으로 밝아지고 어두워지는 방식

값이 정해둔 낮은 경계 밑으로 내려가면 막대의 밝기 값이 정해진 리듬으로 오르내리기 시작합니다. pulseFor() 가 THRESHOLD 와의 차이로 박동 주기를 PERIOD_MIN 쪽으로 좁혀가며 사인 값 하나로 밝기·글로우를 같이 흔듭니다. 체력이 낮을 때의 경고 표시나 남은 시간이 얼마 없을 때의 알림 막대에 씁니다.

  function pulseFor(v, ts) {
    if (v >= THRESHOLD) return { brightness: 1, glow: 0 };
    // 계산 겹: 잔량이 낮을수록 박동 주기는 짧아지고 진폭은 커진다(반비례) — 매 프레임 v 에서 다시 계산
    var ratio = (THRESHOLD - v) / THRESHOLD; // 0..1
    var period = PERIOD_MAX - (PERIOD_MAX - PERIOD_MIN) * ratio;
    var amp = AMP_MIN + (AMP_MAX - AMP_MIN) * ratio;
    var phase = (ts % period) / period * Math.PI * 2;
    var osc = Math.sin(phase);
    return { brightness: 1 + amp * osc, glow: Math.max(0, osc) * amp };
  }

  function render(ts) {
    var v = Math.max(0, Math.min(HP_MAX, value));
    fill.style.width = v.toFixed(2) + '%';
    var p = pulseFor(v, ts);
    fill.style.filter = 'brightness(' + p.brightness.toFixed(3) + ')';
    fill.style.boxShadow = p.glow > 0.02
      ? '0 0 ' + (6 + p.glow * 18).toFixed(1) + 'px ' + (p.glow * 6).toFixed(1) + 'px rgba(255,120,90,' + p.glow.toFixed(3) + ')'
      : 'none';
    track.setAttribute('aria-valuenow', String(Math.round(v)));
    valueEl.textContent = Math.round(v) + ' / ' + HP_MAX;
  }
위험 구간 박동 전환 — 값이 정해둔 낮은 구간에 들어가면 막대 전체가 일정한 리듬으로 밝아지고 어두워지는 방식
새 창에서 열기

05 · 위험 구간 박동 전환 — 값이 정해둔 낮은 구간에 들어가면 막대 전체가 일정한 리듬으로 밝아지고 어두워지는 방식 — 측정 누적면적 1.985% · 움직인 프레임 7/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

pulseFor()THRESHOLDPERIOD_MIN

06충전 유지 방출 전환 — 누르고 있는 동안 값이 천천히 차오르고 손을 떼는 순간 한 번에 쏟아지는 방식

누르고 있는 동안 값이 정해둔 빠르기로 차오르고, 떼는 순간 그 값이 0으로 곧장 떨어지며 별도의 방출 표시가 켜집니다. burstAt() 이 방출 경과 시간을 RELEASE_ZETA 로 감쇠시킨 뒤 코사인 값과 곱해 튕기는 위치를 계산합니다. 강공격 충전 표시나 대기 후 발사하는 자원 막대에 씁니다.

  function burstAt(t) {
    // 계산 겹: 방출 스프링 — 감쇠 코사인 진동으로 목표를 넘어섰다 되돌아오는 위치를 계산
    if (t < 0 || t > RELEASE_DUR) return { scale: 0, alpha: 0 };
    var x = t / RELEASE_DUR;
    var amp = Math.exp(-RELEASE_ZETA * x);
    var pos = 1 - amp * Math.cos(RELEASE_FREQ * t);
    return { scale: 1 + pos * 1.1, alpha: Math.max(0, amp * 1.15) };
  }

  function render(ts) {
    fill.style.width = value.toFixed(2) + '%';
    valueEl.textContent = Math.round(value) + '%';
    track.setAttribute('aria-valuenow', String(Math.round(value)));
    var b = burstAt(ts - releaseStart);
    burst.style.left = releaseFromPct.toFixed(2) + '%';
    burst.style.opacity = b.alpha < 0.02 ? '0' : b.alpha.toFixed(3);
    burst.style.transform = 'translate(-50%, -50%) scale(' + b.scale.toFixed(3) + ')';
  }
충전 유지 방출 전환 — 누르고 있는 동안 값이 천천히 차오르고 손을 떼는 순간 한 번에 쏟아지는 방식
새 창에서 열기

06 · 충전 유지 방출 전환 — 누르고 있는 동안 값이 천천히 차오르고 손을 떼는 순간 한 번에 쏟아지는 방식 — 측정 누적면적 3.328% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

burstAt()RELEASE_ZETACHARGE_RATE

07다중 레인 대비 전환 — 같은 자리에 놓인 두 개 이상의 막대가 서로 다른 값으로 나란히 채워지는 방식

나란히 놓인 각 막대의 채움 폭 값이 저마다 다른 빠르기로 따로 올라가 서로 벌어지거나 좁혀집니다. valuesAt() 이 레인마다 다른 사인 값을 계산하고, 클릭할 때마다 JOLTS 배열의 값이 레인별 오프셋에 더해집니다. 여러 대상의 체력을 한 화면에서 비교하는 표시나 순위 진행도 비교에 맞습니다.

  function valuesAt(t) {
    return LANES.map(function (l) {
      var v = l.base + l.offset + l.amp * Math.sin(l.freq * t / 1000 + l.phase);
      return Math.max(4, Math.min(100, v));
    });
  }

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

  joltBtn.addEventListener('click', function () {
    human();
    var d = JOLTS[joltIdx % JOLTS.length];
    for (var i = 0; i < LANES.length; i++) LANES[i].offset += d[i];
    joltIdx++;
  });
다중 레인 대비 전환 — 같은 자리에 놓인 두 개 이상의 막대가 서로 다른 값으로 나란히 채워지는 방식
새 창에서 열기

07 · 다중 레인 대비 전환 — 같은 자리에 놓인 두 개 이상의 막대가 서로 다른 값으로 나란히 채워지는 방식 — 측정 누적면적 2.979% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

valuesAt()JOLTSclassList.toggle()

08기절 게이지 붕괴 전환 — 두 번째 값이 상한에 닿으면 막대가 한 번에 부서지듯 사라지는 방식

보조 값이 상한에 닿는 순간 그 막대의 불투명도 값이 짧게 튀었다가 0으로 떨어지며 사라집니다. snapOpacity() 가 SPIKE_DUR 동안 1을 유지한 뒤 DROP_DUR 구간에서 선형으로 0까지 내립니다. 기절·붕괴 상태를 알리는 보조 막대나 한계에 닿으면 사라지는 자원 표시에 씁니다.

  function snapOpacity(dt) {
    // 계산 겹: 상한에 닿는 순간 불투명도가 짧게 튀었다가(스파이크) 감쇠 없이 선형으로 0까지 떨어진다
    if (dt < SPIKE_DUR) return 1;
    if (dt < SPIKE_DUR + DROP_DUR) return 1 - (dt - SPIKE_DUR) / DROP_DUR;
    return 0;
  }
기절 게이지 붕괴 전환 — 두 번째 값이 상한에 닿으면 막대가 한 번에 부서지듯 사라지는 방식
새 창에서 열기

08 · 기절 게이지 붕괴 전환 — 두 번째 값이 상한에 닿으면 막대가 한 번에 부서지듯 사라지는 방식 — 측정 누적면적 2.878% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

snapOpacity()SPIKE_DURDROP_DUR

09모션 감소 즉시 전환 — 모션 감소 설정이 켜지면 앞선 여덟 방식의 애니메이션이 전부 즉시 값으로 바뀌는 방식

모션 감소 값이 켜져 있으면 앞선 여덟 막대 전부 중간 단계 없이 목표 값으로 바로 바뀝니다. 이 항목은 왼쪽 판은 01과 같은 잔상 계산을 그대로 돌리고 오른쪽 판은 계산 없이 값을 바로 대입해, 시스템 설정과 무관하게 두 판을 항상 나란히 돌려 대조를 보여줍니다. 접근성 설정을 켠 사용자의 화면이나 애니메이션을 줄이고 싶은 대시보드를 설계할 때 참고할 수 있습니다.

  // 시스템 모션 감소가 켜져 있어도 이 항목은 대조를 보여주는 목적이라
  // '일반' 판의 자동 루프는 계속 돈다 — 사람이 재생 버튼을 눌러도 멈추지 않는다(BD3 요구)
  requestAnimationFrame(loop);
  if (systemReduce) {
    gb.setAttribute('data-system-reduce', 'true');
  }
모션 감소 즉시 전환 — 모션 감소 설정이 켜지면 앞선 여덟 방식의 애니메이션이 전부 즉시 값으로 바뀌는 방식
새 창에서 열기

09 · 모션 감소 즉시 전환 — 모션 감소 설정이 켜지면 앞선 여덟 방식의 애니메이션이 전부 즉시 값으로 바뀌는 방식 — 측정 누적면적 1.271% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

render()EVENTSmatchMedia()

값이 막대를 채우고 비우는 곳 — 체력 값·상한 값·보조 값 기준

체력 값 하나로 채우고 비우는 게이지 바 디자인은 01·02·05 입니다. 01 은 맞을 때마다 즉시 줄고, 02 는 TICK_INTERVAL 마다 조금씩 오르며, 05 는 그 체력 값이 THRESHOLD 밑으로 내려갔는지만 매 프레임 다시 확인합니다. 셀 다 같은 HP_MAX 기준 값을 쓰지만 값이 언제 바뀌는지(즉시·틱·경계)는 셋이 서로 다릅니다.

상한 값을 기준으로 채우고 비우는 건 04 입니다. 값은 HP_MAX 가 아니라 HP_MAX+OVERHEAL_CAP 까지 오를 수 있고, 그 상한을 넘긴 구간만 반투명 겹으로 따로 그려집니다. 03·08 은 체력이나 상한이 아니라 각자의 보조 값(균열 이벤트 인덱스·기절치)이 기준입니다 — 03 은 이벤트 인덱스가 칸 수를 넘을 때마다 다음 칸이 깨지고, 08 은 보조 값이 STAGGER_MAX 에 닿는 순간 붕괴가 시작됩니다.

06·07 은 값 자체가 두 갈래입니다. 06 은 충전량(value)과 방출 위치(releaseFromPct)가 따로 있어 방출 전후로 기준이 바뀌고, 07 은 애초에 레인마다 독립된 값을 갖고 있어 하나의 상한이 아니라 레인끼리의 상대적인 차이가 화면에 드러나는 기준입니다.

어디서 깨지는가 — 빠른 연타·큰 값 점프·모션 감소

빠른 연타에서 가장 눈에 띄게 갈리는 건 01 입니다. 값이 0 이 된 뒤 다시 클릭하면 onHit() 이 즉시 HP_MAX 로 되돌리기 때문에, 죽기 직전 상태를 한 번 더 확인하려고 빠르게 연타하면 그 직전 상태를 보기도 전에 값이 리셋됩니다. 04 는 반대 방향으로 깨집니다 — HEAL 버튼을 다섯 번 정도 빠르게 누르면 값이 HP_MAX+OVERHEAL_CAP 에 곧장 닿아 그 뒤로는 몇 번을 더 눌러도 겹막 폭이 더 늘어나지 않습니다.

큰 값 점프는 07 이 대표적입니다. JOLTS 배열의 값은 레인 여러 개에 한꺼번에 큰 오프셋을 더하기 때문에, 클릭 한 번으로 선두가 뒤바뀌는 큰 점프가 일어날 수 있습니다. 06 은 오히려 반대로 너무 짧게 누르고 뗀 값 점프에서 깨집니다 — 충전이 거의 시작되지 않은 상태에서 곧장 떼면 releaseFromPct 값이 0에 가까워, 방출 표시가 있어도 거의 보이지 않습니다.

모션 감소 처리는 아홉 개가 자바스크립트와 prefers-reduced-motion 미디어 쿼리를 각자의 style.scss 에서 한 번 더 걸어 이중으로 판정합니다. 01~08 은 reduce 값이 켜지면 requestAnimationFrame 루프를 시작하지 않고 render 함수를 한 번만 호출해 최종 모습으로 즉시 넘어가는데, 09 만 예외로 시스템 reduce 값과 무관하게 항상 루프가 돌아 두 판을 계속 대조해 보여줍니다. 아홉 개 패턴의 html·scss·js 소스 전부가 비번 273ynvg7 으로 열리는 zip 안에 들어 있어, 위에서 짚은 06의 감쇠 코사인 계산과 07의 실측 좌표 계산을 직접 코드에서 확인하고 바꿔볼 수 있습니다.

변형 세 가지와 접근성

게이지 바 UI 를 자기 화면에 맞게 손볼 때는 수치 하나만 바꿔도 느낌이 크게 달라집니다.

이름 값 느낌
방출 스프링 감쇠비 낮추기 06 RELEASE_ZETA 3.6 → 1.8 감쇠 상수를 3.6 에서 1.8 로 낮추면 방출 순간 막대가 목표 지점을 한 번에 지나쳐 몇 차례 더 튕기며 잦아든다.
피해 잔상 감쇠 시간 늘리기 01 FLASH_TAU 70 → 220 감쇠 시간 값을 70 에서 220 으로 늘리면 붉은 잔상 막대가 훨씬 오래 남아 있다가 서서히 사라진다.
붕괴 후 재충전 시간 줄이기 08 RECOVER_DUR 950 → 300 재충전 시간 값을 950 에서 300 으로 줄이면 막대가 부서져 사라진 뒤 거의 곧바로 다시 차오르기 시작한다.

접근성은 01·02·04·05·06·08·09 가 같은 바탕을 갖습니다. 이 일곱은 트랙에 role="progressbar"와 aria-label·aria-valuemin·aria-valuemax·aria-valuenow를 걸어, 값이 바뀔 때마다 setAttribute로 aria-valuenow 값을 갱신해 스크린리더에도 지금 값을 알립니다. 03 도 같은 속성을 갖지만 aria-label 값이 "보스 체력"으로 고정돼 있고 aria-valuemax 가 100 이 아니라 남은 칸 수(5)로 잡혀 있다는 점이 다릅니다. 07 은 이 아홉 개 중 유일하게 role·aria 속성이 하나도 없어, 레인 간 비교라는 구조상 스크린리더로는 지금 어느 레인이 앞서는지 알 방법이 없습니다 — 게이지바 만들기를 할 때 다중 레인처럼 role="progressbar" 를 붙이기 어려운 구조라면 별도의 텍스트 요약을 더해야 합니다.

FAQ

게이지 바 애니메이션에서 값이 갑자기 크게 뛰면 항상 깨지나요?

아니요. 04 는 값이 HP_MAX+OVERHEAL_CAP 위로 튀어도 Math.min으로 상한을 걸어 막고, 그 이후에는 DECAY_RATE 로 서서히 되돌리기 때문에 큰 점프 자체가 시각적으로 깨지지 않습니다. 다만 07 처럼 레인 여러 개가 동시에 큰 오프셋을 받는 구조에서는 순위가 한 번에 뒤바뀌는 큰 점프가 그대로 드러납니다.

06의 방출 계산과 05의 박동 계산은 둘 다 값을 흔드는데 왜 다른가요?

05 는 THRESHOLD 밑으로 내려간 동안에만 사인 함수 하나로 밝기를 계속 오르내리게 하는 반복 계산이고, 06 은 방출 시작(releaseStart)부터 RELEASE_DUR 안에서 한 번만 감쇠 코사인 값을 계산해 스프링처럼 튕겼다가 멈추는 1회성 계산입니다. 하나는 조건이 유지되는 동안 계속 도는 리듬이고, 다른 하나는 이벤트 한 번에 끝나는 반응이라 계산 성격 자체가 다릅니다.

게이지바 만들기를 처음 해볼 때 아홉 개 중 어디부터 손대는 게 좋나요?

01 이나 03 부터 시작하는 걸 권합니다. 둘 다 값 하나가 바뀌는 순간을 감쇠(01)나 시간 비율(03) 중 한 가지 계산만으로 보여주기 때문에, 06 처럼 충전·방출이 따로 있거나 07 처럼 레인이 여러 개인 구조보다 코드 흐름을 따라가기 쉽습니다.

더 읽을거리

원형 프로그레스바 CSS 9종 — 값만 바꾸면 되는는 값을 어떻게 보여줄지의 웹 진행 막대 시각 스타일을 다루는 데 비해, 이 편은 그 값 자체가 게임 상태 이벤트에서 어떻게 파생돼 언제 바뀌는지를 판다는 점에서 계산 축이 다릅니다.

원형 프로그레스바 css 스탯 링 위젯 9종 — 숫자가 진짜 올라가는는 값을 원형 스탯 링으로 그리는 계산을 다루는데, 이 편의 게이지 바는 같은 값이라도 막대 폭·잔상·겹침으로 그린다는 점에서 형태 축이 다릅니다.

로딩페이지 디자인 9종 — 복붙하는 예쁜 CSS 로딩 화면는 로딩 화면 구성을 다루는 반면, 이 편은 로딩처럼 한 방향으로만 차오르는 값이 아니라 게임 상태 이벤트 값에서 파생되는 게이지 계산을 다룬다는 점에서 값의 성격 자체가 다릅니다.

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

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