재생
| 항목 | 값 |
|---|---|
| 상태 | 구현 |
| 관련 결정 | 그림 문법과 배치를 직접 맡고 D2 호환을 버린다, 조건과 대기를 빌드 때 계산해 같은 시간표에 담는다 |
삽입 호스트와 갤러리는 공통 bindDiagramHost로 출처·송신자·높이·테마와 대체 전체화면을 처리한다. 대체 전체화면 동안은 자연 높이 알림을 적용하지 않으며 Escape로 재생기에 종료를 요청한다. 종료 뒤 같은 장면과 자연 높이로 돌아온다.
요약
재생은 시간 흐름을 장면 단위로 보여 주는 기능이다. 모든 보기(구조, 순서, 차트, 시간)가 같은 시간표 하나를 쓰고, 결과는 세 가지다. HTML 재생기는 장면 탭과 도구 막대(문법 복사, HTML 다운로드, 전체화면)만 있고, 장면마다 정지(static), 한 번(once), 반복(loop)을 원본이 정한다. 움직이는 SVG는 스크립트 없이 고른 장면 하나를 그 장면의 방식대로 재생한다. 멈춘 SVG는 장면 하나의 마지막 상태다. 장면은 탭으로만 고르고 저절로 다음 장면으로 넘어가지 않는다. 재생, 일시정지, 배속, 반복을 사용자가 바꾸는 조작과 그림 아래 설명 글은 없다.
필요한 표현만 더하기
카드·선은 선언부에 한 번 적고, 변화만 장면 안에 적는다. 문서 탭의 내용과 도표 장면의 내용은 달라도 선택 줄의 마크업·호버·전환 시간·키보드는 같은 공통 부품이다.
| 원하는 표현 | 적는 곳 |
|---|---|
| 구조만 보기 | 빈 scene "구조" 또는 장면 생략 |
| 순서대로 선을 따라 이동 | 장면 안 a -> b, 다음 줄 b -> c |
| 같은 박자에 동시 이동 | a -> b & c -> d |
| 연속 트래픽 | track a -> b -> c every=1s time=2s |
| 반복·속도 | scene "트래픽" mode=loop speed=1.5 for=6s |
| 도착 때 값 갱신 | set="pending+1", 공통 동작은 선언부의 on |
| 조건·대기·시간 초과 | when=, wait=, timeout=, else= |
| 차트가 그려지는 순서 | reveal chart.series, 강조는 light |
장면을 바꾸면 처음부터 시작하고 같은 탭을 다시 누르면 유지한다. 한 번 재생은 마지막 상태에서 멈추며, 반복은 해당 장면 안에서만 돈다. 움직임 줄이기 설정에서는 마지막 상태를 바로 보여 준다. 시각 효과를 위해 예제별 CSS나 좌표를 추가하지 않는다.
동기
설계 문서의 그림은 "무엇이 어디로 가는지"를 보여야 하는데, 멈춘 그림은 순서를 보여 주지 못한다. README는 스크립트를 실행하지 않아 움직이는 SVG가 따로 필요하고, 인쇄와 PDF에는 멈춘 그림이 필요하다. 구조 그림과 차트가 다른 재생 방식을 쓰면 문서 안에서 움직임과 속도가 달라진다.
예시
HTML에서 장면 고르기
- 사용자가 그림의 HTML을 연다.
- 첫 장면에 들어간다. 정지 장면이면 마지막 모습이 보이고, 한 번 장면이면 저절로 재생된 뒤 마지막 모습에 머물며, 반복 장면이면 그 장면만 되풀이된다. 다음 장면으로는 저절로 넘어가지 않는다.
- 사용자가 둘째 탭을 누르면 둘째 장면에 새로 들어간다(시간 0, 처음 값). 첫째 탭을 다시 누르면 첫 장면에 다시 새로 들어간다. 지금 보고 있는 장면의 탭을 누르면 아무 일도 없다.
README에 움직이는 그림 넣기
- 사용자가
thinkflow render결과인.svg를 README에 이미지로 넣는다. 움직임이 필요한 장면은--scene으로 고른다. - GitHub가 이미지를 보이면 그 장면이 자기
mode대로 재생된다. GitHub는 README의 SVG 이미지에서 CSS keyframes와 SMIL 움직임을 실행한다(2026-10-01 확인, 이 저장소 README).
상세 설계
베지어 곡선과 구간별 이동 시간의 정방향·역방향 계산은 motion-math.js의 순수 함수 하나가 소유한다. 빌드는 그 함수를 가져오고, 단일 HTML은 같은 함수 원문을 넣어 실행한다. 토큰 곡선과 시간표는 입력으로 넘기며 브라우저용 계산식을 별도로 유지하지 않는다.
장면과 재생 방식
문법은 그림 문법이 정한다. 재생이 지키는 계약은 다음과 같다.
- 장면은 박자(beat)의 목록이거나 흐름(track)의 목록이다. 탭 하나가 장면 하나이고 장면은 따로 재생된다. 시간표의
steps[i]는 정확히{ label, mode, speed }다.mode는static,once,loop,speed는 0보다 큰 유한한 숫자다. static은 들어가면 그 장면의 마지막 모습을 바로 그린다. 필드, 큐 카드, 상태 알약, 값 글자, 차트 계열이 모두 마지막 값이고 점, 펄스, 줄어드는 알약은 없다. 그 장면이 남긴light(차트 행의 흐림, 히트맵의 굵기)는 장면의 상태라서 마지막 모습에 남는다. 시계는 흐르지 않는다. 장면에 이동이 있어도 같다.once는 들어가면 저절로 첫 박자부터 재생하고, 표시 길이가 지나면 펄스가 모두 끝난 마지막 모습에 머문다.loop는 고른 장면 하나만 표시 길이마다 처음 모습으로 돌아간다. 시계는 되감지 않는 절대 시각이다.- 들어가기: 처음 열면 첫 장면에 들어간다(장면이 하나뿐이어도 같다). 탭을 누르면 그 장면에 들어가고, 떠났다가 돌아오면 늘 첫 박자, 시간 0, 처음 값에서 시작한다. 지금 장면의 탭을 다시 누르면 아무 일도 없고 시계도 되감기지 않는다.
- 장면이 없는 문서(구성도, 스키마)는 탭도 시간도 없는 정지 그림이다. 선언한 처음 값을 보인다. 카드의 값 줄은
from값을, 큐는from칸 수만큼 찬 칸을 그리고, 값에 묶인 차트는 처음 프레임을 그린다. 이 값은 모든 장면의 처음 카드와 같은 내용이라(같은 번호를 다시 쓴다) 장면이 있는 문서의 모습과 계약이 같다. 장면이 없으므로steps,segs,presentation은[]이고total은 0이다. 선언한 값 줄은 장면 번호 없이 길이 0으로 시간표의values에 놓일 뿐 변화나 펄스를 만들지 않는다. - 시간은 두 가지다. 논리 시각(시간표의 ms, 속도 1 기준)은 원본이 적은 이동·자라는 시간·
wait·for=와 이벤트 처리가 소비한 시각뿐이고 효과 시간을 담지 않는다. 표시 시각은 논리 시각을 장면의speed로 나눈 값에 효과 꼬리를 더한 화면 시간이다. 조건, 시간 초과, 예약은 논리 시각만 읽으므로speed와 꼬리에 따라 결과가 갈리지 않고, 다음 장면은 앞 장면의 논리 끝 시각에서 시작한다. - 표시 길이
D(시간표의presentation, 장면마다 하나)는D = max((T − t0) / speed, (마지막 펄스 − t0) / speed + 400, (마지막 선 이탈 − t0) / speed + 400)이다.T는 장면의 논리 끝(박자 장면은 박자 길이의 합, 흐름 장면은for)이고 400은 펄스 길이(src/pulse.js의PULSE_MS)라 표시 ms이며speed로 나누지 않는다. 마지막 선 이탈은 점이 선을 떠나는 시각이고T를 넘지 않는다. 펄스와 이탈은 시각이 아니라 출처 장면에 속해, 장면의 끝 시각에 일어난 변화도 그 장면의 꼬리를 갖는다. 반복은 효과를 자르지 않고 이 길이마다 돈다. 논리 길이가 0이고 펄스도 선 이탈도 없는 장면(light,show,clear만 있는 장면)은D가 0이고, 재생기는 마지막 모습을 그리고 시계를 돌리지 않는다. 길이 0 장면도 자기 구간을 하나 갖고(그 장면의light와 값이 그 구간에 있다) 시간을 덧붙이지 않는다. 시간표의total은 논리 길이의 합 그대로라서 장면이 없거나 모두 길이 0이면 0이다. - 효과 꼬리를 더하는 것은 값이나 차트 표식의 실제 변화, 점이 도형에 닿는 일, 점이 선을 떠나는 일뿐이다. 켜기(
light), 보이기(show), 지우기(clear), 드러내기(reveal)는 상태라 그 박자 시각에 바뀌고 꼬리를 더하지 않는다. - 한 장면 안에서 밝힌 도형과 카드 내용은 남고, 선은 점이 지나는 동안만 켜지며 조용한 선만 처음 지난 때부터 장면 끝까지 보인다. 다음 장면이 시작하면 모두 꺼진다(장면 상태).
- 마지막 모습은 그 장면이 소유한 상태다. 이웃 장면과 같은 시각에 닿아도(앞 장면의 끝 시각 = 다음 장면의 시작 시각) 서로의 구간, 펄스, 값을 이어받거나 지우지 않는다. 조용한 선이 보이는 구간(
marks)도 장면마다 따로 담겨서 장면 3에서만 지나는 선이 장면 0과 1의 마지막 모습에 보이지 않는다.
시간표
시간표의 모양(steps, segs, values, charts, marks, pulses, tracks)은 구조가 정한다. 원본을 읽고 배치한 뒤 한 번 만들고 HTML, 움직이는 SVG, 멈춘 SVG가 같은 시간표를 쓴다. 이 절은 시간표의 재생 관련 규칙만 적는다.
- 재생기와 SVG는 시간표를 읽기만 하고 상태를 다시 계산하지 않는다. 화면에 필요한 값은 시간표를 만들 때 미리 계산해 담는다.
- 시간표는 상태를 박자마다 완전히 적는다. 앞 박자를 몰라도 그 박자 상태를 그릴 수 있어서, 탭으로 건너뛰어도 상태가 맞다.
- 이동(
hops)은 보기마다 투영 하나씩이다. 같은 이동의 투영은at,ms,cut이 같고, 선 번호(edge)만 보기마다 다르다. 흐름 이동은edge대신track,legEdges(구간마다 선 번호),gaps(도형 안을 지나는 구간의 길이 비율, 개수는legEdges보다 하나 적다)를 갖는다. 박자 이동은 정수edge하나다. 이 모양이 아니거나ms가 0 이하이면 재생기와 움직이는 SVG가 오류로 끝난다. 근사나 대체값은 없다. - 이동 시간(
hops[].ms)은time=이 있으면 그 값이고, 없으면 선 길이에 비례한다(선 길이 / spacing.figure.packet.hop-ref × pace, 아주 짧은 선만 최소duration.hop-min). 흐름에legs=가 있으면 구간별 이동 시간이 정한다. 선 길이는 배치가 끝난 장면에서 구하므로 시간표는 배치 뒤에 만든다. - 박자 길이(논리)는
max(이동 끝, 자라는 시간, 대기 끝) + wait 시간이다. 자라는 시간은 차트reveal이 있을 때pace이고,대기 끝은 이벤트 처리가 그 박자에서 마지막으로 소비한 시각이다. 점이 출발하지 않은 대기(else없는 시간 초과, 기다린 뒤when이 거짓인 대기)도 대기 끝에 들어가므로, 다음 박자와 다음 장면은 그 대기가 끝난 뒤에 시작한다. 박자는 서로 붙어 이어지고 박자 앞뒤에 따로 머무는 시간, 장면 첫 박자의 최소 길이, 효과 시간은 없다.wait과for=는 원본이 적은 논리 시간이라speed와 함께 변한다. - 글 상자 줄은 시간표를 만들 때 토큰
spacing.figure.chip.max-width너비로 나눈다. HTML과 SVG가 같은 줄을 쓰기 위해서다.
시간 상한
- 시간 상한은 1시간(3600000ms)이다. 1시간보다 긴 문서 그림 재생은 쓸모가 없어서 정한 정책 상한이고, 부동소수점 정밀도를 보장하는 값이 아니다(출발 시각의 정밀도는 흐름 장면에서 따로 검사한다).
- 적용 위치는 셋이다. 원본의 시간 값 하나(
pace,for,wait,time=,at=,every=,timeout=,legs=의 항목, 넘으면 그 줄의time-limit오류), 장면 하나의 길이, 그림 전체 시간(모든 장면 길이의 합). 상한과 정확히 같은 값은 통과하고 1ms라도 넘으면 오류다. - 장면과 전체 시간에는 원본에 적은 값뿐 아니라 시간표가 자동으로 더하는 시간이 모두 들어간다. 거리로 정한 이동 시간, 박자의
wait, 대기가 풀리기까지 미룬 출발(hops[].at), 점이 출발하지 않은 대기의 끝, 차트 계열이 자라는 시간이다. 표시 꼬리(효과 400ms)는 논리 시간이 아니라 이 상한에 들어가지 않는다. - 합이 상한을 넘는 구간을 만드는 줄(박자 장면은 그 박자의 줄, 흐름 장면은
scene줄)이 오류 줄이다. 시간표가 유한하지 않은 숫자를 갖는 일은 없고, 재생기 데이터와 SVG를 쓰기 직전의 숫자 서식(roundTo)은 유한하지 않은 숫자를null로 바꾸지 않고 오류로 끝낸다. 이 검사는 앞 단계가 놓친 경로를 잡는 마지막 방어선이라 사용자에게는 줄 번호 있는 진단을 낸다.
시간 정밀도
- 조건이나 대기(
when,wait)를 쓰는 장면의 시각은 모두 0.00001ms 눈금(시간표가 담는 소수 다섯째 자리)의 정수 번호다. 점의 출발과 도착, 값 변화, 대기 해제, 시간 초과, 이벤트 순서가 같은 번호를 세고, 시간표에 담는 ms는 그 번호를 100000으로 나눈 값이다. 시각을 더할 때는 번호를 더하고 반올림하지 않으므로 이진 부동소수점 오차가 순서를 바꾸지 못한다. - 눈금 위의 시각은 번호가 다르면 순서가 다르다. 눈금으로 구별할 수 없는 시각은 오류다. 시간표, 움직이는 SVG의 keyTimes, 재생기 데이터가 모두 소수 다섯째 자리로 시각을 담아서, 그보다 가까운 두 사건은 출력에서 같은 시각이 되고 조건을 읽는 순서가 원본과 달라진다. 자릿수만 늘리면 같은 문제가 더 작은 시각으로 옮겨 갈 뿐이라 눈금은 늘리지 않는다.
- 원본에 적은 시간(
at,every,time=,legs=의 항목,timeout=, 장면for)은 눈금의 정수배여야 한다.0.00001ms,1.00002s는 받고0.000001ms,0.000015ms는 받지 않는다. 이진 오차(0.1ms + 0.2ms)는 눈금 위로 본다. 어긋나면 그 시간을 쓴 줄(장면for는scene줄)의 오류time-precision이고,--strict와 무관하며 파일을 쓰기 전에 끝난다. - 거리와 속도로 정한 이동 시간, 이동 곡선으로 정한 도형 도착 시각은 계산 값이라 가장 가까운 눈금으로 올린다. 이동 시간이 눈금 하나보다 짧아 0이 되거나, 서로 다른 시각에 닿을 두 도형이 같은 눈금으로 합쳐지면 그 이동 줄의 오류
time-precision이다. 그래서 도착은 늘 출발보다 눈금 하나 이상 뒤이고, 경로의 도형 순서가 시각 순서와 같다. at을 생략한 여러 출발지의 기본 시각(every × i / n)은 계산 값이라 가장 가까운 눈금으로 올린다.every자체는 원본 입력 정밀도를 지켜야 한다. 한 주기 안에서 출발지별 기본 시각이 같은 눈금으로 합쳐질 만큼 좁으면 그track줄의time-precision오류다.at을 명시한 동시 출발은 그대로 허용한다. 출발 수 예산도 이 눈금 위 시각으로 센다.- 장면(박자)의 시작 시각은 앞 장면 길이의 합이라 눈금 위가 아닐 수 있다. 시작 시각은 번호 하나로 한 번만 바꾸고 그 뒤는 정수 덧셈이라, 앞 장면이 몇이든 한 장면 안의 사건 순서는 같다.
- 시간표에 담는 대기의 시작과 끝, 건너뜀 시각, 값 변화, 펄스와 선 켜짐 시각, 점의 출발(
at)과 이동 시간(ms)은 모두 이 번호를 ms로 바꾼 값이다. SVG와 HTML 재생기는 이 값을 그대로 읽는다. - 조건과 대기를 쓰지 않는 장면은 이 계약 밖이다. 시각을 반올림하지 않고 계산하고, 파일에 쓸 때 숫자 서식이 소수 다섯째 자리로 줄인다. 이 서식은 출력 단계의 일이라 계산 시각과 섞지 않는다.
흐름 장면
- 기본 흐름 이동 시간에는 선뿐 아니라 중간 도형을 가로지르는 이음 거리도 포함한다.
time=이 없을 때 선과 뒤따르는 이음을 한 구간으로 재며,legs=에서 구간 시간을 생략한 경우와 같은 거리 규칙을 쓴다. - 흐름 장면은 구간 하나다. 길이는
for(없으면 토큰duration.flow-step)이고,hops에 출발마다 점 하나가 들어간다. 점은at(구간 시작 뒤 출발 ms),ms(경로 전체 이동 시간),track(tracks번호),legEdges,gaps,tone을 갖는다. 장면 끝 전에 출발했지만 끝까지 도착하지 못하는 점은cut(그려지는 시간)을 갖고 장면 끝에서duration.cut-fade동안 서서히 사라진다. 움직이는 SVG는 잘린 점의 이동을 이동 곡선에서 잰 지점을 잇는 선형 구간으로 그린다. 잘린 점의 글 상자chipPath와 숨김 키chipFade는 시간표가 잘림 시각(cut)에서 끝내고 그 시각의 보간 값을 마지막 지점으로 둔다. 그래서 점의 보임 구간과 글 상자의 keyTimes가 같은 시각에 끝나고, SVG와 재생기가 같은 자리를 읽는다. - 구간의 시계는 박자 장면과 같은 한 시계다. 재생기의 탭과 장면 들어가기는 그대로 동작한다.
- 한
track이 둘 이상의 보기에 걸치면(그래프 보기가 둘일 때) 점은 그 선을 가진 보기마다 한 번씩 그려진다. 첫 보기가 출발, 이동 시간, 잘림, 도형 도착 시각을 정하고, 다른 보기는 같은 시각에 같은 도형에 닿게 보기별 속도(src/track-pace.js)로 옮긴다. 값과 조건은 첫 보기에서 한 번만 계산한다. 도착 펄스는 논리 도형마다 하나다. - 움직이는 SVG는
<defs>의 경로tp-번호(선 경로를 이어 붙인 것) 하나를mpath로 걸어 한 점이 여러 선을 지나게 하고, 출발마다 점 요소를 하나씩 만든다. 점이 많아지면 그림 검사 14번이 토큰scale.flow-dots-max로 경고한다. - 출발 수는 출발 시각 배열을 만들기 전에 세어 막는다. 흐름 하나의 출발 수는
at이 장면 길이 안이면ceil((for - at) / every)(every가 없으면 1)에서 나눗셈 오차를 한 번 맞춘 값이다. 마지막 출발at + (n - 1) × every가 장면 길이보다 작고 그다음at + n × every가 장면 길이 이상이도록 맞추므로for=0.07ms every=0.01ms는 8이 아니라 7이다. 그림 전체 합이scale.flow-dots-max의 열 배(800)를 넘으면 시간표를 만들지 않고 검사 14번 오류([check 14] the flows would draw N dots, over the limit of 800, 줄은 합이 한도를 넘긴track줄)로 끝낸다. 경고선(80)과 달리 이 선은 입력이 처리 예산을 넘는 경우라 그림을 막는다. 12초 장면에every=0.000001ms는 출발 120억 개라 배열이 메모리를 채우기 전에 이 오류가 된다. 이어 붙인 경로는 구간마다 둥근 모서리 경로를 잇고 도형 안(구간 사이)은 직선으로 잇는다. - 출발 시각은 모두 유한하고 엄격히 늘며 장면 길이보다 작다. 하나라도 어긋나면(
at=3599999ms every=0.000000000001ms처럼at + every가at으로 되돌아오는 경우) 지원하지 않는 시간 정밀도로 그track줄의 오류(time-precision)로 끝낸다. 내부 오류가 아니다. 조건이나 대기를 쓰는 장면은at과every가 눈금의 정수배여야 하므로 이 검사가 입력에서 먼저 끝난다(시간 정밀도). - 점은 도형 안을 지나는 구간(
gaps, 경로 길이 비율)에서 보이지 않는다. 도착 연결점에서 사라져 출발 연결점에서 다시 나타나고, 그 구간의 시간은 그대로 흐른다. 그래서 점이 도형 이름 글자 위를 지나지 않고 멈춘 느낌도 없다. - 흐름이 지나는 선은 점이 그 선에 올라 있는 동안만 켜지고 점이 떠나면 꺼진다(남겨 두는 선은
quiet뿐이다). 도형은 켜 두지 않고 점이 닿을 때마다 도착 펄스만 한 번 쓴다(도착 펄스).
값 변화
values는 장면마다 선언한 값 하나당 한 줄이다.{ si, id, node, t0, t1, initial, changes, periods, slots? }이고changes는 [그림 전체 시각, 새 글] 목록,periods는 값 줄이 보이는 동안 글이 바뀌는 구간[시작, 끝, 글]이다. 바뀌는 순간의 밝힘은 구간이 아니라timeline.pulses의 값 펄스(값과 차트 펄스)가 맡는다.periods는 시간표가 한 번 계산하고 움직이는 SVG와 재생기가 읽기만 한다. 장면이 시작하면initial로 돌아간다.initial은 선언한from이거나,keep과 장면set=을 반영한 시작 값이다(장면 사이 값 유지).- 값은 점이 도형에 닿는 시각(이동 곡선을 거꾸로 푼다) 순서로 바뀐다. 같은 시각이면
on줄이 모두set=보다 앞이고, 같은 종류면 이동과 흐름의 선언 순서다. 참조 값 줄은 가리키는 값이 바뀌는 같은 시각에 같은 글로 바뀐다. 장면이 끝난 뒤에 닿는 점은 값을 바꾸지 못한다. - 큐(
queue)의 값 줄은slots(칸 수)를 더 갖는다. 같은periods를 쓰고, 글자 요소 대신 값 글자마다 그 찬 칸 수(음수는 0,slots초과는slots)만큼 찬 칸을 얹은 묶음을 두어 같은 방식으로 보인다. 밝힘 면은 큐와 같은 윤곽으로 갱신 펄스 동안 보이며 새 테두리를 더하지 않는다. 칸 수로 바꾸는 규칙(filledSlots)은 그리는 쪽 한 곳(src/measure/queue.js)에 있고 재생기는 계산하지 않는다. - 움직이는 SVG는 값마다 글자 요소를 하나씩 두고 SMIL 이산 불투명도와
visibility로 보이고, 값 줄 뒤의 면은 값 펄스(timeline.pulses)로 밝힌다. 바뀌지 않는 값은 한 바퀴 내내 보이고, 값이 바뀌면 마지막 글 하나만 끝까지 남는다(끝 시각에 바뀐 값도 앞 글이 새 글 아래에 겹쳐 남지 않는다). 재생기는 같은 구간으로 지금 시각의 글자 요소를 고른다. 시간표는 앞 박자를 몰라도 시각만으로 값을 정할 수 있게 구간 전체를 담는다. - 값 펄스의 키(
value:값 줄 번호)는 문서 전체의 값 줄 번호다. 장면을 잘라 한 장면만 그리는 SVG도 잘린 값 줄에 전체 번호를 실어 같은 키로 펄스를 찾는다. 그래서 둘째 장면 이후의 값 줄도 같은 시각에 같은 세기로 밝아진다.
장면 사이 값 유지
- 값은 장면이 시작할 때
from으로 돌아간다. 장면의keep에 적은 값은 앞 장면이 끝난 시각의 값에서 시작하고, 첫 장면과 그림 전체의 되풀이(재시작)는 늘 선언한from에서 시작한다. 장면이 끝난 뒤에 닿는 점은 값을 바꾸지 못하므로 끝난 시각의 값은 하나로 정해진다. - 장면
set=은 장면이 시작할 때 한 갱신으로 적용한다. 읽기 식은keep을 반영한 값을 읽고 쓰는 순서는 이벤트 순서와 같다. 장면의set=은 변화가 아니라 시작 값이라changes에 적지 않고 펄스도 없다. - 같은 장면의
set=재설정은keep한 값보다 우선한다. 장면의 시작 값은keep값에 재설정을 적용한 결과이고, 시간표는 이 결과를 값 줄의initial에 담는다. 장면을 바로 선택하면 재생기는 앞 장면을 다시 재생하지 않고 그 장면의initial에서 시작한다. 그래서 장면을 건너뛴 상태가 처음부터 재생한 상태와 같다. keep은 값 글자만 넘긴다. 값 줄의 밝힘, 카드, 밝은 도형, 도형 상태는 장면마다 새로 시작한다.
이벤트 순서
- 이벤트는 값을 바꾸는 일(갱신), 점이 출발하는 일, 대기를 푸는 일이다. 이벤트의 시각은 시간 정밀도의 눈금 번호이고 같은 번호면 같은 시각이다.
- 값, 조건, 대기는 시간표를 만들 때 한 번 계산한다. 계산은 원본과 배치 결과만으로 정해지고 실제 시계, 난수, 재생 상태를 읽지 않는다. 문서 숨김, 장면 속도, 장면 들어가기와 반복은 재생 시계만 움직이고 이 계산을 다시 하지 않는다.
- 한 시각의 이벤트는 아래 순위로 처리한다. 앞 이벤트의 쓰기는 뒤 이벤트가 읽는다.
| 순위 | 이벤트 | 같은 순위 안의 순서 |
|---|---|---|
| 1 | on 줄의 값 갱신 |
원본 선언 순서 |
| 2 | 이동과 흐름 set=의 값 갱신 |
이동과 흐름의 선언 순서, 같은 줄이면 적은 순서 |
| 3 | 풀린 대기의 실행(reserve=가 있으면 예약 포함) |
대기 줄의 선언 순서, 같은 흐름이면 보류한 순서 |
| 4 | 이 시각에 출발하는 점의 실행(reserve=가 있으면 예약 포함) |
선언 순서, 같은 흐름이면 출발 순서 |
- 갱신 하나는 같은 시각에
on줄 하나(점이 도형에 닿는 일 하나)나set=하나가 적용하는 식의 목록이다.@도형을 붙인 식은 점이 그 도형에 닿는 시각의 갱신이다. - 값 갱신 하나는 읽기, 쓰기 순서다. 식에 든 읽기(
:=)는 그 갱신을 시작하는 시점의 값을 읽고, 그 뒤에 식을 적은 순서대로 쓴다. 그래서set="a:=b, b:=a"는 두 값을 맞바꾼다. - 한 시각에 이어진 갱신(
on줄,set=,reserve=)의 결과는 값 줄에 그 시각의 마지막 글 하나로 보인다. 같은 시각에 그 시각 전의 글로 되돌아오면 변화로 적지 않고 펄스도 없다. - 갱신이 끝나면 바뀐 값을 가리키는 참조 값 줄이 같은 글로 바뀌고(갱신마다 별도 이벤트로 센다), 그 값을 조건에서 쓰는 대기를 선언 순서로 다시 평가한다. 참이 되는 대기는 풀려서 순위 3에 놓인다.
- 점의 출발 실행은
wait,when순서로 조건을 보고 둘 다 통과한 출발만 점을 만든다. 출발은 값을 바꾸지 않고 값은 점이 도착할 때 바뀐다. 예외는reserve=가 있는 출발이고, 그 갱신은 원자 예약이 정한다. 이동 시간이 0보다 크므로 도착은 늘 출발보다 늦다. 그래서 같은 시각에서 갱신이 새 갱신을 만드는 닫힌 고리는 없고, 한 시각의 연쇄는 갱신, 참조 값 갱신, 대기 해제, 출발에서 끝난다. 예약이 바꾼 값을 읽는 대기는 같은 시각에 한 번 더 평가하고, 이 연쇄는chain예산이 끊는다. - 박자 장면은 박자마다 같은 처리를 하고, 박자 시작 시각에서 시작한다. 앞 박자가 끝난 뒤의 값을 읽는다. 한 박자의 이동은 모두 박자 시작에 출발할 준비를 하고 선언 순서로 처리한다.
- 점이 도형에 닿는 일은 값을 바꾸지 않아도 이벤트 하나다. 대기가 남았는데 닿을 점이 아직 있으면 처리할 이벤트가 남은 것이다. 닿을 때 적용할 식이 없으면 갱신 없이 이벤트로만 세고, 대기는 다시 평가하지 않는다.
- 시간 초과는 위 표의 순위가 아니라 그 시각의 회차가 모두 끝난 뒤에 정한다. 같은 시각에 대기가 풀렸으면(
released) 그 대기는 시간 초과가 되지 않는다. - 대기의 평가는 한 시각의 갱신(순위 1, 2)을 모두 적용한 뒤에 한다. 그 시각에 글이 실제로 바뀐 값을 읽는 대기만 다시 평가하고, 한 시각 안에서 바뀌었다가 처음 글로 돌아온 값은 바뀐 것이 아니다.
- 점이 출발 도형에 닿는 것으로 정한 식(
set="n+1@출발도형")은 출발과 같은 시각의 갱신이다. 이 갱신이 대기를 풀면 같은 시각을 한 번 더 처리하고, 이 연쇄는 이벤트 예산의chain이 끊는다.
조건과 대기
when은 이동이나 출발을 실행하기 직전에 한 번 평가한다.wait가 있으면 풀린 직후다. 거짓이면 그 이동은 점, 선 켜짐,set,on, 도착 효과를 모두 만들지 않는다. 건너뛴 이동은skips에 줄 번호, 시각과 함께 남고, 박자에서는 그 이동만 빠진다. 이동이 하나도 남지 않은 박자는 점 없이 멈추는 박자로 남아서 박자와 장면이 사라지지 않는다. 흐름의 출발은 조건이 거짓이면 점을 만들지 않지만 출발 수 계산에는 들어간다(흐름 장면).wait는 점이 출발할 시각에 처음 평가한다. 참이면 바로 출발하고, 거짓이면 점은 출발 도형에서 기다린다. 기다리는 점은 그리지 않고 선도 켜지 않는다. 참조한 값이 바뀔 때마다 다시 평가하고, 참이 되면 풀려서 그 시각에 출발한다. 같은 시각에 여러 대기가 풀리면 위 표의 순서(선언 순서)로 출발한다. 점이 출발하면 선과 그 끝 도형은 출발 시각에 켜진다.- 해제 시각은 시간표에
waits로 담긴다. 항목은{ si, line, node, cond, t0, t1, end }이고t0은 대기를 시작한 시각,t1은 끝난 시각,end는 끝난 이유다. 처음 평가에서 참이라 기다리지 않은 점은 담지 않는다. 항목은 대기가 끝난 차례로 쌓인다. 실제 시계로 조건을 확인하거나 재생기에서 조건을 계산하지 않는다. skips항목은{ si, line, node, cond, t }이다.cond는 거짓이었던when글이고t는 평가한 시각이다.wait가 풀린 직후when이 거짓이어도 건너뛴 것으로skips에 남고waits에는released로 남는다.- 조건이 읽는 값의 종류는 평가할 때 정해진다. 숫자와 낱말을 견주거나
<계열에 낱말이 오면 그 줄의 오류value-type으로 끝나고 조용히 거짓이 되지 않는다. 짧게 끊지 않고 조건의 모든 비교를 계산하므로 같은 입력은 같은 결과를 낸다. - 값을 바꾸는 연쇄(참조 값, 대기 해제)는 이벤트 하나씩으로 세어 이벤트 예산에 넣는다.
timeout이 지난 대기는else가 있으면 출발 도형에서else도형으로 가는 점을 출발시킨다. 이 점은 시간 초과 시각에 출발하고 값은on줄만 적용한다(set=는 이어받지 않는다). 같은 시각에 풀림과 시간 초과가 겹치면 풀림이 이긴다.
원자 예약
잠금이나 큐처럼 용량이 정해진 자원은 조건 확인과 값 갱신이 한 사건이어야 한다. wait="n=0"과 set="n+1@a"를 함께 쓰면 두 이동이 같은 시각에 둘 다 n=0을 읽고 출발한 뒤 n이 1, 2가 된다. 이 동작은 그대로 두고, 확인과 갱신을 한 사건으로 묶는 선택 사항 reserve="식, 식"을 따로 둔다.
reserve=의 식은set=와 같은 꼴(id+N,id-N,id=값,id:=원천)이고@도형은 쓰지 않는다. 점이 출발하는 사건에서 적용한다.wait와when이 있으면 둘 다 참일 때만 적용하고, 둘 다 없으면 출발 때 조건 없이 적용한다.- 조건을 읽은 뒤 예약을 쓰기 전에 다른 이벤트가 끼지 않는다. 이 사건은 위 표의 순위 3(풀린 대기)이나 순위 4(출발) 안에서 일어나므로, 같은 시각의 요청은 선언 순서, 같은 흐름에서는 보류한 순서로 처리하고 앞 요청의 예약은 다음 요청의 조건 평가 전에 보인다. 그래서 가용량이 1인 자원에 같은 시각에 닿은 요청은 하나만 통과하고 나머지는 대기에 남는다. 이미 기다리던 요청(순위 3)은 이 시각에 새로 출발하는 요청(순위 4)보다 먼저 처리한다.
- 예약이 값을 바꾸면 그 값을 읽는 다른 대기를 같은 시각에 다시 평가한다. 앞서 처리한 대기도 포함하고, 이 연쇄는 이벤트 예산의
chain이 끊는다. 예약마다 이벤트 하나로 세고, 사전 검사는 예약이 있는 출발에 하나를 더한다. - 식 목록은 모두 적용하거나 하나도 적용하지 않는다. 식을 복사본에 먼저 적용해 읽기, 종류 검사, 합 계산이 모두 성공한 뒤에만 값에 반영한다. 하나라도 실패하면(
value-type) 값은 하나도 바뀌지 않고 빌드는 그 줄의 오류로 끝나 시간표와 결과 파일을 만들지 않는다. 읽기 식(:=)은 갱신을 시작하는 시점의 값을 읽는다. - 값 줄은 이벤트 순서의 같은 시각 규칙을 따른다. 해제와 획득이 겹치면 이전 글에서 새 글로 한 번 바뀐다.
| 상황 | 예약 |
|---|---|
wait가 거짓이라 점이 기다린다 |
하지 않는다. 풀리는 시각에 조건을 다시 확인한 뒤 한다 |
timeout이 지나 대기가 끝난다 |
하지 않는다. else 점도 예약을 이어받지 않는다 |
when이 거짓이다(대기가 풀린 직후도 같다) |
하지 않는다. 건너뛴 이동으로 skips에 남는다 |
점이 lost로 사라진다 |
출발 때 이미 했으므로 남는다. 사라진다고 돌려주지 않는다 |
| 점이 도착한다 | 예약과 상관없다. 도착 때 바뀌는 값은 set=와 on이다 |
| 장면이 끝날 때까지 대기가 풀리지 않는다 | 하지 않는다 |
- 해제는 명시적인 모델 사건이다. 렌더러, 시간표 계산, 재생기는 예약을 스스로 돌려주지 않는다. 점이 닿을 때 풀려면 그 이동의
set=나on줄을 쓰고, 출발할 때 풀려면reserve=를 쓴다(when="holder='A'"처럼 쥔 쪽만 풀게 할 수 있다). 풀린 시각에 기다리던 요청은 위 순서로 예약한다. - 사라진 요청이 잡은 예약은 해제 사건이 있어야 풀린다. 원본이 해제를 그리지 않으면 대기자는
timeout이나 장면 끝까지 기다리고, 처리할 이벤트가 없으면wait-stalled로 알린다. - 시간표는 예약마다
reserves에{ si, line, node, t, writes: [{ id, from, to }] }를 남긴다.reserve=를 쓴 원본만 이 항목을 갖는다. SVG와 HTML 재생기는 값 줄과 점의 시간표만 읽고 예약을 다시 계산하지 않는다. 문서 숨김, 장면 속도, 장면 들어가기와 반복은 결과를 바꾸지 않는다.
잠금, 교착, 큐 역압, 같은 시각의 잠금 요청은 공통 기능(when, wait, timeout, else, reserve, keep, on, set=)만으로 적는다. 예제 전용 문법은 없다. 예는 examples/flow.thinkflow(같은 순간의 잠금 요청과 timeout 분기)와 examples/queue.thinkflow이다. 값을 조건으로 바꾸는 장치는 없어서 임계 판정(회로 차단 같은 것)은 when이 붙은 확인 흐름의 출발이 맡는다. 그래서 상태는 기준에 닿은 그 시각이 아니라 확인 흐름의 다음 출발과 이동 시간만큼 늦게 바뀌고, 시간이 지나면 시험 요청 하나만 통과시키는 반개방 상태와 요청마다 성공과 실패를 가르는 확률은 없다. 실패 횟수는 원본의 set=가 정한다.
대기가 끝나는 때
끝(end) |
조건 | 결과 |
|---|---|---|
released |
조건이 참이 됨 | 그 시각에 출발한다 |
timeout |
timeout=만큼 지났는데 거짓(같은 시각에 풀리면 released) |
else가 있으면 출발 도형에서 else 도형으로 가는 이동을 출발하고, 없으면 그 점은 만들지 않는다 |
step-end |
거짓인 채 처리할 이벤트가 남은 상태로 장면 끝 | 점 없음, 경고 없음 |
stalled |
대기가 남았는데 처리할 이벤트(남은 출발, 도착, 시간 초과)가 하나도 없음 | 그 대기의 계산을 멈추고 stalls에 남긴다. 경고 wait-stalled를 낸다 |
- 같은 시각에 풀림과 시간 초과가 겹치면 풀림이 이긴다. 시간 초과는 그 시각의 회차(갱신, 대기 해제, 출발)가 모두 끝난 뒤에 정하므로, 그 시각에 풀린 대기는 시간 초과가 되지 않는다. 시간 초과의
else출발이 같은 시각에 새 이벤트를 만들면 그 시각의 회차를 다시 돈다. - 대기는 명시한 제한 시간과 장면의 끝(박자 장면은 그 박자의 끝, 흐름 장면은
for) 안에서만 처리한다. 박자 장면에서는 대기가 끝난 시각이 그 박자의 길이를 정하므로 대기는 박자를 넘지 않고, 시간 상한도 그대로 적용된다. stalls항목은{ si, line, node, move, cond, t, refs, stuck? }이고move는 대기한 이동 글(t1 -> l2),t는 멈춘 시각(마지막으로 처리한 이벤트의 시각),stuck은stuck을 쓴 대기만 갖는다.refs는 조건이 읽은 값마다{ id, text, line, at }(현재 글과 마지막으로 쓴 줄과 시각)이고, 값을 쓴 적이 없으면 선언한 줄이다. 서로의 값을 기다리는 교착은 대기 항목들이 서로의refs를 가리키므로 같은 경고에서 원인 줄이 보인다.wait-stalled경고 메시지는 대기한 이동 줄, 기다리는 도형, 조건, 읽은 값의 글과 마지막으로 쓴 줄을 적는다.stuck이 있는 대기는 경고를 내지 않고stalls에는 똑같이 남는다.- 박자 장면에서 이동이 모두 멈춘 박자는 점 없이 멈추는 박자로 남는다. 멈춘 도형에 알약을 보이려면 장면의
status로wait를 적는다. - 예산이 다해 계산이 멈추면 정상 결과로 저장하지 않고 오류로 끝낸다(이벤트 예산).
이벤트 예산
| 예산 이름 | 세는 것 | 기본값 |
|---|---|---|
events |
그림 전체에서 처리하는 이벤트 수. 출발, 점이 도형에 닿는 일, 값 갱신, 참조 값 갱신, 대기 평가, 풀린 대기의 실행, when 평가, 시간 초과 분기 출발이 각각 하나다 |
300000 |
chain |
한 시각에 처리하는 이벤트 수 | 5000 |
- 이 예산은
when이나wait를 쓴 장면에만 센다. 쓰지 않은 원본은 이벤트를 처리하지 않아 세지도 않는다. - 예산 조정은 공통 예산 모듈(
src/budget.js)이 맡는다. CLI는--budget <이름>=<양의 정수>(여러 번), GitHub Action은 입력budget이고, 이름 검사와 값 검사, 초과 진단 모양은 칸 격자의 예산과 같다. 이 문서는 이 기능의 예산 이름과 기본값만 정한다. - 문장과
every로 개수가 정해지는 이벤트(출발과 그 도착)는 시간표를 만들기 전에 합계를 세어events를 넘으면 막는다. 합계는 출발 수에 (1 + 경로의 도형 수)를 곱해 구하고(박자 이동 하나는 출발 하나에 도형 둘이라 3),when으로 건너뛰는 출발도 센다. 합계가 처음 한도를 넘는 줄에 오류를 붙이고 필요한 양을 알린다. 이 검사는 배치와 시간표 계산에 들어가기 전에 끝나므로 출발이 수십억 개인 입력도 바로 끝난다. - 대기 평가, 해제, 참조 값 갱신처럼 동적으로 생기는 이벤트는 처리하기 직전에
events와chain을 검사한다. 한 시각의 이벤트는 그 시각을 처리하는 동안chain에 센다. - 예산이 다하면 오류
budget-exceeded로 끝내고 결과 파일을 쓰지 않는다. 메시지는 예산 이름, 한도, 넘은 시점의 시각, 조정 방법(--budget events=<N>)을 알리고 오류 줄은 한도를 넘게 만든 이벤트의 줄이다. 자동으로 줄이거나 일부만 저장하지 않는다. - 이 예산은 흐름 점 한도, 격자 예산, 시간 상한, 글 상자 계획 메모리를 대신하지 않는다. 시간 상한과 메모리 한계는 이 예산과 별도로 계속 적용된다.
사라짐
lost=60%는 점이 이동 경로 전체 길이의 60%에서 사라진다는 뜻이다. 경로 길이는 점이 따라가는 이어 붙인 경로 전체(도형 안을 지나는 구간 포함)이고, 0%와 100%도 받는다.- 점이 사라지는 시각은 길이 비율을 이동 곡선과 구간 시간(
pace)으로 거꾸로 푼 시각이다. 이 시각이hops[].cut이고, 사라지는 점은 장면 끝에서 잘린 점과 같은 규칙으로duration.cut-fade동안 서서히 사라진다.chipPath와chipFade도cut에서 끝난다. - 사라지기 전에 통과한 도형의 효과(
on,set=, 도착 펄스)만 적용한다. 도형에 닿는 지점(도착 연결점)의 길이 비율이 사라지는 비율보다 작을 때만 통과한 것이다. 같거나 크면 같은 시각의 도착이라도 적용하지 않는다. 그래서lost=100%는 점이 끝 도형에 닿는 지점에서 사라지고 그 도형의 효과는 없다.lost=0%는 점이 출발 지점에서 사라져 아무것도 통과하지 않는다. - 선은 점이 들어서는 비율이 0보다 클 때 켜진다. 끝 도형은 효과가 적용될 때만 켜진다. 박자에서는 사라진 이동이 도착 규칙에서 빠져 도착 도형의 카드가 박자 시작에 바뀐다.
- 화면과 값 변화는 같은 사건을 따른다. 사라진 점의 값 변화는
values에 담지 않고, SVG와 재생기는cut까지만 점을 그린다. 사라진 점은 도착하지 않았으므로 도착 펄스(seg.pulses)도 없다. - 선이 켜지는 기준은 점이 그 선에 들어선 지점(첫 선은 0, 그다음 선은 도형 안 구간을 지난 비율)이 사라지는 비율보다 작은 것이다. 켜지는 시각은 점이 그 선의 앞 도형에 닿는 시각이다.
0%로 사라지는 점은cut이 0이라 한 번도 보이지 않는다. 비율 비교는 퍼센트 글을 비율로 바꾸며 생기는 1e-9 이내의 오차를 같은 값으로 본다.
구간별 이동 시간
- 구간은 흐름 경로의 선 하나와 그 선이 끝나는 도형 안의 이음 구간이다. 마지막 구간은 마지막 선이 끝나는 지점까지다.
- 구간 시간은 다음 순서로 정한다. 구간 시간의 합이 경로 이동 시간
T이고, 이동 곡선은 경로 전체에 한 번만 건다.legs=에 적은 구간은 그 시간이다.time=이 있으면T는time=이고,T에서 1번의 합을 뺀 나머지를-인 구간에 구간 길이 비례로 나눈다.time=이 없으면-인 구간은 거리 기반 시간(구간 길이 / spacing.figure.packet.hop-ref × pace, 아주 짧은 구간은 최소duration.hop-min)이고T는 합이다.
- 다음은
leg-time오류다.time=이 있고-구간이 있는데 1번의 합이time=이상이라 길이가 0보다 큰 구간에 0 이하의 시간이 돌아가는 경우.-구간이 없는데 1번의 합이time=과 다른 경우. - 점의 위치는 시간 비율
u = t / T에 이동 곡선을 건e = easing(u)에서 정한다. 구간 시간의 누적 비율이e가 되는 지점을 구간 길이 누적 비율로 바꾸는 꺾은선(pace,[시간 비율, 길이 비율]목록)으로 길이 비율을 얻는다. 구간 시간이 길이에 비례하면pace는 항등이라legs=가 없는 흐름과 같은 위치다. 시간표는legs=가 있는 이동에만pace를 담는다. - 점이 도형에 닿는 시각과 글 상자 자리의 시각은 이동 곡선과
pace를 거꾸로 풀어 구하고, 값 변화 순서도 이 시각을 쓴다. - 도착 순서는 시각의 순서이고, 같은 시각이면 위 이벤트 순서를 따른다. 재생기의 장면 속도는 재생 시계에만 걸려서 구간 시간, 도착 순서, 값 결과를 바꾸지 않는다.
- 구간 시간 각각과 합,
timeout=은 시간 상한 검사 대상이다(시간 상한). - 시간표의
pace값은 소수 다섯째 자리로 줄여 담는다. 움직이는 SVG는pace가 있는 점의 이동을 구간 경계가 이동 곡선에 닿는 시각과 경계 사이마다 16개 지점에서 잰 길이 비율로 선형으로 잇는다(잘리는 점과 같은 방식이다). 재생기는pace가 있는 그림에만pace를 읽는 코드를 더해 점 위치와 글 상자 지점의 시각을 같은 꺾은선으로 푼다.
장면별 도형 상태
- 장면의
status는 장면이 시작할 때 알약을 켜고 장면 끝에서 끈다. 다음 장면의 도형은 선언 상태(알약 없음)다. 값의keep은 알약과 밝힘을 넘기지 않는다. - 시간표는 박자 구간마다
status({ node, kind }목록)를 담고, 장면의 상태는 그 장면의 모든 구간에 같다. 박자를 알지 못해도 그 박자의 상태를 그릴 수 있고, 장면을 직접 골라도 같다. - 알약은 글자(
OK,WARN,FAIL,WAIT)와 기호를 함께 그린다. 색 하나로만 구분하지 않는다. 문법과 색 역할은 그림 문법이 정한다. - 같은 카드가 여러 보기에 그려지면 알약은 그려진 모든 곳에 붙고, 상태 하나로 모두 함께 켜지고 꺼진다. 재생기는 논리 이름(
카드-종류)으로 찾고, 움직이는 SVG는 그린 도형마다 이름을 붙인다.
장면 상태
한 장면 안에서와 장면이 바뀔 때의 상태는 다음과 같다.
| 상태 | 한 장면 안에서 | 장면이 바뀔 때 |
|---|---|---|
| 밝은 선 | 점이 올라 있는 동안만. 점이 떠나면 알약 색이 줄어들고 중립으로 돌아간다 | 초기화 |
| 밝은 도형 | light 대상, 카드가 찬 도형. 흐름이나 이동이 닿은 도형은 펄스만 쓴다 |
초기화 |
| 밝은 그룹 | light 대상 그룹. 경계선만 강조 색이고 굵기, 면, 모서리, 점선은 그대로다. 그룹 안 도형은 따로 켜지 않는 한 켜지지 않는다 |
초기화 |
| 카드 내용 | show마다 줄이 쌓이고 clear로 비워짐 |
초기화 |
| 조용한 선 | 처음 지나는 박자부터 장면 끝까지 보임. 그 장면이 소유한 구간이다 | 숨김 |
| 순서 그림의 생명선과 머리 | 생성과 소멸은 그 장면 안에서만 효력이 있다. 소멸은 같은 장면의 생명선만 자른다 | 장면마다 따로 시작. 같은 참여자를 둘 이상의 장면이 만들 수 있다 |
| 차트 계열 | 그 장면의 reveal에 나온 계열은 장면 시작에 숨고 reveal 박자부터 보임. 나오지 않은 계열은 처음부터 보임 |
장면마다 이 규칙으로 새로 시작 |
| 값 | 도착할 때 바뀐 글 | keep한 값은 앞 장면이 끝난 값, 나머지는 from |
| 도형 상태 | 장면의 status |
선언 상태(알약 없음) |
글 상자
이동의 글 상자는 점과 함께 가며, 자리는 시간표를 만들 때 한 번 정해 이동의 chipPath(진행 비율마다 [비율, dx, dy, 불투명도])에 담는다. 움직이는 SVG와 재생기는 이 값을 보간만 하고 다시 계산하지 않는다.
글 상자 크기는 sizeChip이 재고 SVG와 충돌 검사가 함께 쓴다. HTML도 같은 크기를 이동의 chipSize로 받아 그리며, 브라우저에서 글 폭을 다시 재지 않는다.
- 점 위가 기본이다. 후보는 점 위, 가리는 것을 조금 비켜 올린 자리, 점 아래, 가리는 사각형 양 끝에 붙게 옆으로 비킨 자리 순서다. 후보마다 그림 안에 있는지, 도형 이름과 부제, 테이블 머리와 열, 그룹 제목, 도형 테두리, 선 라벨 알약을 가리는지, 다른 선과 그룹 틀에 붙는지, 판 위아래와 좌우 끝 여백을 지키는지를 차례로 견주어 가장 나은 것을 쓴다. 선과 그룹 틀은 가리면 읽을 수 없는 것이 아니라 붙어 보이는 것이라 순위만 낮춘다.
- 붙임 거리(
spacing.figure.packet.chip-reach) 밖의 후보는 모두 버린다. 박자 이동은 글이 정보라서 점이 보이는 시간 중 글 상자가 실제로 보이는 비율이 토큰scale.chip-visible-share(0.6) 이상이어야 한다(흐려진 지점 비율이 아니라 보이는 비율이다). 못 넘으면 그 이동만 도형 윤곽(테두리)을 덮는 자리도 쓰는 둘째 계획을 세우고, 보이는 비율이 더 높은 계획을 고른다. 글자, 아이콘, 카드와 큐 안쪽, 선 라벨 알약, 상태 알약은 어느 계획에서도 가리지 않는다. 흐름은 점 색이 흐름을 구분하므로 둘째 계획을 세우지 않는다. - 일반 간격 늘림으로 푼 뒤에도 박자 이동의 보이는 비율이 0.6에 못 미치면 그림 검사 7번이 경고한다. 간격 늘림은 지나는 선의 층 간격과 선 옆 폭을 요구하고(배치 7단계), 옆 폭 밖에 그룹 제목과 끝 라벨을 놓는다.
- 이동 전체의 자리는 지점마다 따로 고르지 않고 이동 하나를 통째로 푼다(
src/chip-plan.js). 계획 지점(2프레임 간격)마다 깨끗했던 자리 종류를 모두 다시 만들고 자리 바꿈 횟수가 가장 적은 열을 동적 계획으로 고른다. - 자리를 바꿔야 하면 순간 이동하지 않는다. 두 자리 사이를 토큰
duration.chip-slide이상 시간에 선형으로 미끄러지고, 거리가 멀면 한 프레임에CHIP_STEP_MAX(6px)를 넘지 않게 시간을 늘린다. 미끄러지는 중간 프레임이 도형, 글자, 알약을 가리거나 그림 밖이면 그 미끄러짐은 쓰지 않는다. 미끄러질 길이 없으면 바꾸지 않고, 겹치는 구간에서 글 상자를 흐리게 한다(chipPath의 불투명도, 복귀에duration.chip-fade). - 서로 다른 점의 글 상자는 겹치지 않는다. 60fps 프레임마다 먼저 출발한 점부터 글 상자를 놓고 이미 놓인 상자와 겹치면 나중에 출발한 점의 글 상자를 숨긴다. 시간표는 이동마다 숨김 불투명도 키
hop.chipFade를 한 번 계산해 담고, 움직이는 SVG, 재생기, 그림 검사 7번이 같은 키를 읽는다. 흐름과&동시 이동이 같은 규칙을 쓴다. - 계획 메모리는 이동 시간 20초(
PLAN_MAX_MS)에서 멈춘다. 그보다 긴 이동은 20초짜리 이동으로 계획을 세우고, 계획 경로가 진행 비율이라 같은 계획이 긴 이동에 그대로 맞는다. 같은 선, 글, 시간, 방향의 이동은 계획을 한 번만 세우고 그림 검사 7번도 그 계획을 쓴다. 겹침과 가까운 선은 층별 격자 색인(src/chip-grid.js, 글 상자 계획의 공간 색인)으로 잰다. - 점의 자리는 점이 실제로 따라가는 둥근 모서리 경로 기준으로 잰다. 지점 사이는 시간에 선형으로 잇는다(SVG의
animateTransform과 재생기가 같다). 후보를 견주기 전에 좌표와 거리를 소수 아홉 자리로 정규화해 미세한 계산 차이가 다른 경로 선택으로 번지지 않게 한다.
HTML 재생기
재생기(src/html.js가 만드는 HTML 한 장)는 시간표를 읽기만 하고 사건을 다시 계산하지 않는다. 파일 구성은 다음과 같다.
| 파일 | 하는 일 |
|---|---|
src/html/content.js |
컴파일 결과(scene, timeline)를 판마다의 SVG와 재생 데이터로 바꾼다. 그림은 한 번만 그려 판으로 나눈다 |
src/html/metrics.js |
토큰에서 읽은 값(펄스 시간, 흐름 색, 점과 글 상자 크기)을 재생 데이터에 싣는다 |
src/html/player-script.js |
브라우저 스크립트(src/player/)를 한 글로 잇고 그림 틀 마크업을 만든다 |
src/player/sample.js |
순수 표본 추출기. 시각을 주면 그 순간의 모습을 돌려준다(DOM, 시계, 난수 없음) |
src/player/stage.js, effects.js, values.js |
모습을 그림 요소에 쓴다. 이전 프레임에서 사건을 읽지 않는다 |
src/player/play.js |
시작, 시계, 장면 들어가기와 탭 선택, 가려짐, 움직임 줄이기 |
src/player/controls.js |
백틱으로 나눈 글을 글자·코드 DOM 노드로 만든다 |
src/player/view.js, responsive.js |
전체화면, 확대, 좁은 배치 바꾸기 |
src/player/export.js |
공통 도구 막대의 문법 복사와 정본 템플릿 HTML 다운로드 연결 |
읽는 데이터
figurePlay(root, data)의 data는 html/content.js figureContent가 만든다. 모양을 정하는 쪽은 컴파일러(시간표)이고 재생기는 아래 필드를 그대로 읽는다.
| 필드 | 내용 |
|---|---|
steps[i] |
{ label, mode, speed }. 키가 다르거나 mode가 static, once, loop가 아니거나 speed가 양의 유한수가 아니면 steps[i]: ... 오류다. 글 장면이나 기본값은 받지 않는다 |
segs[k] |
{ si, t0, t1, hops, cards, cardsBefore, cardsAt, charts, status?, pulses? }. 도형 키는 논리 id다 |
marks |
timeline.marks. 조용한 선이 보이는 구간(quiet:번호)의 목록이고 구간은 [시작, 끝, 장면] 세 쌍이다. 같은 장면 안에서만 합치고 길이 0 구간도 버리지 않는다. 재생기는 자기 장면의 구간만 읽고, 세 쌍이 아니면 marks의 구간이 정본이 아니다 오류로 끝난다. edge:를 포함해 이 밖의 키는 오류다. 켜 둔 도형, 부분, 차트 행은 구간(seg.nodesOn, partsOn, charts[id].lights)이 가진다 |
presentation |
장면마다 표시 길이 D(화면 ms). 0 이상의 유한수가 아니면 오류다 |
pulses |
timeline.pulses. 값(value:번호)과 차트 표식(chart:차트:표식 id)의 펄스 시각 |
values[vi] |
{ si, periods }. 값 줄의 기간. 장면이 없는 문서의 선언한 값 줄은 si가 없고 길이가 0이다 |
charts, chartFrames, chartMeta |
차트 카드별 장면 행(배열), 프레임 묶음, 계열과 행 이름 목록 |
itemIds, groupIds, cardCounts, edgePanels, trackPanels, panels |
도형 번호 → 논리 id, 선과 흐름 길이 속한 판, 판 상자 |
metrics |
move, pulse, 크기 값과 차트 표식이 따라갈 속성(markGeometry). 흐름이 있으면 tones, toneInks, toneOutlines(src/tone.js)도 싣는다. pulse는 { riseMs, holdMs, decayMs, fadeMs }이고 fadeMs는 세 시간의 합이다 |
- 펄스 시간은
duration.effect-rise,effect-hold,effect-decay(80, 80, 240ms)가 주인이다. 재생기에 상수를 두지 않는다. - 읽는 것: 켜진 구간(
marks), 값과 차트 프레임이 바뀐 시각의 펄스(pulses), 박자의 도착 펄스(seg.pulses), 값 기간, 차트 기간과 프레임, 카드가 바뀌는 시각, 점의 이동(hops). 구하는 것: 점이 선에 올라 있는 구간과 고정 알약의 줄어듦(이동 목록과 이동 곡선에서), 펄스 세기(시작 시각과 효과 곡선에서), 표시 길이와 반복 꼬리. 이것들은 사람이 보는 표시 시각의 효과라 시간표의 논리 시각을 바꾸지 않는다. - 점이 선에 올라 있는 구간은 흐름 이동의
hop.legEdges와hop.gaps로 구간별로 나눈다. 박자 이동은 정수edge하나다. 이 모양이 아니거나ms가 0 이하이거나 모르는hop.tone, 모르는marks키가 있으면 재생기는 대체값 없이 오류로 끝난다.
시각과 모습
시계는 장면에 들어선 뒤 흐른 표시 시각 하나(clock.elapsed)다. 시작 기준 프레임 시각에서 바로 재므로 프레임 사이에 시간을 자르거나 버리지 않고 어긋남이 쌓이지 않는다. 들어올 때와 다시 보일 때는 다음 프레임이 기준이 된다.
sampleScene(scene, data, elapsed, isSettled)은 같은 시각이면 앞서 어떤 시각을 샘플했든(앞으로, 뒤로, 건너뛰어, 무작위 순서로) 같은 모습을 돌려준다. 이전 프레임의 상태, 웹 애니메이션 끝 알림, 요소 충돌에서 사건을 읽지 않는다.
- 마지막 모습에는 점, 펄스, 알약 색이 없다. 켜진 구간은 장면 끝 바로 앞의 것을 읽고, 끝 시각에서 시작하는 다음 장면의 구간은 보지 않는다. 길이 0과 정확히 끝 시각은 자기 장면이 소유한 구간으로 읽으며 경계 여유나 시간 덧붙임은 없다. 그 장면의 지속하는
light는 마지막 모습에 남는다. - 문서가 가려지면(
visibilitychange,document.hidden) 시계를 얼리고(clock.isPlaying이 꺼지고clock.wantsPlay는 남는다), 다시 보이면 멈춘elapsed에서 이어서 재생한다. 가려진 동안 흐른 시간은 장면 시간에 더하지 않는다. 이것은 문서가 가려질 때만의 일이고, 그림이 화면 어디에 있는지는 보지 않는다. 그림이 보일 때까지 재생을 기다리거나 붙들거나 화면을 옮기지 않는다. - 움직임 줄이기(
prefers-reduced-motion)가 켜진 채 장면에 들어가면 방식과 상관없이 고른 장면의 마지막 모습만 보인다. 재생 중에 켜지면 반복 장면도 바로 마지막 모습으로 바꾸고 시계를 멈춘다. 꺼져도 저절로 재생하지 않고, 다음에 장면에 들어갈 때 그 방식을 따른다. - 상태를 정하는 CSS 전환은 없다. 같은 장면, 같은 시각의 모습은 실제로 흐른 시간이나 앞서 본 시각에 따르지 않고, 시계를 옮긴 바로 뒤의 계산된 스타일이 곧 그 모습이다. 카드 내용 층은 한 카드에 보이는 층이 최대 하나이고, 숨은 층은
visibility: hidden이라 화면과 접근성 트리가 같다. 전환이 남은 것은 도구 막대의 나타남(opacity)뿐이고, 움직임 줄이기에서는 그것도 없다. - 전체 화면 열고 닫기와 폭 전환(좁은 배치로 그림을 바꿀 때)은 장면, 절대
elapsed, 마지막 모습 여부를 그대로 두고 새 그림에 지금 시각의 모습을 다시 그린다. - 점은 경로 위를 출발과 도착이 느린 곡선(토큰
ease.move)으로 지난다. 점을 만들면 같은 프레임에 경로 시작점에 둔다.
선과 고정 알약
- 선은 점이 하나 이상 올라 있는 동안
is-current다. 한 점이 나가는 시각에 다른 점이 들어서면(같은 물리 선의 역방향 포함) 구간이 이어져 꺼지는 프레임이 없다. 여러 선을 지나는 점은 도형 안을 지나는 구간에서 어느 선에도 올라 있지 않고, 사라지는 점은cut시각에 선을 떠난다. - 보통 고정 알약은 어느 프레임에서도 투명해지지 않는다(불투명도 1). 점이 올라 있는 동안 알약의 활성 색(
--pill-tint)이 1이고(켜질 때 오르는 구간은 없다), 점이 떠난 뒤fadeMs동안 1에서 0으로 줄어 중립 색으로 돌아온다. 불투명도는 건드리지 않는다.fadeMs는 표시 ms라speed로 나누지 않는다. 같은 세기를 알약 묶음과 선 묶음(관계 끝 글edgelabel이 선 묶음 안에 있다)이 읽는다. - 선이 활성인 구간은
[시작, 끝)이고 이동 목록(hop.legEdges,gaps,cut)에서 구해 재생기와 움직이는 SVG가 같은 규칙으로 읽는다. 점이 지나간 선을 켜 둔 채 남기는 목록은 없다. 마지막 모습에서는 모든 선이 중립이고 알약의 활성 색이 0이다(알약 자체는 보인다). 움직이는 SVG도 점이 떠난 선의 색, 알약, 알약 글자, 관계 끝 글의 마지막 keyframe(100%)과 마지막 모습 층이 평소 모습이다. 조용한 알약(quiet)의 보임은marks의 조용한 선 구간이거나 활성 색이 남은 동안(on)이 정하고, 움직이는 SVG에서 보임만 처음 지난 때부터 한 바퀴 끝까지 이어진다. 이 보임은 그 장면이 소유한 구간이라 다른 장면의 마지막 모습에 새지 않는다.
도착 펄스
- 점이 도형에 닿으면 그 도형의 기존 윤곽(
.fl-stroke) 위에 같은 모양의 겹침 선(.fl-pulse) 하나를 두고 그 불투명도만 펄스 세기(올라가는 80, 유지 80, 내려오는 240 표시 ms)로 쓴다. 도형 면, 윤곽 굵기, 상자 크기는 바뀌지 않는다. 윤곽이 여럿인 도형(저장소)은 모두 복제한다. 도형이 가만히 숨 쉬거나 움직이는 효과는 없다. - 키가 같은 펄스가 겹치면 더하지 않고 가장 센 하나만 쓴다. 키는
node:논리 id(흐름이나 이동이 닿은 도형),value:값 줄 번호,chart:차트 id:표식 id다. 펄스는 논리 사건 하나이고 같은 카드가 여러 판에 그려지면 모든 그림에 같은 세기로 쓴다. - 도착 후광은 도형의
tone과 관계없이 공통 UI 강조 색(--fx-edge)과 얇은 경계 굵기 토큰을 쓴다. 도형의 평소 면과 경계는tone과appearance가 정한다. - 도착 펄스는 시간표의 도착 목록(
seg.pulses의{ id, at })이 정한다. 흐름 구간과 박자 구간이 같은 목록을 갖고, 사라진 점은 목록에 없다. 도착이 면을 칠하지는 않는다. 면은light가 켠 도형만 칠한다. - 값이 같은 순간에 1 → 2 → 1처럼 돌아오면 변화가 아니다. 이 판단은 시간표가 하고(
timeline.pulses에 넣지 않는다) 재생기는 목록을 그대로 쓴다.
값과 차트 펄스
- 값이 바뀌는 순간에는 바뀐 값의 줄(카드 값 줄, 큐 도형)에 배경 면(
.fl-flash)이 같은 펄스 세기로 켜진다. 큐는 도형과 같은 윤곽(outlineOf)을 쓰고 카드 줄은 줄 모양이며, 둘 다 테두리가 없어 기본 경계 위에 두 번째 윤곽이 생기지 않는다. 같은 시각에 쓴 값은 마지막 글만 보이고 시작 글과 같으면 펄스가 없다. 움직임 줄이기에서는 면을 그리지 않고 값 글자가 바로 바뀐다. - 갱신 효과는 값 줄, 보이는 글이 바뀐 카드 글 줄, 차트 표식에 있다.
show와clear는 장면 구성이라 그 박자 시각에 바뀌고, 그때 이전 내용에 없던 글 줄(태그, 글, 덧붙임, 표시를 이어 붙인 보이는 글로 맞추고, 같은 글이 둘이면 개수까지 맞춘다. 관계 그래프 줄은 그래프 모양 전체가 글이다)은 값 줄과 같은 배경 면을 같은 펄스 세기로 켠다(timeline.rowPulses, 키row:도형:줄 번호). 같은 장면, 같은 도형, 같은 시각의 바뀜은 첫 이전 내용과 마지막 이후 내용만 맞대므로 지웠다 다시 쓴 줄은 효과가 없다. 지운 줄과 값 줄(값의 펄스가 맡는다)은 이 효과가 없고 카드 전체를 깜빡이지 않는다. - 차트 프레임이 바뀔 때는 달라진 표만 속성과 글을 쓰고, 바뀐 표식에만 같은 펄스를 건다. 효과는 표식 자신의 면과 선을 바꾸지 않고, 표식마다 미리 넣어 둔 겹침(
src/chart/pulse-overlay.js)의 불투명도로 낸다. 겹침은 표식이 싣고 온 계열의 effect 단계(data-effect, 그 계열 테두리를 같은 색상에서 밝힌 값)만 쓴다. 모양은 같은 색의 옅은 면(opacity.halo)과 표식 자신의 테두리 굵기인 고리이고, 면이 없는 선과 속이 빈 모양은 선만 덮으며, 글은 effect 윤곽 후광이다. 그래서 도착 펄스의 상태 파랑이나 회색으로 통일하지 않는다. 값 글자는 글 뒤 배경 사각형(.b)이 펄스를 받는다. 움직이는 SVG와 HTML이 같은 겹침을 쓴다. - 장면이 시작하는 시각(t0)에 보이는 값은
set=이나keep이 정한 시작 값의 프레임이고, 그 시각에 펄스를 켜지 않는다. 바뀐 표식을 가르는 기준도 문서의 0번 프레임이 아니라 이 시작 프레임이며, 장면 안의 실제 변화가 그 기준과 달라지게 한 원자료만 강조한다. - 이 장면에 시간표 행이 없는 차트는 장면에 들어서는 순간 처음 프레임(0번)이다. 앞 장면의 마지막 프레임이 남지 않는다. 값에 묶인 차트는 프레임 목록으로 바뀌고, 보일 프레임과 지금 그려진 프레임을 표식 단위로 견주어 달라진 표식만 쓰고, 어느 프레임에서든 쓰인 속성 가운데 보일 프레임에 없는 것은 지운다. 그래서 프레임을 건너뛰거나 되돌려도 같은 결과다. 차트의 CSS 애니메이션(자라기)은 늘 멈춰 두고
currentTime을 모습의 시각으로 쓴다.
판과 여러 곳에 그려진 카드
그림은 장면 하나로 한 번 그리고 판(scene.panels)마다 SVG 한 장으로 나눈다(html/content.js splitPanels). 요소 번호(#n-번호, #e-번호)는 문서 전체에서 하나뿐이다. 같은 논리 카드가 둘 이상의 판에 그려지면 도형 번호는 다르고 data-id가 같다. 재생기는 논리 id로 모든 그림을 찾아 같은 카드, 켜짐, 펄스, 값 글자, 차트 프레임, 상태 알약으로 맞춘다. 값과 차트 쓰기는 논리 사건 하나이고 판마다 중복해서 쓰지 않는다. 점은 그 선이 있는 판의 점 층(.fl-packets)에 놓인다. 차트 카드는 data-chart="카드 id"로 찾고, 한 차트의 움직임이 다른 차트로 번지지 않는다.
패턴 정의(<defs>)는 차트 무늬를 내용 해시 id로 한 번만 두고(patternDefs), HTML에서는 숨은 .dp-defs SVG에 모아 판들이 참조한다. 한 id에 본문이 둘이면 오류다.
화면 배치와 좁은 화면
- 판 묶음(
.dp-panels)은 위에서 아래로 쌓이고 판마다 구역(.dp-panel)이 있다. 보기 폭(--view-w)은 가장 넓은 판의 폭이고, 묶음은 이 자연 폭을 유지한다. 판의 표시 폭은 보기 폭에 대한 판 상자 폭(--panel-w)의 비율이므로 판과 글자는 원래 크기 그대로다. 좁은 배치로 바뀌어도 남는 가로 부분은 그림 영역(.fl-canvas)에서 네이티브 스크롤로 읽으며 페이지와 조작부는 가로로 넘치지 않는다. 작은 그림은 가운데에 놓인다. 좁은 배치를 고르는 기준은 가장 넓은 판의 상자 폭(box.w)이다. - 그래프 판, 차트 판(
plot), 시간 판(time) 가운데 하나가 컨테이너 폭(COMPACT_WIDTH, 현재 272)보다 넓으면 좁은 배치(reflowFigure)를 함께 싣는다. 컨테이너가 공통 모바일 분기점(breakpoint.mobile)보다 좁고 원래 가장 넓은 판의 폭보다 작을 때만 판 묶음을 좁은 배치로 바꾼다. 일반 본문 폭에서는 넓은 배치의 자연 크기를 유지한다. 배치를 바꾸면 새 데이터로 장면 모델을 다시 만든다. 장면, 사건 시각, 값 기간, 차트 프레임 순서는 그대로이고 판의 기하만 다르다. 차트는 폭을 컨테이너 이하로 다시 그리고 글자 크기는 줄이지 않는다(차트). 시간 판은 좁은 목표 폭으로 다시 만들고, 순서 판은 다시 만들지 않고 자연 크기를 유지한다. 좁은 배치가 어느 판도 좁히지 못하면 싣지 않는다. 좁은 배치는 HTML을 만들 때 한 번 만들고 같은 그림 검사를 받는다. 넓은 배치가 통과한 원본도 좁은 폭에서 글이 칸에 들지 않으면 HTML 만들기가 줄 번호 있는 오류로 끝난다. 브라우저는 미리 만든 좁은 배치로 바꾸기만 하고 배치를 다시 계산하지 않는다. - 표시 폭 규칙은 문서 미리보기, 목록 카드, 재생기가 같다. 컨테이너 폭은 화면 폭에서 좌우 바깥 여백(
spacing.6)을 둘 뺀 값이고, 그림 표시 폭은min(표준 캔버스 폭 spacing.figure.figure-canvas, 컨테이너 폭)이다. 컨테이너는 그림 둘레에 좌우 여백을 더하지 않는다. - 뷰어는 시간표와 원본을 보이기만 하는 순수 시각화기다. 자동 스크롤, 첫 변화로 화면 옮기기, 그림이 화면 안에 보일 때까지 기다리거나 붙들기, 그림 위에 겹쳐 뜨는 고정 탭, 수동 복사 팝업을 두지 않고 새 화면 이동도 더하지 않는다. 사용자 조작은 장면 탭, 문법 복사, HTML 다운로드, 전체화면(과 그 안의 확대)뿐이고, 재생 설정을 새로 만들지 않는다. 원본의
mode(static,once,loop)와speed가 재생을 정하는 전부다. - 장면 탭은 장면이 둘 이상일 때만 그림 아래 가운데에 놓이고(좁으면 줄바꿈, 긴 이름도 탭 안에서 줄바꿈), 하나뿐이거나 장면이 없으면 숨는다. 그림 다음에 놓고 그림 위에 겹쳐 띄우거나 화면에 붙여 고정(sticky)하지 않으며, 탭을 눌러도 자동 스크롤하지 않는다. 전체 화면에서는 탭 줄의 실제 높이를 뺀 영역에 그림 전체를 맞춘다. 장면 탭은 홈페이지와 같은 공통 탭 크기를 사용하고, 도구 막대 단추는 공통 도구 막대의 44px 영역을 사용한다.
- 탭은 장면 선택만 맡는다. 진행 요소, 끝난 탭 표시, 단계 번호를 두지 않는다. 방향키와 Home, End로 장면을 고르고 초점도 선택을 따르며, 선택한 탭만 Tab 키 순서에 들어간다. 선택한 탭은 공통
TabList의 파란 면·흰 글자·그림자를 사용한다. 색과 크기, 호버·초점·전환은 공통 탭 토큰과styles/tabs.css가 소유한다. - 그림 틀의 DOM은 하나다. 단독 재생기, 일반 보기, 삽입된 재생기, 전체 화면이 같은 틀과 같은 장면 탭, 같은 도구 막대 한 벌을 쓰고 보기마다 다른 사본을 만들지 않는다. 단독 재생기와 삽입된 재생기 모두 판의 자연 폭에서 SVG 높이가 비율대로 따라오는 자연 높이를 갖고 보기 창 높이로 제한하지 않는다. 장면 탭은 그림 전체 아래에 놓이고 문서가 세로로 스크롤한다. 전체 화면과 가로로 넘치는 일반 캔버스는 키보드 초점(
tabindex=0)을 받아 방향키로 이동한다. 캔버스의 접근 가능한 이름은 현재 장면 탭이고, 장면이 없으면그림이다. - 본문이나 목록 카드에 iframe으로 삽입된 재생기(
html.embedded)는 바깥 여백과 호스트 바탕 없이 둘러싼 면 위에 놓이고, 틀이 카드 너비를 채우며 자연 높이를 바깥 쪽에 알리기만 한다. 바깥에서 받은 높이가 다시 그림의 크기를 정하는 되먹임은 없다. 보기 영역은 그림 둘레 안쪽 여백(spacing.7)을spacing.1만 남기고 걷은tight(viewBox)이며 그림은 자연 크기를 유지하고 넘치는 부분은 캔버스 안에서 가로로 스크롤한다. 전체 화면을 위해 iframe에는allow="fullscreen"이 필요하다. 삽입된 재생기의tight보기는 판이 하나인 그림에만 적용한다. - 검증 범위.
npm test는 브라우저를 열지 않는다.node --test test/browser/narrow-view.test.js는 설치된 Chrome에서 architecture와 sequence의 모든 장면을 폭 320·390·430·1440의 독립 HTML과 공통 호스트에 삽입된 iframe으로 확인한다. 자연 글자 크기, 페이지 가로 넘침 없음, 캔버스 세로 잘림 없음, 키보드 가로 스크롤과 오른쪽 끝 접근, iframe 높이 통신과 컨테이너 폭 변경, 전체화면 확대·맞춤·초점 복귀를 확인한다. 이 검사는 모든 예제의 의미·색·움직임이나 시각적 읽힘을 보증하지 않는다. 자연 크기로 그린 넓고 긴 그림은 페이지가 길어지고 수평 이동이 필요하다. 실제 화면 검토 범위는 표현 범위의 표가 정하며 실제 휴대폰과 스크린 리더 검수는 별도 요구사항이다. 자동 반복 장면에 일시정지 조작이 없으므로 접근성 기준 전체를 충족한다고 주장하지 않는다.
도구 막대와 전체 화면
장면 탭은 홈페이지의 TabList와 같은 시스템 글꼴, 버튼 상자·안쪽 여백, 패널 뒤 간격을 쓴다. 재생기 푸터에서 별도 여백을 더하지 않는다. 본문 삽입에서는 자연 높이를 부모에게 알려 세로로는 페이지 스크롤을 쓰고, 넘치는 가로 부분은 캔버스 안에서 이동한다.
- 도구 막대는 모든 그림이 같다. 단독 재생기, 갤러리에 넣은 재생기, 문서에 삽입한 재생기, 내려받은 HTML이 같은 한 가지 조작부를 쓰고 그림마다 원본에서 켜거나 끄는 선택 사항은 없다. 그림 면 안쪽 오른쪽 위에 공통
ToolHeader와Toolbar를 조립해 왼쪽부터 문법 복사, HTML 다운로드, 전체화면 순서로 놓고 전체화면이 맨 오른쪽이다. 전체화면에서는 확대, 축소, 전체 보기가 그 아래에 붙는다. 도구 막대는 그림 틀 안에 한 벌뿐이라 일반 보기, 삽입, 전체화면이 같은 단추를 쓰고 따로 만들어 복제하지 않는다. 코드 블록도 같은 버튼·아이콘·호버·초점·복사 결과 알림을 사용한다. ThinkFlow는 원문과 HTML 다운로드 작업만 공통bindToolbar에 넘긴다. 각 단추에 접근성 이름과 툴팁이 있고 아이콘 옆에 글자를 반복하지 않으며, 표시 명칭은 “문법 복사”, “HTML 다운로드”, “전체화면” 하나씩이다. 마우스에서는 그림에 올리거나 키보드 초점이 들어올 때(:focus-visible)duration.fast동안 서서히 나타나고(움직임 줄이기에서는 전환 없음), 터치와 거친 포인터(hover: none,pointer: coarse)에서는 늘 보인다. 입력 방식과 관계없이 블록 안의 머리글이 막대 높이를 차지하므로 좁은 마우스 창에서도 첫 카드를 가리지 않으며, 막대가 나타날 때 그림이 움직이지 않는다. - 재생을 대신하는 단추는 없다. 재생, 일시정지, 배속, 반복 단추, 진행선, 단계 번호 표시, 그림 아래 설명 글을 두지 않고 스페이스나 그림을 누르는 손짓으로 멈추는 숨은 조작도 없다.
- 조작 아이콘은 공통 카탈로그의
controls역할을@woonyong-choi/design-tokens/ui/icons.mjs의ControlIcon으로 그린다. 원본 도형의 격자와 비율을 보존하고 크기는 공통 조작 토큰을 따른다. 중립색을 쓰고 호버와 눌림은 단추 면의 변화, 키보드 초점은 외곽 표시로 구분한다. 도형 안 의미 아이콘의 역할색을 쓰지 않는다. - 전체 화면은 브라우저 전체 화면을 쓰고, 쓸 수 없으면 같은 재생기를 창에 고정해 덮는 모양으로 대신한다. 어느 쪽이든 장면, 시간, 값, 재생 상태는 그대로이고 되감기지 않는다. 닫기와 Esc는 보기만 닫고 재생에 영향이 없다. 닫으면 전체 화면에 들어가기 직전의 초점으로 돌아가며, 그 요소가 문서에서 사라졌으면 전체화면 단추로 돌아간다. 처음 재생기를 초기화할 때는 초점을 옮기지 않는다. 갤러리의 iframe도 부모 페이지에 요청해 창 전체로 확장하고 닫으면 원래 카드 높이와 페이지 스크롤을 복원한다.
- 확대, 축소, 끌어 옮기기는 전체 화면에서만 켠다. 배율은 1배에서 토큰
scale.zoom-max배 사이이고 휠 확대는 커서 아래 그림 가로 자리를 고정한다. 문서 안에서는 그림 전체가 자연 크기를 유지하고 남는 가로 부분은 스크롤한다. 전체 화면은 판 묶음 전체를 영역에 맞춘 뒤(1배) 읽기 위한 확대는 사용자가 한다. 전체 화면 1배의 복잡한 그림은 글자가 작을 수 있으며 수동 확대로 읽는다. - 전체 화면의 맞춤 폭은 판 SVG의 viewBox에서 읽은 묶음의 실제 좌표 크기(폭은 가장 넓은 판, 높이는 판 높이의 합)로 잰다:
min(영역 폭, 영역 높이 × 묶음 폭 / 묶음 높이). 문서 안의 보기 폭(--view-w, 가장 넓은 판의 폭이라 같은 값이다)이나 표준 캔버스 폭 960을 분모로 쓰지 않아 줄어드는 비율은 한 번뿐이고, 판의 상대 크기는 문서 안과 같다. 같은 폭을--bundle-w로 판 폭의 분모에 쓰고 전체 화면을 닫으면 지운다. 맞춤은 그림 전체가 영역에 들어가게 하는 값이라 작은 그림은 영역 폭이나 높이 한쪽까지 커질 수 있고, 키가 큰 복잡한 판은 영역에 맞게 줄어든다. 창 크기를 바꾸거나 좁은 배치로 바뀌면 다시 맞춘다. - 도형이나 카드에 마우스를 올려도 선이나 카드가 켜지지 않는다. 장식용 호버 활성은 없다.
문법 복사
- 복사하는 글은
.thinkflow원본이다.buildFigure가 받은 원본 글 그대로(줄바꿈이 CRLF여도, 첫 글자부터 마지막 글자까지 고치거나 정리하지 않고)이고 문서 모형의figure.source에서 온다. 모형을 다시 배치해도(reflowFigure) 같은 글이 유지된다. - HTML 소스나 이스케이프한 글을 복사하지 않는다. 클립보드에 쓰는 글은 원본 글자 그대로이고 두 번 이스케이프하지 않는다. 원본에
</script>나 따옴표, 마크업 글자가 있어도 문서 구조는 깨지지 않고 복사한 글은 원본과 같다. - 복사는 클립보드에 쓰는 일만 하고 어디로도 보내지 않는다. 클립보드를 쓸 수 없으면 성공으로 표시하지 않는다. 브라우저가 허용하는 대체 복사가 있으면 쓰고, 그것도 안 되면 단추의 상태 표시로 복사하지 못했음을 알리는 것으로 끝낸다. 글을 보여 주고 직접 복사하게 하는 팝업, 글 상자, 대화상자는 두지 않는다.
HTML 다운로드
문서 머리 <meta name="thinkflow-canonical" content="…"> 칸에 칸이 빈 정본 템플릿을 UTF-8 base64로 담고(toHtml), 내려받을 때 그 base64를 풀어 칸에 같은 base64를 다시 채운다(player/export.js). 그래서 파일이 자기 자신과 바이트까지 같고 다시 내려받아도 크기와 내용이 늘지 않으며, 모든 장면 설정, 글꼴, 아이콘이 들어 있어 서버나 바깥 자원 없이 열린다. 현재 장면, 시간, 확대, 전체 화면, 테마는 담기지 않고 열면 첫 장면에서 시작한다. 정본 안의 내용은 이미 이스케이프된 HTML이고 스크립트 글에 끼워 넣지 않으며, base64는 <, ", &를 담지 않아 속성 값을 벗어나지 않는다. 칸 머리글이 문서에 정확히 하나가 아니면 만들기가 실패한다. 칸 때문에 파일은 정본의 약 2.3배다. 갤러리 iframe처럼 샌드박스가 다운로드를 막은 곳에서는 내려받기가 동작하지 않는다.
움직이는 SVG
- SVG는 차트 카드나 추적의 시간 보기(
view time)가 있으면 공통 차트 기본 CSS를 포함한다. 레인·구간·시간축 글자와 축·격자는 같은 테마 토큰을 읽으며, 차트 움직임 CSS(chartMotionCss)는 차트 카드가 있을 때만 포함한다. 멈춘 SVG에도 같은 기본 CSS 포함 규칙을 적용한다. - 고른 장면 하나(
--scene, 생략하면 첫 장면)의 시간표 상태를 CSS keyframes와 SMIL로 옮긴다. 장면의mode가 재생 방식을 정한다.loop는repeatCount="indefinite",once는 한 번 재생하고 마지막 상태에 머물고(repeatCount="1" fill="freeze"),static은 움직임이 없고 마지막 상태 하나를 그린다. 저절로 다음 장면으로 넘어가지 않고, 그림 아래 설명 글과 장면 이름 줄은 없다. 움직이는 그림이 필요한 장면이 여럿이면 장면마다 SVG를 따로 만든다. - 켜짐과 꺼짐 순서가 같은 요소끼리 CSS keyframes 하나를 나눠 쓴다. 의미 상태(차트 계열 보임, 막대 흐림과 히트맵 굵기, 도형과 그룹 켜짐, 카드 내용 층, 조용한 선, 선 강조)는 논리 경계 시각에 HTML 재생기와 움직이는 SVG 모두 바로 바뀌고, 상태에 CSS 전환을 걸지 않는다. 같은 시각에 두 표면의 모습이 같아야 한다. 켜짐과 평소의 색은
figure.css의 효과 한 벌(--fx-*, 배치)이 정하고, keyframes는fill: var(--fx-face-on)처럼 속성 이름만 쓰며 토큰 값을 다시 적지 않는다. 출력이 같은 정의를 읽는지와 움직이는 SVG의 선, 알약, 조용한 선이 재생기와 같은 시각에 평소로 돌아오거나 남는지는test/component-state.test.js가 보고, 계산된 색이 화면에서 같은지는 시험이 없다. 서서히 가는 것은 렌더러가 시간을 명시한 효과뿐이다: 도착·값·차트 펄스(올라감 80ms, 유지 80ms, 내려옴 240ms), 고정 알약의 활성 색 꼬리(fade로 직접 적은 시간. 켜질 때는 바로 켜지고, 꺼질 때 400ms는 표시 ms라 구간의 논리 ms에speed를 곱해 놓는다), 선 차트 점이 나타나는duration.fast, 장면 끝에서 잘리는 점의 사라짐(duration.cut-fade). 한 요소에 켜짐 class가 둘 이상 걸리면animation속성 하나에 쌓아 뒤의 class가 앞의 보임을 덮지 않게 한다. 이 경계 일치를 확인하는 자동 시험은 지금 없고(검증 요구사항, 미완료), 브라우저에서 시계를 같은 시각에 고정해 불투명도·보임·칠·굵기를 견주는 검수가 필요하다. 재생기가 프레임 단위로 읽는 시각이라 경계와 정확히 같은 프레임은 잴 수 없다. - 점은 SMIL
animateMotion으로 경로를 지난다. 곡선은keySplines에 넣은 토큰ease.move라서 HTML 재생기와 같다. 이동 전에는 선의 시작에, 이동 뒤에는 끝에 머문다. - 그리는 순서는 도형, 선, 점과 글 상자(
.fl-packets), 선 라벨 알약(.fl-pills) 순이다. 알약이 점 위에 그려져 점이 알약 뒤로 지나가므로, 점이 알약 글자 위를 지나도 글자 대비는 알약 면과의 값(4.5 이상)이다. 알약 묶음은 선과 같은 class를 달아 선과 함께 켜지고 조용해진다. - 점의 보임(
opacity, 이산 값)과 이동과 글 상자 옮김과 흐려짐은 모두 SMIL이라 한 시계로 돈다. 보임을 CSS keyframes에 두면 CSS와 SMIL 시계가 따로 반복해, 한 바퀴가 돌아올 때 점이 이전 바퀴의 끝 지점에 잠깐 보였다가 시작 지점으로 뛴다. 보임 창과 이동 구간은 같은 keyTimes 값을 쓴다. 선, 도형, 카드의 켜짐은 점과 위치를 다투지 않아 CSS keyframes로 남긴다. - 한 바퀴 길이(
dur)는 시간표의 표시 길이D(presentation)다. 재생기와 같은 값을 읽는다. keyTimes는 한 바퀴를 1로 본 비율로 소수 다섯째 자리까지 쓰고 값은 늘어나기만 한다. - 움직이는 SVG에는 스크립트가 없다. 그래서 HTML 재생기와 달리 문서가 가려질 때 시계를 얼린다고 약속하지 않고, 가려진 탭이나 화면 밖
<img>에서 SMIL 시계가 멈추는지는 브라우저가 정한다.once장면은 시계가 도는 동안 마지막 상태에서 끝난다. - 움직이는 SVG는 움직임 층(
.fl-motion)과 스크립트 없는 마지막 모습 층(.fl-still)을 함께 싣는다. 마지막 모습 층은 정지 장면과 같은 마지막 상태(남은light포함)이고 기본으로는 숨어 있다. 움직임 줄이기(prefers-reduced-motion: reduce)에서는 CSS 미디어 질의가 움직임 층을 숨기고 마지막 모습 층을 보인다. 정지 장면과 표시 길이가 0인 장면은 층이 하나뿐이다. - 이 미디어 질의는 SVG를 문서에 직접(인라인) 넣거나 SVG 파일을 직접 열 때 동작한다.
<img>로 넣은 SVG는 Chrome이 SVG 이미지 안의prefers-reduced-motion을 평가하지 않아, 움직임 줄이기에서도 처음 모습이 마지막 모습이 아니고 움직임이 그대로 돈다. 이 한계는 스크립트 없이 고칠 수 없다. 움직임 줄이기를 지켜야 하는 곳에는<picture>의media소스에--static으로 만든 멈춘 SVG를 두는 방법이 있다. WebKit과 Firefox의<img>는 확인하지 않았다. - 처음에 보이지 않는 글자 변형(값 글자, 큐 찬 칸, 상태 알약, 카드 내용 층, 차트 글 변형)은 속성으로
visibility="hidden"을 갖고, 보이는 창에서만visibility와 불투명도를 SMIL로 같이 바꾼다. 그래서 한 시각에 한 변형만 화면과 접근성 트리에 있다. 프레임마다 달라지는aria-label은 SMIL로 바꾸지 못하므로 요소에서 빼고, 묶인 표식(원·도넛 조각)은<title>을 두지 않는다. 바뀌지 않는 값처럼 키가 하나뿐인 이산 애니메이션은 한 바퀴 끝에 같은 값을 한 번 더 적어(keyTimes="0;1"), 키 하나짜리 이산 애니메이션을 적용하지 않는 WebKit에서도 보이게 한다. - 글 상자는 시간표의
chipPath를 그대로 건다. 이동 곡선을 거꾸로 풀어 점이 그 지점에 닿는 시각을 구한 뒤 SMILanimateTransform(옮김)과animate(불투명도)로 그 사이를 잇는다. 모든 지점이 점 위 기본 자리면 옮김을, 모두 불투명이면 불투명도를 넣지 않는다. - 도착 펄스, 값 펄스, 차트 펄스는 HTML과 같은 세기 곡선(
envelopeKeys)과 같은 겹침 요소를 쓴다. 차트 모양을 바꾸는 움직임(공통chartMotionCss·CHART_GROWTH)도 같은 토큰을 쓴다. - 시간 흐름이 없는 그림(장면 0개), 정지 장면, 표시 길이가 0인 장면은 움직이지 않고 마지막 상태 하나를 그린다.
멈춘 SVG
- 멈춘 SVG는 고른 장면의 마지막 상태 하나다. 장면의
mode와 상관없이--static이 정지로 만든다. 값 글자, 카드, 차트 계열, 상태 알약이 마지막 상태이고 점, 글 상자, 펄스, 줄어드는 알약은 없다. 그 장면의 지속하는light는 마지막 상태라서 남는다. 차트 행의 흐림(면opacity.dim, 글자opacity.dim-ink)과 히트맵의 굵기를 움직임 없이 적은 선언(opacity: …;,font-weight: …;)으로 쓰고, 켜진 선언만 적어서light가 없는 그림은 한 바이트도 늘지 않는다. 움직임이 없으므로 애니메이션 요소(animate,set,animateMotion)와animation:스타일이 없다. - 장면이 없는 문서의 멈춘 SVG는 선언한 처음 값을 그린다(장면과 재생 방식). 카드의 값 줄은
from값, 큐는from칸 수다. 장면이 있는 문서의 멈춘 SVG는 고른 장면의 마지막 값과 칸 수를 그리고from을 그리지 않는다.
결과 파일
| 명령 | 쓰는 파일 |
|---|---|
render 원본.thinkflow |
움직이는 SVG {이름}.svg(첫 장면, `–scene n |
render 원본.thinkflow --static |
멈춘 SVG {이름}.svg |
render 원본.thinkflow --html |
{이름}.svg와 HTML 재생기 {이름}.html. --static과 함께 주면 SVG가 멈춘 SVG다 |
md 문서 |
마크다운 반영과 배포가 정한다. 블록마다 SVG {문서 이름}-{이름 또는 순번}.svg와 문서의 이미지 줄 |
render, check, gallery, md에 --budget 이름=값 |
생성 예산을 올린다(여러 번 쓸 수 있다). 이름이 없거나 값이 양의 안전한 정수가 아니면 사용법 오류(종료 2)다. 초과한 그림은 파일을 쓰지 않는다(칸 격자) |
gallery 폴더 |
폴더 안 .thinkflow 원본마다 {이름}.svg와 {이름}.html, 목록 쪽 index.html, 문서 미리보기 document.html. 원본이 하나도 없거나 하나라도 오류면 전체가 실패(종료 1)이고 아무 파일도 쓰지 않는다 |
--scene n|이름의 번호는 1부터 센다. 이름이 번호처럼 숫자만으로 된 장면은 문법이 받지 않으므로(scene "2"는 오류) 숫자는 늘 번호이고 이름과 겹치지 않는다. 장면 수를 넘는 번호, 없는 이름, 장면이 없는 원본에 준--scene은 값이 무엇이든--scene: ...오류로 종료 1이고 아무것도 쓰지 않는다(--scene을 주지 않으면 정지 그림 하나를 쓴다). 오류 글은 사용자가 쓴 값과 장면 목록을 말한다(no scene "9". Scenes: 1 "첫째", 2 "둘째", 장면이 없으면This figure has no scenes). 이 오류는 다른 원본 오류와 같은 진단 한 줄이다: 표준 오류에파일:줄: --scene: ...이고,--json이면 표준 출력에file과 codescene을 담은 JSON 한 줄이다.render는 원본 전체의 선택이라 줄이 없고(0), 원본마다 한 줄씩 낸다.md는 장면 없는 블록에 준--scene을 그 블록의 줄과 함께 같은 진단으로 거절한다.check는 파일을 쓰는render전용 선택 사항(--html,--static,--out,--scene)과--title을 받지 않고 사용법 오류(종료 2)로 거절한다.render도gallery전용인--title을 사용법 오류로 거절한다.- 값을 받는 선택 사항(
--out,--title,--out-dir,--fold-title,--scene)의 빈 값과 같은 선택 사항을 두 번 준 것은--out needs a value,--out is given twice같은 사용법 오류(종료 2)이고,--budget ""와 빈 파일 이름도 같다.--뒤의 인자는 모두 파일 이름이라-로 시작하는 이름도 읽는다(--없이는 선택 사항으로 읽어 오류다).gallery는 폴더 하나만 받아 여러 경로는gallery takes one folder, not N사용법 오류(종료 2)다. - 원본은
.thinkflow파일만 읽는다. 다른 확장자를render나check에 주면 일반 오류(unsupported-extension)이고 아무것도 쓰지 않는다. 폴더 안의 다른 파일은gallery가 원본으로 보지 않는다. - 한 번의 실행은 원본마다
{이름}.svg를 하나만 쓴다. 움직이는 SVG와 멈춘 SVG가 같은 이름을 다투지 않게 하기 위해서다. render는 쓰기 전에 모든 출력 경로({출력 폴더 또는 원본 폴더}/{이름}.svg,--html이면.html)를 견준다. 폴더의 실제 경로, 유니코드 정규화(NFC), 대소문자 무시로 같으면 겹친 것이고(같은 원본을 두 번 줘도 겹친다), 출력 파일이 이미 있는 심볼릭 링크이면 가리키는 실제 파일로 풀어 비교하므로 이름이 다른 링크 둘이 같은 파일을 가리켜도 겹친 것이다(md의 그림 링크도 같은 비교 한 함수를 쓰고, 링크 둘과 가리키는 파일은 그대로 둔다. 링크 하나만 쓰면 링크는 두고 가리키는 파일을 바꾼다),{파일} would be written twice: for A and for B. Rename one of the sources를 알리고 종료 1이며 아무 파일도 출력 폴더도 만들지 않는다.--json이면 코드output-collision한 줄이다.gallery도 같은 함수로 이름을 견준다.render와gallery는 쓸 파일을 모아 한 묶음으로 쓴다. 임시 파일에 쓴 뒤 이름을 바꾸고, 하나라도 실패하면 이미 바꾼 파일을 되돌린다. 경로는 모두 성공한 뒤에만 표준 출력에 알린다. 가리키는 곳이 없는 깨진 심볼릭 링크 자리는 파일로 몰래 바꾸지 않고io진단 한 줄과 종료 1로 거절하며 링크는 그대로다. 실패는파일: cannot write the file: EISDIR처럼 한 줄로 알리고 종료 1이며, 되돌리지 못한 파일은 따로 한 줄로 알린다. 이번 실행이 새로 만든 출력 폴더는 실패하면 치운다.render가 받은 여러 원본 가운데 오류가 있는 원본은 다른 원본의 쓰기를 막지 않는다.- 진단에 읽은 파일의 내용을 싣지 않는다. 차트
data파일을 읽지 못하면 코드(ENOENT)나not valid JSON과 위치만 알린다(차트). - HTML은 미리보기와 목록 쪽용이다. 문서 저장소에는 SVG만 넣는다(문서 스킬 연동).
목록 쪽
gallery명령은 HTML 결과를 iframe으로 모은index.html을 쓴다.- 갤러리의 예약 파일은 목록 쪽
index.html과 문서 미리보기document.html이고 이름은 그대로 둔다. 원본 이름이index나document(대소문자 무시)면 그 원본의 재생 화면만{이름}-player.html로 쓰고 목록 카드가 그 파일을 가리킨다. SVG는{이름}.svg로 겹치지 않아 그대로다. - 쓰기 전에 모든 출력 이름(
{이름}.svg, 재생 화면, 예약 파일 둘)을 대소문자 없이 겹치는지 확인한다. 겹치면{파일} would be written twice: for ... Rename one of the sources를 stderr에 알리고 종료 1이며 아무 파일도 쓰지 않아 출력 폴더의 기존 파일이 그대로다. - 원본 파일 이름은 링크에서 늘 상대 경로로만 쓰인다. 목록 쪽과 문서 미리보기의
href,src는 경로 조각마다encodeURIComponent(괄호도 인코딩)를 거친 뒤./를 앞에 붙이고 HTML 속성 이스케이프를 한다. 그래서javascript:...같은 이름이 URL 스킴으로 읽혀 실행되지 않고,#,?, 공백, 한글이 든 이름도 그 이름의 출력 파일을 연다. 마크다운 이미지 줄도 같은 함수(src/href.js)로 조각을 인코딩한다. gallery가 받는 선택 사항은--out,--title,--strict,--budget이고--html은 받기만 한다(늘 HTML을 쓴다). 그 밖의 선택 사항(--static,--json)은 사용법 오류(종료 2)다.--strict는 폴더의 모든 원본에 걸리고, 경고가 있는 원본이 하나라도 있으면 전체가 실패한다.- iframe 높이는 그림 쪽이 알린 자연 높이에 맞추고(그림은 그 높이를 되돌려 받지 않는다), 카드는 줄에서 가장 긴 카드 높이로 늘이지 않는다.
- 위쪽에 테마 단추 "시스템 / 라이트 / 다크"가 있다. 고르면 목록 쪽과 모든 iframe 그림이 그 모드로 바뀐다. 시스템은 루트에
color-scheme: light dark를 걸어 OS 설정을 따르고, 라이트와 다크는 루트에color-scheme을 그 값으로 걸고 목록 쪽 자체 색은 토큰 CSS의data-theme로 바꾼다. iframe 안 문서는 Chrome에서 부모의color-scheme을prefers-color-scheme에 안정적으로 받지 못해, 목록 쪽이 iframe에{ theme }메시지를 보내고 iframe 문서가 자기 루트의data-theme과color-scheme을 바꾼다. 새로 뜬 iframe은themeRequest로 현재 테마를 받는다. - 고른 값은
localStorage의thinkflow-theme에 기억하고 첫 그림 전에 적용한다. 저장한 값이 없으면 시스템 설정을 따른다. 단독 재생기 HTML에는 이 단추가 없고 OS 설정만 따른다. 그래서 목록에서 라이트를 골라도 거기서 따로 연 단독 재생 화면은 OS가 다크이면 어둡게 열린다(미해결).
문서 미리보기
document.html은 README처럼 흰(라이트) 또는 어두운(다크) 문서 바탕에 제목, 문단, 그림마다 움직이는 SVG를<img>로 넣은 쪽이다. GitHub README가 SVG를 넣는 방식과 같아, 회색 판이 문서 위에서 어떻게 보이는지 확인한다. 모든 그림이 같은 캔버스 폭이고max-width: 100%라 본문 폭(spacing.figure.document.column)에 맞춰 목록 카드와 재생기와 같은 폭으로 보인다. 그림 문단은 가운데 정렬이다.- 위쪽에 목록 쪽과 같은 테마 단추와 목록으로 가는 링크가 있고, 목록 쪽 위쪽에는 이 쪽으로 가는 링크가 있다. 고른 테마는 같은
localStorage값을 쓰고, 루트color-scheme이<img>로 넣은 SVG의prefers-color-scheme을 따른다.
요구사항
시험은 공개 진입점(buildFigure, toSvg, toHtml, 명령)과 실제 공통 부품의 진입점으로 계약을 확인한다. 탭과 도구 막대처럼 모든 그림이 같이 쓰는 요소의 부품 시험은 한 곳에 한 번만 두고 종류별로 되풀이하지 않는다. 그 부품 시험은 test/components.test.js(U9)다(시험 번호). 표는 시험이 맡은 계약의 범위를 보일 뿐 재생 전체를 확인했다는 근거가 아니다. 통과 수와 실행 결과는 이 문서가 아니라 CI와 GitHub 이슈의 기록이 맡는다. npm test는 브라우저를 열지 않으므로 시계, 탭 조작, 전체화면, 복사와 다운로드 같은 브라우저 동작은 실제 Chrome에서 하는 별도 검수의 몫이다. Chrome에서 어디까지 확인했는지는 표현 범위가 정하고, 거기 적히지 않은 동작은 검증 요구사항, 미완료다. 시험 칸이 "검증 요구사항, 미완료"인 항목은 직접 확인하는 자동 시험이 아직 없다는 뜻이다.
| 요구사항 | 시험 |
|---|---|
장면 방식이 정지, 한 번, 반복대로 SVG에 나온다. 정지는 처음부터 마지막 모습이고, 한 번은 한 번 재생하고 마지막 모습에 머물며, 반복은 되풀이한다. mode를 적지 않으면 줄이 있는 장면은 한 번, 없는 장면은 정지다. --scene으로 장면을 고른다. 없는 장면은 --json에서도 파일 이름이 붙은 진단 한 줄이다 |
test/exports.test.js, test/authoring.test.js, test/cli.test.js(L1), test/markdown.test.js(M12) |
표시 길이 D가 논리 길이를 speed로 나눈 값과 마지막 펄스·선 이탈에 400ms를 더한 값 중 큰 값이다. 마지막 모습은 반올림한 표시 길이가 장면 끝보다 짧아도 장면 끝의 박자를 읽는다(#174). 논리 시각에 효과 시간이나 박자 뒤 머묾이 없다 |
멈춘 SVG와 움직이는 SVG의 구성은 test/exports.test.js(X1, X2, X5). 표본 추출기가 반올림한 표시 길이 뒤에도 마지막 값을 유지하고 소수 배속의 효과가 끝난 모습을 돌려주는 것은 test/sampler.test.js(Q)가 본다. 표시 길이 D의 공식을 직접 확인하는 시험은 확인되지 않았다(검증 요구사항, 미완료). 정지 장면과 한 번 장면의 마지막 모습이 브라우저에서 보이는 것은 대표 예제 하나로 Chrome에서 확인했다 |
| 이동은 보기마다 한 번씩 보이고 값은 첫 투영의 도착에 한 번만 바뀐다. 이동의 방향, 값 갱신 순서, 사라짐, 동시 이동이 공개 출력의 값 줄에 맞게 나온다 | test/values.test.js, test/cards.test.js(S5) |
조건과 대기: when이 거짓인 이동은 건너뛰고 박자와 장면은 남는다. 대기 해제 순서는 이벤트 순서 표를 따르고, 같은 시각에 풀림과 시간 초과가 겹치면 풀림이 이긴다(#173) |
test/values.test.js |
reserve=는 한 사건 안에서 확인하고 예약해 가용량 1에서 하나만 통과시키고, 모두 적용하거나 하나도 적용하지 않는다 |
test/values.test.js |
값 유지(keep)와 읽기(:=) 결과가 같은 원본에서 같다 |
test/values.test.js |
순서 보기에만 놓인 카드의 값 on=, show, clear는 줄 번호가 붙은 오류이고, 그래프 보기에도 놓으면 선언 순서와 상관없이 카드 내용이 그려진다(#172, #175) |
test/cards.test.js(S7, S8)가 오류와 멈춘 SVG, HTML 판 마크업을 읽는다. 재생기가 once 장면이 끝난 뒤 칠하는 모습은 검증 요구사항, 미완료 |
| 같은 원본은 같은 SVG와 같은 HTML이다. 바깥 자원을 부르지 않고 글은 이스케이프된다 | test/exports.test.js, test/escaping.test.js |
| 내려받기는 정본 템플릿이다. 문서 머리의 정본을 다시 채우면 문서와 바이트까지 같고, 제목과 장면 이름이 스크립트로 새지 않는다 | test/exports.test.js(X7, X8). Chrome에서 받은 HTML은 독립한 문서였고, 탭을 바꾼 뒤 두 번째로 받은 파일이 처음 받은 파일과 바이트까지 같으며 크기가 늘지 않았다. 내려받은 파일을 오프라인에서 처음 여는 일은 확인하지 못했다(검증 요구사항, 미완료) |
원본 복사에 쓰는 글(figure.source)이 받은 글 그대로(CRLF 포함)이고, 안전하게 실려 문서 구조를 깨지 않는다 |
test/exports.test.js(X11, X7). Chrome에서 원본 복사가 원래 글의 바이트와 줄바꿈을 그대로 보존함을 확인했다. 클립보드를 쓸 수 없을 때의 실패 표시와 호버, 초점, 터치 노출은 확인하지 않았다(검증 요구사항, 미완료) |
| 탭과 도구 막대가 계약대로다. 재생, 배속, 반복, 진행선, 설명 줄 마크업이 없고, 도구 막대는 문법 복사, HTML 다운로드, 전체화면 순서이며, 장면이 하나이거나 없으면 탭이 없다 | 마크업의 정적 확인은 test/components.test.js(U9: 모든 그림이 같은 순서의 도구 막대와 빈 탭 줄 마크업을 갖고, 탭 줄은 그림 밖이며 페이지에 탭이 없다. 재생기가 읽는 장면 수)와 test/exports.test.js(X10: 재생 조작 없음)다. Chrome에서 확인한 것은 탭이 그림 전체 아래에 놓이고(보기 창 아래에 있으면 페이지 스크롤로 닿는다) 누르는 높이가 44px라는 것, 지금 탭을 다시 눌러도 다시 시작하지 않고 다른 탭에 갔다 오면 다시 시작한다는 것, 폭 320에서 장면이 없는 시험 원본에 탭 줄이 없다는 것이다. 장면이 하나일 때 탭 줄을 숨기는지, 방향키와 초점, 호버는 확인하지 않았다(검증 요구사항, 미완료) |
| 뷰어는 순수 시각화기다. 자동 스크롤, 가시성 대기, 그림 위 고정 탭, 수동 복사 팝업이 없고 탭은 그림 아래 가운데에 줄바꿈해 놓인다 | 재생 조작이 마크업에 없음은 test/exports.test.js(X10). 동작은 실제 Chrome 검수의 몫이다(검증 요구사항, 미완료) |
| 일반 보기, 삽입, 전체화면이 같은 그림 틀 DOM과 도구 막대 한 벌을 쓴다. 단독과 삽입은 자연 높이이고 전체화면만 창을 채운다 | 공통 마크업은 test/components.test.js(U9), 삽입 높이·폭 변경·전체화면 복귀는 test/browser/narrow-view.test.js, 넘치는 캔버스의 초점 유지는 test/player-view.test.js가 확인한다 |
출력 이름이 겹치면 쓰기 전에 실패하고 아무것도 쓰지 않으며(#176), 쓰기가 실패하면 이미 쓴 파일을 되돌린다(#177). 원본은 .thinkflow만 읽는다 |
test/cli.test.js |
| 좁은 HTML에서도 자연 글자 크기를 유지하고 남는 가로 부분에 키보드로 접근할 수 있다. 페이지 전체는 가로로 넘치지 않는다 | test/browser/narrow-view.test.js의 architecture·sequence 모든 장면, 320·390·430·1440px 독립·삽입 HTML. 나머지 예제와 모든 의미·움직임의 시각 검수는 별도 요구사항이다 |
| 같은 시각이면 앞서 어떤 시각을 샘플했든 같은 모습이다(순수 표본 추출기). 펄스는 올라감 80ms, 유지 80ms, 내려옴 240ms다 | 같은 모습은 test/sampler.test.js(Q1~Q8)가 본다. 표본 추출기(sample.js)는 DOM, 시계, 난수가 없는 순수 부품이라 시험이 HTML에 넣는 같은 PLAYER_SCRIPT를 격리 컨텍스트(node:vm)에 올려 표본 추출기를 부르고, 재생 데이터는 공개 진입점(toHtml)이 HTML에 실은 JSON을 읽는다. 이 시험은 이력과 무관한 같은 모습, 정지 장면의 마지막 모습, 마지막 값 유지, 소수 배속의 끝난 효과를 포함하고 브라우저 DOM에 쓰는 일은 보지 않는다. 펄스 80/80/240ms라는 숫자는 시험이 고정하지 않아 검증 요구사항, 미완료이고, 브라우저의 시계가 표본을 부르는 일과 DOM에 쓰는 모습은 실제 Chrome 검수의 몫이다 |
| 선은 점이 하나 이상 있는 동안 켜지고, 역방향 점은 같은 물리 선을 쓰며, 사라지는 점은 자르는 시각에 선을 떠난다. 고정 알약은 400ms 줄어든다 | 선이 끊기지 않고 켜져 있는 것, 거꾸로 가는 점이 같은 선을 쓰는 것, 잃은 점이 도착지에 후광과 값 변화를 만들지 않는 것, 선 라벨 알약이 재생 데이터의 줄어드는 시간 동안 1에서 0으로 주는 것, 같은 시각에 한 점이 선을 떠나고 다른 점이 들어서는 넘겨받기는 test/sampler.test.js(Q)가 본다. 400ms라는 숫자와 화면에서 보이는 모습은 검증 요구사항, 미완료이고 실제 Chrome 검수의 몫이다 |
| 의미 상태(차트 계열 보임, 막대 흐림, 도형과 그룹 켜짐, 카드 층, 조용한 선)가 논리 경계에서 HTML과 움직이는 SVG 모두 바로 바뀐다. 명시한 효과만 서서히 간다 | 움직이는 SVG가 읽는 정의(선과 알약의 평소 복귀, 400 표시 ms 꼬리, 조용한 선의 보임, 켜진 그룹의 경계 색)는 test/component-state.test.js(S4, S5)가 keyframes로 읽는다. 브라우저에서 두 표면의 같은 시각 모습을 견주는 일은 검증 요구사항, 미완료이고 실제 Chrome 검수의 몫이다. 켜진 차트 카드의 바탕 면이 켜짐 전환과 재생 중 라이트·다크 전환에서 따라가는 것만 Chrome에서 본 범위가 있고, 범위는 표현 범위의 “차트 바탕” 행이 정한다 |
| 이벤트 예산을 넘으면 오류로 끝나고 결과 파일이 없다. 시간 눈금, 시간 상한, 출발 시각 정밀도가 계약대로다 | 검증 요구사항, 미완료 |
| 글 상자가 모든 장면에서 도형 이름, 열, 그룹 제목, 알약을 가리지 않고 그림 안에 있다. SVG와 재생기가 같은 계획을 쓴다 | 검증 요구사항, 미완료 |
| 목록 쪽 테마 단추가 목록과 iframe 그림을 함께 바꾼다. 예약 이름 원본이 목록을 덮지 않는다 | thinkflow gallery의 목록 쪽을 Chrome 폭 320에서 열어 API 예제 iframe 하나를 봤다. 라이트 선택이 iframe에 전달됐고 iframe 안에서 장면 탭이 바뀌었으며 부모에 가로 넘침이 없었다. 이는 대표 하나이고 모든 iframe, 전체화면 대체 동작, 시스템·다크 전환, 예약 이름 원본은 확인하지 않았다(검증 요구사항, 미완료) |
단점
- 움직이는 SVG는 마우스 반응과 탭이 없다. 장면이 여럿이면 장면마다 SVG를 따로 만들어야 한다.
- 시간표가 박자마다 상태를 다 적어서 박자가 많으면 HTML이 커진다.
- HTML은 정본 템플릿을 한 벌 더 base64로 담아 정본의 약 2.3배 크기다.
- 반복 장면은 표시 길이(마지막 펄스가 끝날 때까지)가 지나면 바로 처음으로 돌아간다. 따로 머무는 시간은 없다.
- 갤러리 iframe처럼 샌드박스가 다운로드를 막은 곳에서는 내려받기가 동작하지 않는다.
대안
- 박자마다 바뀐 것만 적는 시간표: 탭으로 건너뛸 때 앞 박자를 다시 계산해야 하고, HTML과 SVG가 같은 계산을 두 번 구현해야 해서 버렸다.
- 재생기에서 조건과 대기를 계산하는 방식: 움직이는 SVG에는 스크립트가 없고, 장면 속도와 장면 들어가기에서 결과가 갈려 버렸다. 결정 기록
- 조건을 쓰지 않고 사용자가 결과 시각을 직접 적는 방식: 값에 따라 이동이 달라지는 그림은 값이 바뀔 때마다 시각을 손으로 다시 계산해야 해서 버렸다. 결정 기록
미해결 질문
- 차트와 그래프의 좁은 배치는 구현되어 있고 예제 전체의 기하는 Chrome에서 쟀다. 눈으로 읽힘을 본 범위는 폭 390 라이트의 모든 예제 종류 첫 장면, 폭 390 다크의 통합 예제, 폭 1440 다크의 면적, 워터폴, 스택, 상태, 지표 예제이고, 이 범위에서 글자 잘림, 글자 겹침, 범례 깨짐, 조작 넘침은 보이지 않았다. 같은 검수에서 찾은 눈금 솎기, 형제 도형 순서, 워터폴 바닥 띠, 자기 전이 고리, 점 라벨 소유의 모양 결함은 공통 규칙으로 고쳤고, 고친 현재 소스에서 영향받은 화면(폭 390 라이트의 면적, 지표, 상태, 워터폴, 산점도, 흐름, 메모리, 스택, 막대 예제와 폭 390 다크의 통합 예제)을 다시 눈으로 봤다. 받아들인 남은 한계는 스택의 고정 연결점에서 생기는 선 교차, 점이 몰린 산점도에서 점 라벨의 소유가 늘 분명하다는 보장이 없는 것이다. 복잡한 개요 그림은 좁은 화면에서도 자연 글자 크기를 유지하지만, 첫 화면의 빈 영역과 화면 밖 핵심 노드, 그림 아래 장면 탭까지의 긴 거리가 남아 있어 좁은 개요의 수용 검수는 미완료다. 이 검수를 화면 전체의 수용 판정으로 쓰지 않고, 그 밖의 폭, 테마, 장면, 재생 중 프레임, 조합이 읽히는지와 실제 휴대폰의 Safari는 확인하지 않았다(검증 요구사항, 미완료, 확인한 범위는 표현 범위가 정한다). 차트 보기를 숨기거나 내용을 줄이는 방식은 받지 않는다.
- 목록에서 고른 테마가 새 탭에서 연 단독 재생 화면과 목록 안에 넣은
<img>SVG에 이어지지 않는다. 테마 값을 공유하는 초기 설정 경로가 필요하다. - 차트 표식의 펄스는 계열의 effect 단계를, 도형의 도착 후광은 공통 UI 강조 색을 쓰지만, 다크의 짙은 면 위와 작은 크기에서 번쩍임이 얼마나 잘 읽히는지는 사람이 따로 평가하지 않았다(검증 요구사항, 미완료). 도구 막대는 Chrome의 홈페이지 본문에서 블록 안 배치·호버·복사 알림·다운로드·대체 전체화면·확대·Escape 복귀를 확인했다. 번호 배지와 모든 다크 의미 아이콘 조합은 따로 검수하지 않았다(검증 요구사항, 미완료). 차트 표식의 효과 색은 대상 계열의 색에서 고른다(문서 스킬 연동).
- 접근성 트리와 화면의 일치, 스크린 리더로 읽은 검수는 이 변경 뒤에 하지 않았다(검증 요구사항, 미완료). 실제 화면 검수는 Chrome에서 사람이 직접 한다.
카드 안 관계 그래프는 노드 이름, 연결선 또는 강조 노드가 바뀌면 본문 변경 시각에 강조를 표시한다. 같은 그래프를 같은 시각에 다시 표시하는 것은 변경으로 세지 않는다.