|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>SnapEdit Pro - Advanced Image Editor</title> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script> |
|
|
<style> |
|
|
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap'); |
|
|
|
|
|
body { |
|
|
font-family: 'Poppins', sans-serif; |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
height: 100vh; |
|
|
overflow: hidden; |
|
|
background-color: #1a1a1a; |
|
|
} |
|
|
|
|
|
#editorCanvas { |
|
|
position: absolute; |
|
|
top: 0; |
|
|
left: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
cursor: default; |
|
|
transform-origin: 0 0; |
|
|
} |
|
|
|
|
|
.floating-toolbar { |
|
|
position: absolute; |
|
|
top: 20px; |
|
|
left: 50%; |
|
|
transform: translateX(-50%); |
|
|
background: rgba(30, 30, 30, 0.8); |
|
|
backdrop-filter: blur(10px); |
|
|
-webkit-backdrop-filter: blur(10px); |
|
|
border-radius: 12px; |
|
|
padding: 8px; |
|
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); |
|
|
display: flex; |
|
|
gap: 4px; |
|
|
z-index: 100; |
|
|
user-select: none; |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
|
|
|
.floating-toolbar.dragging { |
|
|
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.4); |
|
|
} |
|
|
|
|
|
.tool-btn { |
|
|
width: 40px; |
|
|
height: 40px; |
|
|
border-radius: 8px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
color: #e0e0e0; |
|
|
background: transparent; |
|
|
border: none; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s ease; |
|
|
} |
|
|
|
|
|
.tool-btn:hover { |
|
|
background: rgba(99, 102, 241, 0.2); |
|
|
color: white; |
|
|
transform: translateY(-2px); |
|
|
} |
|
|
|
|
|
.tool-btn.active { |
|
|
background: rgba(99, 102, 241, 0.4); |
|
|
color: white; |
|
|
box-shadow: 0 4px 8px rgba(99, 102, 241, 0.2); |
|
|
} |
|
|
|
|
|
.tool-btn i { |
|
|
font-size: 16px; |
|
|
} |
|
|
|
|
|
.tool-separator { |
|
|
width: 1px; |
|
|
height: 24px; |
|
|
background: rgba(255, 255, 255, 0.1); |
|
|
margin: 0 4px; |
|
|
align-self: center; |
|
|
} |
|
|
|
|
|
.floating-properties { |
|
|
position: absolute; |
|
|
top: 80px; |
|
|
right: 20px; |
|
|
background: rgba(30, 30, 30, 0.8); |
|
|
backdrop-filter: blur(10px); |
|
|
-webkit-backdrop-filter: blur(10px); |
|
|
border-radius: 12px; |
|
|
padding: 16px; |
|
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); |
|
|
width: 280px; |
|
|
z-index: 100; |
|
|
user-select: none; |
|
|
transition: all 0.3s ease; |
|
|
color: #e0e0e0; |
|
|
} |
|
|
|
|
|
.floating-properties.dragging { |
|
|
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.4); |
|
|
} |
|
|
|
|
|
.properties-header { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
margin-bottom: 12px; |
|
|
padding-bottom: 8px; |
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1); |
|
|
} |
|
|
|
|
|
.properties-title { |
|
|
font-size: 14px; |
|
|
font-weight: 600; |
|
|
color: white; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 8px; |
|
|
} |
|
|
|
|
|
.properties-close { |
|
|
width: 24px; |
|
|
height: 24px; |
|
|
border-radius: 6px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
color: #e0e0e0; |
|
|
background: transparent; |
|
|
border: none; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s ease; |
|
|
} |
|
|
|
|
|
.properties-close:hover { |
|
|
background: rgba(255, 255, 255, 0.1); |
|
|
} |
|
|
|
|
|
.property-group { |
|
|
margin-bottom: 16px; |
|
|
} |
|
|
|
|
|
.property-label { |
|
|
font-size: 12px; |
|
|
color: #a0a0a0; |
|
|
margin-bottom: 6px; |
|
|
display: block; |
|
|
} |
|
|
|
|
|
.color-picker { |
|
|
width: 100%; |
|
|
height: 32px; |
|
|
border-radius: 6px; |
|
|
border: 1px solid rgba(255, 255, 255, 0.1); |
|
|
background: rgba(40, 40, 40, 0.8); |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
input[type="range"] { |
|
|
-webkit-appearance: none; |
|
|
width: 100%; |
|
|
height: 4px; |
|
|
border-radius: 2px; |
|
|
background: rgba(255, 255, 255, 0.1); |
|
|
outline: none; |
|
|
} |
|
|
|
|
|
input[type="range"]::-webkit-slider-thumb { |
|
|
-webkit-appearance: none; |
|
|
width: 16px; |
|
|
height: 16px; |
|
|
border-radius: 50%; |
|
|
background: #6366f1; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s ease; |
|
|
} |
|
|
|
|
|
.range-value { |
|
|
font-size: 12px; |
|
|
color: #a0a0a0; |
|
|
text-align: right; |
|
|
margin-top: 4px; |
|
|
} |
|
|
|
|
|
.dropdown { |
|
|
width: 100%; |
|
|
padding: 8px; |
|
|
border-radius: 6px; |
|
|
border: 1px solid rgba(255, 255, 255, 0.1); |
|
|
background: rgba(40, 40, 40, 0.8); |
|
|
color: #e0e0e0; |
|
|
font-size: 12px; |
|
|
outline: none; |
|
|
} |
|
|
|
|
|
.dropdown option { |
|
|
background: #2d2d2d; |
|
|
} |
|
|
|
|
|
.notification-container { |
|
|
position: fixed; |
|
|
top: 20px; |
|
|
right: 20px; |
|
|
z-index: 1000; |
|
|
} |
|
|
|
|
|
.notification { |
|
|
padding: 12px 16px; |
|
|
border-radius: 8px; |
|
|
margin-bottom: 10px; |
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 8px; |
|
|
font-size: 14px; |
|
|
animation: slideIn 0.3s ease-out forwards; |
|
|
background: rgba(30, 30, 30, 0.9); |
|
|
color: white; |
|
|
border-left: 4px solid; |
|
|
} |
|
|
|
|
|
.notification.success { |
|
|
border-color: #10b981; |
|
|
} |
|
|
|
|
|
.notification.error { |
|
|
border-color: #ef4444; |
|
|
} |
|
|
|
|
|
.notification.info { |
|
|
border-color: #3b82f6; |
|
|
} |
|
|
|
|
|
@keyframes slideIn { |
|
|
from { transform: translateX(20px); opacity: 0; } |
|
|
to { transform: translateX(0); opacity: 1; } |
|
|
} |
|
|
|
|
|
.zoom-controls { |
|
|
position: fixed; |
|
|
bottom: 20px; |
|
|
left: 20px; |
|
|
background: rgba(30, 30, 30, 0.8); |
|
|
backdrop-filter: blur(10px); |
|
|
-webkit-backdrop-filter: blur(10px); |
|
|
border-radius: 12px; |
|
|
padding: 8px; |
|
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 4px; |
|
|
z-index: 100; |
|
|
} |
|
|
|
|
|
.zoom-btn { |
|
|
width: 40px; |
|
|
height: 40px; |
|
|
border-radius: 8px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
color: #e0e0e0; |
|
|
background: transparent; |
|
|
border: none; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s ease; |
|
|
} |
|
|
|
|
|
.zoom-btn:hover { |
|
|
background: rgba(99, 102, 241, 0.2); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.zoom-value { |
|
|
width: 40px; |
|
|
text-align: center; |
|
|
font-size: 12px; |
|
|
color: white; |
|
|
padding: 4px 0; |
|
|
} |
|
|
|
|
|
.fullscreen-btn { |
|
|
position: fixed; |
|
|
bottom: 20px; |
|
|
right: 20px; |
|
|
width: 40px; |
|
|
height: 40px; |
|
|
border-radius: 50%; |
|
|
background: rgba(30, 30, 30, 0.8); |
|
|
backdrop-filter: blur(10px); |
|
|
-webkit-backdrop-filter: blur(10px); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
color: white; |
|
|
cursor: pointer; |
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); |
|
|
z-index: 100; |
|
|
transition: all 0.2s ease; |
|
|
} |
|
|
|
|
|
.fullscreen-btn:hover { |
|
|
background: rgba(99, 102, 241, 0.8); |
|
|
transform: scale(1.1); |
|
|
} |
|
|
|
|
|
.hidden { |
|
|
display: none !important; |
|
|
} |
|
|
|
|
|
.canvas-container { |
|
|
position: absolute; |
|
|
top: 0; |
|
|
left: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
.lock-btn { |
|
|
position: absolute; |
|
|
top: 5px; |
|
|
right: 5px; |
|
|
width: 24px; |
|
|
height: 24px; |
|
|
border-radius: 4px; |
|
|
background: rgba(30, 30, 30, 0.8); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
color: white; |
|
|
cursor: pointer; |
|
|
z-index: 10; |
|
|
transition: all 0.2s ease; |
|
|
} |
|
|
|
|
|
.lock-btn:hover { |
|
|
background: rgba(99, 102, 241, 0.8); |
|
|
} |
|
|
|
|
|
.object-controls { |
|
|
position: absolute; |
|
|
top: 5px; |
|
|
right: 5px; |
|
|
display: flex; |
|
|
gap: 4px; |
|
|
z-index: 10; |
|
|
} |
|
|
|
|
|
.control-btn { |
|
|
width: 24px; |
|
|
height: 24px; |
|
|
border-radius: 4px; |
|
|
background: rgba(30, 30, 30, 0.8); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
color: white; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s ease; |
|
|
} |
|
|
|
|
|
.control-btn:hover { |
|
|
background: rgba(99, 102, 241, 0.8); |
|
|
} |
|
|
|
|
|
.locked { |
|
|
color: #6366f1; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
|
|
|
<div class="canvas-container"> |
|
|
<canvas id="editorCanvas"></canvas> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="floating-toolbar" id="floatingToolbar"> |
|
|
|
|
|
<button class="tool-btn" id="uploadBtn" title="Upload Image"> |
|
|
<i class="fas fa-upload"></i> |
|
|
</button> |
|
|
|
|
|
<div class="tool-separator"></div> |
|
|
|
|
|
|
|
|
<button class="tool-btn active" id="selectTool" title="Select Tool"> |
|
|
<i class="fas fa-mouse-pointer"></i> |
|
|
</button> |
|
|
|
|
|
|
|
|
<button class="tool-btn" id="textTool" title="Text Tool"> |
|
|
<i class="fas fa-font"></i> |
|
|
</button> |
|
|
<button class="tool-btn" id="drawTool" title="Draw Tool"> |
|
|
<i class="fas fa-pencil-alt"></i> |
|
|
</button> |
|
|
<button class="tool-btn" id="shapeTool" title="Shapes"> |
|
|
<i class="fas fa-square"></i> |
|
|
</button> |
|
|
<button class="tool-btn" id="blurTool" title="Blur Tool"> |
|
|
<i class="fas fa-eye-slash"></i> |
|
|
</button> |
|
|
|
|
|
<div class="tool-separator"></div> |
|
|
|
|
|
|
|
|
<button class="tool-btn" id="aiRemoveBg" title="Remove Background"> |
|
|
<i class="fas fa-robot"></i> |
|
|
</button> |
|
|
<button class="tool-btn" id="aiEnhance" title="AI Enhance"> |
|
|
<i class="fas fa-magic"></i> |
|
|
</button> |
|
|
|
|
|
<div class="tool-separator"></div> |
|
|
|
|
|
|
|
|
<button class="tool-btn" id="undoBtn" title="Undo"> |
|
|
<i class="fas fa-undo"></i> |
|
|
</button> |
|
|
<button class="tool-btn" id="redoBtn" title="Redo"> |
|
|
<i class="fas fa-redo"></i> |
|
|
</button> |
|
|
|
|
|
<div class="tool-separator"></div> |
|
|
|
|
|
|
|
|
<button class="tool-btn" id="saveBtn" title="Save"> |
|
|
<i class="fas fa-save"></i> |
|
|
</button> |
|
|
<button class="tool-btn" id="exportBtn" title="Export"> |
|
|
<i class="fas fa-file-export"></i> |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="floating-properties" id="floatingProperties"> |
|
|
<div class="properties-header"> |
|
|
<div class="properties-title"> |
|
|
<i class="fas fa-sliders-h"></i> |
|
|
<span>Properties</span> |
|
|
</div> |
|
|
<button class="properties-close" id="propertiesClose"> |
|
|
<i class="fas fa-times"></i> |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
<div class="property-group"> |
|
|
<label class="property-label">Canvas Background</label> |
|
|
<input type="color" class="color-picker" id="canvasBg" value="#1a1a1a"> |
|
|
</div> |
|
|
|
|
|
<div class="property-group"> |
|
|
<label class="property-label">Opacity</label> |
|
|
<input type="range" id="canvasOpacity" min="0" max="100" value="100"> |
|
|
<div class="range-value" id="canvasOpacityValue">100%</div> |
|
|
</div> |
|
|
|
|
|
<div class="property-group hidden" id="textProps"> |
|
|
<label class="property-label">Text Color</label> |
|
|
<input type="color" class="color-picker" id="textColor" value="#ffffff"> |
|
|
|
|
|
<label class="property-label">Font Size</label> |
|
|
<input type="range" id="textSize" min="8" max="72" value="24"> |
|
|
<div class="range-value" id="textSizeValue">24px</div> |
|
|
|
|
|
<label class="property-label">Font Family</label> |
|
|
<select class="dropdown" id="textFont"> |
|
|
<option value="Arial">Arial</option> |
|
|
<option value="Helvetica">Helvetica</option> |
|
|
<option value="Times New Roman">Times New Roman</option> |
|
|
<option value="Courier New">Courier New</option> |
|
|
<option value="Georgia">Georgia</option> |
|
|
<option value="Verdana">Verdana</option> |
|
|
</select> |
|
|
</div> |
|
|
|
|
|
<div class="property-group hidden" id="drawProps"> |
|
|
<label class="property-label">Brush Color</label> |
|
|
<input type="color" class="color-picker" id="brushColor" value="#ffffff"> |
|
|
|
|
|
<label class="property-label">Brush Size</label> |
|
|
<input type="range" id="brushSize" min="1" max="50" value="5"> |
|
|
<div class="range-value" id="brushSizeValue">5px</div> |
|
|
</div> |
|
|
|
|
|
<div class="property-group hidden" id="objectProps"> |
|
|
<label class="property-label">Object Controls</label> |
|
|
<div class="flex gap-2 mt-2"> |
|
|
<button class="control-btn" id="lockBtn" title="Lock/Unlock"> |
|
|
<i class="fas fa-lock"></i> |
|
|
</button> |
|
|
<button class="control-btn" id="deleteBtn" title="Delete"> |
|
|
<i class="fas fa-trash"></i> |
|
|
</button> |
|
|
<button class="control-btn" id="bringToFrontBtn" title="Bring to Front"> |
|
|
<i class="fas fa-arrow-up"></i> |
|
|
</button> |
|
|
<button class="control-btn" id="sendToBackBtn" title="Send to Back"> |
|
|
<i class="fas fa-arrow-down"></i> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="zoom-controls"> |
|
|
<button class="zoom-btn" id="zoomInBtn" title="Zoom In"> |
|
|
<i class="fas fa-search-plus"></i> |
|
|
</button> |
|
|
<div class="zoom-value" id="zoomValue">100%</div> |
|
|
<button class="zoom-btn" id="zoomOutBtn" title="Zoom Out"> |
|
|
<i class="fas fa-search-minus"></i> |
|
|
</button> |
|
|
<button class="zoom-btn" id="zoomResetBtn" title="Reset Zoom"> |
|
|
<i class="fas fa-sync-alt"></i> |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="fullscreen-btn" id="fullscreenBtn" title="Toggle Fullscreen"> |
|
|
<i class="fas fa-expand"></i> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="notification-container" id="notificationContainer"></div> |
|
|
|
|
|
|
|
|
<input type="file" id="fileInput" accept="image/*" class="hidden"> |
|
|
|
|
|
<script> |
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
|
|
const canvas = new fabric.Canvas('editorCanvas', { |
|
|
backgroundColor: '#1a1a1a', |
|
|
preserveObjectStacking: true, |
|
|
selection: true, |
|
|
selectionColor: 'rgba(99, 102, 241, 0.3)', |
|
|
selectionBorderColor: '#6366f1', |
|
|
selectionLineWidth: 1, |
|
|
selectionDashArray: [5, 5], |
|
|
enableRetinaScaling: true |
|
|
}); |
|
|
|
|
|
|
|
|
let zoomLevel = 1; |
|
|
const zoomStep = 0.1; |
|
|
const minZoom = 0.1; |
|
|
const maxZoom = 3; |
|
|
|
|
|
|
|
|
function resizeCanvas() { |
|
|
const width = window.innerWidth; |
|
|
const height = window.innerHeight; |
|
|
|
|
|
canvas.setWidth(width); |
|
|
canvas.setHeight(height); |
|
|
|
|
|
|
|
|
canvas.setZoom(zoomLevel); |
|
|
canvas.renderAll(); |
|
|
|
|
|
|
|
|
document.getElementById('zoomValue').textContent = `${Math.round(zoomLevel * 100)}%`; |
|
|
} |
|
|
|
|
|
|
|
|
resizeCanvas(); |
|
|
window.addEventListener('resize', resizeCanvas); |
|
|
|
|
|
|
|
|
let currentTool = 'select'; |
|
|
let activeObject = null; |
|
|
let history = []; |
|
|
let historyIndex = -1; |
|
|
let isDrawing = false; |
|
|
let drawingPath = null; |
|
|
let blurArea = null; |
|
|
let polygonPoints = []; |
|
|
|
|
|
|
|
|
const floatingToolbar = document.getElementById('floatingToolbar'); |
|
|
const floatingProperties = document.getElementById('floatingProperties'); |
|
|
const fileInput = document.getElementById('fileInput'); |
|
|
const uploadBtn = document.getElementById('uploadBtn'); |
|
|
const propertiesClose = document.getElementById('propertiesClose'); |
|
|
const fullscreenBtn = document.getElementById('fullscreenBtn'); |
|
|
const notificationContainer = document.getElementById('notificationContainer'); |
|
|
const zoomInBtn = document.getElementById('zoomInBtn'); |
|
|
const zoomOutBtn = document.getElementById('zoomOutBtn'); |
|
|
const zoomResetBtn = document.getElementById('zoomResetBtn'); |
|
|
const lockBtn = document.getElementById('lockBtn'); |
|
|
const deleteBtn = document.getElementById('deleteBtn'); |
|
|
const bringToFrontBtn = document.getElementById('bringToFrontBtn'); |
|
|
const sendToBackBtn = document.getElementById('sendToBackBtn'); |
|
|
|
|
|
|
|
|
makeDraggable(floatingToolbar); |
|
|
makeDraggable(floatingProperties); |
|
|
|
|
|
function makeDraggable(element) { |
|
|
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; |
|
|
|
|
|
element.onmousedown = dragMouseDown; |
|
|
|
|
|
function dragMouseDown(e) { |
|
|
e = e || window.event; |
|
|
e.preventDefault(); |
|
|
|
|
|
|
|
|
if (e.target.classList.contains('tool-btn') || |
|
|
e.target.classList.contains('properties-close') || |
|
|
e.target.closest('.tool-btn') || |
|
|
e.target.closest('.properties-close')) { |
|
|
return; |
|
|
} |
|
|
|
|
|
element.classList.add('dragging'); |
|
|
|
|
|
|
|
|
pos3 = e.clientX; |
|
|
pos4 = e.clientY; |
|
|
|
|
|
document.onmouseup = closeDragElement; |
|
|
document.onmousemove = elementDrag; |
|
|
} |
|
|
|
|
|
function elementDrag(e) { |
|
|
e = e || window.event; |
|
|
e.preventDefault(); |
|
|
|
|
|
|
|
|
pos1 = pos3 - e.clientX; |
|
|
pos2 = pos4 - e.clientY; |
|
|
pos3 = e.clientX; |
|
|
pos4 = e.clientY; |
|
|
|
|
|
|
|
|
element.style.top = (element.offsetTop - pos2) + "px"; |
|
|
element.style.left = (element.offsetLeft - pos1) + "px"; |
|
|
} |
|
|
|
|
|
function closeDragElement() { |
|
|
|
|
|
document.onmouseup = null; |
|
|
document.onmousemove = null; |
|
|
element.classList.remove('dragging'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
zoomInBtn.addEventListener('click', zoomIn); |
|
|
zoomOutBtn.addEventListener('click', zoomOut); |
|
|
zoomResetBtn.addEventListener('click', resetZoom); |
|
|
|
|
|
function zoomIn() { |
|
|
if (zoomLevel < maxZoom) { |
|
|
zoomLevel = Math.min(zoomLevel + zoomStep, maxZoom); |
|
|
applyZoom(); |
|
|
} |
|
|
} |
|
|
|
|
|
function zoomOut() { |
|
|
if (zoomLevel > minZoom) { |
|
|
zoomLevel = Math.max(zoomLevel - zoomStep, minZoom); |
|
|
applyZoom(); |
|
|
} |
|
|
} |
|
|
|
|
|
function resetZoom() { |
|
|
zoomLevel = 1; |
|
|
applyZoom(); |
|
|
} |
|
|
|
|
|
function applyZoom() { |
|
|
canvas.setZoom(zoomLevel); |
|
|
document.getElementById('zoomValue').textContent = `${Math.round(zoomLevel * 100)}%`; |
|
|
canvas.renderAll(); |
|
|
} |
|
|
|
|
|
|
|
|
fullscreenBtn.addEventListener('click', toggleFullscreen); |
|
|
|
|
|
function toggleFullscreen() { |
|
|
if (!document.fullscreenElement) { |
|
|
document.documentElement.requestFullscreen().then(() => { |
|
|
fullscreenBtn.innerHTML = '<i class="fas fa-compress"></i>'; |
|
|
}); |
|
|
} else { |
|
|
document.exitFullscreen().then(() => { |
|
|
fullscreenBtn.innerHTML = '<i class="fas fa-expand"></i>'; |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
uploadBtn.addEventListener('click', () => fileInput.click()); |
|
|
|
|
|
fileInput.addEventListener('change', handleFileUpload); |
|
|
|
|
|
function handleFileUpload(e) { |
|
|
const file = e.target.files[0]; |
|
|
if (!file) return; |
|
|
|
|
|
|
|
|
if (!file.type.match('image.*')) { |
|
|
showNotification('Please select an image file (JPG, PNG, GIF)', 'error'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (file.size > 10 * 1024 * 1024) { |
|
|
showNotification('Image size should be less than 10MB', 'error'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const reader = new FileReader(); |
|
|
|
|
|
reader.onload = function(f) { |
|
|
fabric.Image.fromURL(f.target.result, function(img) { |
|
|
|
|
|
canvas.clear(); |
|
|
|
|
|
|
|
|
canvas.add(img); |
|
|
canvas.setActiveObject(img); |
|
|
canvas.renderAll(); |
|
|
|
|
|
|
|
|
img.center(); |
|
|
|
|
|
|
|
|
addLockControls(img); |
|
|
|
|
|
showNotification('Image loaded successfully!', 'success'); |
|
|
|
|
|
|
|
|
saveHistory(); |
|
|
}, { |
|
|
crossOrigin: 'anonymous', |
|
|
|
|
|
selectable: true, |
|
|
hasControls: true, |
|
|
hasBorders: true |
|
|
}); |
|
|
}; |
|
|
|
|
|
reader.onerror = function() { |
|
|
showNotification('Error reading file. Please try another image.', 'error'); |
|
|
}; |
|
|
|
|
|
reader.readAsDataURL(file); |
|
|
} |
|
|
|
|
|
|
|
|
function addLockControls(obj) { |
|
|
|
|
|
const lockButton = document.createElement('div'); |
|
|
lockButton.className = 'lock-btn'; |
|
|
lockButton.innerHTML = '<i class="fas fa-lock-open"></i>'; |
|
|
lockButton.title = 'Lock/Unlock'; |
|
|
|
|
|
|
|
|
lockButton.addEventListener('click', function(e) { |
|
|
e.stopPropagation(); |
|
|
toggleLock(obj); |
|
|
}); |
|
|
|
|
|
|
|
|
obj.lockButton = lockButton; |
|
|
updateLockButtonPosition(obj); |
|
|
|
|
|
|
|
|
document.querySelector('.canvas-container').appendChild(lockButton); |
|
|
|
|
|
|
|
|
obj.on('moving', function() { |
|
|
if (!obj.locked) { |
|
|
updateLockButtonPosition(obj); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
obj.on('scaling', function() { |
|
|
if (!obj.locked) { |
|
|
updateLockButtonPosition(obj); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
obj.on('removed', function() { |
|
|
if (obj.lockButton && obj.lockButton.parentNode) { |
|
|
obj.lockButton.parentNode.removeChild(obj.lockButton); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function updateLockButtonPosition(obj) { |
|
|
if (!obj.lockButton) return; |
|
|
|
|
|
const zoom = canvas.getZoom(); |
|
|
const vpt = canvas.viewportTransform; |
|
|
const absoluteLeft = obj.left * zoom + vpt[4]; |
|
|
const absoluteTop = obj.top * zoom + vpt[5]; |
|
|
|
|
|
obj.lockButton.style.left = `${absoluteLeft + obj.width * zoom - 30}px`; |
|
|
obj.lockButton.style.top = `${absoluteTop + 5}px`; |
|
|
} |
|
|
|
|
|
|
|
|
function toggleLock(obj) { |
|
|
obj.locked = !obj.locked; |
|
|
|
|
|
if (obj.locked) { |
|
|
obj.lockButton.innerHTML = '<i class="fas fa-lock locked"></i>'; |
|
|
obj.selectable = false; |
|
|
obj.hasControls = false; |
|
|
obj.hasBorders = false; |
|
|
obj.evented = false; |
|
|
showNotification('Object locked', 'success'); |
|
|
} else { |
|
|
obj.lockButton.innerHTML = '<i class="fas fa-lock-open"></i>'; |
|
|
obj.selectable = true; |
|
|
obj.hasControls = true; |
|
|
obj.hasBorders = true; |
|
|
obj.evented = true; |
|
|
showNotification('Object unlocked', 'success'); |
|
|
} |
|
|
|
|
|
canvas.renderAll(); |
|
|
saveHistory(); |
|
|
} |
|
|
|
|
|
|
|
|
propertiesClose.addEventListener('click', () => { |
|
|
floatingProperties.classList.add('hidden'); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('canvasBg').addEventListener('input', function(e) { |
|
|
canvas.setBackgroundColor(e.target.value, canvas.renderAll.bind(canvas)); |
|
|
saveHistory(); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('canvasOpacity').addEventListener('input', function(e) { |
|
|
const opacity = e.target.value / 100; |
|
|
canvas.setBackgroundColor(canvas.backgroundColor, canvas.renderAll.bind(canvas), { |
|
|
opacity: opacity |
|
|
}); |
|
|
document.getElementById('canvasOpacityValue').textContent = `${e.target.value}%`; |
|
|
saveHistory(); |
|
|
}); |
|
|
|
|
|
|
|
|
lockBtn.addEventListener('click', function() { |
|
|
if (activeObject) { |
|
|
toggleLock(activeObject); |
|
|
} |
|
|
}); |
|
|
|
|
|
deleteBtn.addEventListener('click', function() { |
|
|
if (activeObject) { |
|
|
deleteSelectedObject(); |
|
|
} |
|
|
}); |
|
|
|
|
|
bringToFrontBtn.addEventListener('click', function() { |
|
|
if (activeObject) { |
|
|
activeObject.bringToFront(); |
|
|
canvas.renderAll(); |
|
|
saveHistory(); |
|
|
} |
|
|
}); |
|
|
|
|
|
sendToBackBtn.addEventListener('click', function() { |
|
|
if (activeObject) { |
|
|
activeObject.sendToBack(); |
|
|
canvas.renderAll(); |
|
|
saveHistory(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
function deleteSelectedObject() { |
|
|
if (activeObject) { |
|
|
canvas.remove(activeObject); |
|
|
activeObject = null; |
|
|
canvas.renderAll(); |
|
|
saveHistory(); |
|
|
showNotification('Object deleted', 'success'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function showNotification(message, type) { |
|
|
const notification = document.createElement('div'); |
|
|
notification.className = `notification ${type}`; |
|
|
|
|
|
let icon; |
|
|
if (type === 'success') { |
|
|
icon = '<i class="fas fa-check-circle"></i>'; |
|
|
} else if (type === 'error') { |
|
|
icon = '<i class="fas fa-exclamation-circle"></i>'; |
|
|
} else { |
|
|
icon = '<i class="fas fa-info-circle"></i>'; |
|
|
} |
|
|
|
|
|
notification.innerHTML = `${icon} ${message}`; |
|
|
notificationContainer.appendChild(notification); |
|
|
|
|
|
setTimeout(() => { |
|
|
notification.remove(); |
|
|
}, 3000); |
|
|
} |
|
|
|
|
|
|
|
|
function saveHistory() { |
|
|
const canvasState = JSON.stringify(canvas.toJSON()); |
|
|
|
|
|
if (historyIndex < history.length - 1) { |
|
|
history = history.slice(0, historyIndex + 1); |
|
|
} |
|
|
|
|
|
history.push(canvasState); |
|
|
historyIndex++; |
|
|
|
|
|
if (history.length > 50) { |
|
|
history.shift(); |
|
|
historyIndex--; |
|
|
} |
|
|
} |
|
|
|
|
|
function loadHistory(index) { |
|
|
if (index >= 0 && index < history.length) { |
|
|
canvas.loadFromJSON(history[index], function() { |
|
|
canvas.renderAll(); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
document.getElementById('undoBtn').addEventListener('click', function() { |
|
|
if (historyIndex > 0) { |
|
|
historyIndex--; |
|
|
loadHistory(historyIndex); |
|
|
} |
|
|
}); |
|
|
|
|
|
document.getElementById('redoBtn').addEventListener('click', function() { |
|
|
if (historyIndex < history.length - 1) { |
|
|
historyIndex++; |
|
|
loadHistory(historyIndex); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
function setActiveTool(tool) { |
|
|
currentTool = tool; |
|
|
|
|
|
|
|
|
document.querySelectorAll('.tool-btn').forEach(btn => { |
|
|
btn.classList.remove('active'); |
|
|
}); |
|
|
document.getElementById(`${tool}Tool`).classList.add('active'); |
|
|
|
|
|
|
|
|
switch(tool) { |
|
|
case 'select': |
|
|
canvas.defaultCursor = 'default'; |
|
|
canvas.selection = true; |
|
|
break; |
|
|
case 'text': |
|
|
canvas.defaultCursor = 'text'; |
|
|
canvas.selection = false; |
|
|
break; |
|
|
case 'draw': |
|
|
canvas.defaultCursor = 'crosshair'; |
|
|
canvas.selection = false; |
|
|
break; |
|
|
case 'shape': |
|
|
canvas.defaultCursor = 'crosshair'; |
|
|
canvas.selection = false; |
|
|
break; |
|
|
case 'blur': |
|
|
canvas.defaultCursor = 'crosshair'; |
|
|
canvas.selection = false; |
|
|
break; |
|
|
} |
|
|
|
|
|
|
|
|
document.querySelectorAll('.property-group').forEach(group => { |
|
|
group.classList.add('hidden'); |
|
|
}); |
|
|
|
|
|
if (tool === 'text') { |
|
|
document.getElementById('textProps').classList.remove('hidden'); |
|
|
} else if (tool === 'draw') { |
|
|
document.getElementById('drawProps').classList.remove('hidden'); |
|
|
} |
|
|
|
|
|
|
|
|
if (tool !== 'select') { |
|
|
floatingProperties.classList.remove('hidden'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
document.getElementById('selectTool').addEventListener('click', () => setActiveTool('select')); |
|
|
document.getElementById('textTool').addEventListener('click', () => setActiveTool('text')); |
|
|
document.getElementById('drawTool').addEventListener('click', () => setActiveTool('draw')); |
|
|
document.getElementById('shapeTool').addEventListener('click', () => setActiveTool('shape')); |
|
|
document.getElementById('blurTool').addEventListener('click', () => setActiveTool('blur')); |
|
|
|
|
|
|
|
|
setActiveTool('select'); |
|
|
|
|
|
|
|
|
canvas.on('mouse:down', function(options) { |
|
|
if (options.target && options.target.locked) { |
|
|
return; |
|
|
} |
|
|
|
|
|
if (options.target) { |
|
|
activeObject = options.target; |
|
|
return; |
|
|
} |
|
|
|
|
|
const pointer = canvas.getPointer(options.e); |
|
|
|
|
|
switch(currentTool) { |
|
|
case 'text': |
|
|
addText(pointer); |
|
|
break; |
|
|
case 'draw': |
|
|
startDrawing(pointer); |
|
|
break; |
|
|
case 'shape': |
|
|
addRectangle(pointer); |
|
|
break; |
|
|
case 'blur': |
|
|
addBlur(pointer); |
|
|
break; |
|
|
} |
|
|
}); |
|
|
|
|
|
canvas.on('mouse:move', function(options) { |
|
|
if (isDrawing && drawingPath && currentTool === 'draw') { |
|
|
const pointer = canvas.getPointer(options.e); |
|
|
drawingPath.path.push([ |
|
|
'L', |
|
|
pointer.x, |
|
|
pointer.y |
|
|
]); |
|
|
canvas.renderAll(); |
|
|
} |
|
|
}); |
|
|
|
|
|
canvas.on('mouse:up', function() { |
|
|
if (isDrawing) { |
|
|
isDrawing = false; |
|
|
saveHistory(); |
|
|
} |
|
|
}); |
|
|
|
|
|
canvas.on('object:selected', function(options) { |
|
|
if (options.target.locked) { |
|
|
canvas.discardActiveObject(); |
|
|
return; |
|
|
} |
|
|
|
|
|
activeObject = options.target; |
|
|
floatingProperties.classList.remove('hidden'); |
|
|
|
|
|
|
|
|
document.getElementById('objectProps').classList.remove('hidden'); |
|
|
|
|
|
|
|
|
if (activeObject.locked) { |
|
|
lockBtn.innerHTML = '<i class="fas fa-lock locked"></i>'; |
|
|
} else { |
|
|
lockBtn.innerHTML = '<i class="fas fa-lock-open"></i>'; |
|
|
} |
|
|
}); |
|
|
|
|
|
canvas.on('selection:cleared', function() { |
|
|
activeObject = null; |
|
|
document.getElementById('objectProps').classList.add('hidden'); |
|
|
}); |
|
|
|
|
|
|
|
|
function addText(position) { |
|
|
const text = new fabric.IText('Type here', { |
|
|
left: position.x, |
|
|
top: position.y, |
|
|
fontFamily: document.getElementById('textFont').value, |
|
|
fontSize: parseInt(document.getElementById('textSize').value), |
|
|
fill: document.getElementById('textColor').value, |
|
|
selectable: true, |
|
|
hasControls: true, |
|
|
hasBorders: true |
|
|
}); |
|
|
|
|
|
canvas.add(text); |
|
|
canvas.setActiveObject(text); |
|
|
text.enterEditing(); |
|
|
text.hiddenTextarea.focus(); |
|
|
canvas.renderAll(); |
|
|
saveHistory(); |
|
|
} |
|
|
|
|
|
function startDrawing(position) { |
|
|
isDrawing = true; |
|
|
drawingPath = new fabric.Path(`M ${position.x} ${position.y}`, { |
|
|
stroke: document.getElementById('brushColor').value, |
|
|
strokeWidth: parseInt(document.getElementById('brushSize').value), |
|
|
fill: 'transparent', |
|
|
selectable: true, |
|
|
hasControls: true, |
|
|
hasBorders: true |
|
|
}); |
|
|
|
|
|
canvas.add(drawingPath); |
|
|
} |
|
|
|
|
|
function addRectangle(position) { |
|
|
const rect = new fabric.Rect({ |
|
|
left: position.x, |
|
|
top: position.y, |
|
|
width: 100, |
|
|
height: 100, |
|
|
fill: 'transparent', |
|
|
stroke: '#ffffff', |
|
|
strokeWidth: 2, |
|
|
selectable: true, |
|
|
hasControls: true, |
|
|
hasBorders: true |
|
|
}); |
|
|
|
|
|
canvas.add(rect); |
|
|
canvas.setActiveObject(rect); |
|
|
canvas.renderAll(); |
|
|
saveHistory(); |
|
|
|
|
|
|
|
|
addLockControls(rect); |
|
|
} |
|
|
|
|
|
function addBlur(position) { |
|
|
const circle = new fabric.Circle({ |
|
|
left: position.x, |
|
|
top: position.y, |
|
|
radius: 30, |
|
|
fill: 'rgba(0,0,0,0.5)', |
|
|
selectable: true, |
|
|
hasControls: true, |
|
|
hasBorders: true |
|
|
}); |
|
|
|
|
|
canvas.add(circle); |
|
|
canvas.setActiveObject(circle); |
|
|
canvas.renderAll(); |
|
|
saveHistory(); |
|
|
|
|
|
|
|
|
addLockControls(circle); |
|
|
} |
|
|
|
|
|
|
|
|
document.getElementById('textColor').addEventListener('input', function(e) { |
|
|
if (activeObject && activeObject.type === 'i-text') { |
|
|
activeObject.set('fill', e.target.value); |
|
|
canvas.renderAll(); |
|
|
saveHistory(); |
|
|
} |
|
|
}); |
|
|
|
|
|
document.getElementById('textSize').addEventListener('input', function(e) { |
|
|
if (activeObject && activeObject.type === 'i-text') { |
|
|
activeObject.set('fontSize', parseInt(e.target.value)); |
|
|
document.getElementById('textSizeValue').textContent = `${e.target.value}px`; |
|
|
canvas.renderAll(); |
|
|
saveHistory(); |
|
|
} |
|
|
}); |
|
|
|
|
|
document.getElementById('textFont').addEventListener('change', function(e) { |
|
|
if (activeObject && activeObject.type === 'i-text') { |
|
|
activeObject.set('fontFamily', e.target.value); |
|
|
canvas.renderAll(); |
|
|
saveHistory(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('brushColor').addEventListener('input', function(e) { |
|
|
|
|
|
}); |
|
|
|
|
|
document.getElementById('brushSize').addEventListener('input', function(e) { |
|
|
document.getElementById('brushSizeValue').textContent = `${e.target.value}px`; |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('saveBtn').addEventListener('click', function() { |
|
|
const dataURL = canvas.toDataURL({ |
|
|
format: 'png', |
|
|
quality: 1 |
|
|
}); |
|
|
|
|
|
const link = document.createElement('a'); |
|
|
link.download = 'snapedit-export.png'; |
|
|
link.href = dataURL; |
|
|
link.click(); |
|
|
|
|
|
showNotification('Image exported successfully!', 'success'); |
|
|
}); |
|
|
|
|
|
document.getElementById('exportBtn').addEventListener('click', function() { |
|
|
|
|
|
showNotification('Export options would appear here', 'info'); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('aiRemoveBg').addEventListener('click', function() { |
|
|
showNotification('AI Background Removal would run here', 'info'); |
|
|
}); |
|
|
|
|
|
document.getElementById('aiEnhance').addEventListener('click', function() { |
|
|
showNotification('AI Enhancement would run here', 'info'); |
|
|
}); |
|
|
|
|
|
|
|
|
document.addEventListener('keydown', function(e) { |
|
|
|
|
|
const ctrlKey = e.ctrlKey || e.metaKey; |
|
|
|
|
|
|
|
|
if (ctrlKey && e.key === '+') { |
|
|
e.preventDefault(); |
|
|
zoomIn(); |
|
|
} else if (ctrlKey && e.key === '-') { |
|
|
e.preventDefault(); |
|
|
zoomOut(); |
|
|
} else if (ctrlKey && e.key === '0') { |
|
|
e.preventDefault(); |
|
|
resetZoom(); |
|
|
} |
|
|
|
|
|
|
|
|
if ((e.key === 'Delete' || e.key === 'Backspace') && activeObject) { |
|
|
e.preventDefault(); |
|
|
deleteSelectedObject(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
canvas.on('after:render', function() { |
|
|
canvas.forEachObject(function(obj) { |
|
|
if (obj.lockButton) { |
|
|
updateLockButtonPosition(obj); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
</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=sqibhe/snapeidit" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
|
</html> |