Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Image Editor - ChatNimbus</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://html2canvas.hertzen.com/dist/html2canvas.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/fabric@5.3.1/dist/fabric.min.js"></script> | |
| <style> | |
| #canvas-container { | |
| width: 100%; | |
| height: 70vh; | |
| border: 2px dashed #ccc; | |
| position: relative; | |
| } | |
| .tool-btn { | |
| transition: all 0.2s; | |
| } | |
| .tool-btn:hover { | |
| transform: scale(1.1); | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h1 class="text-3xl font-bold text-gray-800">Image Editor</h1> | |
| <a href="index.html" class="flex items-center text-blue-500 hover:text-blue-700"> | |
| <i data-feather="arrow-left" class="mr-1"></i> Back to Chat | |
| </a> | |
| </div> | |
| <div class="bg-white rounded-lg shadow-md p-6"> | |
| <div class="flex flex-col md:flex-row gap-6"> | |
| <!-- Tools Panel --> | |
| <div class="w-full md:w-1/4 bg-gray-50 p-4 rounded-lg"> | |
| <div class="mb-6"> | |
| <h2 class="text-lg font-semibold mb-3">Upload Image</h2> | |
| <input type="file" id="image-upload" accept="image/*" class="hidden"> | |
| <button onclick="document.getElementById('image-upload').click()" class="w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 flex items-center justify-center"> | |
| <i data-feather="upload" class="mr-2"></i> Choose File | |
| </button> | |
| </div> | |
| <div class="mb-6"> | |
| <h2 class="text-lg font-semibold mb-3">Drawing Tools</h2> | |
| <div class="grid grid-cols-3 gap-2"> | |
| <button id="draw-btn" class="tool-btn bg-gray-200 p-2 rounded-lg" title="Draw"> | |
| <i data-feather="edit-3"></i> | |
| </button> | |
| <button id="text-btn" class="tool-btn bg-gray-200 p-2 rounded-lg" title="Add Text"> | |
| <i data-feather="type"></i> | |
| </button> | |
| <button id="shape-btn" class="tool-btn bg-gray-200 p-2 rounded-lg" title="Add Shape"> | |
| <i data-feather="square"></i> | |
| </button> | |
| <button id="eraser-btn" class="tool-btn bg-gray-200 p-2 rounded-lg" title="Eraser"> | |
| <i data-feather="x"></i> | |
| </button> | |
| <button id="crop-btn" class="tool-btn bg-gray-200 p-2 rounded-lg" title="Crop"> | |
| <i data-feather="crop"></i> | |
| </button> | |
| <button id="clear-btn" class="tool-btn bg-gray-200 p-2 rounded-lg" title="Clear All"> | |
| <i data-feather="trash-2"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mb-6"> | |
| <h2 class="text-lg font-semibold mb-3">Filters</h2> | |
| <select id="filter-select" class="w-full p-2 border rounded-lg"> | |
| <option value="none">No Filter</option> | |
| <option value="grayscale">Grayscale</option> | |
| <option value="sepia">Sepia</option> | |
| <option value="invert">Invert</option> | |
| <option value="blur">Blur</option> | |
| </select> | |
| </div> | |
| <div class="mb-6"> | |
| <h2 class="text-lg font-semibold mb-3">Text Options</h2> | |
| <input type="text" id="text-input" placeholder="Enter text" class="w-full p-2 border rounded-lg mb-2"> | |
| <div class="flex justify-between"> | |
| <input type="color" id="text-color" value="#000000" class="w-8 h-8"> | |
| <select id="font-select" class="w-3/4 p-1 border rounded-lg"> | |
| <option value="Arial">Arial</option> | |
| <option value="Times New Roman">Times New Roman</option> | |
| <option value="Courier New">Courier New</option> | |
| <option value="Georgia">Georgia</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="mb-6"> | |
| <h2 class="text-lg font-semibold mb-3">Brush Options</h2> | |
| <div class="flex items-center mb-2"> | |
| <span class="mr-2">Size:</span> | |
| <input type="range" id="brush-size" min="1" max="50" value="5" class="flex-1"> | |
| </div> | |
| <div class="flex items-center"> | |
| <span class="mr-2">Color:</span> | |
| <input type="color" id="brush-color" value="#000000" class="w-8 h-8"> | |
| </div> | |
| </div> | |
| <div> | |
| <button id="save-btn" class="w-full bg-green-500 text-white py-2 px-4 rounded-lg hover:bg-green-600 flex items-center justify-center"> | |
| <i data-feather="save" class="mr-2"></i> Save Image | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Canvas Area --> | |
| <div class="w-full md:w-3/4"> | |
| <div id="canvas-container"> | |
| <canvas id="canvas"></canvas> | |
| </div> | |
| <div class="mt-4 flex justify-center gap-4"> | |
| <button id="undo-btn" class="bg-gray-200 py-2 px-4 rounded-lg hover:bg-gray-300 flex items-center"> | |
| <i data-feather="rotate-ccw" class="mr-2"></i> Undo | |
| </button> | |
| <button id="redo-btn" class="bg-gray-200 py-2 px-4 rounded-lg hover:bg-gray-300 flex items-center"> | |
| <i data-feather="rotate-cw" class="mr-2"></i> Redo | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| feather.replace(); | |
| // Initialize Fabric.js canvas | |
| const canvas = new fabric.Canvas('canvas', { | |
| width: document.getElementById('canvas-container').offsetWidth - 4, | |
| height: document.getElementById('canvas-container').offsetHeight - 4, | |
| backgroundColor: '#ffffff' | |
| }); | |
| // Upload image | |
| document.getElementById('image-upload').addEventListener('change', function(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = function(event) { | |
| fabric.Image.fromURL(event.target.result, function(img) { | |
| canvas.clear(); | |
| canvas.add(img); | |
| canvas.renderAll(); | |
| }); | |
| }; | |
| reader.readAsDataURL(file); | |
| }); | |
| // Drawing tools | |
| document.getElementById('draw-btn').addEventListener('click', function() { | |
| canvas.isDrawingMode = true; | |
| }); | |
| document.getElementById('text-btn').addEventListener('click', function() { | |
| const text = new fabric.IText('Double click to edit', { | |
| left: 100, | |
| top: 100, | |
| fontFamily: document.getElementById('font-select').value, | |
| fill: document.getElementById('text-color').value | |
| }); | |
| canvas.add(text); | |
| canvas.setActiveObject(text); | |
| canvas.renderAll(); | |
| }); | |
| document.getElementById('shape-btn').addEventListener('click', function() { | |
| const rect = new fabric.Rect({ | |
| left: 100, | |
| top: 100, | |
| fill: 'transparent', | |
| stroke: document.getElementById('brush-color').value, | |
| strokeWidth: document.getElementById('brush-size').value, | |
| width: 200, | |
| height: 100 | |
| }); | |
| canvas.add(rect); | |
| canvas.renderAll(); | |
| }); | |
| document.getElementById('eraser-btn').addEventListener('click', function() { | |
| canvas.isDrawingMode = true; | |
| canvas.freeDrawingBrush.color = '#ffffff'; | |
| }); | |
| document.getElementById('clear-btn').addEventListener('click', function() { | |
| if (confirm('Are you sure you want to clear the canvas?')) { | |
| canvas.clear(); | |
| } | |
| }); | |
| // Brush settings | |
| document.getElementById('brush-size').addEventListener('input', function() { | |
| canvas.freeDrawingBrush.width = parseInt(this.value); | |
| }); | |
| document.getElementById('brush-color').addEventListener('input', function() { | |
| canvas.freeDrawingBrush.color = this.value; | |
| }); | |
| // Text settings | |
| document.getElementById('text-color').addEventListener('input', function() { | |
| const activeObject = canvas.getActiveObject(); | |
| if (activeObject && activeObject.type === 'i-text') { | |
| activeObject.set('fill', this.value); | |
| canvas.renderAll(); | |
| } | |
| }); | |
| document.getElementById('font-select').addEventListener('change', function() { | |
| const activeObject = canvas.getActiveObject(); | |
| if (activeObject && activeObject.type === 'i-text') { | |
| activeObject.set('fontFamily', this.value); | |
| canvas.renderAll(); | |
| } | |
| }); | |
| // Filters | |
| document.getElementById('filter-select').addEventListener('change', function() { | |
| const objects = canvas.getObjects(); | |
| objects.forEach(obj => { | |
| if (obj.type === 'image') { | |
| obj.filters = []; | |
| switch(this.value) { | |
| case 'grayscale': | |
| obj.filters.push(new fabric.Image.filters.Grayscale()); | |
| break; | |
| case 'sepia': | |
| obj.filters.push(new fabric.Image.filters.Sepia()); | |
| break; | |
| case 'invert': | |
| obj.filters.push(new fabric.Image.filters.Invert()); | |
| break; | |
| case 'blur': | |
| obj.filters.push(new fabric.Image.filters.Blur({ | |
| blur: 0.2 | |
| })); | |
| break; | |
| } | |
| obj.applyFilters(); | |
| canvas.renderAll(); | |
| } | |
| }); | |
| }); | |
| // Save image | |
| document.getElementById('save-btn').addEventListener('click', function() { | |
| const link = document.createElement('a'); | |
| link.download = 'edited-image.png'; | |
| link.href = canvas.toDataURL({ | |
| format: 'png', | |
| quality: 1 | |
| }); | |
| link.click(); | |
| }); | |
| // Undo/Redo | |
| const history = []; | |
| let historyIndex = -1; | |
| canvas.on('object:modified', saveState); | |
| canvas.on('object:added', saveState); | |
| function saveState() { | |
| if (historyIndex < history.length - 1) { | |
| history.splice(historyIndex + 1); | |
| } | |
| history.push(JSON.stringify(canvas)); | |
| historyIndex = history.length - 1; | |
| } | |
| document.getElementById('undo-btn').addEventListener('click', function() { | |
| if (historyIndex <= 0) return; | |
| historyIndex--; | |
| canvas.loadFromJSON(history[historyIndex], function() { | |
| canvas.renderAll(); | |
| }); | |
| }); | |
| document.getElementById('redo-btn').addEventListener('click', function() { | |
| if (historyIndex >= history.length - 1) return; | |
| historyIndex++; | |
| canvas.loadFromJSON(history[historyIndex], function() { | |
| canvas.renderAll(); | |
| }); | |
| }); | |
| // Window resize | |
| window.addEventListener('resize', function() { | |
| canvas.setWidth(document.getElementById('canvas-container').offsetWidth - 4); | |
| canvas.setHeight(document.getElementById('canvas-container').offsetHeight - 4); | |
| canvas.renderAll(); | |
| }); | |
| }); | |
| </script> | |
| </body> | |
| </html> |