홈을 카드 한 장씩 넘기는 덱으로 다시 만들었어요
게임을 격자에 늘어놓으니 목록처럼 보였어요. 무대 하나에 카드 한 장만 세우고, 넘길 때마다 화면 전체의 빛이 그 카드 색으로 바뀌게 했습니다.
홈에 게임을 격자로 늘어놓고 있었어요. 셋만 돼도 “목록”처럼 보이더라고요. 목록은 훑고 지나가는 화면이지 머무는 화면이 아니에요. 그래서 갈아엎었습니다. 화면 한가운데 무대를 놓고, 거기에 카드 한 장만 세웠어요.
레퍼런스는 Hello Monday의 How to Talk to White Kids about Racism입니다. 어두운 무대에 책 한 권이 떠 있고, 넘길 때마다 화면 전체의 빛이 바뀌는 사이트예요. 거기서 여섯 가지를 가져왔어요.
- 어두운 무대 한가운데 원근감 있는 오브젝트 하나, 그 아래 바닥 그림자
- 무대에는 그것 말고 아무것도 없다 — 섹션 제목도, 설명도, 더 보기 링크도
- 카드마다 고유색이 있고, 넘기면 그 색이 무대 전체로 번진다
- 인디케이터 점이 각자 자기 카드의 색을 갖는다
- 넘기는 법이 네 가지 — 화살표 · 드래그 · 휠 · 키보드
- 캡션이 한 줄씩 밀려 들어오는 등장 리듬
카드 위에 남긴 건 셋뿐이에요
키아트, 레터링, 바로 플레이 버튼. 설명도 태그도 없어요. 상단 헤더도 지웠습니다(<Layout header={false}>). 사이트의 다른 곳으로 가는 길은 화면 아래 구석의 아이콘 줄이 냅니다.
카드 앞면 전체가 게임으로 가는 링크이고, PLAY 버튼은 그 위에 얹힌 “보이는 손잡이”예요. 버튼에는 pointer-events: none이 걸려 있습니다. 링크 안에 링크를 넣을 수 없기도 하고, 이러면 카드 어디를 눌러도 게임이 열려요.
여기서 두 번 걸려 넘어졌어요
앞면이 <a>가 되는 순간 브라우저의 “링크 끌어다 놓기”가 드래그를 가로챕니다. 카드를 옆으로 끌 수가 없어요. draggable="false"와 -webkit-user-drag: none, 그리고 안쪽 이미지에 pointer-events: none까지 줘야 풀립니다.
pointerdown에서 바로 setPointerCapture를 해도 안 돼요. 포인터가 캡처되면 뒤이은 click이 캡처 대상(무대)으로 가서 링크에 닿지 않습니다. 누른 채 4px 이상 실제로 움직인 순간에만 캡처를 겁니다.
function beginDrag() {
stage.setPointerCapture(pointerId);
state.dragging = true; // 여기서 켜야 한다
}
state.dragging도 같은 자리에서 켜야 해요. 누르자마자 켜면 render()가 카드에 pointer-events: none을 걸어서, 그냥 클릭했을 뿐인데 게임이 안 열립니다.
보이는 모든 것이 실수 하나의 함수예요
덱의 상태는 pos 하나입니다. 정수가 아니어도 돼요 — 0.5면 반쯤 넘어간 상태죠. 드래그 중이든 스프링으로 되돌아오는 중이든 render(pos) 하나가 전부를 그립니다. 넘기기는 pos를 다음 정수로 스프링 보간하는 것뿐이에요.
anime.js v4에서는 스프링 감속과 stagger만 빌려 씁니다. 매 프레임 그리는 일은 직접 해요. 그래야 드래그와 애니메이션이 같은 값을 놓고 싸우지 않습니다.
카드가 깔리는 시점은 인트로가 끝나는 바로 그 프레임이에요. 미리 깔아 두면 노트북 화면에서 진짜 사이트로 넘어갈 때 카드가 이미 놓인 채로 툭 나타나거든요. 인트로가 끝나는 순간에 깔면, 그게 곧 등장 연출이 됩니다.