Spaces:
Running
Running
design me an app where i can add comic book style thought bubbles to existing pictures that i choose and upload, and automatically put them into a comic book context, so they look like you're reading a manga. It should be able to accept pictures in any format, the ability to flip and crop them, or resize them, and it should then have a half dozen page layout templates to choose from, and when you drag/drop which photos you want to which frames you want, you can then add text speech or thought bubbles that you can customize with any font intalled locally in your system, and the app will automatically resize it to work within the size of the speech bubble youve chosen, and it won't let you add any more characters than can fit in the font style and size you've chosen, but make sure it does all that automatically, and allows light and dark mode, should have an impressive ui and smooth-- needs to befast because image editing-- needs to have integration with ComfyUI so that if you have a comfy local install you can just tell it the port it's on, and it'll auto pull any new generated images and put them into your library which should be stored locally in the user's browsercache and it should be authenticatged with OAuth only and it should have a really catchy and clever name but it should be all white red and black. tailwind.css, react, python, flask, postgre, mongodb-- these are all tools you can use and have at your disposal- you can even use things i haven't mentioned, like any .js librarie you can imagine, like D3.js, gsap animations, three.js, particle.js, next.js-- doesn't matter, as long as it makes the site work better.. vanilla javascript, javascript, etc, etc. websockets (though i dont know what for-- maybe for getting real time generation progress from the local comfy install? anyways, there you go.
c5a3464 verified | <html lang="en" class="h-full"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>MangaMatic Pro - Transform Photos into Manga Masterpieces</title> | |
| <link rel="icon" type="image/x-icon" href="/static/favicon.ico"> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.0/fabric.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script> | |
| <script> | |
| tailwind.config = { | |
| darkMode: 'class', | |
| theme: { | |
| extend: { | |
| colors: { | |
| primary: '#dc2626', | |
| secondary: '#000000', | |
| accent: '#ffffff' | |
| }, | |
| fontFamily: { | |
| 'manga': ['Comic Neue', 'cursive'], | |
| 'japanese': ['Noto Sans JP', 'sans-serif'] | |
| }, | |
| animation: { | |
| 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite', | |
| 'bounce-slow': 'bounce 2s infinite', | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Comic+Neue:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&family=Noto+Sans+JP:wght@100;300;400;500;700;900&display=swap'); | |
| .manga-border { | |
| border: 3px solid #000; | |
| box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.3); | |
| } | |
| .speech-bubble { | |
| background: white; | |
| border: 2px solid black; | |
| border-radius: 20px; | |
| position: relative; | |
| padding: 15px; | |
| } | |
| .speech-bubble:after { | |
| content: ''; | |
| position: absolute; | |
| border-style: solid; | |
| border-width: 15px 15px 0; | |
| border-color: white transparent; | |
| display: block; | |
| width: 0; | |
| z-index: 1; | |
| bottom: -15px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| } | |
| .thought-bubble { | |
| background: white; | |
| border: 2px solid black; | |
| border-radius: 50%; | |
| position: relative; | |
| padding: 15px; | |
| } | |
| .thought-bubble:before, .thought-bubble:after { | |
| content: ''; | |
| background: white; | |
| border-radius: 50%; | |
| border: 2px solid black; | |
| position: absolute; | |
| } | |
| .thought-bubble:before { | |
| width: 10px; | |
| height: 10px; | |
| bottom: -15px; | |
| left: 30%; | |
| } | |
| .thought-bubble:after { | |
| width: 6px; | |
| height: 6px; | |
| bottom: -25px; | |
| left: 40%; | |
| } | |
| .drop-zone { | |
| border: 2px dashed #dc2626; | |
| transition: all 0.3s ease; | |
| } | |
| .drop-zone.dragover { | |
| border-color: #000; | |
| background-color: rgba(220, 38, 38, 0.1); | |
| } | |
| .comic-panel { | |
| background: white; | |
| border: 3px solid black; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .page-turn { | |
| animation: pageTurn 0.6s ease-in-out; | |
| } | |
| @keyframes pageTurn { | |
| 0% { transform: rotateY(0deg); } | |
| 50% { transform: rotateY(90deg); } | |
| 100% { transform: rotateY(0deg); } | |
| } | |
| .manga-text { | |
| font-family: 'Comic Neue', cursive; | |
| text-shadow: 2px 2px 0px rgba(0,0,0,0.1); | |
| } | |
| </style> | |
| </head> | |
| <body class="h-full bg-white dark:bg-black transition-colors duration-300"> | |
| <!-- Navigation --> | |
| <nav class="bg-white dark:bg-black border-b-2 border-black sticky top-0 z-50"> | |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | |
| <div class="flex justify-between h-16"> | |
| <div class="flex items-center"> | |
| <div class="flex-shrink-0 flex items-center"> | |
| <h1 class="text-2xl font-bold text-black dark:text-white manga-text"> | |
| Manga<span class="text-primary">Matic</span> Pro 🎨 | |
| </h1> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <button id="themeToggle" class="p-2 rounded-lg bg-white dark:bg-black border border-black dark:border-white"> | |
| <i data-feather="moon" class="text-black dark:text-white"></i> | |
| </button> | |
| <button class="bg-primary text-white px-4 py-2 rounded-lg manga-border hover:scale-105 transition-transform"> | |
| <i data-feather="user" class="inline mr-2"></i> | |
| Sign In | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </nav> | |
| <!-- Main App Container --> | |
| <div class="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8"> | |
| <div class="grid grid-cols-1 lg:grid-cols-4 gap-8"> | |
| <!-- Sidebar - Tools & Library --> | |
| <div class="lg:col-span-1 space-y-6"> | |
| <!-- Upload Section --> | |
| <div class="bg-white dark:bg-gray-900 p-6 rounded-lg manga-border"> | |
| <h3 class="text-lg font-bold mb-4 text-black dark:text-white">Upload Images</h3> | |
| <div class="drop-zone p-8 text-center rounded-lg cursor-pointer" id="dropZone"> | |
| <i data-feather="upload-cloud" class="w-12 h-12 mx-auto text-primary mb-4"></i> | |
| <p class="text-black dark:text-white mb-2">Drag & drop images here</p> | |
| <p class="text-gray-500 dark:text-gray-400 text-sm">or click to browse</p> | |
| <input type="file" id="fileInput" multiple accept="image/*" class="hidden"> | |
| </div> | |
| </div> | |
| <!-- Image Library --> | |
| <div class="bg-white dark:bg-gray-900 p-6 rounded-lg manga-border"> | |
| <h3 class="text-lg font-bold mb-4 text-black dark:text-white">Your Library</h3> | |
| <div class="grid grid-cols-2 gap-2" id="imageLibrary"> | |
| <!-- Images will be populated here --> | |
| </div> | |
| </div> | |
| <!-- ComfyUI Integration --> | |
| <div class="bg-white dark:bg-gray-900 p-6 rounded-lg manga-border"> | |
| <h3 class="text-lg font-bold mb-4 text-black dark:text-white">ComfyUI Integration</h3> | |
| <div class="space-y-4"> | |
| <input type="number" placeholder="ComfyUI Port (8188)" | |
| class="w-full p-2 border border-black rounded-lg bg-white dark:bg-black text-black dark:text-white" | |
| id="comfyPort"> | |
| <button class="w-full bg-primary text-white py-2 rounded-lg manga-border hover:scale-105 transition-transform" onclick="connectToComfyUI(document.getElementById('comfyPort').value || '8188')"> | |
| Connect to ComfyUI | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Editor Area --> | |
| <div class="lg:col-span-3"> | |
| <!-- Template Selection --> | |
| <div class="bg-white dark:bg-gray-900 p-6 rounded-lg manga-border mb-6"> | |
| <h3 class="text-lg font-bold mb-4 text-black dark:text-white">Page Layout Templates</h3> | |
| <div class="grid grid-cols-2 md:grid-cols-3 gap-4" id="templateGrid"> | |
| <!-- Templates will be populated here --> | |
| </div> | |
| </div> | |
| <!-- Comic Editor Canvas --> | |
| <div class="bg-white dark:bg-gray-900 p-6 rounded-lg manga-border"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-lg font-bold text-black dark:text-white">Comic Editor</h3> | |
| <div class="flex space-x-2"> | |
| <button class="p-2 border border-black rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800" onclick="saveProject()"> | |
| <i data-feather="save"></i> | |
| </button> | |
| <button class="p-2 border border-black rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800" onclick="exportComic()"> | |
| <i data-feather="download"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="relative" id="editorContainer"> | |
| <canvas id="comicCanvas" width="800" height="1000" class="w-full h-auto border-2 border-black bg-white"></canvas> | |
| </div> | |
| <!-- Bubble Tools --> | |
| <div class="mt-6 flex space-x-4"> | |
| <button class="flex items-center space-x-2 bg-white dark:bg-black border border-black dark:border-white px-4 py-2 rounded-lg hover:scale-105 transition-transform" id="addSpeechBubble"> | |
| <i data-feather="message-circle"></i> | |
| <span class="text-black dark:text-white">Speech Bubble</span> | |
| </button> | |
| <button class="flex items-center space-x-2 bg-white dark:bg-black border border-black dark:border-white px-4 py-2 rounded-lg hover:scale-105 transition-transform" id="addThoughtBubble"> | |
| <i data-feather="cloud"></i> | |
| <span class="text-black dark:text-white">Thought Bubble</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Footer --> | |
| <footer class="bg-white dark:bg-black border-t-2 border-black mt-12"> | |
| <div class="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8"> | |
| <div class="flex justify-between items-center"> | |
| <p class="text-black dark:text-white">© 2024 MangaMatic Pro. Transform your world into manga.</p> | |
| <div class="flex space-x-4"> | |
| <a href="#" class="text-black dark:text-white hover:text-primary transition-colors"> | |
| <i data-feather="github"></i> | |
| </a> | |
| <a href="#" class="text-black dark:text-white hover:text-primary transition-colors"> | |
| <i data-feather="twitter"></i> | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| </footer> | |
| <script> | |
| // Initialize Fabric.js Canvas | |
| const canvas = new fabric.Canvas('comicCanvas', { | |
| backgroundColor: '#ffffff', | |
| preserveObjectStacking: true | |
| }); | |
| // Initialize Feather Icons | |
| feather.replace(); | |
| // Theme Toggle | |
| const themeToggle = document.getElementById('themeToggle'); | |
| themeToggle.addEventListener('click', () => { | |
| document.documentElement.classList.toggle('dark'); | |
| const icon = themeToggle.querySelector('i'); | |
| if (document.documentElement.classList.contains('dark')) { | |
| feather.icons['sun'].replace(icon); | |
| } else { | |
| feather.icons['moon'].replace(icon); | |
| } | |
| }); | |
| // File Upload Handling | |
| const dropZone = document.getElementById('dropZone'); | |
| const fileInput = document.getElementById('fileInput'); | |
| dropZone.addEventListener('click', () => fileInput.click()); | |
| dropZone.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| dropZone.classList.add('dragover'); | |
| }); | |
| dropZone.addEventListener('dragleave', () => { | |
| dropZone.classList.remove('dragover'); | |
| }); | |
| dropZone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| dropZone.classList.remove('dragover'); | |
| const files = e.dataTransfer.files; | |
| handleFiles(files); | |
| }); | |
| fileInput.addEventListener('change', (e) => { | |
| handleFiles(e.target.files); | |
| }); | |
| function handleFiles(files) { | |
| Array.from(files).forEach(file => { | |
| if (file.type.startsWith('image/')) { | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| addImageToLibrary(e.target.result, file.name); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }); | |
| } | |
| function addImageToLibrary(dataUrl, filename) { | |
| const library = document.getElementById('imageLibrary'); | |
| const imgDiv = document.createElement('div'); | |
| imgDiv.className = 'comic-panel aspect-square cursor-pointer hover:scale-105 transition-transform'; | |
| imgDiv.innerHTML = ` | |
| <img src="${dataUrl}" alt="${filename}" class="w-full h-full object-cover"> | |
| `; | |
| library.appendChild(imgDiv); | |
| // Make image draggable to canvas | |
| imgDiv.setAttribute('draggable', 'true'); | |
| imgDiv.addEventListener('dragstart', (e) => { | |
| e.dataTransfer.setData('text/plain', dataUrl); | |
| }); | |
| } | |
| // Initialize templates | |
| const templates = [ | |
| { id: 1, name: 'Single Panel', layout: [[1]], cols: 1, rows: 1 }, | |
| { id: 2, name: 'Double Spread', layout: [[1, 1]], cols: 2, rows: 1 }, | |
| { id: 3, name: 'Classic 4-Panel', layout: [[1, 1], [1, 1]], cols: 2, rows: 2 }, | |
| { id: 4, name: 'Dynamic Action', layout: [[2], [1, 1]], cols: 2, rows: 2 }, | |
| { id: 5, name: 'Manga Page', layout: [[1], [1, 1], [1]], cols: 2, rows: 3 }, | |
| { id: 6, name: 'Cinematic', layout: [[3], [1, 1, 1]], cols: 3, rows: 2 } | |
| ]; | |
| // Populate template grid | |
| const templateGrid = document.getElementById('templateGrid'); | |
| templates.forEach(template => { | |
| const templateDiv = document.createElement('div'); | |
| templateDiv.className = 'comic-panel aspect-video cursor-pointer hover:scale-105 transition-transform'; | |
| templateDiv.innerHTML = ` | |
| <div class="w-full h-full flex flex-wrap border-2 border-black"> | |
| ${generateTemplatePreview(template.layout)} | |
| </div> | |
| <p class="text-center mt-2 text-black dark:text-white">${template.name}</p> | |
| `; | |
| templateDiv.addEventListener('click', () => applyTemplate(template)); | |
| templateGrid.appendChild(templateDiv); | |
| }); | |
| function generateTemplatePreview(layout) { | |
| let html = ''; | |
| layout.forEach(row => { | |
| row.forEach(cell => { | |
| html += `<div class="bg-white border border-black" style="flex: ${cell}"></div>`; | |
| }); | |
| }); | |
| return html; | |
| } | |
| function applyTemplate(template) { | |
| // Clear canvas and apply new template | |
| canvas.clear(); | |
| canvas.setBackgroundColor('#ffffff', canvas.renderAll.bind(canvas)); | |
| // Create panel guides | |
| const panelWidth = canvas.width / template.cols; | |
| const panelHeight = canvas.height / template.rows; | |
| // Add dashed border guides | |
| template.layout.forEach((row, rowIndex) => { | |
| row.forEach((cell, colIndex) => { | |
| const rect = new fabric.Rect({ | |
| left: colIndex * panelWidth, | |
| top: rowIndex * panelHeight, | |
| width: cell * panelWidth, | |
| height: panelHeight, | |
| fill: 'transparent', | |
| stroke: '#cccccc', | |
| strokeDashArray: [5, 5], | |
| selectable: false, | |
| evented: false | |
| }); | |
| canvas.add(rect); | |
| }); | |
| }); | |
| } | |
| // Drag and drop to canvas | |
| canvas.on('drop', (event) => { | |
| const dataUrl = event.e.dataTransfer.getData('text/plain'); | |
| if (dataUrl) { | |
| fabric.Image.fromURL(dataUrl, (img) => { | |
| const pointer = canvas.getPointer(event.e); | |
| img.set({ | |
| left: pointer.x - img.width / 2, | |
| top: pointer.y - img.height / 2 | |
| }); | |
| canvas.add(img); | |
| canvas.setActiveObject(img); | |
| }); | |
| } | |
| }); | |
| canvas.on('dragover', (event) => { | |
| event.e.preventDefault(); | |
| }); | |
| // Bubble Tools | |
| document.getElementById('addSpeechBubble').addEventListener('click', () => { | |
| addSpeechBubble(); | |
| }); | |
| document.getElementById('addThoughtBubble').addEventListener('click', () => { | |
| addThoughtBubble(); | |
| }); | |
| function addSpeechBubble() { | |
| const bubble = new fabric.Rect({ | |
| left: 100, | |
| top: 100, | |
| width: 200, | |
| height: 100, | |
| rx: 20, | |
| ry: 20, | |
| fill: 'white', | |
| stroke: 'black', | |
| strokeWidth: 2 | |
| }); | |
| const text = new fabric.IText('Enter text here...', { | |
| left: 120, | |
| top: 120, | |
| fontSize: 16, | |
| fontFamily: 'Comic Neue, cursive', | |
| fill: 'black', | |
| editable: true | |
| }); | |
| const group = new fabric.Group([bubble, text], { | |
| selectable: true, | |
| hasControls: true | |
| }); | |
| canvas.add(group); | |
| canvas.setActiveObject(group); | |
| } | |
| function addThoughtBubble() { | |
| const bubble = new fabric.Circle({ | |
| left: 100, | |
| top: 100, | |
| radius: 50, | |
| fill: 'white', | |
| stroke: 'black', | |
| strokeWidth: 2 | |
| }); | |
| const text = new fabric.IText('Thinking...', { | |
| left: 120, | |
| top: 120, | |
| fontSize: 16, | |
| fontFamily: 'Comic Neue, cursive', | |
| fill: 'black', | |
| editable: true | |
| }); | |
| const group = new fabric.Group([bubble, text], { | |
| selectable: true, | |
| hasControls: true | |
| }); | |
| canvas.add(group); | |
| canvas.setActiveObject(group); | |
| } | |
| // ComfyUI Integration | |
| let comfyUIWebSocket = null; | |
| document.querySelector('#comfyPort').addEventListener('change', (e) => { | |
| const port = e.target.value || '8188'; | |
| connectToComfyUI(port); | |
| }); | |
| function connectToComfyUI(port) { | |
| if (comfyUIWebSocket) { | |
| comfyUIWebSocket.close(); | |
| } | |
| comfyUIWebSocket = new WebSocket(`ws://localhost:${port}/ws`); | |
| comfyUIWebSocket.onopen = () => { | |
| console.log('Connected to ComfyUI'); | |
| // Subscribe to progress updates | |
| comfyUIWebSocket.send(JSON.stringify({ | |
| type: 'subscribe', | |
| data: { nodes: [] } | |
| })); | |
| }; | |
| comfyUIWebSocket.onmessage = (event) => { | |
| const data = JSON.parse(event.data); | |
| if (data.type === 'executing' && data.data.prompt_id) { | |
| // Track generation progress | |
| gsap.to('.comfy-progress', { | |
| duration: 0.3, | |
| width: `${data.data.progress * 100}%` | |
| }); | |
| if (data.type === 'executed') { | |
| // Fetch generated image and add to library | |
| fetchGeneratedImage(data.data.output.images[0]); | |
| } | |
| }; | |
| comfyUIWebSocket.onerror = (error) => { | |
| console.error('ComfyUI WebSocket error:', error); | |
| }; | |
| } | |
| function fetchGeneratedImage(imagePath) { | |
| fetch(`http://localhost:${document.getElementById('comfyPort').value || '8188'}/view?filename=${imagePath}`) | |
| .then(response => response.blob()) | |
| .then(blob => { | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| addImageToLibrary(e.target.result, 'generated-image.png'); | |
| }); | |
| } | |
| // Initialize with default template | |
| applyTemplate(templates[0]); | |
| </body> | |
| <script> | |
| // Additional utility functions | |
| function saveProject() { | |
| const projectData = JSON.stringify(canvas.toJSON()); | |
| localStorage.setItem('mangamatic-project', projectData); | |
| // Show success animation | |
| gsap.fromTo('.save-indicator', | |
| { scale: 0, opacity: 0 }, | |
| { scale: 1, opacity: 1, duration: 0.5, onComplete: () => { | |
| gsap.to('.save-indicator', { scale: 0, opacity: 0, duration: 0.5, delay: 1 }); | |
| }); | |
| } | |
| function exportComic() { | |
| const dataURL = canvas.toDataURL({ | |
| format: 'png', | |
| quality: 1 | |
| }); | |
| const link = document.createElement('a'); | |
| link.download = 'manga-comic.png'; | |
| link.href = dataURL; | |
| link.click(); | |
| } | |
| // Load saved project if exists | |
| const savedProject = localStorage.getItem('mangamatic-project'); | |
| if (savedProject) { | |
| canvas.loadFromJSON(savedProject, () => { | |
| canvas.renderAll(); | |
| }); | |
| } | |
| // Auto-resize text in bubbles | |
| canvas.on('text:changed', (e) => { | |
| const text = e.target; | |
| if (text && text.group) { | |
| autoResizeText(text); | |
| } | |
| }); | |
| function autoResizeText(text) { | |
| const group = text.group; | |
| const bubble = group.getObjects()[0]; | |
| // Calculate maximum width for text | |
| const maxWidth = bubble.width * 0.8; | |
| const fontSize = Math.min(16, maxWidth / (text.text.length * 0.6)); | |
| text.set('fontSize', fontSize); | |
| // Center text in bubble | |
| text.set({ | |
| left: bubble.left + (bubble.width - text.width) / 2, | |
| top: bubble.top + (bubble.height - text.height) / 2 | |
| }); | |
| group.setCoords(); | |
| canvas.renderAll(); | |
| } | |
| </script> | |
| </html> |