기록

진행률 게이지와 구간별 거리 매핑

TL;DR

  • 이동거리 / 목표거리를 그대로 CSS height에 넣으면 배경 이미지 위 마커와 맞지 않는다.
  • 실제 거리 구간과 화면상의 마커 간격은 서로 다른 축이다.
  • 보장해야 할 것은 전체 비례가 아니라 각 체크포인트에서 게이지 끝과 마커가 만나는 것이다.
  • 마커 위치와 게이지 계산은 같은 anchor 배열에서 나와야 한다.

정적 HTML·CSS·바닐라 JS로 웹뷰 화면을 만들었다. 배경은 도로 일러스트이고, 사용자의 이동 거리만큼 도로 위 게이지가 아래에서 위로 차오른다. 단순한 계산은 이렇게 생긴다.

const progress = traveled / goal;
fill.style.height = `${progress * 100}%`;

하지만 배경 이미지 위에 고정된 도착지 마커가 있으면 이 비례식은 맞지 않는다. 실제 거리와 화면 좌표가 같은 비율로 배치되어 있지 않기 때문이다.

두 개의 축

도착지 데이터는 km 단위다.

지점 거리
출발 1,157km
도착지 1 1,483km
도착지 2 2,091km
최종 도착지 2,625km

구간 길이는 균등하지 않다.

326km / 608km / 534km

화면 좌표도 별도다. 마커는 배경 이미지에 맞춰 CSS로 배치된다.

.checkpoint-0 { top: 88%; }
.checkpoint-1 { bottom: 45%; }
.checkpoint-2 { bottom: 80%; }
.checkpoint-3 { top: 0%; }

아래에서부터 보면 대략 12% / 45% / 80% / 100%다. 화면 구간은 33% / 35% / 20%에 가깝다.

거리 구간: 326 / 608 / 534
화면 구간: 33 / 35 / 20

두 축의 불균등 정도가 다르다. 따라서 전체 거리 비율을 화면 높이 비율로 바로 쓰면 체크포인트마다 어긋난다.

구간별 매핑

이 화면에서 반드시 맞아야 하는 조건은 체크포인트다.

1,483km
  -> 게이지 끝이 도착지 1 마커에 닿아야 함

2,091km
  -> 게이지 끝이 도착지 2 마커에 닿아야 함

그래서 전체 비율 대신 구간별 비율을 계산한다.

const distanceAnchors = [1157, 1483, 2091, 2625];
const screenAnchors = [12, 45, 80, 100];

function mapDistanceToScreen(traveled: number) { if (traveled <= distanceAnchors[0]) return screenAnchors[0]; if (traveled >= distanceAnchors.at(-1)!) return screenAnchors.at(-1)!; const index = distanceAnchors.findIndex((current, i) => { const next = distanceAnchors[i + 1]; return next != null && traveled >= current && traveled <= next; }); const d0 = distanceAnchors[index]; const d1 = distanceAnchors[index + 1]; const s0 = screenAnchors[index]; const s1 = screenAnchors[index + 1];

const ratio = (traveled - d0) / (d1 - d0);
  return s0 + (s1 - s0) * ratio;
}

거리 축에서는 현재 구간의 시작과 끝을 잡고, 화면 축에서도 같은 index의 시작과 끝을 잡는다. 각 구간 안에서만 선형 보간한다.

분모 오류

실제 코드에는 분자와 분모의 기준점이 달랐다.

const move = traveled - checkpoints[index];
const total = checkpoints[index + 1] - start;
const ratio = move / total;

move는 현재 구간 시작점에서 재고, total은 전체 stage 시작점에서 잰다. 첫 구간에서는 우연히 맞지만 두 번째 구간부터 어긋난다. 예를 들어 2,090km는 두 번째 도착지 1km 전이다.

move  = 2090 - 1483 = 607
total = 2091 - 1157 = 934
ratio = 0.650

현재 구간의 끝에 거의 도착했는데 ratio가 65%로 나온다. 올바른 분모는 현재 구간 길이다.

const total = checkpoints[index + 1] - checkpoints[index];

이런 오류는 체크포인트 근처에서 가장 치명적이다. 게이지가 마커에 닿아야 하는 바로 그 지점에서 오차가 커진다.

게이지 좌표계

CSS에서도 별도 좌표계가 있었다.

.fill {
  position: absolute;
  bottom: 16%;
  min-height: 10.13%;
  max-height: 84%;
}

게이지의 0은 화면 아래 0%가 아니라 bottom: 16% 지점이다. height: 100%를 줘도 max-height: 84%에서 잘린다. 따라서 JS에서 계산하는 값은 화면 전체 퍼센트인지, 게이지 내부 퍼센트인지 분명해야 한다.

screen position = 16% + fill height

마커가 화면 전체 기준 좌표라면 게이지 높이는 원점을 뺀 값이어야 한다.

const gaugeBottom = 16;
const gaugeMax = 84;

const screenY = mapDistanceToScreen(traveled);
const fillHeight = Math.min(
  gaugeMax,
  Math.max(0, screenY - gaugeBottom),
);

findIndex의 바깥 구간

findIndex-1을 반환하는 경우도 하나가 아니다.

출발 전
  -> traveled < first checkpoint

도착 후
  -> traveled >= last checkpoint

둘을 같은 fallback으로 처리하면 출발 전인데 게이지가 가득 차는 문제가 생긴다.

if (traveled <= first) return 0;
if (traveled >= last) return gaugeMax;

바깥 구간은 명시적으로 clamp한다. -1 fallback에 의미가 다른 상태를 묶지 않는다.

정리

배경 이미지 위 진행률 게이지는 전체 비율 계산으로 풀 수 없다. 실제 거리 축과 화면 좌표 축이 다르기 때문이다. 구현 기준은 세 가지다.

  1. 거리 anchor와 화면 anchor를 같은 배열 구조로 둔다.
  2. 전체 비율이 아니라 현재 구간 안에서 보간한다.
  3. JS 계산 좌표계와 CSS 게이지 원점을 분리한다.

화면의 도착지 위치가 이미지에 묶여 있다면, 그 위치는 데이터다. CSS와 JS에 따로 흩어져 있으면 비례식은 조용히 어긋난다.