Spaces:
Running
Running
| <html lang="ko"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>์นํฐ ํธ์ง ์คํ๋์ค Pro</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@100;300;400;500;700;900&family=Black+Han+Sans&family=Jua&family=Do+Hyeon&family=Sunflower:wght@300;500;700&family=Cute+Font&family=Gamja+Flower&family=Hi+Melody&family=Nanum+Pen+Script&family=Stylish&family=Single+Day&family=Gaegu:wght@300;400;700&display=swap" rel="stylesheet"> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Noto Sans KR', sans-serif; | |
| background: #1a1a1a; | |
| color: #fff; | |
| overflow: hidden; | |
| } | |
| .container { | |
| display: flex; | |
| height: 100vh; | |
| } | |
| /* ์ข์ธก ํด๋ฐ */ | |
| .toolbar { | |
| width: 320px; | |
| background: #2a2a2a; | |
| padding: 20px; | |
| overflow-y: auto; | |
| border-right: 1px solid #444; | |
| } | |
| .toolbar::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| .toolbar::-webkit-scrollbar-track { | |
| background: #1a1a1a; | |
| } | |
| .toolbar::-webkit-scrollbar-thumb { | |
| background: #4a9eff; | |
| border-radius: 4px; | |
| } | |
| .toolbar h2 { | |
| font-size: 18px; | |
| margin-bottom: 20px; | |
| color: #fff; | |
| border-bottom: 2px solid #4a9eff; | |
| padding-bottom: 10px; | |
| } | |
| .tool-section { | |
| margin-bottom: 30px; | |
| } | |
| .tool-section h3 { | |
| font-size: 14px; | |
| margin-bottom: 15px; | |
| color: #aaa; | |
| text-transform: uppercase; | |
| } | |
| /* ํ์ผ ์ ๋ก๋ ์์ญ */ | |
| .file-upload { | |
| border: 2px dashed #4a9eff; | |
| border-radius: 8px; | |
| padding: 20px; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| margin-bottom: 20px; | |
| } | |
| .file-upload:hover { | |
| background: rgba(74, 158, 255, 0.1); | |
| border-color: #6ab7ff; | |
| } | |
| .file-upload input { | |
| display: none; | |
| } | |
| /* ํ์ผ ๋ฆฌ์คํธ */ | |
| .file-list { | |
| background: #333; | |
| border-radius: 8px; | |
| padding: 10px; | |
| margin-top: 10px; | |
| max-height: 150px; | |
| overflow-y: auto; | |
| } | |
| .file-item { | |
| font-size: 12px; | |
| color: #aaa; | |
| padding: 3px 0; | |
| border-bottom: 1px solid #444; | |
| } | |
| .file-item:last-child { | |
| border-bottom: none; | |
| } | |
| /* ์ปดํฌ๋ํธ ๋ฒํผ๋ค */ | |
| .component-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 8px; | |
| } | |
| .component-btn { | |
| background: #3a3a3a; | |
| border: 1px solid #555; | |
| border-radius: 8px; | |
| padding: 10px; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| text-align: center; | |
| color: #fff; | |
| } | |
| .component-btn:hover { | |
| background: #4a4a4a; | |
| border-color: #4a9eff; | |
| transform: translateY(-2px); | |
| } | |
| .component-btn svg { | |
| width: 35px; | |
| height: 35px; | |
| margin-bottom: 3px; | |
| } | |
| .component-btn span { | |
| display: block; | |
| font-size: 11px; | |
| } | |
| /* ์์ฑ ํธ์ง ํจ๋ */ | |
| .properties-panel { | |
| background: #333; | |
| border-radius: 8px; | |
| padding: 15px; | |
| margin-top: 20px; | |
| } | |
| .property-group { | |
| margin-bottom: 15px; | |
| } | |
| .property-group label { | |
| display: block; | |
| font-size: 12px; | |
| color: #aaa; | |
| margin-bottom: 5px; | |
| } | |
| .property-group input, | |
| .property-group select, | |
| .property-group textarea { | |
| width: 100%; | |
| padding: 8px; | |
| background: #2a2a2a; | |
| border: 1px solid #444; | |
| border-radius: 4px; | |
| color: #fff; | |
| } | |
| .color-picker { | |
| display: grid; | |
| grid-template-columns: repeat(6, 1fr); | |
| gap: 5px; | |
| } | |
| .color-option { | |
| width: 35px; | |
| height: 35px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| border: 2px solid transparent; | |
| } | |
| .color-option.selected { | |
| border-color: #4a9eff; | |
| box-shadow: 0 0 10px rgba(74, 158, 255, 0.5); | |
| } | |
| /* ๋ฉ์ธ ์์ ์์ญ */ | |
| .workspace { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| background: #1a1a1a; | |
| } | |
| .workspace-header { | |
| background: #2a2a2a; | |
| padding: 15px 20px; | |
| border-bottom: 1px solid #444; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .workspace-tools { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .tool-btn { | |
| background: #3a3a3a; | |
| border: 1px solid #555; | |
| border-radius: 6px; | |
| padding: 8px 16px; | |
| color: #fff; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| } | |
| .tool-btn:hover { | |
| background: #4a4a4a; | |
| border-color: #4a9eff; | |
| } | |
| .tool-btn.primary { | |
| background: #4a9eff; | |
| border-color: #4a9eff; | |
| } | |
| .tool-btn.primary:hover { | |
| background: #6ab7ff; | |
| } | |
| .tool-btn.secondary { | |
| background: #28a745; | |
| border-color: #28a745; | |
| } | |
| .tool-btn.secondary:hover { | |
| background: #5cbf5c; | |
| } | |
| /* ์บ๋ฒ์ค ์์ญ */ | |
| .canvas-container { | |
| flex: 1; | |
| overflow: auto; | |
| padding: 20px; | |
| display: flex; | |
| justify-content: center; | |
| background: #222; | |
| } | |
| .canvas-wrapper { | |
| position: relative; | |
| background: white; | |
| box-shadow: 0 10px 40px rgba(0,0,0,0.5); | |
| min-height: 800px; | |
| width: 700px; | |
| } | |
| /* ์ด๋ฏธ์ง ์ปจํ ์ด๋ */ | |
| .image-container { | |
| position: relative; | |
| width: 100%; | |
| } | |
| .image-number { | |
| position: absolute; | |
| left: -40px; | |
| top: 10px; | |
| background: #4a9eff; | |
| color: white; | |
| width: 30px; | |
| height: 30px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: bold; | |
| font-size: 14px; | |
| z-index: 10; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.3); | |
| } | |
| .webtoon-image { | |
| width: 100%; | |
| display: block; | |
| margin-bottom: 20px; | |
| } | |
| /* ๋๋๊ทธ ๊ฐ๋ฅํ ์ปดํฌ๋ํธ */ | |
| .draggable-component { | |
| position: absolute; | |
| cursor: move; | |
| user-select: none; | |
| } | |
| .draggable-component.selected { | |
| outline: 2px solid #4a9eff; | |
| z-index: 100; | |
| } | |
| .draggable-component .resize-handle { | |
| position: absolute; | |
| width: 10px; | |
| height: 10px; | |
| background: #4a9eff; | |
| border: 1px solid #fff; | |
| display: none; | |
| } | |
| .draggable-component.selected .resize-handle { | |
| display: block; | |
| } | |
| .resize-handle.nw { top: -5px; left: -5px; cursor: nw-resize; } | |
| .resize-handle.ne { top: -5px; right: -5px; cursor: ne-resize; } | |
| .resize-handle.sw { bottom: -5px; left: -5px; cursor: sw-resize; } | |
| .resize-handle.se { bottom: -5px; right: -5px; cursor: se-resize; } | |
| /* ๋งํ์ ์คํ์ผ๋ค */ | |
| .speech-bubble { | |
| background: white; | |
| border: 2px solid black; | |
| border-radius: 20px; | |
| padding: 30px 15px 15px 15px; | |
| min-width: 100px; | |
| min-height: 50px; | |
| position: relative; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .speech-bubble-tail { | |
| position: absolute; | |
| bottom: -10px; | |
| left: 20px; | |
| width: 0; | |
| height: 0; | |
| border-left: 10px solid transparent; | |
| border-right: 10px solid transparent; | |
| border-top: 10px solid black; | |
| } | |
| .speech-bubble-tail::after { | |
| content: ''; | |
| position: absolute; | |
| bottom: 3px; | |
| left: -10px; | |
| width: 0; | |
| height: 0; | |
| border-left: 10px solid transparent; | |
| border-right: 10px solid transparent; | |
| border-top: 10px solid white; | |
| } | |
| .thought-bubble { | |
| background: white; | |
| border: 2px solid black; | |
| border-radius: 50%; | |
| padding: 40px 20px 20px 20px; | |
| min-width: 120px; | |
| min-height: 80px; | |
| position: relative; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .thought-bubble-dots { | |
| position: absolute; | |
| bottom: -25px; | |
| left: 20px; | |
| } | |
| .thought-dot { | |
| position: absolute; | |
| background: white; | |
| border: 2px solid black; | |
| border-radius: 50%; | |
| } | |
| .thought-dot.large { | |
| width: 20px; | |
| height: 20px; | |
| } | |
| .thought-dot.small { | |
| width: 12px; | |
| height: 12px; | |
| left: -5px; | |
| top: 10px; | |
| } | |
| .shout-bubble { | |
| background: white; | |
| border: 3px solid black; | |
| padding: 30px 15px 15px 15px; | |
| min-width: 100px; | |
| min-height: 50px; | |
| position: relative; | |
| width: 100%; | |
| height: 100%; | |
| clip-path: polygon(10% 0%, 90% 0%, 100% 10%, 100% 90%, 90% 100%, 10% 100%, 0% 90%, 0% 10%); | |
| } | |
| .narration-box { | |
| background: rgba(0, 0, 0, 0.85); | |
| border: 2px solid white; | |
| padding: 30px 15px 15px 15px; | |
| min-width: 150px; | |
| min-height: 40px; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .narration-box .component-text { | |
| color: white ; | |
| } | |
| .text-box { | |
| background: white; | |
| border: 2px solid black; | |
| padding: 20px 10px 10px 10px; | |
| min-width: 100px; | |
| min-height: 40px; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .transparent-box { | |
| background: rgba(255, 255, 255, 0.8); | |
| border: 1px dashed #999; | |
| padding: 20px 10px 10px 10px; | |
| min-width: 100px; | |
| min-height: 40px; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .action-effect { | |
| background: transparent; | |
| border: none; | |
| padding: 20px 10px 10px 10px; | |
| min-width: 80px; | |
| min-height: 40px; | |
| width: 100%; | |
| height: 100%; | |
| font-weight: bold; | |
| text-shadow: 2px 2px 0 white, -2px -2px 0 white, 2px -2px 0 white, -2px 2px 0 white; | |
| } | |
| .emotion-bubble { | |
| background: #ffeb3b; | |
| border: 2px solid black; | |
| border-radius: 15px; | |
| padding: 20px 10px 10px 10px; | |
| min-width: 80px; | |
| min-height: 40px; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .whisper-bubble { | |
| background: #f0f0f0; | |
| border: 1px dashed #666; | |
| border-radius: 15px; | |
| padding: 20px 10px 10px 10px; | |
| min-width: 100px; | |
| min-height: 40px; | |
| width: 100%; | |
| height: 100%; | |
| opacity: 0.9; | |
| } | |
| .gradient-box { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| border: 2px solid white; | |
| border-radius: 10px; | |
| padding: 30px 15px 15px 15px; | |
| min-width: 100px; | |
| min-height: 40px; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .gradient-box .component-text { | |
| color: white ; | |
| } | |
| .component-text { | |
| width: 100%; | |
| height: 100%; | |
| border: none; | |
| background: transparent; | |
| resize: none; | |
| outline: none; | |
| font-family: inherit; | |
| font-size: 14px; | |
| color: black; | |
| text-align: center; | |
| padding: 5px; | |
| overflow: visible; | |
| line-height: 1.4; | |
| } | |
| /* ๋ก๋ฉ ์ธ๋์ผ์ดํฐ */ | |
| .loading { | |
| display: none; | |
| position: fixed; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| background: rgba(0,0,0,0.9); | |
| padding: 30px; | |
| border-radius: 10px; | |
| z-index: 1000; | |
| text-align: center; | |
| } | |
| .loading.active { | |
| display: block; | |
| } | |
| .loading-spinner { | |
| border: 3px solid #f3f3f3; | |
| border-top: 3px solid #4a9eff; | |
| border-radius: 50%; | |
| width: 40px; | |
| height: 40px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 10px; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| /* ์ด๋ฏธ์ง ๊ฐ๊ฒฉ ์กฐ์ */ | |
| .spacing-control { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .spacing-control input[type="range"] { | |
| flex: 1; | |
| } | |
| .spacing-value { | |
| width: 50px; | |
| text-align: center; | |
| } | |
| /* ๊ฐ๋ณ ์ ์ฅ ์ ๋ ฅ */ | |
| .save-individual { | |
| display: flex; | |
| gap: 5px; | |
| margin-top: 10px; | |
| } | |
| .save-individual input { | |
| width: 60px; | |
| } | |
| .save-individual button { | |
| flex: 1; | |
| padding: 5px; | |
| background: #28a745; | |
| border: none; | |
| border-radius: 4px; | |
| color: white; | |
| cursor: pointer; | |
| } | |
| .save-individual button:hover { | |
| background: #5cbf5c; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <!-- ์ข์ธก ํด๋ฐ --> | |
| <div class="toolbar"> | |
| <h2>๐จ ์นํฐ ํธ์ง ๋๊ตฌ</h2> | |
| <!-- ํ์ผ ์ ๋ก๋ --> | |
| <div class="tool-section"> | |
| <h3>๐ ์ด๋ฏธ์ง ์ ๋ก๋</h3> | |
| <div class="file-upload" onclick="document.getElementById('fileInput').click()"> | |
| <svg width="40" height="40" fill="#4a9eff"> | |
| <rect x="5" y="10" width="30" height="25" fill="none" stroke="currentColor" stroke-width="2"/> | |
| <polyline points="15,25 20,20 25,25" fill="none" stroke="currentColor" stroke-width="2"/> | |
| <line x1="20" y1="20" x2="20" y2="30" stroke="currentColor" stroke-width="2"/> | |
| </svg> | |
| <p>ํด๋ฆญํ์ฌ ์ด๋ฏธ์ง ์ ํ</p> | |
| <p style="font-size: 11px; color: #888; margin-top: 5px;">ํ์ผ๋ช ๊ธฐ์ค ์๋ ์ ๋ ฌ (1.jpg, 2.jpg...)</p> | |
| <input type="file" id="fileInput" multiple accept="image/*"> | |
| </div> | |
| <div id="fileList"></div> | |
| <!-- ์ด๋ฏธ์ง ๊ฐ๊ฒฉ ์กฐ์ --> | |
| <div class="property-group"> | |
| <label>์ด๋ฏธ์ง ๊ฐ๊ฒฉ</label> | |
| <div class="spacing-control"> | |
| <input type="range" id="imageSpacing" min="0" max="100" value="20" onchange="updateImageSpacing()"> | |
| <span class="spacing-value" id="spacingValue">20px</span> | |
| </div> | |
| </div> | |
| <!-- ๊ฐ๋ณ ์ด๋ฏธ์ง ์ ์ฅ --> | |
| <div class="property-group"> | |
| <label>๊ฐ๋ณ ์ด๋ฏธ์ง ์ ์ฅ</label> | |
| <div class="save-individual"> | |
| <input type="number" id="imageNumber" placeholder="๋ฒํธ" min="1"> | |
| <button onclick="exportSingleImage()">์ ์ฅ</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ์ปดํฌ๋ํธ --> | |
| <div class="tool-section"> | |
| <h3>๐ฌ ๋งํ์ & ํจ๊ณผ</h3> | |
| <div class="component-grid"> | |
| <div class="component-btn" onclick="addComponent('speech')"> | |
| <svg viewBox="0 0 100 100" fill="none" stroke="white" stroke-width="2"> | |
| <ellipse cx="50" cy="40" rx="40" ry="30"/> | |
| <path d="M 30 55 L 25 75 L 40 55"/> | |
| </svg> | |
| <span>๋งํ์ </span> | |
| </div> | |
| <div class="component-btn" onclick="addComponent('thought')"> | |
| <svg viewBox="0 0 100 100" fill="none" stroke="white" stroke-width="2"> | |
| <ellipse cx="50" cy="40" rx="35" ry="25"/> | |
| <circle cx="30" cy="65" r="8"/> | |
| <circle cx="20" cy="75" r="5"/> | |
| </svg> | |
| <span>์๊ฐํ์ </span> | |
| </div> | |
| <div class="component-btn" onclick="addComponent('shout')"> | |
| <svg viewBox="0 0 100 100" fill="none" stroke="white" stroke-width="3"> | |
| <polygon points="50,10 90,30 85,70 50,90 15,70 10,30"/> | |
| </svg> | |
| <span>์ธ์นจํ์ </span> | |
| </div> | |
| <div class="component-btn" onclick="addComponent('whisper')"> | |
| <svg viewBox="0 0 100 100" fill="none" stroke="white" stroke-width="1" stroke-dasharray="3,3"> | |
| <ellipse cx="50" cy="45" rx="35" ry="25"/> | |
| </svg> | |
| <span>์์ญ์</span> | |
| </div> | |
| <div class="component-btn" onclick="addComponent('emotion')"> | |
| <svg viewBox="0 0 100 100" fill="#ffeb3b" stroke="white" stroke-width="2"> | |
| <ellipse cx="50" cy="45" rx="35" ry="25"/> | |
| </svg> | |
| <span>๊ฐ์ ํํ</span> | |
| </div> | |
| <div class="component-btn" onclick="addComponent('action')"> | |
| <svg viewBox="0 0 100 100" fill="none" stroke="white" stroke-width="2"> | |
| <text x="50" y="50" text-anchor="middle" font-size="30" fill="white">!</text> | |
| </svg> | |
| <span>ํจ๊ณผ์</span> | |
| </div> | |
| <div class="component-btn" onclick="addComponent('narration')"> | |
| <svg viewBox="0 0 100 100" fill="#333" stroke="white" stroke-width="2"> | |
| <rect x="15" y="30" width="70" height="40"/> | |
| </svg> | |
| <span>๋๋ ์ด์ </span> | |
| </div> | |
| <div class="component-btn" onclick="addComponent('box')"> | |
| <svg viewBox="0 0 100 100" fill="none" stroke="white" stroke-width="2"> | |
| <rect x="20" y="30" width="60" height="40"/> | |
| </svg> | |
| <span>ํ ์คํธ๋ฐ์ค</span> | |
| </div> | |
| <div class="component-btn" onclick="addComponent('transparent')"> | |
| <svg viewBox="0 0 100 100" fill="none" stroke="white" stroke-width="2" stroke-dasharray="5,5"> | |
| <rect x="20" y="30" width="60" height="40"/> | |
| </svg> | |
| <span>ํฌ๋ช ๋ฐ์ค</span> | |
| </div> | |
| <div class="component-btn" onclick="addComponent('gradient')"> | |
| <svg viewBox="0 0 100 100"> | |
| <defs> | |
| <linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%"> | |
| <stop offset="0%" style="stop-color:#667eea;stop-opacity:1" /> | |
| <stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" /> | |
| </linearGradient> | |
| </defs> | |
| <rect x="20" y="30" width="60" height="40" fill="url(#grad1)" stroke="white" stroke-width="2"/> | |
| </svg> | |
| <span>๊ทธ๋ผ๋์ธํธ</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ์์ฑ ํธ์ง --> | |
| <div class="tool-section"> | |
| <h3>โ๏ธ ์์ฑ ํธ์ง</h3> | |
| <div class="properties-panel"> | |
| <div class="property-group"> | |
| <label>ํฐํธ</label> | |
| <select id="fontSelect" onchange="updateSelectedComponent()"> | |
| <optgroup label="๊ธฐ๋ณธ ํฐํธ"> | |
| <option value="Arial">Arial</option> | |
| <option value="Verdana">Verdana</option> | |
| <option value="Georgia">Georgia</option> | |
| <option value="'Times New Roman'">Times New Roman</option> | |
| <option value="'Courier New'">Courier New</option> | |
| <option value="'Comic Sans MS'">Comic Sans MS</option> | |
| </optgroup> | |
| <optgroup label="ํ๊ธ ํฐํธ"> | |
| <option value="'Noto Sans KR'">๋ ธํ ์ฐ์ค</option> | |
| <option value="'Black Han Sans'">๊ฒ์๊ณ ๋</option> | |
| <option value="'Jua'">์ฃผ์</option> | |
| <option value="'Do Hyeon'">๋ํ</option> | |
| <option value="'Sunflower'">ํด๋ฐ๋ผ๊ธฐ</option> | |
| <option value="'Cute Font'">๊ท์ฌ์ด ๊ธ๊ผด</option> | |
| <option value="'Gamja Flower'">๊ฐ์๊ฝ</option> | |
| <option value="'Hi Melody'">ํ์ด ๋ฉ๋ก๋</option> | |
| <option value="'Nanum Pen Script'">๋๋ํ</option> | |
| <option value="'Stylish'">์คํ์ผ๋ฆฌ์</option> | |
| <option value="'Single Day'">์ฑ๊ธ๋ฐ์ด</option> | |
| <option value="'Gaegu'">๊ฐ๊ตฌ์์ด</option> | |
| </optgroup> | |
| </select> | |
| </div> | |
| <div class="property-group"> | |
| <label>๊ธ์ ํฌ๊ธฐ</label> | |
| <input type="number" id="fontSizeInput" value="14" min="8" max="72" onchange="updateSelectedComponent()"> | |
| </div> | |
| <div class="property-group"> | |
| <label>๊ธ์ ๊ตต๊ธฐ</label> | |
| <select id="fontWeightSelect" onchange="updateSelectedComponent()"> | |
| <option value="100">์์ฃผ ์๊ฒ</option> | |
| <option value="300">์๊ฒ</option> | |
| <option value="400" selected>๋ณดํต</option> | |
| <option value="500">์ฝ๊ฐ ๊ตต๊ฒ</option> | |
| <option value="700">๊ตต๊ฒ</option> | |
| <option value="900">์์ฃผ ๊ตต๊ฒ</option> | |
| </select> | |
| </div> | |
| <div class="property-group"> | |
| <label>๊ธ์ ์์</label> | |
| <div class="color-picker"> | |
| <div class="color-option selected" style="background: black" onclick="selectColor(this, 'black')"></div> | |
| <div class="color-option" style="background: white; border: 1px solid #ccc;" onclick="selectColor(this, 'white')"></div> | |
| <div class="color-option" style="background: #ff0000" onclick="selectColor(this, '#ff0000')"></div> | |
| <div class="color-option" style="background: #ff6b6b" onclick="selectColor(this, '#ff6b6b')"></div> | |
| <div class="color-option" style="background: #0066ff" onclick="selectColor(this, '#0066ff')"></div> | |
| <div class="color-option" style="background: #4ecdc4" onclick="selectColor(this, '#4ecdc4')"></div> | |
| <div class="color-option" style="background: #00b300" onclick="selectColor(this, '#00b300')"></div> | |
| <div class="color-option" style="background: #95e1d3" onclick="selectColor(this, '#95e1d3')"></div> | |
| <div class="color-option" style="background: #ffe100" onclick="selectColor(this, '#ffe100')"></div> | |
| <div class="color-option" style="background: #ff9800" onclick="selectColor(this, '#ff9800')"></div> | |
| <div class="color-option" style="background: #9c27b0" onclick="selectColor(this, '#9c27b0')"></div> | |
| <div class="color-option" style="background: #ff00ff" onclick="selectColor(this, '#ff00ff')"></div> | |
| </div> | |
| </div> | |
| <div class="property-group"> | |
| <label>ํ ์คํธ</label> | |
| <textarea id="textInput" rows="3" placeholder="ํ ์คํธ ์ ๋ ฅ..." onkeyup="updateSelectedComponent()"></textarea> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ๋ฉ์ธ ์์ ์์ญ --> | |
| <div class="workspace"> | |
| <div class="workspace-header"> | |
| <h1 style="font-size: 20px;">๐จ ์นํฐ ํธ์ง ์คํ๋์ค Pro</h1> | |
| <div class="workspace-tools"> | |
| <button class="tool-btn" onclick="deleteSelected()">๐๏ธ ์ ํ ์ญ์ </button> | |
| <button class="tool-btn" onclick="clearAll()">๐ ์ ์ฒด ์ด๊ธฐํ</button> | |
| <button class="tool-btn primary" onclick="exportSingleImage()">๐พ ์ด๋ฏธ์ง ์ ์ฅ</button> | |
| </div> | |
| </div> | |
| <div class="canvas-container"> | |
| <div class="canvas-wrapper" id="canvas"> | |
| <!-- ์ ๋ก๋๋ ์ด๋ฏธ์ง์ ์ปดํฌ๋ํธ๋ค์ด ์ฌ๊ธฐ์ ์ถ๊ฐ๋ฉ๋๋ค --> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="loading" id="loading"> | |
| <div class="loading-spinner"></div> | |
| <p>์ฒ๋ฆฌ ์ค...</p> | |
| </div> | |
| <!-- html2canvas ๋ผ์ด๋ธ๋ฌ๋ฆฌ --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> | |
| <script> | |
| let selectedComponent = null; | |
| let isDragging = false; | |
| let isResizing = false; | |
| let dragOffset = { x: 0, y: 0 }; | |
| let componentCounter = 0; | |
| let selectedColor = 'black'; | |
| let uploadedImages = []; | |
| // ์์ฐ์ค๋ฌ์ด ์ ๋ ฌ ํจ์ (์ซ์๊ฐ ํฌํจ๋ ํ์ผ๋ช ์ ๋ ฌ) | |
| function naturalSort(a, b) { | |
| // ํ์ผ๋ช ์์ ์ซ์ ์ถ์ถ | |
| const extractNumber = (filename) => { | |
| const match = filename.match(/(\d+)/); | |
| return match ? parseInt(match[1]) : 0; | |
| }; | |
| const aNum = extractNumber(a.name); | |
| const bNum = extractNumber(b.name); | |
| // ์ซ์๋ก ๋น๊ต | |
| if (aNum !== bNum) { | |
| return aNum - bNum; | |
| } | |
| // ์ซ์๊ฐ ๊ฐ์ผ๋ฉด ์ ์ฒด ํ์ผ๋ช ์ผ๋ก ๋น๊ต | |
| return a.name.localeCompare(b.name); | |
| } | |
| // ํ์ผ ์ ๋ก๋ ์ฒ๋ฆฌ | |
| document.getElementById('fileInput').addEventListener('change', async function(e) { | |
| const files = Array.from(e.target.files); | |
| // ์์ฐ์ค๋ฌ์ด ์ ๋ ฌ (1.jpg, 2.jpg, 10.jpg ์์๋ก) | |
| files.sort(naturalSort); | |
| const canvas = document.getElementById('canvas'); | |
| // ๊ธฐ์กด ์ด๋ฏธ์ง ์ ๊ฑฐ | |
| canvas.querySelectorAll('.image-container').forEach(container => container.remove()); | |
| uploadedImages = []; | |
| // ํ์ผ ๋ฆฌ์คํธ HTML | |
| let fileListHTML = '<div class="file-list">'; | |
| // ๋ชจ๋ ์ด๋ฏธ์ง๋ฅผ ์์๋๋ก ๋ก๋ํ๊ธฐ ์ํ Promise ๋ฐฐ์ด | |
| const loadPromises = files.map((file, index) => { | |
| return new Promise((resolve) => { | |
| if (file.type.startsWith('image/')) { | |
| const reader = new FileReader(); | |
| reader.onload = function(event) { | |
| resolve({ | |
| index: index, | |
| src: event.target.result, | |
| name: file.name | |
| }); | |
| }; | |
| reader.readAsDataURL(file); | |
| } else { | |
| resolve(null); | |
| } | |
| }); | |
| }); | |
| // ๋ชจ๋ ์ด๋ฏธ์ง ๋ก๋ ์๋ฃ ํ ์์๋๋ก ์ถ๊ฐ | |
| const loadedImages = await Promise.all(loadPromises); | |
| loadedImages.forEach((imageData, index) => { | |
| if (imageData) { | |
| // ์ด๋ฏธ์ง ์ปจํ ์ด๋ ์์ฑ | |
| const container = document.createElement('div'); | |
| container.className = 'image-container'; | |
| container.id = 'image-container-' + (index + 1); | |
| // ๋๋ฒ๋ง ํ์ | |
| const numberLabel = document.createElement('div'); | |
| numberLabel.className = 'image-number'; | |
| numberLabel.textContent = index + 1; | |
| // ์ด๋ฏธ์ง ์์ฑ | |
| const img = document.createElement('img'); | |
| img.src = imageData.src; | |
| img.className = 'webtoon-image'; | |
| img.alt = imageData.name; | |
| img.id = 'webtoon-image-' + (index + 1); | |
| container.appendChild(numberLabel); | |
| container.appendChild(img); | |
| // ์ด๋ฏธ์ง๋ฅผ ์์๋๋ก ์ถ๊ฐ | |
| canvas.appendChild(container); | |
| // ์ ๋ก๋๋ ์ด๋ฏธ์ง ์ ๋ณด ์ ์ฅ | |
| uploadedImages.push({ | |
| id: index + 1, | |
| name: imageData.name, | |
| element: img | |
| }); | |
| // ํ์ผ ๋ฆฌ์คํธ์ ์ถ๊ฐ | |
| fileListHTML += `<div class="file-item">${index + 1}. ${imageData.name}</div>`; | |
| } | |
| }); | |
| fileListHTML += '</div>'; | |
| // ํ์ผ ๋ฆฌ์คํธ ํ์ | |
| const fileList = document.getElementById('fileList'); | |
| fileList.innerHTML = '<p style="font-size: 12px; color: #4a9eff; margin-bottom: 10px;">โ ์ ๋ก๋๋ ํ์ผ: ' + files.length + '๊ฐ</p>' + fileListHTML; | |
| updateImageSpacing(); | |
| }); | |
| // ์ด๋ฏธ์ง ๊ฐ๊ฒฉ ์ ๋ฐ์ดํธ | |
| function updateImageSpacing() { | |
| const spacing = document.getElementById('imageSpacing').value; | |
| document.getElementById('spacingValue').textContent = spacing + 'px'; | |
| document.querySelectorAll('.webtoon-image').forEach(img => { | |
| img.style.marginBottom = spacing + 'px'; | |
| }); | |
| } | |
| // ์ปดํฌ๋ํธ ์ถ๊ฐ | |
| function addComponent(type) { | |
| const canvas = document.getElementById('canvas'); | |
| const component = document.createElement('div'); | |
| component.className = 'draggable-component'; | |
| component.id = 'component-' + componentCounter++; | |
| // ์บ๋ฒ์ค์ ์คํฌ๋กค ์์น๋ฅผ ๊ณ ๋ คํ ์ด๊ธฐ ์์น | |
| const canvasRect = canvas.getBoundingClientRect(); | |
| const scrollTop = document.querySelector('.canvas-container').scrollTop; | |
| component.style.left = '50px'; | |
| component.style.top = (scrollTop + 50) + 'px'; | |
| component.style.width = '150px'; | |
| component.style.height = '80px'; | |
| let innerContent = ''; | |
| switch(type) { | |
| case 'speech': | |
| innerContent = ` | |
| <div class="speech-bubble"> | |
| <textarea class="component-text" placeholder="๋์ฌ ์ ๋ ฅ..."></textarea> | |
| <div class="speech-bubble-tail"></div> | |
| </div>`; | |
| break; | |
| case 'thought': | |
| innerContent = ` | |
| <div class="thought-bubble"> | |
| <textarea class="component-text" placeholder="์๊ฐ ์ ๋ ฅ..."></textarea> | |
| <div class="thought-bubble-dots"> | |
| <div class="thought-dot large"></div> | |
| <div class="thought-dot small"></div> | |
| </div> | |
| </div>`; | |
| break; | |
| case 'shout': | |
| innerContent = ` | |
| <div class="shout-bubble"> | |
| <textarea class="component-text" placeholder="์ธ์นจ!" style="font-weight: bold;"></textarea> | |
| </div>`; | |
| break; | |
| case 'whisper': | |
| innerContent = ` | |
| <div class="whisper-bubble"> | |
| <textarea class="component-text" placeholder="์์ญ์..." style="font-style: italic;"></textarea> | |
| </div>`; | |
| break; | |
| case 'emotion': | |
| innerContent = ` | |
| <div class="emotion-bubble"> | |
| <textarea class="component-text" placeholder="โฅ โช โ "></textarea> | |
| </div>`; | |
| break; | |
| case 'action': | |
| innerContent = ` | |
| <div class="action-effect"> | |
| <textarea class="component-text" placeholder="์ฟต!" style="font-size: 24px; font-weight: bold;"></textarea> | |
| </div>`; | |
| break; | |
| case 'narration': | |
| innerContent = ` | |
| <div class="narration-box"> | |
| <textarea class="component-text" placeholder="๋๋ ์ด์ ..."></textarea> | |
| </div>`; | |
| break; | |
| case 'box': | |
| innerContent = ` | |
| <div class="text-box"> | |
| <textarea class="component-text" placeholder="ํ ์คํธ ์ ๋ ฅ..."></textarea> | |
| </div>`; | |
| break; | |
| case 'transparent': | |
| innerContent = ` | |
| <div class="transparent-box"> | |
| <textarea class="component-text" placeholder="ํ ์คํธ ์ ๋ ฅ..."></textarea> | |
| </div>`; | |
| break; | |
| case 'gradient': | |
| innerContent = ` | |
| <div class="gradient-box"> | |
| <textarea class="component-text" placeholder="ํน์ ํจ๊ณผ!"></textarea> | |
| </div>`; | |
| break; | |
| } | |
| component.innerHTML = innerContent + ` | |
| <div class="resize-handle nw"></div> | |
| <div class="resize-handle ne"></div> | |
| <div class="resize-handle sw"></div> | |
| <div class="resize-handle se"></div> | |
| `; | |
| canvas.appendChild(component); | |
| // ์ด๋ฒคํธ ๋ฆฌ์ค๋ ์ถ๊ฐ | |
| component.addEventListener('mousedown', startDrag); | |
| // ํ ์คํธ ์์ญ ํด๋ฆญ ์ ๋๋๊ทธ ๋ฐฉ์ง | |
| const textArea = component.querySelector('.component-text'); | |
| if (textArea) { | |
| textArea.addEventListener('mousedown', function(e) { | |
| e.stopPropagation(); | |
| }); | |
| textArea.addEventListener('click', function(e) { | |
| e.stopPropagation(); | |
| selectComponent(component); | |
| }); | |
| } | |
| // ๋ฆฌ์ฌ์ด์ฆ ํธ๋ค ์ด๋ฒคํธ | |
| component.querySelectorAll('.resize-handle').forEach(handle => { | |
| handle.addEventListener('mousedown', startResize); | |
| }); | |
| // ์๋ ์ ํ | |
| selectComponent(component); | |
| } | |
| // ์ปดํฌ๋ํธ ์ ํ | |
| function selectComponent(component) { | |
| // ์ด์ ์ ํ ํด์ | |
| document.querySelectorAll('.draggable-component').forEach(c => { | |
| c.classList.remove('selected'); | |
| }); | |
| // ์๋ก์ด ์ปดํฌ๋ํธ ์ ํ | |
| component.classList.add('selected'); | |
| selectedComponent = component; | |
| // ์์ฑ ํจ๋ ์ ๋ฐ์ดํธ | |
| const textArea = component.querySelector('.component-text'); | |
| if (textArea) { | |
| document.getElementById('textInput').value = textArea.value; | |
| document.getElementById('fontSizeInput').value = parseInt(textArea.style.fontSize) || 14; | |
| document.getElementById('fontSelect').value = textArea.style.fontFamily || "'Noto Sans KR'"; | |
| document.getElementById('fontWeightSelect').value = textArea.style.fontWeight || '400'; | |
| } | |
| } | |
| // ๋๋๊ทธ ์์ | |
| function startDrag(e) { | |
| if (e.target.classList.contains('resize-handle')) return; | |
| if (e.target.classList.contains('component-text')) return; | |
| const component = e.currentTarget; | |
| selectComponent(component); | |
| isDragging = true; | |
| const rect = component.getBoundingClientRect(); | |
| dragOffset.x = e.clientX - rect.left; | |
| dragOffset.y = e.clientY - rect.top; | |
| // ๋๋๊ทธ ์ค ํฌ์ธํฐ ์ด๋ฒคํธ ๋นํ์ฑํ | |
| component.style.pointerEvents = 'none'; | |
| document.addEventListener('mousemove', drag); | |
| document.addEventListener('mouseup', stopDrag); | |
| e.preventDefault(); | |
| } | |
| // ๋๋๊ทธ ์ค | |
| function drag(e) { | |
| if (!isDragging || !selectedComponent) return; | |
| const canvas = document.getElementById('canvas'); | |
| const canvasRect = canvas.getBoundingClientRect(); | |
| const containerScrollTop = document.querySelector('.canvas-container').scrollTop; | |
| let newX = e.clientX - canvasRect.left - dragOffset.x; | |
| let newY = e.clientY - canvasRect.top - dragOffset.y + containerScrollTop; | |
| // ์บ๋ฒ์ค ๋ฒ์ ๋ด๋ก ์ ํ | |
| newX = Math.max(0, Math.min(newX, canvas.offsetWidth - selectedComponent.offsetWidth)); | |
| newY = Math.max(0, newY); | |
| selectedComponent.style.left = newX + 'px'; | |
| selectedComponent.style.top = newY + 'px'; | |
| e.preventDefault(); | |
| } | |
| // ๋๋๊ทธ ์ข ๋ฃ | |
| function stopDrag(e) { | |
| if (selectedComponent) { | |
| selectedComponent.style.pointerEvents = 'auto'; | |
| } | |
| isDragging = false; | |
| document.removeEventListener('mousemove', drag); | |
| document.removeEventListener('mouseup', stopDrag); | |
| e.preventDefault(); | |
| } | |
| // ๋ฆฌ์ฌ์ด์ฆ ์์ | |
| function startResize(e) { | |
| e.stopPropagation(); | |
| isResizing = true; | |
| const handle = e.target; | |
| const component = handle.parentElement; | |
| selectComponent(component); | |
| const startX = e.clientX; | |
| const startY = e.clientY; | |
| const startWidth = component.offsetWidth; | |
| const startHeight = component.offsetHeight; | |
| const startLeft = component.offsetLeft; | |
| const startTop = component.offsetTop; | |
| function resize(e) { | |
| if (!isResizing) return; | |
| const dx = e.clientX - startX; | |
| const dy = e.clientY - startY; | |
| if (handle.classList.contains('se')) { | |
| component.style.width = Math.max(80, startWidth + dx) + 'px'; | |
| component.style.height = Math.max(40, startHeight + dy) + 'px'; | |
| } else if (handle.classList.contains('sw')) { | |
| component.style.width = Math.max(80, startWidth - dx) + 'px'; | |
| component.style.height = Math.max(40, startHeight + dy) + 'px'; | |
| component.style.left = Math.min(startLeft + dx, startLeft + startWidth - 80) + 'px'; | |
| } else if (handle.classList.contains('ne')) { | |
| component.style.width = Math.max(80, startWidth + dx) + 'px'; | |
| component.style.height = Math.max(40, startHeight - dy) + 'px'; | |
| component.style.top = Math.min(startTop + dy, startTop + startHeight - 40) + 'px'; | |
| } else if (handle.classList.contains('nw')) { | |
| component.style.width = Math.max(80, startWidth - dx) + 'px'; | |
| component.style.height = Math.max(40, startHeight - dy) + 'px'; | |
| component.style.left = Math.min(startLeft + dx, startLeft + startWidth - 80) + 'px'; | |
| component.style.top = Math.min(startTop + dy, startTop + startHeight - 40) + 'px'; | |
| } | |
| } | |
| function stopResize() { | |
| isResizing = false; | |
| document.removeEventListener('mousemove', resize); | |
| document.removeEventListener('mouseup', stopResize); | |
| } | |
| document.addEventListener('mousemove', resize); | |
| document.addEventListener('mouseup', stopResize); | |
| } | |
| // ์ ํ๋ ์ปดํฌ๋ํธ ์ ๋ฐ์ดํธ | |
| function updateSelectedComponent() { | |
| if (!selectedComponent) return; | |
| const textArea = selectedComponent.querySelector('.component-text'); | |
| if (textArea) { | |
| textArea.value = document.getElementById('textInput').value; | |
| textArea.style.fontSize = document.getElementById('fontSizeInput').value + 'px'; | |
| textArea.style.fontFamily = document.getElementById('fontSelect').value; | |
| textArea.style.fontWeight = document.getElementById('fontWeightSelect').value; | |
| textArea.style.color = selectedColor; | |
| } | |
| } | |
| // ์์ ์ ํ | |
| function selectColor(element, color) { | |
| document.querySelectorAll('.color-option').forEach(opt => { | |
| opt.classList.remove('selected'); | |
| }); | |
| element.classList.add('selected'); | |
| selectedColor = color; | |
| updateSelectedComponent(); | |
| } | |
| // ์ ํ๋ ์ปดํฌ๋ํธ ์ญ์ | |
| function deleteSelected() { | |
| if (selectedComponent) { | |
| selectedComponent.remove(); | |
| selectedComponent = null; | |
| document.getElementById('textInput').value = ''; | |
| } | |
| } | |
| // ์ ์ฒด ์ด๊ธฐํ | |
| function clearAll() { | |
| if (confirm('๋ชจ๋ ์ปดํฌ๋ํธ๋ฅผ ์ญ์ ํ์๊ฒ ์ต๋๊น?')) { | |
| document.querySelectorAll('.draggable-component').forEach(c => c.remove()); | |
| selectedComponent = null; | |
| document.getElementById('textInput').value = ''; | |
| } | |
| } | |
| // ์ ์ฒด ์ด๋ฏธ์ง ์ ์ฅ ๊ธฐ๋ฅ ์ ๊ฑฐ๋จ | |
| // ๊ฐ๋ณ ์ด๋ฏธ์ง ์ ์ฅ | |
| function exportSingleImage() { | |
| const imageNumber = parseInt(document.getElementById('imageNumber').value); | |
| if (!imageNumber || imageNumber < 1) { | |
| alert('โ ๏ธ ์ ์ฅํ ์ด๋ฏธ์ง ๋ฒํธ๋ฅผ ์ ๋ ฅํด์ฃผ์ธ์.'); | |
| return; | |
| } | |
| const imageContainer = document.getElementById('image-container-' + imageNumber); | |
| if (!imageContainer) { | |
| alert('โ ๏ธ ' + imageNumber + '๋ฒ ์ด๋ฏธ์ง๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.'); | |
| return; | |
| } | |
| const loading = document.getElementById('loading'); | |
| loading.classList.add('active'); | |
| // ์ ํ ํด์ ๋ฐ ๋ฒํธ ์จ๊ธฐ๊ธฐ | |
| document.querySelectorAll('.draggable-component').forEach(c => { | |
| c.classList.remove('selected'); | |
| }); | |
| const numberLabel = imageContainer.querySelector('.image-number'); | |
| if (numberLabel) numberLabel.style.display = 'none'; | |
| // ์์ ์ปจํ ์ด๋ ์์ฑ | |
| const tempContainer = document.createElement('div'); | |
| tempContainer.style.position = 'relative'; | |
| tempContainer.style.width = imageContainer.offsetWidth + 'px'; | |
| tempContainer.style.background = 'white'; | |
| // ์ด๋ฏธ์ง ๋ณต์ฌ | |
| const imgClone = imageContainer.querySelector('.webtoon-image').cloneNode(true); | |
| imgClone.style.marginBottom = '0'; | |
| tempContainer.appendChild(imgClone); | |
| // ํด๋น ์ด๋ฏธ์ง ์์ ์ปดํฌ๋ํธ๋ค๋ง ๋ณต์ฌ | |
| const imageRect = imageContainer.getBoundingClientRect(); | |
| const canvasRect = document.getElementById('canvas').getBoundingClientRect(); | |
| const imageTop = imageContainer.offsetTop; | |
| const imageBottom = imageTop + imageContainer.offsetHeight; | |
| document.querySelectorAll('.draggable-component').forEach(component => { | |
| const compTop = parseInt(component.style.top); | |
| const compBottom = compTop + component.offsetHeight; | |
| // ์ปดํฌ๋ํธ๊ฐ ์ด๋ฏธ์ง ์์ญ ๋ด์ ์๋์ง ํ์ธ | |
| if (compTop < imageBottom && compBottom > imageTop) { | |
| const compClone = component.cloneNode(true); | |
| compClone.classList.remove('selected'); | |
| compClone.style.top = (compTop - imageTop) + 'px'; | |
| tempContainer.appendChild(compClone); | |
| } | |
| }); | |
| // ์์๋ก DOM์ ์ถ๊ฐ (๋ณด์ด์ง ์๊ฒ) | |
| tempContainer.style.position = 'absolute'; | |
| tempContainer.style.left = '-9999px'; | |
| document.body.appendChild(tempContainer); | |
| html2canvas(tempContainer, { | |
| backgroundColor: '#ffffff', | |
| scale: 2, | |
| useCORS: true, | |
| logging: false | |
| }).then(function(canvas) { | |
| // ์ด๋ฏธ์ง ๋ค์ด๋ก๋ | |
| const link = document.createElement('a'); | |
| link.download = 'webtoon_' + imageNumber + '_' + new Date().getTime() + '.png'; | |
| link.href = canvas.toDataURL('image/png'); | |
| link.click(); | |
| // ์์ ์ปจํ ์ด๋ ์ ๊ฑฐ | |
| document.body.removeChild(tempContainer); | |
| // ๋ฒํธ ๋ค์ ํ์ | |
| if (numberLabel) numberLabel.style.display = 'flex'; | |
| loading.classList.remove('active'); | |
| // ์ฑ๊ณต ๋ฉ์์ง | |
| alert('โ ' + imageNumber + '๋ฒ ์ด๋ฏธ์ง๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์ ์ฅ๋์์ต๋๋ค!'); | |
| }).catch(function(error) { | |
| document.body.removeChild(tempContainer); | |
| if (numberLabel) numberLabel.style.display = 'flex'; | |
| loading.classList.remove('active'); | |
| console.error('Error:', error); | |
| alert('โ ๏ธ ์ด๋ฏธ์ง ์ ์ฅ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.'); | |
| }); | |
| } | |
| // ์บ๋ฒ์ค ํด๋ฆญ ์ด๋ฒคํธ (๋น ๊ณต๊ฐ ํด๋ฆญ ์ ์ ํ ํด์ ) | |
| document.getElementById('canvas').addEventListener('click', function(e) { | |
| if (e.target === this || e.target.classList.contains('webtoon-image')) { | |
| document.querySelectorAll('.draggable-component').forEach(c => { | |
| c.classList.remove('selected'); | |
| }); | |
| selectedComponent = null; | |
| document.getElementById('textInput').value = ''; | |
| } | |
| }); | |
| // ํค๋ณด๋ ๋จ์ถํค | |
| document.addEventListener('keydown', function(e) { | |
| if (e.key === 'Delete' && selectedComponent) { | |
| deleteSelected(); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |