Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Mandalas Creator - Interactive Radial Art Maker</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/fabric@5.3.1/dist/fabric.min.js"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap'); | |
| :root { | |
| --primary: #6366f1; | |
| --dark: #0f172a; | |
| --light: #f8fafc; | |
| --gold: linear-gradient(135deg, #FFD700 0%, #D4AF37 100%); | |
| --silver: linear-gradient(135deg, #C0C0C0 0%, #A8A8A8 100%); | |
| --copper: linear-gradient(135deg, #B87333 0%, #9C5B2D 100%); | |
| } | |
| body { | |
| font-family: 'Poppins', sans-serif; | |
| background-color: var(--dark); | |
| color: var(--light); | |
| overflow: hidden; | |
| touch-action: none; | |
| } | |
| .gradient-bg { | |
| background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); | |
| } | |
| .tool-btn { | |
| @apply p-2 rounded-lg hover:bg-slate-700 transition-all duration-200; | |
| } | |
| .tool-btn.active { | |
| @apply bg-indigo-600 text-white; | |
| } | |
| .color-swatch { | |
| @apply w-8 h-8 rounded-full cursor-pointer border-2 border-transparent hover:border-white transition-all; | |
| } | |
| .metallic { | |
| background-size: 200% 200%; | |
| background-position: center; | |
| } | |
| .metallic.gold { | |
| background-image: var(--gold); | |
| } | |
| .metallic.silver { | |
| background-image: var(--silver); | |
| } | |
| .metallic.copper { | |
| background-image: var(--copper); | |
| } | |
| canvas { | |
| touch-action: none; | |
| } | |
| .slide-fade-enter-active { | |
| transition: all 0.3s ease-out; | |
| } | |
| .slide-fade-leave-active { | |
| transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1); | |
| } | |
| .slide-fade-enter-from, | |
| .slide-fade-leave-to { | |
| transform: translateX(20px); | |
| opacity: 0; | |
| } | |
| .glow { | |
| filter: drop-shadow(0 0 8px currentColor); | |
| } | |
| .neon-text { | |
| text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #fff, 0 0 20px #ff00de; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar { | |
| width: 6px; | |
| height: 6px; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-track { | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 10px; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-thumb { | |
| background: rgba(255,255,255,0.3); | |
| border-radius: 10px; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-thumb:hover { | |
| background: rgba(255,255,255,0.5); | |
| } | |
| </style> | |
| </head> | |
| <body class="gradient-bg h-screen flex flex-col"> | |
| <!-- Header --> | |
| <header class="bg-slate-900/50 backdrop-blur-md border-b border-slate-800 p-4 flex justify-between items-center"> | |
| <div class="flex items-center space-x-2"> | |
| <i class="fas fa-mandala text-2xl text-indigo-500"></i> | |
| <h1 class="text-xl font-bold neon-text">Mandalas Creator</h1> | |
| </div> | |
| <div class="flex space-x-3"> | |
| <button id="exportBtn" class="bg-indigo-600 hover:bg-indigo-700 px-4 py-2 rounded-lg flex items-center space-x-2 transition-all"> | |
| <i class="fas fa-download"></i> | |
| <span>Export</span> | |
| </button> | |
| <button id="clearBtn" class="bg-rose-600 hover:bg-rose-700 px-4 py-2 rounded-lg flex items-center space-x-2 transition-all"> | |
| <i class="fas fa-trash"></i> | |
| <span>Clear</span> | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <div class="flex flex-1 overflow-hidden"> | |
| <!-- Left Toolbar --> | |
| <div class="w-16 bg-slate-900/50 border-r border-slate-800 flex flex-col items-center py-4 space-y-6"> | |
| <div class="tool-group"> | |
| <div class="text-xs text-slate-400 mb-1">Tools</div> | |
| <button id="brushTool" class="tool-btn active" title="Brush"> | |
| <i class="fas fa-paintbrush"></i> | |
| </button> | |
| <button id="shapeTool" class="tool-btn" title="Shapes"> | |
| <i class="fas fa-shapes"></i> | |
| </button> | |
| <button id="gradientTool" class="tool-btn" title="Gradient"> | |
| <i class="fas fa-fill-drip"></i> | |
| </button> | |
| <button id="eraseTool" class="tool-btn" title="Eraser"> | |
| <i class="fas fa-eraser"></i> | |
| </button> | |
| </div> | |
| <div class="tool-group"> | |
| <div class="text-xs text-slate-400 mb-1">Symmetry</div> | |
| <button id="symmetry6" class="tool-btn" title="6-fold"> | |
| <span>6</span> | |
| </button> | |
| <button id="symmetry8" class="tool-btn active" title="8-fold"> | |
| <span>8</span> | |
| </button> | |
| <button id="symmetry12" class="tool-btn" title="12-fold"> | |
| <span>12</span> | |
| </button> | |
| </div> | |
| <div class="tool-group"> | |
| <div class="text-xs text-slate-400 mb-1">Effects</div> | |
| <button id="glowEffect" class="tool-btn" title="Glow"> | |
| <i class="fas fa-lightbulb"></i> | |
| </button> | |
| <button id="shadowEffect" class="tool-btn" title="Shadow"> | |
| <i class="fas fa-cloud"></i> | |
| </button> | |
| <button id="embossEffect" class="tool-btn" title="Emboss"> | |
| <i class="fas fa-mountain"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Canvas Area --> | |
| <div class="flex-1 relative overflow-hidden"> | |
| <div id="canvas-container" class="absolute inset-0 flex items-center justify-center"> | |
| <canvas id="mandalaCanvas" width="800" height="800"></canvas> | |
| </div> | |
| <!-- Floating Controls --> | |
| <div class="absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-slate-900/80 backdrop-blur-md rounded-full px-4 py-2 flex items-center space-x-4 shadow-lg border border-slate-800"> | |
| <div class="flex items-center space-x-2"> | |
| <span class="text-sm text-slate-300">Size:</span> | |
| <input id="brushSize" type="range" min="1" max="50" value="5" class="w-24"> | |
| <span id="brushSizeValue" class="text-sm w-8 text-center">5</span> | |
| </div> | |
| <div class="flex items-center space-x-2"> | |
| <span class="text-sm text-slate-300">Opacity:</span> | |
| <input id="brushOpacity" type="range" min="10" max="100" value="100" class="w-24"> | |
| <span id="brushOpacityValue" class="text-sm w-8 text-center">100</span> | |
| </div> | |
| <div class="h-6 w-px bg-slate-700"></div> | |
| <button id="undoBtn" class="text-slate-300 hover:text-white" title="Undo"> | |
| <i class="fas fa-undo"></i> | |
| </button> | |
| <button id="redoBtn" class="text-slate-300 hover:text-white" title="Redo"> | |
| <i class="fas fa-redo"></i> | |
| </button> | |
| <div class="h-6 w-px bg-slate-700"></div> | |
| <button id="rotateBtn" class="text-slate-300 hover:text-white" title="Rotate Canvas"> | |
| <i class="fas fa-sync-alt"></i> | |
| </button> | |
| <button id="centerBtn" class="text-slate-300 hover:text-white" title="Center View"> | |
| <i class="fas fa-crosshairs"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Right Panel --> | |
| <div class="w-64 bg-slate-900/50 border-l border-slate-800 flex flex-col"> | |
| <div class="p-4 border-b border-slate-800"> | |
| <h3 class="font-medium text-slate-200 mb-2">Color Palette</h3> | |
| <div class="grid grid-cols-5 gap-2"> | |
| <div class="color-swatch bg-red-500" data-color="#ef4444"></div> | |
| <div class="color-swatch bg-orange-500" data-color="#f97316"></div> | |
| <div class="color-swatch bg-yellow-500" data-color="#eab308"></div> | |
| <div class="color-swatch bg-green-500" data-color="#22c55e"></div> | |
| <div class="color-swatch bg-blue-500" data-color="#3b82f6"></div> | |
| <div class="color-swatch bg-indigo-500" data-color="#6366f1"></div> | |
| <div class="color-swatch bg-purple-500" data-color="#a855f7"></div> | |
| <div class="color-swatch bg-pink-500" data-color="#ec4899"></div> | |
| <div class="color-swatch bg-white" data-color="#ffffff"></div> | |
| <div class="color-swatch bg-black" data-color="#000000"></div> | |
| <div class="color-swatch metallic gold" data-color="gold"></div> | |
| <div class="color-swatch metallic silver" data-color="silver"></div> | |
| <div class="color-swatch metallic copper" data-color="copper"></div> | |
| <div class="color-swatch bg-gradient-to-br from-purple-500 to-pink-500" data-color="purple-pink-gradient"></div> | |
| <div class="color-swatch bg-gradient-to-br from-blue-500 to-teal-400" data-color="blue-teal-gradient"></div> | |
| </div> | |
| <div class="mt-4"> | |
| <label class="text-sm text-slate-300 block mb-1">Custom Color</label> | |
| <input type="color" id="customColor" value="#3b82f6" class="w-full h-10 cursor-pointer"> | |
| </div> | |
| </div> | |
| <div class="p-4 border-b border-slate-800"> | |
| <h3 class="font-medium text-slate-200 mb-2">Shapes</h3> | |
| <div class="grid grid-cols-3 gap-2"> | |
| <button class="shape-option p-2 rounded border border-slate-700 hover:bg-slate-800" data-shape="circle"> | |
| <i class="fas fa-circle text-lg"></i> | |
| </button> | |
| <button class="shape-option p-2 rounded border border-slate-700 hover:bg-slate-800" data-shape="teardrop"> | |
| <i class="fas fa-tint text-lg"></i> | |
| </button> | |
| <button class="shape-option p-2 rounded border border-slate-700 hover:bg-slate-800" data-shape="petal"> | |
| <i class="fas fa-leaf text-lg"></i> | |
| </button> | |
| <button class="shape-option p-2 rounded border border-slate-700 hover:bg-slate-800" data-shape="star"> | |
| <i class="fas fa-star text-lg"></i> | |
| </button> | |
| <button class="shape-option p-2 rounded border border-slate-700 hover:bg-slate-800" data-shape="ring"> | |
| <i class="fas fa-ring text-lg"></i> | |
| </button> | |
| <button class="shape-option p-2 rounded border border-slate-700 hover:bg-slate-800" data-shape="lotus"> | |
| <i class="fas fa-spa text-lg"></i> | |
| </button> | |
| </div> | |
| <div class="mt-3"> | |
| <label class="text-sm text-slate-300 block mb-1">Shape Size</label> | |
| <input type="range" id="shapeSize" min="10" max="100" value="30" class="w-full"> | |
| </div> | |
| </div> | |
| <div class="p-4 border-b border-slate-800"> | |
| <h3 class="font-medium text-slate-200 mb-2">Layers</h3> | |
| <div id="layersList" class="space-y-2 max-h-40 overflow-y-auto custom-scrollbar"> | |
| <!-- Layers will be added here dynamically --> | |
| </div> | |
| <button id="addLayerBtn" class="mt-2 w-full bg-slate-800 hover:bg-slate-700 py-1 rounded text-sm"> | |
| <i class="fas fa-plus mr-1"></i> Add Layer | |
| </button> | |
| </div> | |
| <div class="p-4"> | |
| <h3 class="font-medium text-slate-200 mb-2">Effects</h3> | |
| <div class="space-y-3"> | |
| <div> | |
| <label class="text-sm text-slate-300 block mb-1">Glow Intensity</label> | |
| <input type="range" id="glowIntensity" min="0" max="20" value="0" class="w-full"> | |
| </div> | |
| <div> | |
| <label class="text-sm text-slate-300 block mb-1">Shadow Blur</label> | |
| <input type="range" id="shadowBlur" min="0" max="20" value="0" class="w-full"> | |
| </div> | |
| <div> | |
| <label class="text-sm text-slate-300 block mb-1">Light Angle</label> | |
| <input type="range" id="lightAngle" min="0" max="360" value="45" class="w-full"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Export Modal --> | |
| <div id="exportModal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 hidden"> | |
| <div class="bg-slate-800 rounded-lg p-6 w-full max-w-md"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-xl font-bold">Export Mandala</h3> | |
| <button id="closeExportModal" class="text-slate-400 hover:text-white"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-300 mb-1">Format</label> | |
| <select id="exportFormat" class="w-full bg-slate-700 border border-slate-600 rounded px-3 py-2 text-white"> | |
| <option value="png">PNG (Transparent)</option> | |
| <option value="png-black">PNG (Black Background)</option> | |
| <option value="svg">SVG (Vector)</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-300 mb-1">Resolution</label> | |
| <select id="exportResolution" class="w-full bg-slate-700 border border-slate-600 rounded px-3 py-2 text-white"> | |
| <option value="1x">1x (800×800)</option> | |
| <option value="2x">2x (1600×1600)</option> | |
| <option value="4x">4x (3200×3200)</option> | |
| </select> | |
| </div> | |
| <div class="pt-2"> | |
| <button id="confirmExport" class="w-full bg-indigo-600 hover:bg-indigo-700 py-2 rounded-lg"> | |
| Export Now | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Initialize Fabric.js canvas | |
| const canvas = new fabric.Canvas('mandalaCanvas', { | |
| isDrawingMode: true, | |
| backgroundColor: 'transparent', | |
| selection: false, | |
| preserveObjectStacking: true | |
| }); | |
| // Canvas setup | |
| const canvasContainer = document.getElementById('canvas-container'); | |
| function resizeCanvas() { | |
| const size = Math.min(window.innerWidth - 80, window.innerHeight - 120); | |
| canvas.setWidth(size); | |
| canvas.setHeight(size); | |
| canvas.renderAll(); | |
| } | |
| resizeCanvas(); | |
| window.addEventListener('resize', resizeCanvas); | |
| // App state | |
| const state = { | |
| currentTool: 'brush', | |
| currentColor: '#3b82f6', | |
| brushSize: 5, | |
| brushOpacity: 1, | |
| symmetry: 8, | |
| glowIntensity: 0, | |
| shadowBlur: 0, | |
| lightAngle: 45, | |
| currentShape: 'circle', | |
| shapeSize: 30, | |
| layers: [], | |
| currentLayer: null, | |
| history: [], | |
| historyIndex: -1 | |
| }; | |
| // Initialize first layer | |
| addNewLayer(); | |
| // Tools selection | |
| document.getElementById('brushTool').addEventListener('click', () => { | |
| setTool('brush'); | |
| canvas.isDrawingMode = true; | |
| }); | |
| document.getElementById('shapeTool').addEventListener('click', () => { | |
| setTool('shape'); | |
| canvas.isDrawingMode = false; | |
| }); | |
| document.getElementById('gradientTool').addEventListener('click', () => { | |
| setTool('gradient'); | |
| canvas.isDrawingMode = false; | |
| }); | |
| document.getElementById('eraseTool').addEventListener('click', () => { | |
| setTool('erase'); | |
| canvas.isDrawingMode = true; | |
| }); | |
| function setTool(tool) { | |
| state.currentTool = tool; | |
| document.querySelectorAll('.tool-btn').forEach(btn => btn.classList.remove('active')); | |
| switch(tool) { | |
| case 'brush': | |
| document.getElementById('brushTool').classList.add('active'); | |
| canvas.freeDrawingBrush = new fabric.PencilBrush(canvas); | |
| canvas.freeDrawingBrush.color = state.currentColor; | |
| canvas.freeDrawingBrush.width = state.brushSize; | |
| break; | |
| case 'shape': | |
| document.getElementById('shapeTool').classList.add('active'); | |
| break; | |
| case 'gradient': | |
| document.getElementById('gradientTool').classList.add('active'); | |
| break; | |
| case 'erase': | |
| document.getElementById('eraseTool').classList.add('active'); | |
| canvas.freeDrawingBrush = new fabric.PencilBrush(canvas); | |
| canvas.freeDrawingBrush.color = 'rgba(0,0,0,0)'; | |
| canvas.freeDrawingBrush.width = state.brushSize; | |
| break; | |
| } | |
| } | |
| // Symmetry selection | |
| document.getElementById('symmetry6').addEventListener('click', () => setSymmetry(6)); | |
| document.getElementById('symmetry8').addEventListener('click', () => setSymmetry(8)); | |
| document.getElementById('symmetry12').addEventListener('click', () => setSymmetry(12)); | |
| function setSymmetry(count) { | |
| state.symmetry = count; | |
| document.querySelectorAll('.tool-group:nth-child(2) .tool-btn').forEach(btn => btn.classList.remove('active')); | |
| document.getElementById(`symmetry${count}`).classList.add('active'); | |
| } | |
| // Brush settings | |
| document.getElementById('brushSize').addEventListener('input', function() { | |
| state.brushSize = parseInt(this.value); | |
| document.getElementById('brushSizeValue').textContent = state.brushSize; | |
| if (canvas.freeDrawingBrush) { | |
| canvas.freeDrawingBrush.width = state.brushSize; | |
| } | |
| }); | |
| document.getElementById('brushOpacity').addEventListener('input', function() { | |
| state.brushOpacity = parseInt(this.value) / 100; | |
| document.getElementById('brushOpacityValue').textContent = parseInt(this.value); | |
| if (canvas.freeDrawingBrush && state.currentTool !== 'erase') { | |
| const color = fabric.util.colorValues(state.currentColor); | |
| canvas.freeDrawingBrush.color = `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${state.brushOpacity})`; | |
| } | |
| }); | |
| // Color selection | |
| document.querySelectorAll('.color-swatch').forEach(swatch => { | |
| swatch.addEventListener('click', function() { | |
| const color = this.getAttribute('data-color'); | |
| if (color.includes('gradient')) { | |
| // Handle gradient colors | |
| state.currentColor = color; | |
| } else { | |
| // Solid and metallic colors | |
| state.currentColor = color; | |
| const colorEl = document.getElementById('customColor'); | |
| if (colorEl) colorEl.value = color; | |
| if (canvas.freeDrawingBrush && state.currentTool !== 'erase') { | |
| const colorValues = fabric.util.colorValues(color); | |
| canvas.freeDrawingBrush.color = `rgba(${colorValues[0]}, ${colorValues[1]}, ${colorValues[2]}, ${state.brushOpacity})`; | |
| } | |
| } | |
| }); | |
| }); | |
| document.getElementById('customColor').addEventListener('input', function() { | |
| state.currentColor = this.value; | |
| if (canvas.freeDrawingBrush && state.currentTool !== 'erase') { | |
| const colorValues = fabric.util.colorValues(this.value); | |
| canvas.freeDrawingBrush.color = `rgba(${colorValues[0]}, ${colorValues[1]}, ${colorValues[2]}, ${state.brushOpacity})`; | |
| } | |
| }); | |
| // Shape selection | |
| document.querySelectorAll('.shape-option').forEach(option => { | |
| option.addEventListener('click', function() { | |
| state.currentShape = this.getAttribute('data-shape'); | |
| }); | |
| }); | |
| document.getElementById('shapeSize').addEventListener('input', function() { | |
| state.shapeSize = parseInt(this.value); | |
| }); | |
| // Effects | |
| document.getElementById('glowIntensity').addEventListener('input', function() { | |
| state.glowIntensity = parseInt(this.value); | |
| }); | |
| document.getElementById('shadowBlur').addEventListener('input', function() { | |
| state.shadowBlur = parseInt(this.value); | |
| }); | |
| document.getElementById('lightAngle').addEventListener('input', function() { | |
| state.lightAngle = parseInt(this.value); | |
| }); | |
| // Canvas interaction | |
| canvas.on('mouse:down', function(options) { | |
| if (state.currentTool === 'shape') { | |
| createSymmetricalShape(options.e.clientX, options.e.clientY); | |
| } | |
| }); | |
| canvas.on('touch:start', function(options) { | |
| if (state.currentTool === 'shape') { | |
| const touch = options.e.touches[0]; | |
| createSymmetricalShape(touch.clientX, touch.clientY); | |
| } | |
| }); | |
| function createSymmetricalShape(x, y) { | |
| const pointer = canvas.getPointer(new fabric.Event(x, y)); | |
| const center = { x: canvas.width / 2, y: canvas.height / 2 }; | |
| for (let i = 0; i < state.symmetry; i++) { | |
| const angle = (i * (360 / state.symmetry)) * (Math.PI / 180); | |
| const distance = Math.sqrt( | |
| Math.pow(pointer.x - center.x, 2) + | |
| Math.pow(pointer.y - center.y, 2) | |
| ); | |
| const newX = center.x + distance * Math.cos(angle); | |
| const newY = center.y + distance * Math.sin(angle); | |
| let shape; | |
| switch (state.currentShape) { | |
| case 'circle': | |
| shape = new fabric.Circle({ | |
| left: newX - state.shapeSize / 2, | |
| top: newY - state.shapeSize / 2, | |
| radius: state.shapeSize / 2, | |
| fill: state.currentColor, | |
| originX: 'center', | |
| originY: 'center' | |
| }); | |
| break; | |
| case 'teardrop': | |
| // Teardrop shape using Path | |
| const teardropPath = `M ${newX} ${newY - state.shapeSize/2} | |
| Q ${newX + state.shapeSize/2} ${newY} ${newX} ${newY + state.shapeSize/2} | |
| Q ${newX - state.shapeSize/2} ${newY} ${newX} ${newY - state.shapeSize/2} Z`; | |
| shape = new fabric.Path(teardropPath, { | |
| fill: state.currentColor, | |
| originX: 'center', | |
| originY: 'center' | |
| }); | |
| break; | |
| case 'petal': | |
| // Petal shape using Path | |
| const petalPath = `M ${newX} ${newY} | |
| C ${newX + state.shapeSize/2} ${newY - state.shapeSize/3}, | |
| ${newX + state.shapeSize/3} ${newY - state.shapeSize}, | |
| ${newX} ${newY - state.shapeSize/1.5} | |
| C ${newX - state.shapeSize/3} ${newY - state.shapeSize}, | |
| ${newX - state.shapeSize/2} ${newY - state.shapeSize/3}, | |
| ${newX} ${newY} Z`; | |
| shape = new fabric.Path(petalPath, { | |
| fill: state.currentColor, | |
| originX: 'center', | |
| originY: 'center' | |
| }); | |
| break; | |
| case 'star': | |
| shape = new fabric.Polygon(createStarPoints(newX, newY, 5, state.shapeSize/2, state.shapeSize/4), { | |
| fill: state.currentColor, | |
| originX: 'center', | |
| originY: 'center' | |
| }); | |
| break; | |
| case 'ring': | |
| shape = new fabric.Circle({ | |
| left: newX, | |
| top: newY, | |
| radius: state.shapeSize / 2, | |
| fill: 'transparent', | |
| stroke: state.currentColor, | |
| strokeWidth: 3, | |
| originX: 'center', | |
| originY: 'center' | |
| }); | |
| break; | |
| case 'lotus': | |
| // Lotus shape with multiple petals | |
| const lotusGroup = new fabric.Group([], { | |
| left: newX, | |
| top: newY, | |
| originX: 'center', | |
| originY: 'center' | |
| }); | |
| // Center circle | |
| lotusGroup.add(new fabric.Circle({ | |
| radius: state.shapeSize / 6, | |
| fill: state.currentColor, | |
| originX: 'center', | |
| originY: 'center' | |
| })); | |
| // Petals | |
| for (let j = 0; j < 8; j++) { | |
| const petalAngle = j * (360 / 8) * (Math.PI / 180); | |
| const petalPath = `M 0 0 | |
| C ${state.shapeSize/3} ${-state.shapeSize/4}, | |
| ${state.shapeSize/2} ${-state.shapeSize/6}, | |
| ${state.shapeSize/2} 0 | |
| C ${state.shapeSize/2} ${state.shapeSize/6}, | |
| ${state.shapeSize/3} ${state.shapeSize/4}, | |
| 0 0 Z`; | |
| const petal = new fabric.Path(petalPath, { | |
| fill: state.currentColor, | |
| originX: 'center', | |
| originY: 'center' | |
| }); | |
| petal.rotate(j * 45); | |
| lotusGroup.add(petal); | |
| } | |
| shape = lotusGroup; | |
| break; | |
| } | |
| if (shape) { | |
| applyEffects(shape); | |
| canvas.add(shape); | |
| saveToHistory(); | |
| } | |
| } | |
| } | |
| function createStarPoints(centerX, centerY, points, outerRadius, innerRadius) { | |
| const starPoints = []; | |
| const angle = Math.PI / points; | |
| for (let i = 0; i < 2 * points; i++) { | |
| const radius = i % 2 === 0 ? outerRadius : innerRadius; | |
| starPoints.push({ | |
| x: centerX + radius * Math.sin(i * angle), | |
| y: centerY - radius * Math.cos(i * angle) | |
| }); | |
| } | |
| return starPoints; | |
| } | |
| function applyEffects(object) { | |
| if (state.glowIntensity > 0) { | |
| object.set({ | |
| shadow: new fabric.Shadow({ | |
| color: object.fill || object.stroke || '#ffffff', | |
| blur: state.glowIntensity * 2, | |
| offsetX: 0, | |
| offsetY: 0 | |
| }) | |
| }); | |
| } | |
| if (state.shadowBlur > 0) { | |
| const angleRad = (state.lightAngle * Math.PI) / 180; | |
| const distance = state.shadowBlur; | |
| object.set({ | |
| shadow: new fabric.Shadow({ | |
| color: 'rgba(0,0,0,0.5)', | |
| blur: state.shadowBlur * 2, | |
| offsetX: Math.cos(angleRad) * distance, | |
| offsetY: Math.sin(angleRad) * distance | |
| }) | |
| }); | |
| } | |
| } | |
| // Layers management | |
| function addNewLayer() { | |
| const layerId = Date.now(); | |
| const layer = { | |
| id: layerId, | |
| name: `Layer ${state.layers.length + 1}`, | |
| visible: true, | |
| locked: false | |
| }; | |
| state.layers.push(layer); | |
| state.currentLayer = layerId; | |
| updateLayersUI(); | |
| saveToHistory(); | |
| } | |
| document.getElementById('addLayerBtn').addEventListener('click', addNewLayer); | |
| function updateLayersUI() { | |
| const layersList = document.getElementById('layersList'); | |
| layersList.innerHTML = ''; | |
| state.layers.forEach((layer, index) => { | |
| const layerItem = document.createElement('div'); | |
| layerItem.className = `flex items-center justify-between p-2 rounded ${state.currentLayer === layer.id ? 'bg-slate-700' : 'bg-slate-800'}`; | |
| layerItem.innerHTML = ` | |
| <div class="flex items-center space-x-2"> | |
| <button class="toggle-visibility" data-id="${layer.id}"> | |
| <i class="fas fa-eye${layer.visible ? '' : '-slash'}"></i> | |
| </button> | |
| <span class="text-sm">${layer.name}</span> | |
| </div> | |
| <div class="flex space-x-1"> | |
| <button class="lock-layer" data-id="${layer.id}"> | |
| <i class="fas fa-${layer.locked ? 'lock' : 'lock-open'}"></i> | |
| </button> | |
| <button class="delete-layer" data-id="${layer.id}"> | |
| <i class="fas fa-trash"></i> | |
| </button> | |
| </div> | |
| `; | |
| layerItem.addEventListener('click', () => { | |
| state.currentLayer = layer.id; | |
| updateLayersUI(); | |
| }); | |
| layersList.appendChild(layerItem); | |
| }); | |
| // Add event listeners for the buttons | |
| document.querySelectorAll('.toggle-visibility').forEach(btn => { | |
| btn.addEventListener('click', function(e) { | |
| e.stopPropagation(); | |
| const layerId = parseInt(this.getAttribute('data-id')); | |
| const layer = state.layers.find(l => l.id === layerId); | |
| if (layer) { | |
| layer.visible = !layer.visible; | |
| updateLayersUI(); | |
| saveToHistory(); | |
| } | |
| }); | |
| }); | |
| document.querySelectorAll('.lock-layer').forEach(btn => { | |
| btn.addEventListener('click', function(e) { | |
| e.stopPropagation(); | |
| const layerId = parseInt(this.getAttribute('data-id')); | |
| const layer = state.layers.find(l => l.id === layerId); | |
| if (layer) { | |
| layer.locked = !layer.locked; | |
| updateLayersUI(); | |
| saveToHistory(); | |
| } | |
| }); | |
| }); | |
| document.querySelectorAll('.delete-layer').forEach(btn => { | |
| btn.addEventListener('click', function(e) { | |
| e.stopPropagation(); | |
| const layerId = parseInt(this.getAttribute('data-id')); | |
| if (state.layers.length > 1) { | |
| state.layers = state.layers.filter(l => l.id !== layerId); | |
| if (state.currentLayer === layerId) { | |
| state.currentLayer = state.layers[0].id; | |
| } | |
| updateLayersUI(); | |
| saveToHistory(); | |
| } else { | |
| alert("You can't delete the last layer."); | |
| } | |
| }); | |
| }); | |
| } | |
| // History management | |
| function saveToHistory() { | |
| // Trim history if we've undone some actions | |
| if (state.historyIndex < state.history.length - 1) { | |
| state.history = state.history.slice(0, state.historyIndex + 1); | |
| } | |
| const canvasState = JSON.stringify(canvas.toJSON()); | |
| state.history.push(canvasState); | |
| state.historyIndex = state.history.length - 1; | |
| } | |
| document.getElementById('undoBtn').addEventListener('click', function() { | |
| if (state.historyIndex > 0) { | |
| state.historyIndex--; | |
| loadFromHistory(); | |
| } | |
| }); | |
| document.getElementById('redoBtn').addEventListener('click', function() { | |
| if (state.historyIndex < state.history.length - 1) { | |
| state.historyIndex++; | |
| loadFromHistory(); | |
| } | |
| }); | |
| function loadFromHistory() { | |
| if (state.historyIndex >= 0 && state.historyIndex < state.history.length) { | |
| canvas.loadFromJSON(state.history[state.historyIndex], function() { | |
| canvas.renderAll(); | |
| }); | |
| } | |
| } | |
| // Canvas events that should trigger history saves | |
| canvas.on('path:created', saveToHistory); | |
| canvas.on('object:added', saveToHistory); | |
| canvas.on('object:modified', saveToHistory); | |
| canvas.on('object:removed', saveToHistory); | |
| // Export functionality | |
| document.getElementById('exportBtn').addEventListener('click', function() { | |
| document.getElementById('exportModal').classList.remove('hidden'); | |
| }); | |
| document.getElementById('closeExportModal').addEventListener('click', function() { | |
| document.getElementById('exportModal').classList.add('hidden'); | |
| }); | |
| document.getElementById('confirmExport').addEventListener('click', function() { | |
| const format = document.getElementById('exportFormat').value; | |
| const resolution = document.getElementById('exportResolution').value; | |
| let scale = 1; | |
| switch (resolution) { | |
| case '2x': scale = 2; break; | |
| case '4x': scale = 4; break; | |
| } | |
| if (format === 'svg') { | |
| // Export as SVG | |
| const svg = canvas.toSVG(); | |
| const blob = new Blob([svg], {type: 'image/svg+xml'}); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'mandala-art.svg'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } else { | |
| // Export as PNG | |
| const bgColor = format === 'png-black' ? '#000000' : 'rgba(0,0,0,0)'; | |
| // Create a temporary canvas for export | |
| const exportCanvas = document.createElement('canvas'); | |
| exportCanvas.width = canvas.width * scale; | |
| exportCanvas.height = canvas.height * scale; | |
| const exportContext = exportCanvas.getContext('2d'); | |
| // Fill background if needed | |
| if (bgColor !== 'rgba(0,0,0,0)') { | |
| exportContext.fillStyle = bgColor; | |
| exportContext.fillRect(0, 0, exportCanvas.width, exportCanvas.height); | |
| } | |
| // Draw the canvas content scaled up | |
| const dataUrl = canvas.toDataURL({ | |
| format: 'png', | |
| multiplier: scale | |
| }); | |
| const img = new Image(); | |
| img.onload = function() { | |
| exportContext.drawImage(img, 0, 0); | |
| // Create download link | |
| const a = document.createElement('a'); | |
| a.href = exportCanvas.toDataURL('image/png'); | |
| a.download = 'mandala-art.png'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| }; | |
| img.src = dataUrl; | |
| } | |
| document.getElementById('exportModal').classList.add('hidden'); | |
| }); | |
| // Clear canvas | |
| document.getElementById('clearBtn').addEventListener('click', function() { | |
| if (confirm('Are you sure you want to clear the canvas?')) { | |
| canvas.clear(); | |
| saveToHistory(); | |
| } | |
| }); | |
| // Canvas navigation | |
| document.getElementById('rotateBtn').addEventListener('click', function() { | |
| // Rotate the canvas view | |
| const angle = (canvas.angle || 0) + 15; | |
| canvas.angle = angle % 360; | |
| canvas.renderAll(); | |
| }); | |
| document.getElementById('centerBtn').addEventListener('click', function() { | |
| // Center the canvas view | |
| canvas.angle = 0; | |
| canvas.renderAll(); | |
| }); | |
| // Initialize brush | |
| setTool('brush'); | |
| setSymmetry(8); | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=madansa7/mandala-new" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |