| name | webtoon-panel-assembly |
| description | 생성된 컷 이미지와 shot_list의 텍스트 메타를 결합하여 마우스/터치 스크롤로 읽을 수 있는 단일 세로 스크롤 웹툰 HTML 페이지(index.html)를 만드는 스킬. 말풍선·내레이션·시스템 UI·글리치를 모두 HTML/CSS로 오버레이한다. panel-assembler 에이전트가 호출한다. |
Webtoon Panel Assembly — 세로 스크롤 HTML 페이지 조립
핵심 룰
- 모든 텍스트는 HTML/CSS 오버레이, 이미지에 박지 않는다 (수정·번역·접근성 모두 유리)
- 컷 간격 0px 기본, 침묵 컷 앞뒤만 4px
- 반응형: 모바일 100vw / 데스크탑 800px 가운데 정렬
- scroll_length를 CSS min-height vh로 매핑 — 작가의 스크롤 호흡 의도 보존
- 이미지는
loading="lazy" + decoding="async" — 첫 컷 빠르게, 나머지 점진 로드
- 6가지 말풍선 스타일 지원 — dialogue / thought / shout / whisper / narration / sfx (아래 CSS 클래스 표 참조)
6가지 말풍선 CSS 클래스 (필수 구현)
dialogue-writer가 출력한 text_overlay 안의 style 필드에 따라 다른 CSS 클래스로 렌더링한다.
| style | CSS 클래스 | 시각 |
|---|
dialogue | .bubble-dialogue | 흰 둥근 말풍선 + 검정 굵은 글씨, 꼬리 있음 |
thought | .bubble-thought | 구름 모양 옅은 말풍선 + 회색 이탤릭 (border-radius로 구름 흉내) |
shout | .bubble-shout | 가시 모양 빨강 말풍선 + 큰 진한 글씨 (clip-path로 가시) |
whisper | .bubble-whisper | 점선 테두리 작은 말풍선 + 흐린 글씨 |
narration | .narration-doh | 무말풍선 + 손글씨 회색, 컷 상/하단 가로 |
sfx | .sfx-text | 무말풍선 + 굵은 산세리프 + 외곽선, 컷에 자유 배치 |
필수 CSS 정의:
.bubble-dialogue {
position: absolute;
background: rgba(255,255,255,0.96);
color: #111;
border-radius: 18px;
padding: 10px 16px;
font-weight: 700;
font-size: 16px;
line-height: 1.35;
max-width: 60%;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
}
.bubble-dialogue::after {
content: '';
position: absolute;
bottom: -8px;
left: 24px;
width: 0; height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 10px solid rgba(255,255,,);
}
{
: absolute;
: (,,,);
: ;
: italic;
: ;
: ;
: ;
: ;
: (,,,), (,,,), (,,,);
}
{
: absolute;
: ;
: ;
: ;
: ;
: ;
: center;
: (
, , , , , , , ,
, , , , , , , , , , ,
);
: ;
}
{
: absolute;
: (,,,);
: ;
: dashed ;
: ;
: ;
: ;
: italic;
: ;
}
{
: absolute;
: ;
: ;
: , cursive;
: (--bone);
: ;
: ;
: ;
: (,,,);
}
{
: absolute;
: , sans-serif;
: (--bone);
: ;
: ;
: (-);
:
(--void),
- - (--void),
- (--void),
- (--void);
: ;
: none;
}
위치 클래스 (모든 말풍선 공통)
| 클래스 | 위치 |
|---|
.pos-top-left | top: 24px; left: 24px; |
.pos-top-right | top: 24px; right: 24px; |
.pos-bottom-left | bottom: 32px; left: 24px; |
.pos-bottom-right | bottom: 32px; right: 24px; |
.pos-center | top: 50%; left: 50%; transform: translate(-50%, -50%); |
화자 위치 추론
- 컷의
characters 배열에서 화자가 좌측이면 → .pos-bottom-left
- 우측이면 →
.pos-bottom-right
- 단독 등장이면 → 화자가 보이는 쪽
- 다인 대화는 2말풍선 좌우 분할
워크플로우
Step 1 — 입력 로드
_workspace/episodes/ep{NN}/shot_list.json
_workspace/episodes/ep{NN}/prompts.json (post_overlay_text 정본)
_workspace/episodes/ep{NN}/images/cut_*.png
_workspace/episodes/ep{NN}/images/cut_*.meta.json (GEN_FAILED 확인)
Step 2 — index.html 골격 생성
하나의 자급자족 HTML 파일. CSS는 인라인. 이미지는 상대 경로.
Step 3 — 컷 단위 섹션 렌더링
컷마다 <section class="cut">을 만들고 다음을 결합:
<img> 또는 placeholder
- 시그니처 효과 오버레이 (cyan_grid, error_glitch 등)
- 텍스트 오버레이 (내레이션, 시스템 UI, 대사, 효과음, 운영자 텍스트, 몬스터 감정)
Step 4 — 회차 인덱스 갱신
_workspace/episodes/INDEX.html에 이번 회차 항목 추가/갱신.
HTML 템플릿
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>{{TITLE}}</title>
<style>
:root {
--cyan: #00E5D1;
--void: #050608;
--magenta: #FF1C6B;
--bone: #F2EEE3;
--gold: #C9A24B;
--pink: #FFB3D1;
--grey-text: #6E6E72;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; background: var(--void); color: var(--bone); -webkit-font-smoothing: antialiased; }
body { font-family: 'Pretendard', 'Noto Sans KR', system-ui, sans-serif; }
{ : ; : auto; }
{ : relative; : hidden; }
{ : block; : ; : auto; }
{ : (--bone); : ; }
{ : (--void); : ; : flex; : center; : center; }
{ : ; : ; : ; : (--bone); : ; }
{
: absolute; : ; : ;
: , , monospace;
: (--cyan); : ;
: (,,,);
: ;
}
{
: absolute; : ; : ;
: , , cursive;
: (--grey-text); : ;
: ;
}
{ : ; } { : ; }
{
: absolute;
: (,,,); : ;
: ; : ;
: ; : ; : ;
: (,,,);
}
{
: absolute;
: , sans-serif;
: (--bone); : ; : ;
: (-); : (--void);
}
{
: absolute; : ; : ;
: (-, -);
: , serif;
: (--bone); : ; : ;
: ;
}
{
: absolute;
: , serif;
: (--void); : ; : ;
: (,,,); : ;
}
{
: absolute;
: , monospace;
: (--magenta); : ;
: glitch () infinite;
: (-);
}
glitch {
{ : (-) (); }
{ : () (-); }
{ : (-) (); }
}
{
: absolute; : ;
:
(to right, (,,,) , transparent ),
(to bottom, (,,,) , transparent );
: ;
: none;
}
{
: absolute; : ; : none;
:
(to bottom, transparent , (,,,) , transparent ),
(to bottom, transparent , (,,,) , transparent );
: screen;
}
{
: ; : /;
:
(, , , , );
: flex; : center; : center;
: (--magenta); : ;
}
, {
: ; : center; : (--bone);
}
{ : ; : ; : ; }
{ : (--grey-text); : ; : ; }
{
: inline-block; : ;
: ; : solid (--cyan);
: (--cyan); : none;
: monospace; : ;
}
{{EPISODE_TITLE}}
Ep.{{NN}} · {{ACT_LABEL}}
{{#each cuts}}
{{cut_body}}
{{/each}}
다음 화 →
컷 본문 렌더링 (의사 코드)
function render_cut(cut):
html = ''
if cut.shot_type == 'silence':
return '<!-- silence: no image -->'
if image_status == 'GEN_FAILED':
html += '<div class="gen-failed-placeholder">컷 {N} 생성 실패 — 재생성 대기</div>'
else:
html += '<img src="images/cut_{NN}.png" alt="{purpose}" loading="lazy" decoding="async">'
# 시그니처 오버레이
if 'cyan_grid' in cut.signatures_used:
html += '<div class="cyan-grid-overlay"></div>'
if cut.glitch:
html += '<div class="error-glitch-overlay"></div>'
# 텍스트 오버레이
if cut.text_overlay.system_ui_text:
html += f'<div class="system-ui-text">{cut.text_overlay.system_ui_text}</div>'
if cut.text_overlay.narration_doh_yun:
html += f'<div class="narration-doh bottom">{cut.text_overlay.narration_doh_yun}</div>'
for d in cut.text_overlay.dialogue or []:
pos = d.get('position', {'bottom': '20px', 'left': '20px'})
html += f'<div class="dialogue-bubble" style="...">{d.text}</div>'
for s in cut.text_overlay.onomatopoeia or []:
html += f'<div class="onomatopoeia">{s}</div>'
if cut.text_overlay.operator_text:
html += f'<div class="operator-text">{cut.text_overlay.operator_text}</div>'
return html
scroll_length → vh 매핑
| scroll_length | min-height vh |
|---|
| 1 | 60vh (정사각/침묵) |
| 2 | 110vh (와이드) |
| 3 | 170vh (큰 임팩트) |
| 4 | 230vh (시즌 단일 컷 최대) |
화면 한 번에 1.x 정도 들어가도록 의도 — 스크롤 호흡 보존.
INDEX.html (회차 색인)
시즌 1 30화 전체를 보여주는 색인. 회차 카드형 배치:
<a href="ep01/index.html" class="card">
<img src="ep01/images/cut_01.png" alt="">
<div>Ep.1 — F급은 미끼다</div>
</a>
체크리스트