/ INDEX
FILL: REMBRANDT
ARISTOTLE WITH A BUST OF HOMER
1653 · MET CC0 · 로컬 하프톤
연구노트READ 6MIN2026.07.06TRACK 01 / 연구 로그

이름의 활자 응결 — 히어로 제작기

터미널 글리프 안개가 카라바조 인물들 뒤에서 Fraunces 활자로 응결되는 히어로 장면. 픽셀 옵티마이저, 물림 예산, 그리고 죽인 것들의 기록.

사이트 첫 화면에는 카라바조의 「성 베드로의 부인」(1610)이 걸려 있다. 내 이름은 거기 없다. 처음 2초 동안은. 이름 자리에는 터미널 글리프 — 0-9 A-F · : / — 로 만든 안개가 떠 있고, 안개는 왼쪽부터 152px Fraunces 활자로 응결된다. 인물들 뒤에서. 이 글은 그 한 장면을 만든 기록이다. 수치는 전부 실제 값이다.

문제 — 이름이 그림에 먹힌다

히어로는 그림 전면 확대다. 1440×900 스테이지에 유화가 가득 차고, 그 위 상단 밴드에 이름 OH YOUNGWOONG이 놓인다. Fraunces 420, opsz 144, 152px.

쉬운 답은 두 개였고, 둘 다 틀렸다.

글자를 인물 앞에 올리면 읽기는 쉽다. 대신 스티커가 된다. 그림과 글자가 서로를 모른 채 붙어 있는 병치. 글자를 인물 뒤로 넣으면 그림과 한 몸이 된다. 대신 먹힌다. 하녀의 머리가 U를 삼키고 베드로가 O를 삼킨다.

여기서 오너 교정이 들어왔다. 이름은 반드시 인물 뒤. 레이어를 뒤집어서 판독을 사면 안 된다. 판독은 커닝으로 다시 벌어라. 그래서 규칙이 하나 남았다 — 인물은 항상 글자를 가린다. 가려도 읽히게 만드는 것이 이 장면의 일이다.

가리는 쪽을 먼저 실측했다. 스테이지 px 기준으로 벽이 세 개다.

  • 병사의 투구: x 246–404
  • 하녀의 머리: x 502–660
  • 베드로의 머리: x 1052–1250

이 세 벽 사이에 열두 글리프를 앉혀야 한다.

설치 전 라이브 히어로 — 이름이 정적 DOM 활자로 한 번에 떠 있다
설치 전 히어로(1920). 이름이 그냥, 한 번에, 떠 있다.

재료 — 활자가 안개가 된다

응결 이전에 안개가 필요했다. 원칙은 하나. 안개는 글자 밖에서 빌려오지 않는다. 글자가 자기 자신의 물질로 만들어져야 한다.

