Skip to main content

webtoon-panel-assembly

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

معلومات المصدر

المستودع
revfactory/systemless-player
آخر نشاط في المصدر
١٨ مايو ٢٠٢٦ في ٠٦:١٥
لغة SKILL.md المكتشفة
الكورية
النجوم
١
التفرعات
٠

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
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> ``` ## 컷 본문 렌더링 (의사 코드) ```
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub