소리꽃 KeyBloom TIL 28
나선의 “높이” 슬라이더가 사실 상승 속도를 바꿔서 속도와 겹쳤다. 높이는 “어디까지 오를지(목표)”로, 속도는 “얼마나 빨리”로 나누고 싶었다. 그 과정에서 파생값·클램프·편차에 대해 배웠다.
높이=목표, 수명은 파생
세 값 높이 = 속도 × 수명이 한 식에 묶여 있어 셋을 다 독립으로 못 준다(코일 간격 = 상승 ÷ 회전이 같은 계열 — TIL 23). 그래서 둘만 손잡이로 두고 나머지는 파생시킨다. 높이(목표)와 속도를 정하고 수명을 뽑았다.
const vy = speed * (0.24 + r() * 0.12); // 속도 = 상승 빠르기
const targetHeight = (rise / 3) * SPAWN_Y * 1.1; // 높이 = 도달 목표(preview 상단까지)
const life = targetHeight / vy; // 수명은 파생 — vy·life = 목표
vy·life = 목표가 성립하니 속도가 빠르든 느리든 같은 높이에서 죽는다. 빠르면 짧게 살고 느리면 오래 산다. 슬라이더를 화면 실측(SPAWN_Y=스폰~상단)에 매핑해 최대치가 미리보기 꼭대기 살짝 위가 되게 했다.
파생값을 클램프하면 불변식이 깨진다
처음엔 수명이 너무 짧거나 길까 봐 클램프(값을 상·하한 안에 가두기)를 걸었다. 그런데 이게 vy·life = 목표를 무너뜨렸다.
// ❌ 하한 0.4 → 빨리 닿아 짧게 죽어야 할 게 억지로 살아남아 그 시간 계속 올라가 목표 초과
// ❌ 상한 12 → 느린 속도서 목표에 닿기 전에 죽어 미달
const life = Math.min(Math.max(targetHeight / vy, 0.4), 12);
// ✅ 클램프 제거 — 입력 범위로 자연 한계에 맡긴다
const life = targetHeight / vy;
“높이 낮추고 속도 높였는데 더 높이 올라감”이 딱 하한 클램프 탓이었다. 파생값을 억지로 자르면, 그 값이 지키고 있던 관계(vy·life=목표)가 함께 틀어진다. 대신 입력의 범위로 한계를 준다 — 속도 하한(0.2)이 vy를 받쳐 수명이 무한정 커지지 않고(최대 ~19초), 총량은 별도 상한(MAX_PARTICLES)이 막는다.
너무 정확하면 오히려 부자연스럽다
그렇게 정확히 맞추니 이번엔 꼭대기가 자로 자른 듯 평평했다. life = 목표 ÷ vy가 모든 파티클에 정확히 성립해서, 속도 지터(파티클마다 주는 미세한 무작위 흔들림)가 상쇄되고 전부 똑같은 높이에서 죽은 것이다.
// 도달 목표에 파티클별 편차 → 조금씩 다른 높이에서 죽어 유기적 taper
const targetHeight = (rise / 3) * SPAWN_Y * 1.1 * (0.6 + 0.4 * r());
정밀함이 오히려 인공적으로 보이는 사례였다. 자연스러움은 딱 맞는 값이 아니라 적당한 흩어짐에서 온다. 각 파티클이 제 목표에서 죽는 성질(속도 무관)은 그대로 두고, 목표 자체만 흔들었다.
단일 토글을 독립 축으로
“발광 영역에 가두기” 하나였던 토글을 폭·높이 둘로 쪼갰다. 내부가 이미 높이(ceilY)와 폭(bx0/bx1)으로 나뉘어 있어서, 각 축을 따로 켜는 것만으로 독립 동작이 된다.
const ceilY = params.boundH ? topFrac - keyGlowLen : -Infinity; // 높이 = 천장
const bx0 = params.boundW ? centerX - keyWidth / 2 : -Infinity; // 폭 = 좌우
폭만 켜면 좌우만 좁고 높이는 파티클대로(발광 밴드보다 위까지), 높이만 켜면 세로만 천장. UI는 section + 자식 체크박스 2개. 켠 축만 클램프하고, 끈 축은 ±Infinity라 비교가 항상 통과한다(플래그 없이).
요약
- 세 값이 한 식에 묶이면(높이=속도×수명) 둘만 손잡이로, 나머지는 파생(TIL 23의 커플링을 높이에 적용).
- 파생값을 클램프하면 지키려던 불변식(
vy·life=목표)이 깨진다 — 한계는 클램프가 아니라 입력 범위로. life=목표÷vy가 정확히 성립하면 다 같은 높이서 죽어 평평 — 자연스러움은 의도적 편차에서.- 단일 토글도 내부가 축으로 나뉘어 있으면 독립 체크박스로 쪼갤 수 있다(끈 축은 ±Infinity 센티넬).