mandala-new / index.html
madansa7's picture
Add 3 files
a24c6e5 verified
<!DOCTYPE html>
<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>