파이프라인은 이렇다. 각 글리프를 오프스크린 캔버스에 렌더하고, 잉크 마스크를 5px 간격으로 샘플한다. 잉크가 있는 셀마다 터미널 글리프 한 자를 배정한다 — 0123456789ABCDEF·:/ 중 하나, 6.5–9px, 색은 활자와 같은 상아색(#e6d7b8). 셀은 자기 홈 좌표(글자 안의 원래 자리)를 기억한 채 무작위 방향으로 흩어져 시작한다.

응결은 왼쪽에서 오른쪽으로 지나가는 웨이브프런트다. 장면 진행도 P(0→1)에서 글리프 i의 진행도는:

// 응결 웨이브프런트 — 글리프 i의 진행도
q(i) = clamp((P − i·0.055) / 0.34, 0, 1)
// 안개 산란 반경 = 8.5px · (1−q)^1.9, 상시 드리프트 ±0.7px
// q > 0.62부터 크리스프 활자 페이드인, q ≥ 0.95에서 안개 소멸

글리프당 0.055씩 시차를 두고, 각자 0.34 폭의 창 안에서 응결을 끝낸다. 안개 셀은 산란 반경이 (1−q)^1.9로 줄며 홈 좌표로 수렴하고, q가 0.62를 넘는 순간부터 진짜 활자가 그 아래에서 올라온다. 크로스페이드 구간(0.62→0.95)에서는 먼지와 활자가 잠깐 겹친다. 그 겹침이 이 장면에서 제일 마음에 드는 프레임이다.

안개 상태 P=0 — 이름 자리에 터미널 글리프 안개만 떠 있다응결 중간 P=0.5 — OH YOUN까지 활자, GWOONG는 아직 안개
왼쪽: P=0, 안개. 오른쪽: P=0.5 — OH YOUN까지 응결, 뒤쪽 여섯 글리프는 아직 먼지다.

캘리브레이션 — 픽셀 옵티마이저와 물림 예산

가장 큰 이동은 밴드 자체였다. 원래 이름은 em-top 196에 있었고, 거기서는 글자가 인물 얼굴 한가운데를 지나갔다. 밴드를 위로 올려 em-top 80(잉크 밴드 대략 y 98–206)에 앉히자 구도가 바뀐다. 머리들이 글자를 자르는 게 아니라, 글자 하단을 무는 그림이 된다.

다음은 커닝. 두 단계로 풀었다.

1차는 1차원 솔버(solve_layout.py)다. 벽 세 개를 상수로 놓고, F=148에서 실측한 글리프 어드밴스(F에 선형)를 가지고 152부터 내려가며 최대 가능 활자 크기를 찾았다. 이때 물림 예산이 정해졌다 — H는 투구에 12px까지만 물리고, Y는 왼쪽 잉크의 42%까지만 내주고, W는 베드로에 22px까지만 물린다.

2차는 픽셀 옵티마이저(optimize.py)다. 1차원 벽은 근사일 뿐이라, 실제로는 글리프 잉크 마스크(3px 격자)를 인물 컷아웃의 알파 채널과 픽셀 단위로 겹쳐서 가시율을 계산했다. 판독 규칙은 이렇다: 가시율 42% 이상, 또는 26% 이상이면서 좌우 플랭크(잉크 양끝 25% 컬럼) 중 한쪽이 70% 이상 보일 것. 글자는 절반이 가려져도 한쪽 윤곽이 살아 있으면 읽힌다.

탐색은 좌표 하강 8스윕. 변수는 시작점 x0과 글리프별 갭 12개. 워드스페이스는 +64px로 미리 열어두고 시작했다 — 투구가 낱말 사이 공백에 들어와 앉게 하는 초기화다. 활자 크기 4개(152/156/160/164) × 밴드 높이 3개(80/88/96) = 후보 12개를 각각 하강시키고, 점수는 판독 실패당 1000점 벌점에 활자가 클수록 보너스. 같은 값이면 큰 활자가 이긴다.

확정값:

// 캘리브레이션 확정 — 1440×900 스테이지 px
const LAYOUT = {
  F: 152,          // Fraunces 420 · opsz 144 · 자간 -0.012em
  x0: 59,          // 펜 시작
  top: 80,         // em-top (잉크 밴드 ≈ y98–206)
  //       O   H   ␣  Y   O    U  N  G  W  O    O  N  G
  gaps: [ 20,  0, 64, 0, 32, -20, 0, 0, 0, 0, -20, 0, 0 ]  // 글리프별 추가 커닝(px)
};

이 레이아웃의 글리프별 가시율(인물이 앞에 있는 상태):

OHYOUNGWOONG
1.00.641.00.41.281.001.001.00.64.42.961.00

판독 실패 0. 제일 깊게 물린 글자는 U다 — 하녀가 72%를 가리지만 왼쪽 윤곽이 통째로 살아 있어서 판독 규칙을 통과한다. 그리고 베드로의 정수리는 WOONG의 두 번째 O 안으로 올라온다. 가시율 42%. 베드로는 세 번 부인한 사람이지만, 내 이름에 대해서는 O 하나를 58% 가리는 선에서 합의했다.

하녀 구간 크롭 — U가 72% 가려지고도 왼쪽 윤곽으로 읽힌다베드로 구간 크롭 — 정수리가 O 안으로 올라온다
물림의 실물. 왼쪽: 하녀와 O·U. 오른쪽: 베드로와 WOONG의 O 둘.

죽인 것들

매트릭스 비. 첫 버전의 안개 셀은 12Hz로 글자를 계속 바꿨다. 화면이 즉시 다른 영화가 됐다. 내리는 초록 코드의 인용이지, 이름의 응결이 아니다. 셀 글자는 생성 시 한 번만 배정하고 동결했다. 바뀌는 것은 위치뿐이다. 안개는 내리는 코드가 아니라 가라앉는 먼지여야 한다.

스크롤 스크럽. 목업에서는 휠이 응결을 문질렀다 — preventDefault로 스크롤을 가로채고 deltaY/360씩 P를 밀었다. 걸레로 김서린 유리를 닦는 느낌이라 꽤 좋았고, 그래서 오래 잡고 있었다. 그래도 죽였다. 폴드 아래로 가려는 사람의 스크롤을 애니메이션의 담보로 잡는 짓이다. 설치본의 규칙은 하나다: 스크롤 의도가 감지되면 이름은 그 자리에서 즉시 완성되고, 스크롤은 사용자에게 돌아간다. 기다린 사람에게는 응결을, 바쁜 사람에게는 이름을.

시네마 과잉. 오너 코멘트 한 줄 — “너무 시네마처럼 안 해도”. 산란 반경 8.5px, 드리프트 ±0.7px, 안개 알파 최대 0.82, 응결 1.1초. 전부 이 코멘트 이후의 값이다. 장면은 이벤트가 아니라 조판이 도착하는 과정이면 충분하다.

보관용 기록. 목업의 스크럽 버전 — 설치본에서는 스크롤 의도 = 즉시 완성으로 교체됐다.

결과 — 정지 포스터가 판독 게이트를 통과한다

최종 타임라인. 로드하면 안개가 그림 페이드인(0.8초)에 실려 함께 들어온다. 2.0초 유휴가 지나면 장면이 스스로 응결을 시작하고, 1.1초(easeInOutCubic) 만에 이름이 선다. 완성 후에는 캔버스가 정지 포스터를 한 번 그리고 렌더 루프를 끝낸다. 쉬는 히어로가 태우는 프레임은 0이다.

경계 조건이 이 장면의 절반이다.

  • prefers-reduced-motion: 안개도 웨이브도 없다. 정지 포스터를 한 번 그리고 끝. 두 프레임을 찍어 비교했을 때 픽셀 델타 0.
  • 스크롤 선행: 유휴 2초를 기다리지 않고 스크롤하면 즉시 완성. 다시 올라와도 캔버스는 완성된 이름을 들고 있다.
  • 모바일 390: 이름 2행 스택, 가로 스크롤 0.
  • 이름은 캔버스와 무관하게 sr-only h1으로 항상 존재한다. 캔버스는 장식이고, 접근성 트리에는 처음부터 이름이 서 있다.

설치 인수는 스크립트로 닫았다(accept_install.py) — 정지 포스터 픽셀 일치, 유휴 2.0초/응결 1.1초 타이밍, 스크롤 즉시 완성, 1920/1366 폴드 규율, 콘솔 에러·워닝 0/0, reduced-motion 정지, 글리프당 가시율 게이트, 390 레이아웃. 여덟 항목. 이 장면의 진짜 시험은 마지막 프레임이다. 모션이 전부 빠진 정지 화면 한 장이 판독 게이트를 통과하는가. 통과한다 — 위의 표가 그 증거다.

최종 장면. 아무것도 하지 않으면 이름이 스스로 도착한다.
reduced-motion — 처음부터 정지 포스터
reduced-motion 사용자가 보는 전부. 이것으로도 성립해야 한다.

이 장면이 사이트의 나머지를 감당한다고는 아직 말하지 않는다. 첫 화면 하나를 닫았다는 것까지만 말한다. 그림 제목이 「부인」이다. 이름까지 부인당하게 둘 수는 없었다.


출처·증거

  • Caravaggio, The Denial of Saint Peter, 1610 — The Metropolitan Museum of Art, Public Domain
  • 활자: Fraunces (variable), 안개 폴백: ui-monospace
  • 레이아웃 솔버: solve_layout.py(1D 벽), optimize.py(픽셀 가시율, 좌표 하강) · 인수: accept_install.py(8항목)
  • 본문 수치 전부 위 스크립트와 mock.html 캘리브레이션 헤더에서 검증됨
FILLE. DELACROIX · ABDUCTION OF REBECCA · 1846 · MET CC0 · 1-BIT 베이어
출처·증거 — 이미지 출처는 각 캡션에, 데이터·수치의 근거는 본문에 표기.
연구노트 · 2026.07.06 · 글 오영웅 · 전체 인덱스 →
다른 글
  1. 출시하는 기분을 팝니다 — 바이브 코딩 마케팅 해부연구노트 · 2026.07.08
  2. 존재한 적 없는 잔고 — 계좌 파티클 산맥 제작기연구노트 · 2026.07.06
  3. AI가 만든 앱은 왜 가짜 같아 보이는가연구노트 · 2026.05.28
SUBSCRIBE새 글은 RSS로RSS →