Note: After downloading the greeting card, you can add the birthday kid’s name in the designated space

We are adding more greeting cards soon by categories.
// Photo Editor Application - Complete Implementation with Fixes
class PhotoEditor {
constructor() {
this.canvas = null;
this.originalImage = null;
this.history = [];
this.historyIndex = -1;
this.currentTool = 'basic';
this.isDrawingMode = false;
this.cropMode = false;
this.cropBox = null;
this.zoom = 1;
this.currentFrame = null;
// Application data from JSON
this.stickerCategories = {
"smileys": ["😀","😃","😄","😁","😆","😅","🤣","😂","🙂","🙃","😉","😊","😇","🥰","😍","🤩","😘","😗","😚","😙","😋","😛","😜","🤪","😝","🤑","🤗","🤭","🤫","🤔","🤐","🤨","😐","😑","😶","😏","😒","🙄","😬","🤥","😌","😔","😪","🤤","😴"],
"people": ["🧑","👩","👨","🧔","🧓","👶","🧒","👦","👧","🧑🎓","🧑🏫","🧑💻","🧑🔬","🧑🎨","🧑🚀","👮","🕵️","💂","👷","🤴","👸","👳","👲","🧕","🤵","👰","🤰","🤱","👼","🎅","🤶","🧙","🧚","🧛","🧜","🧝","🧞","🧟","💆","💇","🚶","🏃","💃","🕺"],
"gestures": ["👍","👎","👌","🤌","🤞","✌️","🤟","🤘","🤙","👏","🙌","👐","🤲","🤝","🙏","✍️","💅","🤳","💪","🦾","🦿","🦵","🦶","👂","🦻","👃","🧠","🫀","🫁","🦷","🦴","👀","👁️","👅","👄","💋"],
"nature": ["🌸","🌻","🌼","🌷","🌹","🌺","🌳","🌲","🌵","🍀","🍁","🍂","🍃","🌿","☘️","🎋","🎍","🌾","🌱","🌴","🌊","🏔️","⛰️","🌋","🗻","🏕️","🏖️","🏜️","🏝️","🌅","🌄","🌠","🌌","🌙","🌛","🌜","🌚","🌕","🌖","🌗","🌘","🌑","🌒","🌓","🌔","⭐","🌟","💫","✨","☄️","☀️","🌤️","⛅","🌦️","🌧️","⛈️","🌩️","🌨️","❄️","☃️","⛄","🌬️","💨","🌪️","🌈","☔","💧","💦"],
"food": ["🍎","🍊","🍋","🍌","🍉","🍇","🍓","🫐","🍑","🥭","🍍","🥝","🥥","🍅","🥑","🍆","🥔","🥕","🌶️","🫒","🥒","🥬","🥦","🧄","🧅","🍄","🥜","🌰","🍞","🥐","🥖","🫓","🥨","🥯","🥞","🧇","🧀","🍖","🍗","🥩","🥓","🍔","🍟","🍕","🌭","🥪","🌮","🌯","🫔","🥙","🧆","🥚","🍳","🥘","🍲","🫕","🥣","🥗","🍿","🧈","🧂","🥫"],
"travel": ["✈️","🚗","🚕","🚙","🚌","🚎","🏎️","🚓","🚑","🚒","🚐","🛻","🚚","🚛","🚜","🏍️","🛵","🚲","🛴","🛹","🛼","🚁","🛸","🚀","🛶","⛵","🚤","🛥️","🛳️","⛴️","🚢","⚓","⛽","🚧","🚦","🚥","🗺️","🗿","🗽","🗼","🏰","🏯","🏟️","🎡","🎢","🎠","⛱️","🏖️","🏝️","🏜️"],
"symbols": ["❤️","🧡","💛","💚","💙","💜","🖤","🤍","🤭","💔","❣️","💕","💞","💓","💗","💖","💘","💝","💟","☮️","✝️","☪️","🕉️","☸️","✡️","🔯","🕎","☯️","☦️","🛐","⛎","♈","♉","♊","♋","♌","♍","♎","♏","♐","♑","♒","♓","🆔","⚛️","🉑","☢️","☣️","📴","📳","🈶","🈚","🈸","🈺","🈷️","✴️","🆚","💮","🉐","㊙️","㊗️","🈴","🈵","🈹","🈲","🅰️","🅱️","🆎","🆑","🅾️","🆘","❌","⭕","🛑","⛔","📛","🚫","💯","💢","♨️","🚷","🚯","🚳","🚱","🔞","📵","🚭","❗","❕","❓","❔","‼️","⁉️","🔅","🔆","〽️","⚠️","🚸","🔱","⚜️","🔰","♻️","✅","🈯","💹","❇️","✳️","❎","🌐","💠","Ⓜ️","🌀","💤","🏧","🚾","♿","🅿️","🈳","🈂️","🛂","🛃","🛄","🛅","🚹","🚺","🚼","🚻","🚮","🎦","📶","🈁","🔣","ℹ️","🔤","🔡","🔠","🆖","🆗","🆙","🆒","🆕","🆓","0️⃣","1️⃣","2️⃣","3️⃣","4️⃣","5️⃣","6️⃣","7️⃣","8️⃣","9️⃣","🔟"]
};
this.filterPresets = {
"vintage": {"name": "Vintage", "sepia": 0.3, "brightness": -0.1, "contrast": 0.2},
"blackwhite": {"name": "Black & White", "grayscale": 1},
"vibrant": {"name": "Vibrant", "saturation": 0.4, "brightness": 0.1},
"cool": {"name": "Cool", "tint": "#0066cc", "opacity": 0.1},
"warm": {"name": "Warm", "tint": "#ff6600", "opacity": 0.1},
"sepia": {"name": "Sepia", "sepia": 0.8}
};
this.frameStyles = {
"simple": {"name": "Simple", "color": "#ffffff", "width": 20},
"modern": {"name": "Modern", "color": "#1FB8CD", "width": 30},
"vintage": {"name": "Vintage", "color": "#8b4513", "width": 40},
"neon": {"name": "Neon", "color": "#ff00ff", "width": 25}
};
this.init();
}
init() {
this.setupCanvas();
this.setupEventListeners();
this.setupUI();
this.updateUI();
}
setupCanvas() {
this.canvas = new fabric.Canvas('fabricCanvas', {
width: 800,
height: 600,
backgroundColor: '#ffffff',
preserveObjectStacking: true
});
// Hide canvas initially
document.getElementById('fabricCanvas').style.display = 'none';
}
setupEventListeners() {
// File upload - Fixed implementation
const uploadBtn = document.getElementById('uploadBtn');
const fileInput = document.getElementById('fileInput');
if (uploadBtn && fileInput) {
uploadBtn.addEventListener('click', (e) => {
e.preventDefault();
fileInput.click();
});
fileInput.addEventListener('change', (e) => {
if (e.target.files && e.target.files[0]) {
this.handleFileUpload(e.target.files[0]);
}
});
}
// Drag and drop - Fixed implementation
const uploadArea = document.getElementById('uploadArea');
if (uploadArea) {
uploadArea.addEventListener('click', (e) => {
e.preventDefault();
if (fileInput) fileInput.click();
});
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith('image/')) {
this.handleFileUpload(file);
}
});
}
// Tool buttons - Fixed implementation
document.querySelectorAll('.tool-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
const tool = btn.getAttribute('data-tool');
if (tool) {
this.switchTool(tool);
}
});
});
// Basic adjustments
this.setupSliderListener('brightness', (value) => this.applyBrightness(value));
this.setupSliderListener('contrast', (value) => this.applyContrast(value));
this.setupSliderListener('saturation', (value) => this.applySaturation(value));
this.setupSliderListener('hue', (value) => this.applyHue(value));
const resetBtn = document.getElementById('resetAdjustments');
if (resetBtn) {
resetBtn.addEventListener('click', () => this.resetAdjustments());
}
// Transform tools
const cropBtn = document.getElementById('cropBtn');
const applyCropBtn = document.getElementById('applyCrop');
const cancelCropBtn = document.getElementById('cancelCrop');
const rotateLeftBtn = document.getElementById('rotateLeft');
const rotateRightBtn = document.getElementById('rotateRight');
const flipHBtn = document.getElementById('flipH');
const flipVBtn = document.getElementById('flipV');
if (cropBtn) cropBtn.addEventListener('click', () => this.startCrop());
if (applyCropBtn) applyCropBtn.addEventListener('click', () => this.applyCrop());
if (cancelCropBtn) cancelCropBtn.addEventListener('click', () => this.cancelCrop());
if (rotateLeftBtn) rotateLeftBtn.addEventListener('click', () => this.rotateImage(-90));
if (rotateRightBtn) rotateRightBtn.addEventListener('click', () => this.rotateImage(90));
if (flipHBtn) flipHBtn.addEventListener('click', () => this.flipImage('horizontal'));
if (flipVBtn) flipVBtn.addEventListener('click', () => this.flipImage('vertical'));
// Text tools
const addTextBtn = document.getElementById('addText');
if (addTextBtn) {
addTextBtn.addEventListener('click', () => this.addText());
}
this.setupSliderListener('fontSize', (value) => this.updateTextSize(value));
// Drawing tools
const drawingModeBtn = document.getElementById('drawingMode');
const clearDrawingBtn = document.getElementById('clearDrawing');
const brushModeBtn = document.getElementById('brushMode');
const eraserModeBtn = document.getElementById('eraserMode');
const brushColorInput = document.getElementById('brushColor');
if (drawingModeBtn) drawingModeBtn.addEventListener('click', () => this.toggleDrawingMode());
if (clearDrawingBtn) clearDrawingBtn.addEventListener('click', () => this.clearDrawing());
if (brushModeBtn) brushModeBtn.addEventListener('click', () => this.setBrushMode('brush'));
if (eraserModeBtn) eraserModeBtn.addEventListener('click', () => this.setBrushMode('eraser'));
this.setupSliderListener('brushSize', (value) => this.updateBrushSize(value));
this.setupSliderListener('brushOpacity', (value) => this.updateBrushOpacity(value));
if (brushColorInput) {
brushColorInput.addEventListener('change', (e) => {
this.updateBrushColor(e.target.value);
});
}
// Watermark
const addWatermarkBtn = document.getElementById('addWatermark');
if (addWatermarkBtn) {
addWatermarkBtn.addEventListener('click', () => this.addWatermark());
}
this.setupSliderListener('watermarkOpacity', (value) => this.updateWatermarkOpacity(value));
this.setupSliderListener('watermarkSize', (value) => this.updateWatermarkSize(value));
// Canvas controls
const zoomInBtn = document.getElementById('zoomIn');
const zoomOutBtn = document.getElementById('zoomOut');
const fitCanvasBtn = document.getElementById('fitCanvas');
if (zoomInBtn) zoomInBtn.addEventListener('click', () => this.zoomCanvas(1.2));
if (zoomOutBtn) zoomOutBtn.addEventListener('click', () => this.zoomCanvas(0.8));
if (fitCanvasBtn) fitCanvasBtn.addEventListener('click', () => this.fitCanvas());
// Undo/Redo
const undoBtn = document.getElementById('undoBtn');
const redoBtn = document.getElementById('redoBtn');
if (undoBtn) undoBtn.addEventListener('click', () => this.undo());
if (redoBtn) redoBtn.addEventListener('click', () => this.redo());
// Export
const exportBtn = document.getElementById('exportBtn');
const confirmExportBtn = document.getElementById('confirmExport');
const cancelExportBtn = document.getElementById('cancelExport');
const modalClose = document.querySelector('.modal-close');
if (exportBtn) exportBtn.addEventListener('click', () => this.showExportModal());
if (confirmExportBtn) confirmExportBtn.addEventListener('click', () => this.exportImage());
if (cancelExportBtn) cancelExportBtn.addEventListener('click', () => this.hideExportModal());
if (modalClose) modalClose.addEventListener('click', () => this.hideExportModal());
this.setupSliderListener('exportQuality', (value) => {
const valueEl = document.getElementById('exportQualityValue');
if (valueEl) {
valueEl.textContent = Math.round(value * 100) + '%';
}
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.ctrlKey || e.metaKey) {
switch (e.key) {
case 'z':
e.preventDefault();
if (e.shiftKey) {
this.redo();
} else {
this.undo();
}
break;
case 'y':
e.preventDefault();
this.redo();
break;
}
}
if (e.key === 'Escape' && this.cropMode) {
this.cancelCrop();
}
});
// Canvas events
this.canvas.on('path:created', () => this.saveState());
this.canvas.on('object:modified', () => this.saveState());
this.canvas.on('object:added', (e) => {
if (!e.target.isBaseImage && !e.target.isFrame) {
this.saveState();
}
});
this.canvas.on('object:removed', () => this.saveState());
}
setupSliderListener(sliderId, callback) {
const slider = document.getElementById(sliderId);
const valueElement = document.getElementById(sliderId + 'Value');
if (!slider || !valueElement) return;
slider.addEventListener('input', (e) => {
let value = parseFloat(e.target.value);
let displayValue = value;
if (sliderId === 'hue') {
displayValue = Math.round(value) + '°';
} else if (sliderId.includes('Opacity') || sliderId === 'exportQuality') {
displayValue = Math.round(value * (sliderId === 'exportQuality' ? 100 : 1)) + '%';
} else if (sliderId.includes('Size')) {
displayValue = Math.round(value) + 'px';
} else {
displayValue = Math.round(value);
}
valueElement.textContent = displayValue;
callback(value);
});
}
setupUI() {
this.populateFilters();
this.populateStickers();
this.populateFrames();
}
populateFilters() {
const filterGrid = document.getElementById('filterGrid');
if (!filterGrid) return;
filterGrid.innerHTML = '';
Object.values(this.filterPresets).forEach(filter => {
const btn = document.createElement('button');
btn.className = 'filter-btn';
btn.textContent = filter.name;
btn.addEventListener('click', () => this.applyFilter(filter));
filterGrid.appendChild(btn);
});
const clearFiltersBtn = document.getElementById('clearFilters');
if (clearFiltersBtn) {
clearFiltersBtn.addEventListener('click', () => this.clearFilters());
}
}
populateStickers() {
// Category buttons
document.querySelectorAll('.category-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
document.querySelectorAll('.category-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const category = btn.getAttribute('data-category');
if (category) {
this.showStickerCategory(category);
}
});
});
// Show default category
this.showStickerCategory('smileys');
}
showStickerCategory(category) {
const stickerGrid = document.getElementById('stickerGrid');
if (!stickerGrid) return;
stickerGrid.innerHTML = '';
if (this.stickerCategories[category]) {
this.stickerCategories[category].forEach(sticker => {
const btn = document.createElement('button');
btn.className = 'sticker-btn';
btn.textContent = sticker;
btn.addEventListener('click', () => this.addSticker(sticker));
stickerGrid.appendChild(btn);
});
}
}
populateFrames() {
const frameGrid = document.getElementById('frameGrid');
if (!frameGrid) return;
frameGrid.innerHTML = '';
Object.values(this.frameStyles).forEach(frame => {
const btn = document.createElement('button');
btn.className = 'frame-btn';
// Create frame preview
const preview = document.createElement('div');
preview.className = 'frame-preview';
preview.style.border = `${Math.max(2, frame.width/10)}px solid ${frame.color}`;
preview.textContent = 'Preview';
const label = document.createElement('div');
label.textContent = frame.name;
btn.appendChild(preview);
btn.appendChild(label);
btn.addEventListener('click', () => this.addFrame(frame));
frameGrid.appendChild(btn);
});
const removeFrameBtn = document.getElementById('removeFrame');
if (removeFrameBtn) {
removeFrameBtn.addEventListener('click', () => this.removeFrame());
}
}
handleFileUpload(file) {
if (!file || !file.type.startsWith('image/')) {
alert('Please select a valid image file.');
return;
}
this.showLoading();
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
fabric.Image.fromURL(e.target.result, (fabricImg) => {
this.canvas.clear();
// Scale image to fit canvas while maintaining aspect ratio
const canvasWidth = this.canvas.width;
const canvasHeight = this.canvas.height;
const imgWidth = fabricImg.width;
const imgHeight = fabricImg.height;
const scale = Math.min(canvasWidth / imgWidth, canvasHeight / imgHeight, 1);
fabricImg.scale(scale);
fabricImg.set({
left: (canvasWidth - imgWidth * scale) / 2,
top: (canvasHeight - imgHeight * scale) / 2,
selectable: false,
evented: false,
isBaseImage: true,
lockMovementX: true,
lockMovementY: true,
lockRotation: true,
lockScalingX: true,
lockScalingY: true
});
this.canvas.add(fabricImg);
this.canvas.renderAll();
this.originalImage = fabricImg;
this.showCanvas();
this.history = [];
this.historyIndex = -1;
this.saveState();
this.hideLoading();
}, {crossOrigin: 'anonymous'});
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
showCanvas() {
const uploadArea = document.getElementById('uploadArea');
const fabricCanvas = document.getElementById('fabricCanvas');
if (uploadArea) uploadArea.style.display = 'none';
if (fabricCanvas) fabricCanvas.style.display = 'block';
}
switchTool(tool) {
this.currentTool = tool;
// Update tool buttons - Fixed
document.querySelectorAll('.tool-btn').forEach(btn => {
btn.classList.remove('active');
if (btn.getAttribute('data-tool') === tool) {
btn.classList.add('active');
}
});
// Update tool panels - Fixed
document.querySelectorAll('.tool-panel').forEach(panel => {
panel.classList.remove('active');
if (panel.getAttribute('data-panel') === tool) {
panel.classList.add('active');
}
});
// Disable drawing mode when switching tools
if (tool !== 'draw' && this.isDrawingMode) {
this.toggleDrawingMode();
}
// Disable crop mode when switching tools
if (tool !== 'transform' && this.cropMode) {
this.cancelCrop();
}
}
// Crop Implementation
startCrop() {
if (this.cropMode) {
this.cancelCrop();
return;
}
this.cropMode = true;
this.canvas.selection = false;
// Make all objects unselectable
this.canvas.forEachObject(obj => {
obj.selectable = false;
obj.evented = false;
});
// Show crop overlay
const cropOverlay = document.getElementById('cropOverlay');
if (cropOverlay) {
cropOverlay.classList.remove('hidden');
}
// Update button
const cropBtn = document.getElementById('cropBtn');
if (cropBtn) {
cropBtn.textContent = '❌ Cancel Crop';
cropBtn.classList.remove('btn--primary');
cropBtn.classList.add('btn--secondary');
}
this.setupCropHandlers();
}
setupCropHandlers() {
let isDrawing = false;
let startX, startY;
const mouseDown = (options) => {
if (!this.cropMode) return;
const pointer = this.canvas.getPointer(options.e);
isDrawing = true;
startX = pointer.x;
startY = pointer.y;
// Remove existing crop box
if (this.cropBox) {
this.canvas.remove(this.cropBox);
}
// Create new crop box
this.cropBox = new fabric.Rect({
left: startX,
top: startY,
width: 1,
height: 1,
fill: 'rgba(31, 184, 205, 0.2)',
stroke: '#1FB8CD',
strokeWidth: 2,
strokeDashArray: [5, 5],
selectable: false,
evented: false,
excludeFromExport: true
});
this.canvas.add(this.cropBox);
};
const mouseMove = (options) => {
if (!isDrawing || !this.cropBox) return;
const pointer = this.canvas.getPointer(options.e);
const width = pointer.x - startX;
const height = pointer.y - startY;
// Apply aspect ratio if selected
const cropRatio = document.getElementById('cropRatio');
const ratio = cropRatio ? parseFloat(cropRatio.value) : null;
if (ratio && ratio !== 'free' && !isNaN(ratio)) {
const adjustedHeight = Math.abs(width) / ratio;
this.cropBox.set({
width: Math.abs(width),
height: adjustedHeight,
left: width < 0 ? pointer.x : startX,
top: height < 0 ? startY - adjustedHeight : startY
});
} else {
this.cropBox.set({
width: Math.abs(width),
height: Math.abs(height),
left: width < 0 ? pointer.x : startX,
top: height < 0 ? pointer.y : startY
});
}
this.canvas.renderAll();
};
const mouseUp = () => {
isDrawing = false;
};
this.canvas.on('mouse:down', mouseDown);
this.canvas.on('mouse:move', mouseMove);
this.canvas.on('mouse:up', mouseUp);
// Store handlers for cleanup
this.cropHandlers = { mouseDown, mouseMove, mouseUp };
}
applyCrop() {
if (!this.cropBox || !this.originalImage) return;
const cropRect = {
left: this.cropBox.left,
top: this.cropBox.top,
width: this.cropBox.width,
height: this.cropBox.height
};
// Create cropped image
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = cropRect.width;
canvas.height = cropRect.height;
// Get the current canvas as image
const canvasElement = this.canvas.getElement();
ctx.drawImage(
canvasElement,
cropRect.left, cropRect.top,
cropRect.width, cropRect.height,
0, 0,
cropRect.width, cropRect.height
);
// Create new fabric image from cropped canvas
fabric.Image.fromURL(canvas.toDataURL(), (croppedImg) => {
this.canvas.clear();
// Resize canvas to match cropped image
this.canvas.setDimensions({
width: cropRect.width,
height: cropRect.height
});
croppedImg.set({
left: 0,
top: 0,
selectable: false,
evented: false,
isBaseImage: true
});
this.canvas.add(croppedImg);
this.originalImage = croppedImg;
this.canvas.renderAll();
this.cancelCrop();
this.saveState();
});
}
cancelCrop() {
this.cropMode = false;
this.canvas.selection = true;
// Remove crop box
if (this.cropBox) {
this.canvas.remove(this.cropBox);
this.cropBox = null;
}
// Make objects selectable again
this.canvas.forEachObject(obj => {
if (!obj.isBaseImage) {
obj.selectable = true;
obj.evented = true;
}
});
// Remove crop handlers
if (this.cropHandlers) {
this.canvas.off('mouse:down', this.cropHandlers.mouseDown);
this.canvas.off('mouse:move', this.cropHandlers.mouseMove);
this.canvas.off('mouse:up', this.cropHandlers.mouseUp);
this.cropHandlers = null;
}
// Hide crop overlay
const cropOverlay = document.getElementById('cropOverlay');
if (cropOverlay) {
cropOverlay.classList.add('hidden');
}
// Reset button
const cropBtn = document.getElementById('cropBtn');
if (cropBtn) {
cropBtn.textContent = '✂️ Start Crop';
cropBtn.classList.add('btn--primary');
cropBtn.classList.remove('btn--secondary');
}
this.canvas.renderAll();
}
// Basic Adjustments
applyBrightness(value) {
this.applyImageFilter('Brightness', { brightness: value / 100 });
}
applyContrast(value) {
this.applyImageFilter('Contrast', { contrast: value / 100 });
}
applySaturation(value) {
this.applyImageFilter('Saturation', { saturation: value / 100 });
}
applyHue(value) {
this.applyImageFilter('HueRotation', { rotation: value * Math.PI / 180 });
}
applyImageFilter(filterType, options) {
const baseImage = this.canvas.getObjects().find(obj => obj.isBaseImage);
if (!baseImage) return;
// Remove existing filter of this type
baseImage.filters = baseImage.filters.filter(filter =>
filter.constructor.name !== filterType
);
// Add new filter if value is not zero
const filterValue = Object.values(options)[0];
if (filterValue !== 0) {
const filterClass = fabric.Image.filters[filterType];
if (filterClass) {
baseImage.filters.push(new filterClass(options));
}
}
baseImage.applyFilters();
this.canvas.renderAll();
}
resetAdjustments() {
const baseImage = this.canvas.getObjects().find(obj => obj.isBaseImage);
if (!baseImage) return;
baseImage.filters = [];
baseImage.applyFilters();
this.canvas.renderAll();
// Reset sliders
['brightness', 'contrast', 'saturation', 'hue'].forEach(id => {
const slider = document.getElementById(id);
const valueEl = document.getElementById(id + 'Value');
if (slider && valueEl) {
slider.value = 0;
valueEl.textContent = id === 'hue' ? '0°' : '0';
}
});
this.saveState();
}
// Transform Tools
rotateImage(angle) {
const baseImage = this.canvas.getObjects().find(obj => obj.isBaseImage);
if (!baseImage) return;
baseImage.rotate(baseImage.angle + angle);
this.canvas.centerObject(baseImage);
this.canvas.renderAll();
this.saveState();
}
flipImage(direction) {
const baseImage = this.canvas.getObjects().find(obj => obj.isBaseImage);
if (!baseImage) return;
if (direction === 'horizontal') {
baseImage.set('flipX', !baseImage.flipX);
} else {
baseImage.set('flipY', !baseImage.flipY);
}
this.canvas.renderAll();
this.saveState();
}
// Filters
applyFilter(filter) {
const baseImage = this.canvas.getObjects().find(obj => obj.isBaseImage);
if (!baseImage) return;
// Clear existing filters
baseImage.filters = [];
// Apply filter based on type
if (filter.name === 'Black & White' && filter.grayscale) {
baseImage.filters.push(new fabric.Image.filters.Grayscale());
} else if (filter.name === 'Sepia' && filter.sepia) {
baseImage.filters.push(new fabric.Image.filters.Sepia());
} else if (filter.name === 'Vintage') {
if (filter.sepia) baseImage.filters.push(new fabric.Image.filters.Sepia());
if (filter.brightness) baseImage.filters.push(new fabric.Image.filters.Brightness({ brightness: filter.brightness }));
if (filter.contrast) baseImage.filters.push(new fabric.Image.filters.Contrast({ contrast: filter.contrast }));
} else if (filter.name === 'Cool' && filter.tint) {
baseImage.filters.push(new fabric.Image.filters.BlendColor({
color: filter.tint,
mode: 'multiply',
alpha: filter.opacity || 0.1
}));
} else if (filter.name === 'Warm' && filter.tint) {
baseImage.filters.push(new fabric.Image.filters.BlendColor({
color: filter.tint,
mode: 'multiply',
alpha: filter.opacity || 0.1
}));
} else if (filter.name === 'Vibrant') {
if (filter.saturation) baseImage.filters.push(new fabric.Image.filters.Saturation({ saturation: filter.saturation }));
if (filter.brightness) baseImage.filters.push(new fabric.Image.filters.Brightness({ brightness: filter.brightness }));
}
baseImage.applyFilters();
this.canvas.renderAll();
this.saveState();
// Update UI
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.toggle('active', btn.textContent === filter.name);
});
}
clearFilters() {
const baseImage = this.canvas.getObjects().find(obj => obj.isBaseImage);
if (!baseImage) return;
baseImage.filters = [];
baseImage.applyFilters();
this.canvas.renderAll();
this.saveState();
// Update UI
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.remove('active');
});
}
// Text Tool
addText() {
const textInput = document.getElementById('textInput');
const fontFamily = document.getElementById('fontFamily');
const fontSize = document.getElementById('fontSize');
const textColor = document.getElementById('textColor');
const text = textInput ? textInput.value || 'Sample Text' : 'Sample Text';
const font = fontFamily ? fontFamily.value : 'Arial';
const size = fontSize ? parseInt(fontSize.value) : 30;
const color = textColor ? textColor.value : '#ffffff';
const textObj = new fabric.Text(text, {
left: this.canvas.width / 2,
top: this.canvas.height / 2,
fontFamily: font,
fontSize: size,
fill: color,
originX: 'center',
originY: 'center',
textAlign: 'center'
});
this.canvas.add(textObj);
this.canvas.setActiveObject(textObj);
this.canvas.renderAll();
this.saveState();
// Clear input
if (textInput) textInput.value = '';
}
updateTextSize(size) {
const activeObj = this.canvas.getActiveObject();
if (activeObj && activeObj.type === 'text') {
activeObj.set('fontSize', size);
this.canvas.renderAll();
}
}
// Drawing Tools
toggleDrawingMode() {
this.isDrawingMode = !this.isDrawingMode;
this.canvas.isDrawingMode = this.isDrawingMode;
const btn = document.getElementById('drawingMode');
if (btn) {
if (this.isDrawingMode) {
btn.textContent = 'Disable Drawing';
btn.classList.add('btn--primary');
btn.classList.remove('btn--secondary');
this.canvas.freeDrawingBrush = new fabric.PencilBrush(this.canvas);
this.updateBrushSettings();
} else {
btn.textContent = 'Enable Drawing';
btn.classList.add('btn--secondary');
btn.classList.remove('btn--primary');
}
}
}
setBrushMode(mode) {
const brushModeBtn = document.getElementById('brushMode');
const eraserModeBtn = document.getElementById('eraserMode');
if (brushModeBtn) brushModeBtn.classList.remove('active');
if (eraserModeBtn) eraserModeBtn.classList.remove('active');
if (mode === 'brush') {
if (brushModeBtn) brushModeBtn.classList.add('active');
if (this.canvas.freeDrawingBrush) {
this.canvas.freeDrawingBrush = new fabric.PencilBrush(this.canvas);
this.updateBrushSettings();
}
} else if (mode === 'eraser') {
if (eraserModeBtn) eraserModeBtn.classList.add('active');
if (this.canvas.freeDrawingBrush) {
this.canvas.freeDrawingBrush = new fabric.EraserBrush(this.canvas);
const brushSize = document.getElementById('brushSize');
if (brushSize) {
this.updateBrushSize(parseInt(brushSize.value));
}
}
}
}
updateBrushSize(size) {
if (this.canvas.freeDrawingBrush) {
this.canvas.freeDrawingBrush.width = parseInt(size);
}
}
updateBrushColor(color) {
if (this.canvas.freeDrawingBrush && this.canvas.freeDrawingBrush.constructor.name === 'PencilBrush') {
this.updateBrushSettings();
}
}
updateBrushOpacity(opacity) {
if (this.canvas.freeDrawingBrush && this.canvas.freeDrawingBrush.constructor.name === 'PencilBrush') {
this.updateBrushSettings();
}
}
updateBrushSettings() {
if (!this.canvas.freeDrawingBrush || this.canvas.freeDrawingBrush.constructor.name !== 'PencilBrush') return;
const brushColor = document.getElementById('brushColor');
const brushOpacity = document.getElementById('brushOpacity');
const brushSize = document.getElementById('brushSize');
const color = brushColor ? brushColor.value : '#ffffff';
const opacity = brushOpacity ? parseInt(brushOpacity.value) / 100 : 1;
const size = brushSize ? parseInt(brushSize.value) : 5;
// Convert hex to rgba
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
this.canvas.freeDrawingBrush.color = `rgba(${r}, ${g}, ${b}, ${opacity})`;
this.canvas.freeDrawingBrush.width = size;
}
clearDrawing() {
const objects = this.canvas.getObjects();
const paths = objects.filter(obj => obj.type === 'path');
paths.forEach(path => this.canvas.remove(path));
this.canvas.renderAll();
this.saveState();
}
// Stickers
addSticker(sticker) {
const stickerObj = new fabric.Text(sticker, {
left: this.canvas.width / 2,
top: this.canvas.height / 2,
fontSize: 60,
originX: 'center',
originY: 'center',
selectable: true,
moveable: true
});
this.canvas.add(stickerObj);
this.canvas.setActiveObject(stickerObj);
this.canvas.renderAll();
this.saveState();
}
// Frames
addFrame(frameStyle) {
this.removeFrame(); // Remove existing frame
if (!this.originalImage) return;
const baseImage = this.canvas.getObjects().find(obj => obj.isBaseImage);
if (!baseImage) return;
// Create frame that fits the image dimensions
const imageLeft = baseImage.left - (baseImage.getScaledWidth() / 2);
const imageTop = baseImage.top - (baseImage.getScaledHeight() / 2);
const imageWidth = baseImage.getScaledWidth();
const imageHeight = baseImage.getScaledHeight();
const frameRect = new fabric.Rect({
left: imageLeft - frameStyle.width/2,
top: imageTop - frameStyle.width/2,
width: imageWidth + frameStyle.width,
height: imageHeight + frameStyle.width,
fill: 'transparent',
stroke: frameStyle.color,
strokeWidth: frameStyle.width,
selectable: false,
evented: false,
isFrame: true,
excludeFromExport: false
});
this.canvas.add(frameRect);
this.canvas.bringToFront(frameRect);
this.currentFrame = frameRect;
this.canvas.renderAll();
this.saveState();
// Update UI
document.querySelectorAll('.frame-btn').forEach(btn => {
const labelEl = btn.querySelector('div:last-child');
if (labelEl) {
btn.classList.toggle('active', labelEl.textContent === frameStyle.name);
}
});
}
removeFrame() {
const objects = this.canvas.getObjects();
const frames = objects.filter(obj => obj.isFrame);
frames.forEach(frame => this.canvas.remove(frame));
this.currentFrame = null;
this.canvas.renderAll();
// Update UI
document.querySelectorAll('.frame-btn').forEach(btn => {
btn.classList.remove('active');
});
if (frames.length > 0) {
this.saveState();
}
}
// Watermark
addWatermark() {
const watermarkText = document.getElementById('watermarkText');
const watermarkPosition = document.getElementById('watermarkPosition');
const watermarkOpacity = document.getElementById('watermarkOpacity');
const watermarkSize = document.getElementById('watermarkSize');
const text = watermarkText ? watermarkText.value : '';
if (!text) {
alert('Please enter watermark text');
return;
}
const position = watermarkPosition ? watermarkPosition.value : 'bottomRight';
const opacity = watermarkOpacity ? parseInt(watermarkOpacity.value) / 100 : 0.5;
const size = watermarkSize ? parseInt(watermarkSize.value) : 24;
let left, top, originX = 'left', originY = 'top';
switch (position) {
case 'topLeft':
left = 20;
top = 20;
break;
case 'topRight':
left = this.canvas.width - 20;
top = 20;
originX = 'right';
break;
case 'bottomLeft':
left = 20;
top = this.canvas.height - 20;
originY = 'bottom';
break;
case 'bottomRight':
left = this.canvas.width - 20;
top = this.canvas.height - 20;
originX = 'right';
originY = 'bottom';
break;
case 'center':
left = this.canvas.width / 2;
top = this.canvas.height / 2;
originX = 'center';
originY = 'center';
break;
}
const watermark = new fabric.Text(text, {
left: left,
top: top,
fontSize: size,
fill: `rgba(255, 255, 255, ${opacity})`,
fontFamily: 'Arial',
originX: originX,
originY: originY,
shadow: new fabric.Shadow({
color: 'rgba(0,0,0,0.5)',
blur: 2,
offsetX: 1,
offsetY: 1
})
});
this.canvas.add(watermark);
this.canvas.setActiveObject(watermark);
this.canvas.renderAll();
this.saveState();
}
updateWatermarkOpacity(opacity) {
const activeObj = this.canvas.getActiveObject();
if (activeObj && activeObj.type === 'text') {
const alpha = parseInt(opacity) / 100;
activeObj.set('fill', `rgba(255, 255, 255, ${alpha})`);
this.canvas.renderAll();
}
}
updateWatermarkSize(size) {
const activeObj = this.canvas.getActiveObject();
if (activeObj && activeObj.type === 'text') {
activeObj.set('fontSize', parseInt(size));
this.canvas.renderAll();
}
}
// Canvas Controls
zoomCanvas(factor) {
this.zoom *= factor;
this.zoom = Math.max(0.1, Math.min(5, this.zoom));
this.canvas.setZoom(this.zoom);
this.updateZoomDisplay();
}
fitCanvas() {
this.zoom = 1;
this.canvas.setZoom(1);
this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]);
this.updateZoomDisplay();
}
updateZoomDisplay() {
const zoomLevel = document.getElementById('zoomLevel');
if (zoomLevel) {
zoomLevel.textContent = Math.round(this.zoom * 100) + '%';
}
}
// History Management
saveState() {
const state = JSON.stringify(this.canvas.toJSON(['isBaseImage', 'isFrame', 'excludeFromExport']));
// Remove states after current index
this.history = this.history.slice(0, this.historyIndex + 1);
// Add new state
this.history.push(state);
this.historyIndex++;
// Limit history size
if (this.history.length > 50) {
this.history.shift();
this.historyIndex--;
}
this.updateUndoRedoButtons();
}
undo() {
if (this.historyIndex > 0) {
this.historyIndex--;
this.loadState(this.history[this.historyIndex]);
this.updateUndoRedoButtons();
}
}
redo() {
if (this.historyIndex < this.history.length - 1) {
this.historyIndex++;
this.loadState(this.history[this.historyIndex]);
this.updateUndoRedoButtons();
}
}
loadState(state) {
this.canvas.loadFromJSON(state, () => {
this.canvas.renderAll();
// Restore references
this.originalImage = this.canvas.getObjects().find(obj => obj.isBaseImage);
this.currentFrame = this.canvas.getObjects().find(obj => obj.isFrame);
});
}
updateUndoRedoButtons() {
const undoBtn = document.getElementById('undoBtn');
const redoBtn = document.getElementById('redoBtn');
if (undoBtn) undoBtn.disabled = this.historyIndex <= 0;
if (redoBtn) redoBtn.disabled = this.historyIndex >= this.history.length - 1;
}
// Export with canvas.toBlob()
showExportModal() {
const exportModal = document.getElementById('exportModal');
if (exportModal) {
exportModal.classList.remove('hidden');
}
}
hideExportModal() {
const exportModal = document.getElementById('exportModal');
if (exportModal) {
exportModal.classList.add('hidden');
}
}
exportImage() {
const exportFormat = document.getElementById('exportFormat');
const exportQuality = document.getElementById('exportQuality');
const exportFilename = document.getElementById('exportFilename');
const format = exportFormat ? exportFormat.value : 'png';
const quality = exportQuality ? parseFloat(exportQuality.value) : 0.9;
const filename = exportFilename ? exportFilename.value || 'edited-image' : 'edited-image';
this.showLoading();
// Temporarily hide objects marked as excludeFromExport
const hiddenObjects = [];
this.canvas.forEachObject(obj => {
if (obj.excludeFromExport) {
obj.visible = false;
hiddenObjects.push(obj);
}
});
this.canvas.renderAll();
// Use toBlob for better performance and reliability
const mimeType = format === 'jpg' ? 'image/jpeg' :
format === 'webp' ? 'image/webp' : 'image/png';
this.canvas.getElement().toBlob((blob) => {
if (blob) {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${filename}-${Date.now()}.${format}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
// Restore hidden objects
hiddenObjects.forEach(obj => {
obj.visible = true;
});
this.canvas.renderAll();
this.hideExportModal();
this.hideLoading();
}, mimeType, quality);
}
// UI Helpers
updateUI() {
this.updateUndoRedoButtons();
this.updateZoomDisplay();
}
showLoading() {
const loadingOverlay = document.getElementById('loadingOverlay');
if (loadingOverlay) {
loadingOverlay.classList.remove('hidden');
}
}
hideLoading() {
const loadingOverlay = document.getElementById('loadingOverlay');
if (loadingOverlay) {
loadingOverlay.classList.add('hidden');
}
}
}
// Initialize the application
document.addEventListener('DOMContentLoaded', () => {
new PhotoEditor();
});
//---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Photo Editor - Professional Image Editing Tool</title>
<link rel="stylesheet" href="style.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.0/fabric.min.js"></script>
</head>
<body>
<!-- Header -->
<header class="header">
<div class="header-left">
<h1 class="logo">📸 Photo Editor</h1>
</div>
<div class="header-center">
<button id="uploadBtn" class="btn btn--primary">
📁 Upload Image
</button>
<input type="file" id="fileInput" accept="image/*" style="display: none;">
</div>
<div class="header-right">
<button id="undoBtn" class="btn btn--secondary" title="Undo (Ctrl+Z)">
↶ Undo
</button>
<button id="redoBtn" class="btn btn--secondary" title="Redo (Ctrl+Y)">
↷ Redo
</button>
<button id="exportBtn" class="btn btn--primary">
💾 Export
</button>
</div>
</header>
<!-- Main Content -->
<main class="main-content">
<!-- Left Sidebar - Tools -->
<aside class="sidebar sidebar--left">
<div class="tool-group">
<button class="tool-btn active" data-tool="basic" title="Basic Adjustments">
🎛️ Adjust
</button>
<button class="tool-btn" data-tool="transform" title="Transform & Crop">
✂️ Transform
</button>
<button class="tool-btn" data-tool="filters" title="Filters">
🎨 Filters
</button>
<button class="tool-btn" data-tool="text" title="Add Text">
🔤 Text
</button>
<button class="tool-btn" data-tool="draw" title="Drawing Tools">
✏️ Draw
</button>
<button class="tool-btn" data-tool="stickers" title="Stickers">
😀 Stickers
</button>
<button class="tool-btn" data-tool="frames" title="Frames">
🖼️ Frames
</button>
<button class="tool-btn" data-tool="watermark" title="Watermark">
💧 Watermark
</button>
</div>
</aside>
<!-- Canvas Area -->
<section class="canvas-container">
<div class="upload-area" id="uploadArea">
<div class="upload-content">
<div class="upload-icon">📷</div>
<h3>Drop your image here or click to upload</h3>
<p>Supports JPG, PNG, GIF, WebP formats</p>
<button class="btn btn--primary">Choose File</button>
</div>
</div>
<canvas id="fabricCanvas"></canvas>
<!-- Crop Overlay -->
<div id="cropOverlay" class="crop-overlay hidden">
<div class="crop-controls">
<button id="applyCrop" class="btn btn--primary">✅ Apply Crop</button>
<button id="cancelCrop" class="btn btn--secondary">❌ Cancel</button>
</div>
</div>
<!-- Canvas Controls -->
<div class="canvas-controls">
<button id="zoomOut" class="btn btn--secondary">🔍-</button>
<span id="zoomLevel">100%</span>
<button id="zoomIn" class="btn btn--secondary">🔍+</button>
<button id="fitCanvas" class="btn btn--secondary">📐 Fit</button>
</div>
</section>
<!-- Right Sidebar - Tool Options -->
<aside class="sidebar sidebar--right">
<!-- Basic Adjustments Panel -->
<div class="tool-panel active" data-panel="basic">
<h3>Basic Adjustments</h3>
<div class="control-group">
<label>Brightness</label>
<input type="range" id="brightness" min="-100" max="100" value="0" class="slider">
<span class="value" id="brightnessValue">0</span>
</div>
<div class="control-group">
<label>Contrast</label>
<input type="range" id="contrast" min="-100" max="100" value="0" class="slider">
<span class="value" id="contrastValue">0</span>
</div>
<div class="control-group">
<label>Saturation</label>
<input type="range" id="saturation" min="-100" max="100" value="0" class="slider">
<span class="value" id="saturationValue">0</span>
</div>
<div class="control-group">
<label>Hue</label>
<input type="range" id="hue" min="0" max="360" value="0" class="slider">
<span class="value" id="hueValue">0°</span>
</div>
<button id="resetAdjustments" class="btn btn--secondary btn--full-width">Reset All</button>
</div>
<!-- Transform Panel -->
<div class="tool-panel" data-panel="transform">
<h3>Transform & Crop</h3>
<div class="button-group">
<button id="cropBtn" class="btn btn--primary">✂️ Start Crop</button>
</div>
<div class="control-group">
<label>Crop Ratio</label>
<select id="cropRatio" class="form-control">
<option value="free">Free Form</option>
<option value="1">1:1 (Square)</option>
<option value="1.333">4:3</option>
<option value="1.778">16:9</option>
<option value="1.5">3:2</option>
</select>
</div>
<div class="button-group">
<button id="rotateLeft" class="btn btn--secondary">↺ -90°</button>
<button id="rotateRight" class="btn btn--secondary">↻ +90°</button>
</div>
<div class="button-group">
<button id="flipH" class="btn btn--secondary">⟷ Flip H</button>
<button id="flipV" class="btn btn--secondary">⟺ Flip V</button>
</div>
</div>
<!-- Filters Panel -->
<div class="tool-panel" data-panel="filters">
<h3>Filters & Effects</h3>
<div class="filter-grid" id="filterGrid">
<!-- Filters will be populated by JavaScript -->
</div>
<button id="clearFilters" class="btn btn--secondary btn--full-width">Clear Filters</button>
</div>
<!-- Text Panel -->
<div class="tool-panel" data-panel="text">
<h3>Add Text</h3>
<div class="control-group">
<label>Text</label>
<input type="text" id="textInput" placeholder="Enter text..." class="form-control">
</div>
<div class="control-group">
<label>Font</label>
<select id="fontFamily" class="form-control">
<option value="Arial">Arial</option>
<option value="Helvetica">Helvetica</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Impact">Impact</option>
<option value="Comic Sans MS">Comic Sans MS</option>
<option value="Georgia">Georgia</option>
<option value="Courier New">Courier New</option>
</select>
</div>
<div class="control-group">
<label>Size</label>
<input type="range" id="fontSize" min="10" max="200" value="30" class="slider">
<span class="value" id="fontSizeValue">30px</span>
</div>
<div class="control-group">
<label>Color</label>
<input type="color" id="textColor" value="#ffffff" class="color-input">
</div>
<button id="addText" class="btn btn--primary btn--full-width">Add Text</button>
</div>
<!-- Drawing Panel -->
<div class="tool-panel" data-panel="draw">
<h3>Drawing Tools</h3>
<div class="button-group">
<button id="brushMode" class="btn btn--secondary active">🖌️ Brush</button>
<button id="eraserMode" class="btn btn--secondary">🧽 Eraser</button>
</div>
<div class="control-group">
<label>Brush Size</label>
<input type="range" id="brushSize" min="1" max="50" value="5" class="slider">
<span class="value" id="brushSizeValue">5px</span>
</div>
<div class="control-group">
<label>Color</label>
<input type="color" id="brushColor" value="#ffffff" class="color-input">
</div>
<div class="control-group">
<label>Opacity</label>
<input type="range" id="brushOpacity" min="0" max="100" value="100" class="slider">
<span class="value" id="brushOpacityValue">100%</span>
</div>
<div class="button-group">
<button id="drawingMode" class="btn btn--primary">Enable Drawing</button>
<button id="clearDrawing" class="btn btn--secondary">Clear All</button>
</div>
</div>
<!-- Stickers Panel -->
<div class="tool-panel" data-panel="stickers">
<h3>Stickers</h3>
<div class="sticker-categories">
<button class="category-btn active" data-category="smileys">😀 Smileys</button>
<button class="category-btn" data-category="people">🧑 People</button>
<button class="category-btn" data-category="gestures">👍 Gestures</button>
<button class="category-btn" data-category="nature">🌸 Nature</button>
<button class="category-btn" data-category="food">🍎 Food</button>
<button class="category-btn" data-category="travel">✈️ Travel</button>
<button class="category-btn" data-category="symbols">❤️ Symbols</button>
</div>
<div class="sticker-grid" id="stickerGrid">
<!-- Stickers will be populated by JavaScript -->
</div>
</div>
<!-- Frames Panel -->
<div class="tool-panel" data-panel="frames">
<h3>Frames</h3>
<div class="frame-grid" id="frameGrid">
<!-- Frames will be populated by JavaScript -->
</div>
<button id="removeFrame" class="btn btn--secondary btn--full-width">Remove Frame</button>
</div>
<!-- Watermark Panel -->
<div class="tool-panel" data-panel="watermark">
<h3>Watermark</h3>
<div class="control-group">
<label>Text</label>
<input type="text" id="watermarkText" placeholder="Watermark text..." class="form-control">
</div>
<div class="control-group">
<label>Position</label>
<select id="watermarkPosition" class="form-control">
<option value="topLeft">Top Left</option>
<option value="topRight">Top Right</option>
<option value="bottomLeft">Bottom Left</option>
<option value="bottomRight">Bottom Right</option>
<option value="center">Center</option>
</select>
</div>
<div class="control-group">
<label>Opacity</label>
<input type="range" id="watermarkOpacity" min="0" max="100" value="50" class="slider">
<span class="value" id="watermarkOpacityValue">50%</span>
</div>
<div class="control-group">
<label>Size</label>
<input type="range" id="watermarkSize" min="10" max="60" value="24" class="slider">
<span class="value" id="watermarkSizeValue">24px</span>
</div>
<button id="addWatermark" class="btn btn--primary btn--full-width">Add Watermark</button>
</div>
</aside>
</main>
<!-- Export Modal -->
<div id="exportModal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h3>Export Image</h3>
<button class="modal-close">×</button>
</div>
<div class="modal-body">
<div class="control-group">
<label>Format</label>
<select id="exportFormat" class="form-control">
<option value="png">PNG (Best Quality)</option>
<option value="jpg">JPG (Smaller Size)</option>
<option value="webp">WebP (Modern)</option>
</select>
</div>
<div class="control-group">
<label>Quality</label>
<input type="range" id="exportQuality" min="0.1" max="1" step="0.1" value="0.9" class="slider">
<span class="value" id="exportQualityValue">90%</span>
</div>
<div class="control-group">
<label>Filename</label>
<input type="text" id="exportFilename" value="edited-image" class="form-control">
</div>
</div>
<div class="modal-footer">
<button id="cancelExport" class="btn btn--secondary">Cancel</button>
<button id="confirmExport" class="btn btn--primary">Download</button>
</div>
</div>
</div>
<!-- Loading Overlay -->
<div id="loadingOverlay" class="loading-overlay hidden">
<div class="loading-spinner">⏳ Processing...</div>
</div>
<script src="app.js"></script>
</body>
</html>
//---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
:root {
/* Primitive Color Tokens */
--color-white: rgba(255, 255, 255, 1);
--color-black: rgba(0, 0, 0, 1);
--color-cream-50: rgba(252, 252, 249, 1);
--color-cream-100: rgba(255, 255, 253, 1);
--color-gray-200: rgba(245, 245, 245, 1);
--color-gray-300: rgba(167, 169, 169, 1);
--color-gray-400: rgba(119, 124, 124, 1);
--color-slate-500: rgba(98, 108, 113, 1);
--color-brown-600: rgba(94, 82, 64, 1);
--color-charcoal-700: rgba(31, 33, 33, 1);
--color-charcoal-800: rgba(38, 40, 40, 1);
--color-slate-900: rgba(19, 52, 59, 1);
--color-teal-300: rgba(50, 184, 198, 1);
--color-teal-400: rgba(45, 166, 178, 1);
--color-teal-500: rgba(33, 128, 141, 1);
--color-teal-600: rgba(29, 116, 128, 1);
--color-teal-700: rgba(26, 104, 115, 1);
--color-teal-800: rgba(41, 150, 161, 1);
--color-red-400: rgba(255, 84, 89, 1);
--color-red-500: rgba(192, 21, 47, 1);
--color-orange-400: rgba(230, 129, 97, 1);
--color-orange-500: rgba(168, 75, 47, 1);
/* RGB versions for opacity control */
--color-brown-600-rgb: 94, 82, 64;
--color-teal-500-rgb: 33, 128, 141;
--color-slate-900-rgb: 19, 52, 59;
--color-slate-500-rgb: 98, 108, 113;
--color-red-500-rgb: 192, 21, 47;
--color-red-400-rgb: 255, 84, 89;
--color-orange-500-rgb: 168, 75, 47;
--color-orange-400-rgb: 230, 129, 97;
/* Background color tokens (Light Mode) */
--color-bg-1: rgba(59, 130, 246, 0.08); /* Light blue */
--color-bg-2: rgba(245, 158, 11, 0.08); /* Light yellow */
--color-bg-3: rgba(34, 197, 94, 0.08); /* Light green */
--color-bg-4: rgba(239, 68, 68, 0.08); /* Light red */
--color-bg-5: rgba(147, 51, 234, 0.08); /* Light purple */
--color-bg-6: rgba(249, 115, 22, 0.08); /* Light orange */
--color-bg-7: rgba(236, 72, 153, 0.08); /* Light pink */
--color-bg-8: rgba(6, 182, 212, 0.08); /* Light cyan */
/* Semantic Color Tokens (Light Mode) */
--color-background: var(--color-cream-50);
--color-surface: var(--color-cream-100);
--color-text: var(--color-slate-900);
--color-text-secondary: var(--color-slate-500);
--color-primary: var(--color-teal-500);
--color-primary-hover: var(--color-teal-600);
--color-primary-active: var(--color-teal-700);
--color-secondary: rgba(var(--color-brown-600-rgb), 0.12);
--color-secondary-hover: rgba(var(--color-brown-600-rgb), 0.2);
--color-secondary-active: rgba(var(--color-brown-600-rgb), 0.25);
--color-border: rgba(var(--color-brown-600-rgb), 0.2);
--color-btn-primary-text: var(--color-cream-50);
--color-card-border: rgba(var(--color-brown-600-rgb), 0.12);
--color-card-border-inner: rgba(var(--color-brown-600-rgb), 0.12);
--color-error: var(--color-red-500);
--color-success: var(--color-teal-500);
--color-warning: var(--color-orange-500);
--color-info: var(--color-slate-500);
--color-focus-ring: rgba(var(--color-teal-500-rgb), 0.4);
--color-select-caret: rgba(var(--color-slate-900-rgb), 0.8);
/* Common style patterns */
--focus-ring: 0 0 0 3px var(--color-focus-ring);
--focus-outline: 2px solid var(--color-primary);
--status-bg-opacity: 0.15;
--status-border-opacity: 0.25;
--select-caret-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23134252' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
--select-caret-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23f5f5f5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
/* RGB versions for opacity control */
--color-success-rgb: 33, 128, 141;
--color-error-rgb: 192, 21, 47;
--color-warning-rgb: 168, 75, 47;
--color-info-rgb: 98, 108, 113;
/* Typography */
--font-family-base: "FKGroteskNeue", "Geist", "Inter", -apple-system,
BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-family-mono: "Berkeley Mono", ui-monospace, SFMono-Regular, Menlo,
Monaco, Consolas, monospace;
--font-size-xs: 11px;
--font-size-sm: 12px;
--font-size-base: 14px;
--font-size-md: 14px;
--font-size-lg: 16px;
--font-size-xl: 18px;
--font-size-2xl: 20px;
--font-size-3xl: 24px;
--font-size-4xl: 30px;
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 550;
--font-weight-bold: 600;
--line-height-tight: 1.2;
--line-height-normal: 1.5;
--letter-spacing-tight: -0.01em;
/* Spacing */
--space-0: 0;
--space-1: 1px;
--space-2: 2px;
--space-4: 4px;
--space-6: 6px;
--space-8: 8px;
--space-10: 10px;
--space-12: 12px;
--space-16: 16px;
--space-20: 20px;
--space-24: 24px;
--space-32: 32px;
/* Border Radius */
--radius-sm: 6px;
--radius-base: 8px;
--radius-md: 10px;
--radius-lg: 12px;
--radius-full: 9999px;
/* Shadows */
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.02);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.04),
0 2px 4px -1px rgba(0, 0, 0, 0.02);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.04),
0 4px 6px -2px rgba(0, 0, 0, 0.02);
--shadow-inset-sm: inset 0 1px 0 rgba(255, 255, 255, 0.15),
inset 0 -1px 0 rgba(0, 0, 0, 0.03);
/* Animation */
--duration-fast: 150ms;
--duration-normal: 250ms;
--ease-standard: cubic-bezier(0.16, 1, 0.3, 1);
/* Layout */
--container-sm: 640px;
--container-md: 768px;
--container-lg: 1024px;
--container-xl: 1280px;
}
/* Dark mode colors */
@media (prefers-color-scheme: dark) {
:root {
/* RGB versions for opacity control (Dark Mode) */
--color-gray-400-rgb: 119, 124, 124;
--color-teal-300-rgb: 50, 184, 198;
--color-gray-300-rgb: 167, 169, 169;
--color-gray-200-rgb: 245, 245, 245;
/* Background color tokens (Dark Mode) */
--color-bg-1: rgba(29, 78, 216, 0.15); /* Dark blue */
--color-bg-2: rgba(180, 83, 9, 0.15); /* Dark yellow */
--color-bg-3: rgba(21, 128, 61, 0.15); /* Dark green */
--color-bg-4: rgba(185, 28, 28, 0.15); /* Dark red */
--color-bg-5: rgba(107, 33, 168, 0.15); /* Dark purple */
--color-bg-6: rgba(194, 65, 12, 0.15); /* Dark orange */
--color-bg-7: rgba(190, 24, 93, 0.15); /* Dark pink */
--color-bg-8: rgba(8, 145, 178, 0.15); /* Dark cyan */
/* Semantic Color Tokens (Dark Mode) */
--color-background: var(--color-charcoal-700);
--color-surface: var(--color-charcoal-800);
--color-text: var(--color-gray-200);
--color-text-secondary: rgba(var(--color-gray-300-rgb), 0.7);
--color-primary: var(--color-teal-300);
--color-primary-hover: var(--color-teal-400);
--color-primary-active: var(--color-teal-800);
--color-secondary: rgba(var(--color-gray-400-rgb), 0.15);
--color-secondary-hover: rgba(var(--color-gray-400-rgb), 0.25);
--color-secondary-active: rgba(var(--color-gray-400-rgb), 0.3);
--color-border: rgba(var(--color-gray-400-rgb), 0.3);
--color-error: var(--color-red-400);
--color-success: var(--color-teal-300);
--color-warning: var(--color-orange-400);
--color-info: var(--color-gray-300);
--color-focus-ring: rgba(var(--color-teal-300-rgb), 0.4);
--color-btn-primary-text: var(--color-slate-900);
--color-card-border: rgba(var(--color-gray-400-rgb), 0.2);
--color-card-border-inner: rgba(var(--color-gray-400-rgb), 0.15);
--shadow-inset-sm: inset 0 1px 0 rgba(255, 255, 255, 0.1),
inset 0 -1px 0 rgba(0, 0, 0, 0.15);
--button-border-secondary: rgba(var(--color-gray-400-rgb), 0.2);
--color-border-secondary: rgba(var(--color-gray-400-rgb), 0.2);
--color-select-caret: rgba(var(--color-gray-200-rgb), 0.8);
/* Common style patterns - updated for dark mode */
--focus-ring: 0 0 0 3px var(--color-focus-ring);
--focus-outline: 2px solid var(--color-primary);
--status-bg-opacity: 0.15;
--status-border-opacity: 0.25;
--select-caret-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23134252' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
--select-caret-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23f5f5f5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
/* RGB versions for dark mode */
--color-success-rgb: var(--color-teal-300-rgb);
--color-error-rgb: var(--color-red-400-rgb);
--color-warning-rgb: var(--color-orange-400-rgb);
--color-info-rgb: var(--color-gray-300-rgb);
}
}
/* Data attribute for manual theme switching */
[data-color-scheme="dark"] {
/* RGB versions for opacity control (dark mode) */
--color-gray-400-rgb: 119, 124, 124;
--color-teal-300-rgb: 50, 184, 198;
--color-gray-300-rgb: 167, 169, 169;
--color-gray-200-rgb: 245, 245, 245;
/* Colorful background palette - Dark Mode */
--color-bg-1: rgba(29, 78, 216, 0.15); /* Dark blue */
--color-bg-2: rgba(180, 83, 9, 0.15); /* Dark yellow */
--color-bg-3: rgba(21, 128, 61, 0.15); /* Dark green */
--color-bg-4: rgba(185, 28, 28, 0.15); /* Dark red */
--color-bg-5: rgba(107, 33, 168, 0.15); /* Dark purple */
--color-bg-6: rgba(194, 65, 12, 0.15); /* Dark orange */
--color-bg-7: rgba(190, 24, 93, 0.15); /* Dark pink */
--color-bg-8: rgba(8, 145, 178, 0.15); /* Dark cyan */
/* Semantic Color Tokens (Dark Mode) */
--color-background: var(--color-charcoal-700);
--color-surface: var(--color-charcoal-800);
--color-text: var(--color-gray-200);
--color-text-secondary: rgba(var(--color-gray-300-rgb), 0.7);
--color-primary: var(--color-teal-300);
--color-primary-hover: var(--color-teal-400);
--color-primary-active: var(--color-teal-800);
--color-secondary: rgba(var(--color-gray-400-rgb), 0.15);
--color-secondary-hover: rgba(var(--color-gray-400-rgb), 0.25);
--color-secondary-active: rgba(var(--color-gray-400-rgb), 0.3);
--color-border: rgba(var(--color-gray-400-rgb), 0.3);
--color-error: var(--color-red-400);
--color-success: var(--color-teal-300);
--color-warning: var(--color-orange-400);
--color-info: var(--color-gray-300);
--color-focus-ring: rgba(var(--color-teal-300-rgb), 0.4);
--color-btn-primary-text: var(--color-slate-900);
--color-card-border: rgba(var(--color-gray-400-rgb), 0.15);
--color-card-border-inner: rgba(var(--color-gray-400-rgb), 0.15);
--shadow-inset-sm: inset 0 1px 0 rgba(255, 255, 255, 0.1),
inset 0 -1px 0 rgba(0, 0, 0, 0.15);
--color-border-secondary: rgba(var(--color-gray-400-rgb), 0.2);
--color-select-caret: rgba(var(--color-gray-200-rgb), 0.8);
/* Common style patterns - updated for dark mode */
--focus-ring: 0 0 0 3px var(--color-focus-ring);
--focus-outline: 2px solid var(--color-primary);
--status-bg-opacity: 0.15;
--status-border-opacity: 0.25;
--select-caret-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23134252' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
--select-caret-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23f5f5f5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
/* RGB versions for dark mode */
--color-success-rgb: var(--color-teal-300-rgb);
--color-error-rgb: var(--color-red-400-rgb);
--color-warning-rgb: var(--color-orange-400-rgb);
--color-info-rgb: var(--color-gray-300-rgb);
}
[data-color-scheme="light"] {
/* RGB versions for opacity control (light mode) */
--color-brown-600-rgb: 94, 82, 64;
--color-teal-500-rgb: 33, 128, 141;
--color-slate-900-rgb: 19, 52, 59;
/* Semantic Color Tokens (Light Mode) */
--color-background: var(--color-cream-50);
--color-surface: var(--color-cream-100);
--color-text: var(--color-slate-900);
--color-text-secondary: var(--color-slate-500);
--color-primary: var(--color-teal-500);
--color-primary-hover: var(--color-teal-600);
--color-primary-active: var(--color-teal-700);
--color-secondary: rgba(var(--color-brown-600-rgb), 0.12);
--color-secondary-hover: rgba(var(--color-brown-600-rgb), 0.2);
--color-secondary-active: rgba(var(--color-brown-600-rgb), 0.25);
--color-border: rgba(var(--color-brown-600-rgb), 0.2);
--color-btn-primary-text: var(--color-cream-50);
--color-card-border: rgba(var(--color-brown-600-rgb), 0.12);
--color-card-border-inner: rgba(var(--color-brown-600-rgb), 0.12);
--color-error: var(--color-red-500);
--color-success: var(--color-teal-500);
--color-warning: var(--color-orange-500);
--color-info: var(--color-slate-500);
--color-focus-ring: rgba(var(--color-teal-500-rgb), 0.4);
/* RGB versions for light mode */
--color-success-rgb: var(--color-teal-500-rgb);
--color-error-rgb: var(--color-red-500-rgb);
--color-warning-rgb: var(--color-orange-500-rgb);
--color-info-rgb: var(--color-slate-500-rgb);
}
/* Base styles */
html {
font-size: var(--font-size-base);
font-family: var(--font-family-base);
line-height: var(--line-height-normal);
color: var(--color-text);
background-color: var(--color-background);
-webkit-font-smoothing: antialiased;
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
}
*,
*::before,
*::after {
box-sizing: inherit;
}
/* Typography */
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0;
font-weight: var(--font-weight-semibold);
line-height: var(--line-height-tight);
color: var(--color-text);
letter-spacing: var(--letter-spacing-tight);
}
h1 {
font-size: var(--font-size-4xl);
}
h2 {
font-size: var(--font-size-3xl);
}
h3 {
font-size: var(--font-size-2xl);
}
h4 {
font-size: var(--font-size-xl);
}
h5 {
font-size: var(--font-size-lg);
}
h6 {
font-size: var(--font-size-md);
}
p {
margin: 0 0 var(--space-16) 0;
}
a {
color: var(--color-primary);
text-decoration: none;
transition: color var(--duration-fast) var(--ease-standard);
}
a:hover {
color: var(--color-primary-hover);
}
code,
pre {
font-family: var(--font-family-mono);
font-size: calc(var(--font-size-base) * 0.95);
background-color: var(--color-secondary);
border-radius: var(--radius-sm);
}
code {
padding: var(--space-1) var(--space-4);
}
pre {
padding: var(--space-16);
margin: var(--space-16) 0;
overflow: auto;
border: 1px solid var(--color-border);
}
pre code {
background: none;
padding: 0;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--space-8) var(--space-16);
border-radius: var(--radius-base);
font-size: var(--font-size-base);
font-weight: 500;
line-height: 1.5;
cursor: pointer;
transition: all var(--duration-normal) var(--ease-standard);
border: none;
text-decoration: none;
position: relative;
}
.btn:focus-visible {
outline: none;
box-shadow: var(--focus-ring);
}
.btn--primary {
background: var(--color-primary);
color: var(--color-btn-primary-text);
}
.btn--primary:hover {
background: var(--color-primary-hover);
}
.btn--primary:active {
background: var(--color-primary-active);
}
.btn--secondary {
background: var(--color-secondary);
color: var(--color-text);
}
.btn--secondary:hover {
background: var(--color-secondary-hover);
}
.btn--secondary:active {
background: var(--color-secondary-active);
}
.btn--outline {
background: transparent;
border: 1px solid var(--color-border);
color: var(--color-text);
}
.btn--outline:hover {
background: var(--color-secondary);
}
.btn--sm {
padding: var(--space-4) var(--space-12);
font-size: var(--font-size-sm);
border-radius: var(--radius-sm);
}
.btn--lg {
padding: var(--space-10) var(--space-20);
font-size: var(--font-size-lg);
border-radius: var(--radius-md);
}
.btn--full-width {
width: 100%;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Form elements */
.form-control {
display: block;
width: 100%;
padding: var(--space-8) var(--space-12);
font-size: var(--font-size-md);
line-height: 1.5;
color: var(--color-text);
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
transition: border-color var(--duration-fast) var(--ease-standard),
box-shadow var(--duration-fast) var(--ease-standard);
}
textarea.form-control {
font-family: var(--font-family-base);
font-size: var(--font-size-base);
}
select.form-control {
padding: var(--space-8) var(--space-12);
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: var(--select-caret-light);
background-repeat: no-repeat;
background-position: right var(--space-12) center;
background-size: 16px;
padding-right: var(--space-32);
}
/* Add a dark mode specific caret */
@media (prefers-color-scheme: dark) {
select.form-control {
background-image: var(--select-caret-dark);
}
}
/* Also handle data-color-scheme */
[data-color-scheme="dark"] select.form-control {
background-image: var(--select-caret-dark);
}
[data-color-scheme="light"] select.form-control {
background-image: var(--select-caret-light);
}
.form-control:focus {
border-color: var(--color-primary);
outline: var(--focus-outline);
}
.form-label {
display: block;
margin-bottom: var(--space-8);
font-weight: var(--font-weight-medium);
font-size: var(--font-size-sm);
}
.form-group {
margin-bottom: var(--space-16);
}
/* Card component */
.card {
background-color: var(--color-surface);
border-radius: var(--radius-lg);
border: 1px solid var(--color-card-border);
box-shadow: var(--shadow-sm);
overflow: hidden;
transition: box-shadow var(--duration-normal) var(--ease-standard);
}
.card:hover {
box-shadow: var(--shadow-md);
}
.card__body {
padding: var(--space-16);
}
.card__header,
.card__footer {
padding: var(--space-16);
border-bottom: 1px solid var(--color-card-border-inner);
}
/* Status indicators - simplified with CSS variables */
.status {
display: inline-flex;
align-items: center;
padding: var(--space-6) var(--space-12);
border-radius: var(--radius-full);
font-weight: var(--font-weight-medium);
font-size: var(--font-size-sm);
}
.status--success {
background-color: rgba(
var(--color-success-rgb, 33, 128, 141),
var(--status-bg-opacity)
);
color: var(--color-success);
border: 1px solid
rgba(var(--color-success-rgb, 33, 128, 141), var(--status-border-opacity));
}
.status--error {
background-color: rgba(
var(--color-error-rgb, 192, 21, 47),
var(--status-bg-opacity)
);
color: var(--color-error);
border: 1px solid
rgba(var(--color-error-rgb, 192, 21, 47), var(--status-border-opacity));
}
.status--warning {
background-color: rgba(
var(--color-warning-rgb, 168, 75, 47),
var(--status-bg-opacity)
);
color: var(--color-warning);
border: 1px solid
rgba(var(--color-warning-rgb, 168, 75, 47), var(--status-border-opacity));
}
.status--info {
background-color: rgba(
var(--color-info-rgb, 98, 108, 113),
var(--status-bg-opacity)
);
color: var(--color-info);
border: 1px solid
rgba(var(--color-info-rgb, 98, 108, 113), var(--status-border-opacity));
}
/* Container layout */
.container {
width: 100%;
margin-right: auto;
margin-left: auto;
padding-right: var(--space-16);
padding-left: var(--space-16);
}
@media (min-width: 640px) {
.container {
max-width: var(--container-sm);
}
}
@media (min-width: 768px) {
.container {
max-width: var(--container-md);
}
}
@media (min-width: 1024px) {
.container {
max-width: var(--container-lg);
}
}
@media (min-width: 1280px) {
.container {
max-width: var(--container-xl);
}
}
/* Utility classes */
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.gap-4 {
gap: var(--space-4);
}
.gap-8 {
gap: var(--space-8);
}
.gap-16 {
gap: var(--space-16);
}
.m-0 {
margin: 0;
}
.mt-8 {
margin-top: var(--space-8);
}
.mb-8 {
margin-bottom: var(--space-8);
}
.mx-8 {
margin-left: var(--space-8);
margin-right: var(--space-8);
}
.my-8 {
margin-top: var(--space-8);
margin-bottom: var(--space-8);
}
.p-0 {
padding: 0;
}
.py-8 {
padding-top: var(--space-8);
padding-bottom: var(--space-8);
}
.px-8 {
padding-left: var(--space-8);
padding-right: var(--space-8);
}
.py-16 {
padding-top: var(--space-16);
padding-bottom: var(--space-16);
}
.px-16 {
padding-left: var(--space-16);
padding-right: var(--space-16);
}
.block {
display: block;
}
.hidden {
display: none;
}
/* Accessibility */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
:focus-visible {
outline: var(--focus-outline);
outline-offset: 2px;
}
/* Dark mode specifics */
[data-color-scheme="dark"] .btn--outline {
border: 1px solid var(--color-border-secondary);
}
@font-face {
font-family: 'FKGroteskNeue';
src: url('https://r2cdn.perplexity.ai/fonts/FKGroteskNeue.woff2')
format('woff2');
}
/* END PERPLEXITY DESIGN SYSTEM */
/* Photo Editor Styles with Design System Integration */
/* Dark theme overrides for professional look */
:root {
--color-background: #1a1a1a;
--color-surface: #2d2d2d;
--color-text: #ffffff;
--color-text-secondary: #b0b0b0;
--color-primary: #1FB8CD;
--color-primary-hover: #17a2b8;
--color-primary-active: #138496;
--color-secondary: #404040;
--color-secondary-hover: #525252;
--color-border: #404040;
--color-success: #10b981;
--color-error: #ef4444;
--color-warning: #f59e0b;
--color-bg-overlay: rgba(0, 0, 0, 0.8);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: var(--font-family-base);
background-color: var(--color-background);
color: var(--color-text);
overflow: hidden;
height: 100vh;
}
/* Header */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-12) var(--space-16);
background-color: var(--color-surface);
border-bottom: 1px solid var(--color-border);
height: 60px;
z-index: 100;
}
.header-left .logo {
margin: 0;
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
color: var(--color-primary);
}
.header-center {
display: flex;
gap: var(--space-12);
}
.header-right {
display: flex;
gap: var(--space-8);
}
/* Button Styles */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--space-8) var(--space-16);
border: none;
border-radius: var(--radius-base);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
cursor: pointer;
transition: all var(--duration-fast) var(--ease-standard);
text-decoration: none;
gap: var(--space-4);
}
.btn--primary {
background-color: var(--color-primary);
color: white;
}
.btn--primary:hover {
background-color: var(--color-primary-hover);
}
.btn--primary:active {
background-color: var(--color-primary-active);
}
.btn--secondary {
background-color: var(--color-secondary);
color: var(--color-text);
}
.btn--secondary:hover {
background-color: var(--color-secondary-hover);
}
.btn--full-width {
width: 100%;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Main Content Layout */
.main-content {
display: flex;
height: calc(100vh - 60px);
}
/* Sidebars */
.sidebar {
background-color: var(--color-surface);
border-right: 1px solid var(--color-border);
overflow-y: auto;
}
.sidebar--left {
width: 80px;
min-width: 80px;
padding: var(--space-16) var(--space-8);
}
.sidebar--right {
width: 320px;
min-width: 320px;
padding: var(--space-16);
border-left: 1px solid var(--color-border);
border-right: none;
}
/* Tool Buttons */
.tool-group {
display: flex;
flex-direction: column;
gap: var(--space-8);
}
.tool-btn {
width: 60px;
height: 60px;
background-color: transparent;
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--duration-fast) var(--ease-standard);
font-size: var(--font-size-xs);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-2);
text-align: center;
line-height: 1.2;
}
.tool-btn:hover {
background-color: var(--color-secondary);
border-color: var(--color-primary);
}
.tool-btn.active {
background-color: var(--color-primary);
border-color: var(--color-primary);
color: white;
}
/* Canvas Container */
.canvas-container {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
background-color: #0f1419;
background-image:
linear-gradient(45deg, #1a1a1a 25%, transparent 25%),
linear-gradient(-45deg, #1a1a1a 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #1a1a1a 75%),
linear-gradient(-45deg, transparent 75%, #1a1a1a 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}
#fabricCanvas {
border: 2px solid var(--color-border);
border-radius: var(--radius-base);
box-shadow: var(--shadow-lg);
max-width: 90%;
max-height: 80%;
}
/* Upload Area */
.upload-area {
display: flex;
align-items: center;
justify-content: center;
width: 600px;
height: 400px;
border: 2px dashed var(--color-border);
border-radius: var(--radius-lg);
background-color: var(--color-surface);
transition: all var(--duration-normal) var(--ease-standard);
cursor: pointer;
}
.upload-area:hover {
border-color: var(--color-primary);
background-color: rgba(31, 184, 205, 0.05);
}
.upload-area.dragover {
border-color: var(--color-primary);
background-color: rgba(31, 184, 205, 0.1);
}
.upload-content {
text-align: center;
pointer-events: none;
}
.upload-icon {
font-size: 4rem;
margin-bottom: var(--space-16);
opacity: 0.6;
}
.upload-content h3 {
margin: 0 0 var(--space-8) 0;
color: var(--color-text);
}
.upload-content p {
margin: 0 0 var(--space-16) 0;
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
}
/* Crop Overlay */
.crop-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--color-bg-overlay);
display: flex;
align-items: flex-end;
justify-content: center;
padding: var(--space-24);
z-index: 10;
}
.crop-overlay.hidden {
display: none;
}
.crop-controls {
display: flex;
gap: var(--space-12);
background-color: var(--color-surface);
padding: var(--space-16);
border-radius: var(--radius-lg);
border: 1px solid var(--color-border);
box-shadow: var(--shadow-lg);
}
/* Canvas Controls */
.canvas-controls {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: var(--space-8);
background-color: var(--color-surface);
padding: var(--space-8) var(--space-16);
border-radius: var(--radius-full);
border: 1px solid var(--color-border);
box-shadow: var(--shadow-md);
}
#zoomLevel {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
min-width: 50px;
text-align: center;
}
/* Tool Panels */
.tool-panel {
display: none;
}
.tool-panel.active {
display: block;
}
.tool-panel h3 {
margin: 0 0 var(--space-16) 0;
color: var(--color-text);
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
border-bottom: 1px solid var(--color-border);
padding-bottom: var(--space-8);
}
/* Form Controls */
.control-group {
margin-bottom: var(--space-16);
}
.control-group label {
display: block;
margin-bottom: var(--space-6);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
color: var(--color-text-secondary);
}
.form-control {
width: 100%;
padding: var(--space-8) var(--space-12);
background-color: var(--color-background);
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
color: var(--color-text);
font-size: var(--font-size-sm);
}
.form-control:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(31, 184, 205, 0.2);
}
.slider {
width: 100%;
height: 6px;
background-color: var(--color-background);
border-radius: var(--radius-full);
outline: none;
appearance: none;
}
.slider::-webkit-slider-thumb {
appearance: none;
width: 18px;
height: 18px;
background-color: var(--color-primary);
border-radius: 50%;
cursor: pointer;
}
.slider::-moz-range-thumb {
width: 18px;
height: 18px;
background-color: var(--color-primary);
border-radius: 50%;
cursor: pointer;
border: none;
}
.value {
display: inline-block;
min-width: 45px;
text-align: right;
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
margin-left: var(--space-8);
}
.color-input {
width: 50px;
height: 35px;
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
cursor: pointer;
background: none;
}
/* Button Groups */
.button-group {
display: flex;
gap: var(--space-8);
margin-bottom: var(--space-16);
}
.button-group .btn {
flex: 1;
font-size: var(--font-size-xs);
padding: var(--space-6) var(--space-8);
}
/* Filter Grid */
.filter-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-8);
margin-bottom: var(--space-16);
}
.filter-btn {
padding: var(--space-12);
background-color: var(--color-background);
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
color: var(--color-text);
cursor: pointer;
transition: all var(--duration-fast) var(--ease-standard);
text-align: center;
font-size: var(--font-size-xs);
}
.filter-btn:hover {
border-color: var(--color-primary);
background-color: rgba(31, 184, 205, 0.1);
}
.filter-btn.active {
border-color: var(--color-primary);
background-color: var(--color-primary);
color: white;
}
/* Sticker Categories and Grid */
.sticker-categories {
display: flex;
flex-wrap: wrap;
gap: var(--space-4);
margin-bottom: var(--space-16);
overflow-x: auto;
}
.category-btn {
padding: var(--space-4) var(--space-8);
background-color: var(--color-background);
border: 1px solid var(--color-border);
border-radius: var(--radius-full);
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--duration-fast) var(--ease-standard);
font-size: var(--font-size-xs);
white-space: nowrap;
}
.category-btn:hover {
border-color: var(--color-primary);
}
.category-btn.active {
background-color: var(--color-primary);
border-color: var(--color-primary);
color: white;
}
.sticker-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: var(--space-6);
margin-bottom: var(--space-16);
max-height: 200px;
overflow-y: auto;
}
.sticker-btn {
width: 40px;
height: 40px;
background-color: var(--color-background);
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
cursor: pointer;
transition: all var(--duration-fast) var(--ease-standard);
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
}
.sticker-btn:hover {
border-color: var(--color-primary);
background-color: rgba(31, 184, 205, 0.1);
transform: scale(1.05);
}
/* Frame Grid */
.frame-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-8);
margin-bottom: var(--space-16);
}
.frame-btn {
padding: var(--space-12);
background-color: var(--color-background);
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
color: var(--color-text);
cursor: pointer;
transition: all var(--duration-fast) var(--ease-standard);
text-align: center;
font-size: var(--font-size-xs);
position: relative;
}
.frame-btn:hover {
border-color: var(--color-primary);
background-color: rgba(31, 184, 205, 0.1);
}
.frame-btn.active {
border-color: var(--color-primary);
background-color: var(--color-primary);
color: white;
}
.frame-preview {
width: 100%;
height: 40px;
margin-bottom: var(--space-8);
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-xs);
}
/* Modal */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--color-bg-overlay);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal.hidden {
display: none;
}
.modal-content {
background-color: var(--color-surface);
border-radius: var(--radius-lg);
width: 90%;
max-width: 500px;
border: 1px solid var(--color-border);
box-shadow: var(--shadow-lg);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-16);
border-bottom: 1px solid var(--color-border);
}
.modal-header h3 {
margin: 0;
color: var(--color-text);
}
.modal-close {
background: none;
border: none;
font-size: 24px;
color: var(--color-text-secondary);
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-base);
}
.modal-close:hover {
background-color: var(--color-secondary);
color: var(--color-text);
}
.modal-body {
padding: var(--space-16);
}
.modal-footer {
display: flex;
gap: var(--space-8);
justify-content: flex-end;
padding: var(--space-16);
border-top: 1px solid var(--color-border);
}
/* Loading Overlay */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--color-bg-overlay);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.loading-overlay.hidden {
display: none;
}
.loading-spinner {
font-size: var(--font-size-xl);
color: var(--color-text);
text-align: center;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Crop Selection Box */
.crop-selection {
position: absolute;
border: 2px dashed var(--color-primary);
background-color: rgba(31, 184, 205, 0.1);
pointer-events: none;
z-index: 5;
}
/* Responsive Design */
@media (max-width: 1024px) {
.sidebar--right {
width: 280px;
min-width: 280px;
}
.filter-grid,
.frame-grid {
grid-template-columns: 1fr;
}
.sticker-grid {
grid-template-columns: repeat(5, 1fr);
}
}
@media (max-width: 768px) {
.main-content {
flex-direction: column;
}
.sidebar--left {
width: 100%;
min-width: auto;
height: 80px;
padding: var(--space-8);
order: 2;
}
.tool-group {
flex-direction: row;
overflow-x: auto;
gap: var(--space-4);
}
.tool-btn {
min-width: 60px;
height: 50px;
font-size: 10px;
}
.sidebar--right {
width: 100%;
min-width: auto;
height: 250px;
order: 3;
border-left: none;
border-top: 1px solid var(--color-border);
overflow-y: auto;
}
.canvas-container {
order: 1;
height: calc(100vh - 380px);
}
.upload-area {
width: 90%;
height: 60%;
}
#fabricCanvas {
max-width: 95%;
max-height: 70%;
}
.canvas-controls {
bottom: 10px;
}
.header {
flex-wrap: wrap;
height: auto;
min-height: 60px;
}
.sticker-categories {
overflow-x: auto;
flex-wrap: nowrap;
}
.sticker-grid {
grid-template-columns: repeat(4, 1fr);
}
}
@media (max-width: 480px) {
.header {
padding: var(--space-8);
}
.header-left .logo {
font-size: var(--font-size-lg);
}
.btn {
padding: var(--space-6) var(--space-12);
font-size: var(--font-size-xs);
}
.upload-content h3 {
font-size: var(--font-size-md);
}
.tool-btn {
min-width: 50px;
height: 45px;
font-size: 9px;
}
.modal-content {
width: 95%;
margin: var(--space-16);
}
.sticker-grid {
grid-template-columns: repeat(3, 1fr);
}
.sticker-btn {
width: 35px;
height: 35px;
font-size: 16px;
}
}
/* Fabric.js Canvas Customizations */
.canvas-container .upper-canvas {
border-radius: var(--radius-base);
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--color-background);
}
::-webkit-scrollbar-thumb {
background: var(--color-secondary);
border-radius: var(--radius-base);
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-secondary-hover);
}
/* Animation for smooth transitions */
.tool-panel,
.upload-area,
.btn,
.tool-btn,
.filter-btn,
.sticker-btn,
.frame-btn {
transition: all var(--duration-normal) var(--ease-standard);
}
/* Focus states for accessibility */
.btn:focus,
.tool-btn:focus,
.form-control:focus,
.filter-btn:focus,
.sticker-btn:focus,
.frame-btn:focus {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* High contrast mode support */
@media (prefers-contrast: high) {
:root {
--color-border: #666;
--color-text-secondary: #ccc;
}
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}