Skip to main content

webtoon-panel-assembly

생성된 컷 이미지와 shot_list의 텍스트 메타를 결합하여 마우스/터치 스크롤로 읽을 수 있는 단일 세로 스크롤 웹툰 HTML 페이지(index.html)를 만드는 스킬. 말풍선·내레이션·시스템 UI·글리치를 모두 HTML/CSS로 오버레이한다. panel-assembler 에이전트가 호출한다.

Source facts

Repository
revfactory/systemless-player
Last source activity
May 18, 2026 at 06:15
Detected SKILL.md language
Korean
Stars
1
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
webtoon-panel-assembly
description
생성된 컷 이미지와 shot_list의 텍스트 메타를 결합하여 마우스/터치 스크롤로 읽을 수 있는 단일 세로 스크롤 웹툰 HTML 페이지(index.html)를 만드는 스킬. 말풍선·내레이션·시스템 UI·글리치를 모두 HTML/CSS로 오버레이한다. panel-assembler 에이전트가 호출한다.
# Webtoon Panel Assembly — 세로 스크롤 HTML 페이지 조립 ## 핵심 룰 1. **모든 텍스트는 HTML/CSS 오버레이**, 이미지에 박지 않는다 (수정·번역·접근성 모두 유리) 2. **컷 간격 0px 기본**, 침묵 컷 앞뒤만 4px 3. **반응형:** 모바일 100vw / 데스크탑 800px 가운데 정렬 4. **scroll_length를 CSS min-height vh로 매핑** — 작가의 스크롤 호흡 의도 보존 5. **이미지는 `loading="lazy"` + `decoding="async"`** — 첫 컷 빠르게, 나머지 점진 로드 6. **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 정의:** ```css /* dialogue - 표준 말풍선 */ .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,255,0.96); } /* thought - 구름 모양 */ .bubble-thought { position: absolute; background: rgba(220,220,230,0.92); color: #444; font-style: italic; border-radius: 30px; padding: 10px 18px; font-size: 14px; max-width: 55%; box-shadow: 0 0 0 4px rgba(220,220,230,0.92), 0 0 0 8px rgba(220,220,230,0.6), 0 0 0 12px rgba(220,220,230,0.3); } /* shout - 가시 말풍선 */ .bubble-shout { position: absolute; background: #ff3b3b; color: #fff; font-weight: 900; font-size: 28px; padding: 14px 22px; text-align: center; clip-path: polygon( 0% 20%, 8% 0%, 22% 12%, 38% 0%, 52% 15%, 68% 0%, 82% 14%, 100% 5%, 95% 30%, 100% 55%, 92% 75%, 100% 100%, 80% 88%, 65% 100%, 50% 90%, 35% 100%, 20% 88%, 5% 100%, 0% 70%, 8% 45% ); text-shadow: 2px 2px 0 #800; } /* whisper - 점선 작은 말풍선 */ .bubble-whisper { position: absolute; background: rgba(40,40,50,0.7); color: #aaa; border: 1px dashed #555; border-radius: 12px; padding: 6px 10px; font-size: 13px; font-style: italic; max-width: 45%; } /* narration - 손글씨 가로 무말풍선 */ .narration-doh { position: absolute; left: 24px; right: 24px; font-family: 'Nanum Pen Script', cursive; color: var(--bone); opacity: 0.88; font-size: 22px; line-height: 1.4; text-shadow: 0 2px 6px rgba(0,0,0,0.85); } /* sfx - 의성어/효과음 */ .sfx-text { position: absolute; font-family: 'Black Han Sans', sans-serif; color: var(--bone); font-size: 36px; font-weight: 900; transform: rotate(-3deg); text-shadow: 3px 3px 0 var(--void), -2px -2px 0 var(--void), 2px -2px 0 var(--void), -2px 2px 0 var(--void); letter-spacing: 0.05em; pointer-events: 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 템플릿 ```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; } .webtoon { max-width: 800px; margin: 0 auto; } .cut { position: relative; overflow: hidden; } .cut img { display: block; width: 100%; height: auto; } .cut.silence { background: var(--bone); margin: 4px 0; } .cut.silence-void { background: var(--void); margin: 4px 0; display: flex; align-items: center; justify-content: center; } .cut.silence-void::after { content: ''; width: 4px; height: 4px; background: var(--bone); border-radius: 50%; } /* 텍스트 오버레이 */ .system-ui-text { position: absolute; top: 16px; left: 16px; font-family: 'JetBrains Mono', 'Courier New', monospace; color: var(--cyan); font-size: 14px; text-shadow: 0 0 8px rgba(0,229,209,0.6); letter-spacing: 0.05em; } .narration-doh { position: absolute; left: 24px; right: 24px; font-family: 'Nanum Pen Script', 'Gaegu', cursive; color: var(--grey-text); font-size: 18px; line-height: 1.4; } .narration-doh.top { top: 24px; } .narration-doh.bottom { bottom: 24px; } .dialogue-bubble { position: absolute; background: rgba(255,255,255,0.95); color: #111; border-radius: 12px; padding: 8px 14px; font-weight: 700; font-size: 16px; max-width: 70%; box-shadow: 0 2px 8px rgba(0,0,0,0.3); } .onomatopoeia { position: absolute; font-family: 'Black Han Sans', sans-serif; color: var(--bone); font-size: 32px; font-weight: 900; transform: rotate(-3deg); text-shadow: 2px 2px 0 var(--void); } .operator-text { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); font-family: 'Noto Serif KR', serif; color: var(--bone); font-weight: 300; font-size: 15px; letter-spacing: 0.05em; } .monster-emotion { position: absolute; font-family: 'Noto Serif KR', serif; color: var(--void); font-weight: 300; font-size: 14px; background: rgba(242,238,227,0.85); padding: 4px 10px; } .glitch-text { position: absolute; font-family: 'Courier New', monospace; color: var(--magenta); font-size: 18px; animation: glitch 0.3s steps(2) infinite; transform: skewX(-5deg); } @keyframes glitch { 0% { transform: skewX(-5deg) translateX(0); } 50% { transform: skewX(5deg) translateX(-2px); } 100% { transform: skewX(-5deg) translateX(2px); } } /* 시그니처 효과 오버레이 */ .cyan-grid-overlay { position: absolute; inset: 0; background-image: linear-gradient(to right, rgba(0,229,209,0.08) 1px, transparent 1px), linear-gradient(to bottom, rgba(0,229,209,0.08) 1px, transparent 1px); background-size: 40px 40px; pointer-events: none; } .error-glitch-overlay { position: absolute; inset: 0; pointer-events: none; background: linear-gradient(to bottom, transparent 30%, rgba(255,28,107,0.15) 31%, transparent 32%), linear-gradient(to bottom, transparent 60%, rgba(255,28,107,0.1) 61%, transparent 62%); mix-blend-mode: screen; } /* 실패 자리 표시자 */ .gen-failed-placeholder { width: 100%; aspect-ratio: 9/16; background: repeating-linear-gradient(45deg, #1a1a1a, #1a1a1a 10px, #2a2a2a 10px, #2a2a2a 20px); display: flex; align-items: center; justify-content: center; color: var(--magenta); font-size: 20px; } /* 회차 헤더/푸터 */ .ep-header, .ep-footer { padding: 40px 20px; text-align: center; color: var(--bone); } .ep-header h1 { font-size: 28px; margin: 0; letter-spacing: 0.05em; } .ep-header .sub { color: var(--grey-text); margin-top: 8px; font-size: 14px; } .next-ep-link { display: inline-block; margin-top: 20px; padding: 12px 24px; border: 1px solid var(--cyan); color: var(--cyan); text-decoration: none; font-family: monospace; letter-spacing: 0.1em; } </style> </head> <body> <main class="webtoon"> <header class="ep-header"> <h1>{{EPISODE_TITLE}}</h1> <div class="sub">Ep.{{NN}} · {{ACT_LABEL}}</div> </header> {{#each cuts}} <section class="cut {{cut_classes}}" data-cut-id="{{cut_id}}" style="min-height: {{scroll_vh}}vh;"> {{cut_body}} </section> {{/each}} <footer class="ep-footer"> <a class="next-ep-link" href="../ep{{NEXT_NN}}/index.html">다음 화 →</a> </footer> </main> </body> </html> ``` ## 컷 본문 렌더링 (의사 코드) ```
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub