Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <!-- Tailwind CSS (Play CDN) --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- FontAwesome --> | |
| <link rel="stylesheet" | |
| href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" | |
| /> | |
| <!-- Fabric.js --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/6.6.4/fabric.min.js"></script> | |
| <!-- fabric-brushes addon --> | |
| <script src="https://cdn.jsdelivr.net/gh/av01d/fabric-brushes@1.0/dist/fabric.brushes.min.js"></script> | |
| </head> | |
| <body class="h-screen flex bg-gray-100"> | |
| <!-- Layers Panel --> | |
| <aside class="w-64 bg-white border-r p-4 flex flex-col"> | |
| <h2 class="text-xl font-semibold mb-4">Layers</h2> | |
| <div id="layers" class="flex-1 overflow-auto space-y-2"></div> | |
| <button id="add-layer" | |
| class="mt-4 flex items-center justify-center px-3 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"> | |
| <i class="fa fa-plus mr-2"></i>Add Layer | |
| </button> | |
| </aside> | |
| <!-- Main Canvas Area --> | |
| <main class="flex-1 flex flex-col"> | |
| <!-- Brush Toolbar --> | |
| <div class="p-4 bg-white border-b flex space-x-4"> | |
| <button class="brush-btn" data-brush="pencil" title="Pencil"> | |
| <i class="fa fa-pencil-alt fa-lg"></i> | |
| </button> | |
| <button class="brush-btn" data-brush="chalk" title="Chalk"> | |
| <i class="fa fa-chalkboard fa-lg"></i> | |
| </button> | |
| <button class="brush-btn" data-brush="paint" title="Paint"> | |
| <i class="fa fa-paint-brush fa-lg"></i> | |
| </button> | |
| <button class="brush-btn" data-brush="splatter" title="Splatter"> | |
| <i class="fa fa-spray-can fa-lg"></i> | |
| </button> | |
| </div> | |
| <!-- Canvas Stack --> | |
| <div id="canvas-wrapper" class="relative flex-1 bg-white"></div> | |
| </main> | |
| <!-- Script --> | |
| <script> | |
| const layersContainer = document.getElementById('layers'); | |
| const addLayerBtn = document.getElementById('add-layer'); | |
| const canvasWrapper = document.getElementById('canvas-wrapper'); | |
| let layers = [], current = -1; | |
| // Create a new layer | |
| function addLayer(name='Layer') { | |
| const id = Date.now().toString(); | |
| // Create <canvas> | |
| const c = document.createElement('canvas'); | |
| c.id = 'canvas-' + id; | |
| c.width = canvasWrapper.clientWidth; | |
| c.height = canvasWrapper.clientHeight; | |
| c.classList.add('absolute','top-0','left-0'); | |
| canvasWrapper.appendChild(c); | |
| // Init Fabric canvas | |
| const fc = new fabric.Canvas(c.id, { | |
| isDrawingMode: true, | |
| backgroundColor: 'transparent' | |
| }); | |
| fc.freeDrawingBrush = new fabric.PencilBrush(fc); // default :contentReference[oaicite:5]{index=5} | |
| layers.push({ id, name, el: c, fc }); | |
| selectLayer(layers.length - 1); | |
| updateLayersUI(); | |
| } | |
| // Refresh the layers panel | |
| function updateLayersUI() { | |
| layersContainer.innerHTML = ''; | |
| layers.forEach((ly, i) => { | |
| const div = document.createElement('div'); | |
| div.className = [ | |
| 'flex items-center justify-between p-2 rounded', | |
| i===current ? 'bg-blue-100' : 'hover:bg-gray-100 cursor-pointer' | |
| ].join(' '); | |
| div.innerHTML = ` | |
| <span>${ly.name}</span> | |
| <div class="space-x-1"> | |
| <button onclick="moveLayer(${i},-1)"><i class="fa fa-arrow-up"></i></button> | |
| <button onclick="moveLayer(${i},+1)"><i class="fa fa-arrow-down"></i></button> | |
| <button onclick="removeLayer(${i})"><i class="fa fa-trash"></i></button> | |
| </div>`; | |
| div.addEventListener('click',()=>selectLayer(i)); | |
| layersContainer.appendChild(div); | |
| }); | |
| // Stack canvases by index | |
| layers.forEach((ly,i)=> { | |
| ly.el.style.zIndex = i; | |
| }); | |
| if(current>=0) layers[current].el.style.zIndex = layers.length; | |
| } | |
| // Select a layer | |
| function selectLayer(i) { | |
| current = i; | |
| layers.forEach((ly,idx)=>{ | |
| ly.fc.selection = (idx===i); | |
| }); | |
| updateLayersUI(); | |
| } | |
| // Remove a layer | |
| function removeLayer(i) { | |
| canvasWrapper.removeChild(layers[i].el); | |
| layers.splice(i,1); | |
| if(current >= layers.length) current = layers.length -1; | |
| updateLayersUI(); | |
| } | |
| // Move layer up/down | |
| function moveLayer(i, dir) { | |
| const j = i + dir; | |
| if(j<0||j>=layers.length) return; | |
| [layers[i], layers[j]] = [layers[j], layers[i]]; | |
| if(current === i) current = j; | |
| else if(current === j) current = i; | |
| updateLayersUI(); | |
| } | |
| // Change brush type | |
| document.querySelectorAll('.brush-btn').forEach(btn=>{ | |
| btn.addEventListener('click',()=>{ | |
| if(current<0) return; | |
| const f = layers[current].fc; | |
| switch(btn.dataset.brush) { | |
| case 'pencil': | |
| f.freeDrawingBrush = new fabric.PencilBrush(f); break; | |
| case 'chalk': | |
| f.freeDrawingBrush = new fabric.SketchyBrush(f,{ | |
| width: 10, color:'#222', opacity:0.6 | |
| }); break; // Sketchy for chalk :contentReference[oaicite:6]{index=6} | |
| case 'paint': | |
| f.freeDrawingBrush = new fabric.RibbonBrush(f,{ | |
| width: 20, color:'#c00', opacity:0.8 | |
| }); break; // Ribbon for paint drips :contentReference[oaicite:7]{index=7} | |
| case 'splatter': | |
| f.freeDrawingBrush = new fabric.SpraypaintBrush(f,{ | |
| width: 30, color:'#008', opacity:0.7 | |
| }); break; // Spraypaint for splatter :contentReference[oaicite:8]{index=8} | |
| } | |
| }); | |
| }); | |
| // Init with one layer | |
| window.addEventListener('DOMContentLoaded',()=> addLayer('Layer 1')); | |
| addLayerBtn.addEventListener('click',()=>addLayer(`Layer ${layers.length+1}`)); | |
| </script> | |
| </body> | |
| </html> |