Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Image Stencil FX Tool</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .canvas-container { | |
| margin: 0 auto; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| } | |
| .tab-content { | |
| display: none; | |
| } | |
| .tab-content.active { | |
| display: block; | |
| } | |
| .tab-button { | |
| transition: all 0.3s ease; | |
| } | |
| .tab-button.active { | |
| background-color: #3b82f6; | |
| color: white; | |
| } | |
| .effect-btn { | |
| transition: all 0.2s ease; | |
| } | |
| .effect-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| } | |
| .range-slider { | |
| -webkit-appearance: none; | |
| width: 100%; | |
| height: 8px; | |
| border-radius: 4px; | |
| background: #d1d5db; | |
| outline: none; | |
| } | |
| .range-slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: #3b82f6; | |
| cursor: pointer; | |
| } | |
| .range-slider::-moz-range-thumb { | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: #3b82f6; | |
| cursor: pointer; | |
| } | |
| .dropzone { | |
| border: 2px dashed #9ca3af; | |
| transition: all 0.3s ease; | |
| } | |
| .dropzone.active { | |
| border-color: #3b82f6; | |
| background-color: #f0f7ff; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <h1 class="text-4xl font-bold text-center text-blue-600 mb-2">Image Stencil FX Tool</h1> | |
| <p class="text-center text-gray-600 mb-8">Upload an image and apply various stencil effects</p> | |
| <div class="bg-white rounded-xl shadow-lg overflow-hidden mb-8"> | |
| <!-- Tabs Navigation --> | |
| <div class="flex border-b"> | |
| <button class="tab-button px-6 py-3 font-medium text-gray-600 active" data-tab="upload"> | |
| <i class="fas fa-upload mr-2"></i>Upload Image | |
| </button> | |
| <button class="tab-button px-6 py-3 font-medium text-gray-600" data-tab="effects"> | |
| <i class="fas fa-magic mr-2"></i>Effects | |
| </button> | |
| <button class="tab-button px-6 py-3 font-medium text-gray-600" data-tab="crop"> | |
| <i class="fas fa-crop mr-2"></i>Crop | |
| </button> | |
| <button class="tab-button px-6 py-3 font-medium text-gray-600" data-tab="download"> | |
| <i class="fas fa-download mr-2"></i>Download | |
| </button> | |
| </div> | |
| <!-- Tab Contents --> | |
| <div class="p-6"> | |
| <!-- Upload Tab --> | |
| <div id="upload" class="tab-content active"> | |
| <div id="dropzone" class="dropzone rounded-lg p-12 text-center cursor-pointer mb-6"> | |
| <div class="flex flex-col items-center justify-center"> | |
| <i class="fas fa-cloud-upload-alt text-5xl text-blue-400 mb-4"></i> | |
| <h3 class="text-xl font-semibold text-gray-700">Drag & Drop your image here</h3> | |
| <p class="text-gray-500 mt-2">or click to browse files</p> | |
| <input type="file" id="fileInput" class="hidden" accept="image/*"> | |
| </div> | |
| </div> | |
| <div class="flex justify-center"> | |
| <button id="loadSample" class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-2 px-4 rounded-lg transition"> | |
| <i class="fas fa-image mr-2"></i>Load Sample Image | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Effects Tab --> | |
| <div id="effects" class="tab-content"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div> | |
| <div class="canvas-container"> | |
| <canvas id="canvas" width="500" height="500" class="border border-gray-200"></canvas> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="mb-6"> | |
| <h3 class="text-lg font-semibold mb-3">Basic Adjustments</h3> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Brightness</label> | |
| <input type="range" min="-100" max="100" value="0" class="range-slider" id="brightness"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Contrast</label> | |
| <input type="range" min="-100" max="100" value="0" class="range-slider" id="contrast"> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <h3 class="text-lg font-semibold mb-3">Stencil Effects</h3> | |
| <div class="grid grid-cols-2 gap-3"> | |
| <button class="effect-btn bg-blue-100 hover:bg-blue-200 text-blue-800 py-2 px-4 rounded-lg" id="threshold"> | |
| <i class="fas fa-adjust mr-2"></i>Threshold | |
| </button> | |
| <button class="effect-btn bg-purple-100 hover:bg-purple-200 text-purple-800 py-2 px-4 rounded-lg" id="dither"> | |
| <i class="fas fa-th-large mr-2"></i>Dither | |
| </button> | |
| <button class="effect-btn bg-green-100 hover:bg-green-200 text-green-800 py-2 px-4 rounded-lg" id="halftone"> | |
| <i class="fas fa-circle-notch mr-2"></i>Halftone | |
| </button> | |
| <button class="effect-btn bg-red-100 hover:bg-red-200 text-red-800 py-2 px-4 rounded-lg" id="glitch"> | |
| <i class="fas fa-bolt mr-2"></i>Glitch | |
| </button> | |
| <button class="effect-btn bg-yellow-100 hover:bg-yellow-200 text-yellow-800 py-2 px-4 rounded-lg" id="posterize"> | |
| <i class="fas fa-layer-group mr-2"></i>Posterize | |
| </button> | |
| <button class="effect-btn bg-indigo-100 hover:bg-indigo-200 text-indigo-800 py-2 px-4 rounded-lg" id="edge"> | |
| <i class="fas fa-border-style mr-2"></i>Edge Detect | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mt-6"> | |
| <button id="resetEffects" class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-2 px-4 rounded-lg transition w-full"> | |
| <i class="fas fa-undo mr-2"></i>Reset All Effects | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Crop Tab --> | |
| <div id="crop" class="tab-content"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div> | |
| <div class="canvas-container"> | |
| <canvas id="cropCanvas" width="500" height="500" class="border border-gray-200"></canvas> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="mb-6"> | |
| <h3 class="text-lg font-semibold mb-3">Crop Options</h3> | |
| <div class="grid grid-cols-2 gap-4 mb-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Width</label> | |
| <input type="number" id="cropWidth" class="w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="Width"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Height</label> | |
| <input type="number" id="cropHeight" class="w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="Height"> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-2 gap-4 mb-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">X Position</label> | |
| <input type="number" id="cropX" class="w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="X"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Y Position</label> | |
| <input type="number" id="cropY" class="w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="Y"> | |
| </div> | |
| </div> | |
| <div class="flex space-x-3"> | |
| <button id="setCrop" class="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-lg transition flex-1"> | |
| <i class="fas fa-crop mr-2"></i>Set Crop Area | |
| </button> | |
| <button id="applyCrop" class="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4 rounded-lg transition flex-1"> | |
| <i class="fas fa-check mr-2"></i>Apply Crop | |
| </button> | |
| </div> | |
| </div> | |
| <div class="bg-gray-100 p-4 rounded-lg"> | |
| <h4 class="font-medium text-gray-700 mb-2">Quick Aspect Ratios</h4> | |
| <div class="flex flex-wrap gap-2"> | |
| <button class="aspect-btn bg-gray-200 hover:bg-gray-300 px-3 py-1 rounded" data-ratio="1">1:1</button> | |
| <button class="aspect-btn bg-gray-200 hover:bg-gray-300 px-3 py-1 rounded" data-ratio="1.33">4:3</button> | |
| <button class="aspect-btn bg-gray-200 hover:bg-gray-300 px-3 py-1 rounded" data-ratio="1.77">16:9</button> | |
| <button class="aspect-btn bg-gray-200 hover:bg-gray-300 px-3 py-1 rounded" data-ratio="0.75">3:4</button> | |
| <button class="aspect-btn bg-gray-200 hover:bg-gray-300 px-3 py-1 rounded" data-ratio="1.5">3:2</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Download Tab --> | |
| <div id="download" class="tab-content"> | |
| <div class="flex flex-col items-center justify-center py-12"> | |
| <i class="fas fa-file-image text-5xl text-blue-400 mb-6"></i> | |
| <h3 class="text-xl font-semibold text-gray-700 mb-2">Your Image is Ready!</h3> | |
| <p class="text-gray-500 mb-6">Choose your preferred download format</p> | |
| <div class="flex flex-wrap justify-center gap-4"> | |
| <button id="downloadPNG" class="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-6 rounded-lg transition"> | |
| <i class="fas fa-download mr-2"></i>Download PNG | |
| </button> | |
| <button id="downloadJPG" class="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-6 rounded-lg transition"> | |
| <i class="fas fa-download mr-2"></i>Download JPG | |
| </button> | |
| <button id="downloadWEBP" class="bg-purple-500 hover:bg-purple-600 text-white font-medium py-2 px-6 rounded-lg transition"> | |
| <i class="fas fa-download mr-2"></i>Download WEBP | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="text-center text-gray-500 text-sm mt-8"> | |
| <p>Created with HTML, CSS, and JavaScript | Image Stencil FX Tool</p> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Tab switching functionality | |
| const tabButtons = document.querySelectorAll('.tab-button'); | |
| const tabContents = document.querySelectorAll('.tab-content'); | |
| tabButtons.forEach(button => { | |
| button.addEventListener('click', () => { | |
| const tabId = button.getAttribute('data-tab'); | |
| // Update active tab button | |
| tabButtons.forEach(btn => btn.classList.remove('active')); | |
| button.classList.add('active'); | |
| // Update active tab content | |
| tabContents.forEach(content => content.classList.remove('active')); | |
| document.getElementById(tabId).classList.add('active'); | |
| }); | |
| }); | |
| // Initialize Fabric.js canvases | |
| const canvas = new fabric.Canvas('canvas'); | |
| const cropCanvas = new fabric.Canvas('cropCanvas'); | |
| let currentImage = null; | |
| let originalImageData = null; | |
| // File upload handling | |
| const dropzone = document.getElementById('dropzone'); | |
| const fileInput = document.getElementById('fileInput'); | |
| dropzone.addEventListener('click', () => fileInput.click()); | |
| fileInput.addEventListener('change', handleFileSelect); | |
| // Drag and drop functionality | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| dropzone.addEventListener(eventName, preventDefaults, false); | |
| }); | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| dropzone.addEventListener(eventName, highlight, false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| dropzone.addEventListener(eventName, unhighlight, false); | |
| }); | |
| function highlight() { | |
| dropzone.classList.add('active'); | |
| } | |
| function unhighlight() { | |
| dropzone.classList.remove('active'); | |
| } | |
| dropzone.addEventListener('drop', handleDrop, false); | |
| function handleDrop(e) { | |
| const dt = e.dataTransfer; | |
| const files = dt.files; | |
| handleFiles(files); | |
| } | |
| function handleFileSelect(e) { | |
| const files = e.target.files; | |
| handleFiles(files); | |
| } | |
| function handleFiles(files) { | |
| if (files.length === 0) return; | |
| const file = files[0]; | |
| if (!file.type.match('image.*')) { | |
| alert('Please select an image file.'); | |
| return; | |
| } | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| fabric.Image.fromURL(e.target.result, function(img) { | |
| // Clear previous image | |
| canvas.clear(); | |
| cropCanvas.clear(); | |
| // Scale image to fit canvas if it's too large | |
| const scale = Math.min(1, Math.min(500 / img.width, 500 / img.height)); | |
| img.scale(scale); | |
| // Center the image | |
| img.set({ | |
| left: (500 - img.width * scale) / 2, | |
| top: (500 - img.height * scale) / 2, | |
| originX: 'left', | |
| originY: 'top' | |
| }); | |
| canvas.add(img); | |
| cropCanvas.add(img); | |
| currentImage = img; | |
| originalImageData = img.toDataURL(); | |
| // Switch to effects tab after upload | |
| document.querySelector('[data-tab="effects"]').click(); | |
| }); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| // Load sample image | |
| document.getElementById('loadSample').addEventListener('click', function() { | |
| const sampleImageUrl = 'https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=500&auto=format&fit=crop'; | |
| fabric.Image.fromURL(sampleImageUrl, function(img) { | |
| // Clear previous image | |
| canvas.clear(); | |
| cropCanvas.clear(); | |
| // Scale image to fit canvas if it's too large | |
| const scale = Math.min(1, Math.min(500 / img.width, 500 / img.height)); | |
| img.scale(scale); | |
| // Center the image | |
| img.set({ | |
| left: (500 - img.width * scale) / 2, | |
| top: (500 - img.height * scale) / 2, | |
| originX: 'left', | |
| originY: 'top' | |
| }); | |
| canvas.add(img); | |
| cropCanvas.add(img); | |
| currentImage = img; | |
| originalImageData = img.toDataURL(); | |
| // Switch to effects tab after upload | |
| document.querySelector('[data-tab="effects"]').click(); | |
| }); | |
| }); | |
| // Basic adjustments | |
| document.getElementById('brightness').addEventListener('input', function() { | |
| if (!currentImage) return; | |
| const value = parseInt(this.value); | |
| currentImage.filters = currentImage.filters || []; | |
| // Remove existing brightness filter if any | |
| currentImage.filters = currentImage.filters.filter(f => f.type !== 'brightness'); | |
| if (value !== 0) { | |
| currentImage.filters.push(new fabric.Image.filters.Brightness({ | |
| brightness: value / 100 | |
| })); | |
| } | |
| currentImage.applyFilters(); | |
| canvas.renderAll(); | |
| }); | |
| document.getElementById('contrast').addEventListener('input', function() { | |
| if (!currentImage) return; | |
| const value = parseInt(this.value); | |
| currentImage.filters = currentImage.filters || []; | |
| // Remove existing contrast filter if any | |
| currentImage.filters = currentImage.filters.filter(f => f.type !== 'contrast'); | |
| if (value !== 0) { | |
| currentImage.filters.push(new fabric.Image.filters.Contrast({ | |
| contrast: value / 100 | |
| })); | |
| } | |
| currentImage.applyFilters(); | |
| canvas.renderAll(); | |
| }); | |
| // Effect buttons | |
| document.getElementById('threshold').addEventListener('click', function() { | |
| if (!currentImage) return; | |
| currentImage.filters = currentImage.filters || []; | |
| // Remove existing threshold filter if any | |
| currentImage.filters = currentImage.filters.filter(f => f.type !== 'threshold'); | |
| currentImage.filters.push(new fabric.Image.filters.Threshold({ | |
| threshold: 128 | |
| })); | |
| currentImage.applyFilters(); | |
| canvas.renderAll(); | |
| }); | |
| document.getElementById('dither').addEventListener('click', function() { | |
| if (!currentImage) return; | |
| // Create a temporary canvas to apply dithering | |
| const tempCanvas = document.createElement('canvas'); | |
| tempCanvas.width = canvas.width; | |
| tempCanvas.height = canvas.height; | |
| const tempCtx = tempCanvas.getContext('2d'); | |
| // Draw the current image to the temp canvas | |
| canvas.getElement().toBlob(function(blob) { | |
| const img = new Image(); | |
| img.onload = function() { | |
| tempCtx.drawImage(img, 0, 0, tempCanvas.width, tempCanvas.height); | |
| // Apply Floyd-Steinberg dithering | |
| const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height); | |
| const data = imageData.data; | |
| for (let y = 0; y < tempCanvas.height; y++) { | |
| for (let x = 0; x < tempCanvas.width; x++) { | |
| const idx = (y * tempCanvas.width + x) * 4; | |
| // Convert to grayscale | |
| const gray = 0.299 * data[idx] + 0.587 * data[idx + 1] + 0.114 * data[idx + 2]; | |
| // Threshold | |
| const newVal = gray < 128 ? 0 : 255; | |
| const err = gray - newVal; | |
| // Set new value | |
| data[idx] = data[idx + 1] = data[idx + 2] = newVal; | |
| // Distribute error | |
| if (x + 1 < tempCanvas.width) { | |
| data[idx + 4] += err * 7 / 16; | |
| } | |
| if (x > 0 && y + 1 < tempCanvas.height) { | |
| data[idx + tempCanvas.width * 4 - 4] += err * 3 / 16; | |
| } | |
| if (y + 1 < tempCanvas.height) { | |
| data[idx + tempCanvas.width * 4] += err * 5 / 16; | |
| } | |
| if (x + 1 < tempCanvas.width && y + 1 < tempCanvas.height) { | |
| data[idx + tempCanvas.width * 4 + 4] += err * 1 / 16; | |
| } | |
| } | |
| } | |
| tempCtx.putImageData(imageData, 0, 0); | |
| // Update the fabric image | |
| fabric.Image.fromURL(tempCanvas.toDataURL(), function(newImg) { | |
| canvas.remove(currentImage); | |
| newImg.set({ | |
| left: currentImage.left, | |
| top: currentImage.top, | |
| scaleX: currentImage.scaleX, | |
| scaleY: currentImage.scaleY | |
| }); | |
| canvas.add(newImg); | |
| currentImage = newImg; | |
| canvas.renderAll(); | |
| }); | |
| }; | |
| img.src = URL.createObjectURL(blob); | |
| }); | |
| }); | |
| document.getElementById('halftone').addEventListener('click', function() { | |
| if (!currentImage) return; | |
| // Create a temporary canvas to apply halftone | |
| const tempCanvas = document.createElement('canvas'); | |
| tempCanvas.width = canvas.width; | |
| tempCanvas.height = canvas.height; | |
| const tempCtx = tempCanvas.getContext('2d'); | |
| // Draw the current image to the temp canvas | |
| canvas.getElement().toBlob(function(blob) { | |
| const img = new Image(); | |
| img.onload = function() { | |
| tempCtx.drawImage(img, 0, 0, tempCanvas.width, tempCanvas.height); | |
| // Apply halftone effect | |
| const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height); | |
| const data = imageData.data; | |
| const dotSize = 4; | |
| const spacing = 8; | |
| tempCtx.fillStyle = 'white'; | |
| tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height); | |
| for (let y = 0; y < tempCanvas.height; y += spacing) { | |
| for (let x = 0; x < tempCanvas.width; x += spacing) { | |
| const idx = (y * tempCanvas.width + x) * 4; | |
| // Convert to grayscale | |
| const gray = 0.299 * data[idx] + 0.587 * data[idx + 1] + 0.114 * data[idx + 2]; | |
| // Calculate dot radius based on brightness | |
| const radius = (1 - gray / 255) * (dotSize / 2); | |
| if (radius > 0) { | |
| tempCtx.fillStyle = 'black'; | |
| tempCtx.beginPath(); | |
| tempCtx.arc(x + spacing/2, y + spacing/2, radius, 0, Math.PI * 2); | |
| tempCtx.fill(); | |
| } | |
| } | |
| } | |
| // Update the fabric image | |
| fabric.Image.fromURL(tempCanvas.toDataURL(), function(newImg) { | |
| canvas.remove(currentImage); | |
| newImg.set({ | |
| left: currentImage.left, | |
| top: currentImage.top, | |
| scaleX: currentImage.scaleX, | |
| scaleY: currentImage.scaleY | |
| }); | |
| canvas.add(newImg); | |
| currentImage = newImg; | |
| canvas.renderAll(); | |
| }); | |
| }; | |
| img.src = URL.createObjectURL(blob); | |
| }); | |
| }); | |
| document.getElementById('glitch').addEventListener('click', function() { | |
| if (!currentImage) return; | |
| // Create a temporary canvas to apply glitch effect | |
| const tempCanvas = document.createElement('canvas'); | |
| tempCanvas.width = canvas.width; | |
| tempCanvas.height = canvas.height; | |
| const tempCtx = tempCanvas.getContext('2d'); | |
| // Draw the current image to the temp canvas | |
| canvas.getElement().toBlob(function(blob) { | |
| const img = new Image(); | |
| img.onload = function() { | |
| tempCtx.drawImage(img, 0, 0, tempCanvas.width, tempCanvas.height); | |
| // Apply glitch effect | |
| const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height); | |
| const data = imageData.data; | |
| // Randomly shift some channels | |
| for (let i = 0; i < data.length; i += 4) { | |
| // Randomly shift red channel | |
| if (Math.random() > 0.9) { | |
| const shift = Math.floor(Math.random() * 10) - 5; | |
| if (i + shift * 4 >= 0 && i + shift * 4 < data.length) { | |
| data[i] = data[i + shift * 4]; | |
| } | |
| } | |
| // Randomly shift blue channel | |
| if (Math.random() > 0.9) { | |
| const shift = Math.floor(Math.random() * 10) - 5; | |
| if (i + 2 + shift * 4 >= 0 && i + 2 + shift * 4 < data.length) { | |
| data[i + 2] = data[i + 2 + shift * 4]; | |
| } | |
| } | |
| } | |
| tempCtx.putImageData(imageData, 0, 0); | |
| // Add some random lines | |
| for (let i = 0; i < 5; i++) { | |
| const y = Math.floor(Math.random() * tempCanvas.height); | |
| const height = Math.floor(Math.random() * 5) + 1; | |
| const shift = Math.floor(Math.random() * 20) - 10; | |
| tempCtx.drawImage( | |
| tempCanvas, | |
| 0, y, tempCanvas.width, height, | |
| shift, y, tempCanvas.width, height | |
| ); | |
| } | |
| // Update the fabric image | |
| fabric.Image.fromURL(tempCanvas.toDataURL(), function(newImg) { | |
| canvas.remove(currentImage); | |
| newImg.set({ | |
| left: currentImage.left, | |
| top: currentImage.top, | |
| scaleX: currentImage.scaleX, | |
| scaleY: currentImage.scaleY | |
| }); | |
| canvas.add(newImg); | |
| currentImage = newImg; | |
| canvas.renderAll(); | |
| }); | |
| }; | |
| img.src = URL.createObjectURL(blob); | |
| }); | |
| }); | |
| document.getElementById('posterize').addEventListener('click', function() { | |
| if (!currentImage) return; | |
| currentImage.filters = currentImage.filters || []; | |
| // Remove existing posterize filter if any | |
| currentImage.filters = currentImage.filters.filter(f => f.type !== 'posterize'); | |
| currentImage.filters.push(new fabric.Image.filters.Posterize({ | |
| levels: 4 | |
| })); | |
| currentImage.applyFilters(); | |
| canvas.renderAll(); | |
| }); | |
| document.getElementById('edge').addEventListener('click', function() { | |
| if (!currentImage) return; | |
| currentImage.filters = currentImage.filters || []; | |
| // Remove existing edge detect filter if any | |
| currentImage.filters = currentImage.filters.filter(f => f.type !== 'convolution'); | |
| currentImage.filters.push(new fabric.Image.filters.Convolute({ | |
| matrix: [ -1, -1, -1, | |
| -1, 8, -1, | |
| -1, -1, -1 ] | |
| })); | |
| currentImage.applyFilters(); | |
| canvas.renderAll(); | |
| }); | |
| document.getElementById('resetEffects').addEventListener('click', function() { | |
| if (!currentImage || !originalImageData) return; | |
| fabric.Image.fromURL(originalImageData, function(img) { | |
| canvas.remove(currentImage); | |
| img.set({ | |
| left: currentImage.left, | |
| top: currentImage.top, | |
| scaleX: currentImage.scaleX, | |
| scaleY: currentImage.scaleY | |
| }); | |
| canvas.add(img); | |
| currentImage = img; | |
| canvas.renderAll(); | |
| // Reset sliders | |
| document.getElementById('brightness').value = 0; | |
| document.getElementById('contrast').value = 0; | |
| }); | |
| }); | |
| // Crop functionality | |
| let isCropping = false; | |
| let cropRect = null; | |
| document.getElementById('setCrop').addEventListener('click', function() { | |
| if (!currentImage) return; | |
| if (isCropping) { | |
| // Remove existing crop rectangle | |
| cropCanvas.remove(cropRect); | |
| isCropping = false; | |
| cropRect = null; | |
| return; | |
| } | |
| isCropping = true; | |
| // Create a crop rectangle | |
| cropRect = new fabric.Rect({ | |
| left: 100, | |
| top: 100, | |
| width: 200, | |
| height: 200, | |
| fill: 'rgba(0,0,0,0.3)', | |
| stroke: '#3b82f6', | |
| strokeWidth: 2, | |
| strokeDashArray: [5, 5], | |
| selectable: true, | |
| hasControls: true, | |
| hasBorders: true, | |
| lockRotation: true | |
| }); | |
| cropCanvas.add(cropRect); | |
| cropCanvas.setActiveObject(cropRect); | |
| // Update input fields with initial values | |
| document.getElementById('cropWidth').value = Math.round(cropRect.width); | |
| document.getElementById('cropHeight').value = Math.round(cropRect.height); | |
| document.getElementById('cropX').value = Math.round(cropRect.left); | |
| document.getElementById('cropY').value = Math.round(cropRect.top); | |
| // Listen for changes to the crop rectangle | |
| cropRect.on('moving', updateCropInputs); | |
| cropRect.on('scaling', updateCropInputs); | |
| }); | |
| function updateCropInputs() { | |
| if (!cropRect) return; | |
| document.getElementById('cropWidth').value = Math.round(cropRect.width * cropRect.scaleX); | |
| document.getElementById('cropHeight').value = Math.round(cropRect.height * cropRect.scaleY); | |
| document.getElementById('cropX').value = Math.round(cropRect.left); | |
| document.getElementById('cropY').value = Math.round(cropRect.top); | |
| } | |
| // Update crop rectangle when inputs change | |
| document.getElementById('cropWidth').addEventListener('input', function() { | |
| if (!cropRect) return; | |
| const width = parseInt(this.value); | |
| if (width > 0) { | |
| cropRect.set({ width: width / cropRect.scaleX }); | |
| cropCanvas.renderAll(); | |
| } | |
| }); | |
| document.getElementById('cropHeight').addEventListener('input', function() { | |
| if (!cropRect) return; | |
| const height = parseInt(this.value); | |
| if (height > 0) { | |
| cropRect.set({ height: height / cropRect.scaleY }); | |
| cropCanvas.renderAll(); | |
| } | |
| }); | |
| document.getElementById('cropX').addEventListener('input', function() { | |
| if (!cropRect) return; | |
| const x = parseInt(this.value); | |
| cropRect.set({ left: x }); | |
| cropCanvas.renderAll(); | |
| }); | |
| document.getElementById('cropY').addEventListener('input', function() { | |
| if (!cropRect) return; | |
| const y = parseInt(this.value); | |
| cropRect.set({ top: y }); | |
| cropCanvas.renderAll(); | |
| }); | |
| // Aspect ratio buttons | |
| document.querySelectorAll('.aspect-btn').forEach(button => { | |
| button.addEventListener('click', function() { | |
| if (!cropRect) return; | |
| const ratio = parseFloat(this.getAttribute('data-ratio')); | |
| const newHeight = cropRect.width / ratio; | |
| cropRect.set({ height: newHeight / cropRect.scaleY }); | |
| document.getElementById('cropHeight').value = Math.round(newHeight); | |
| cropCanvas.renderAll(); | |
| }); | |
| }); | |
| document.getElementById('applyCrop').addEventListener('click', function() { | |
| if (!currentImage || !cropRect) return; | |
| // Get crop coordinates | |
| const left = Math.max(0, cropRect.left); | |
| const top = Math.max(0, cropRect.top); | |
| const width = Math.min(cropRect.width * cropRect.scaleX, currentImage.width - left); | |
| const height = Math.min(cropRect.height * cropRect.scaleY, currentImage.height - top); | |
| // Create a temporary canvas for cropping | |
| const tempCanvas = document.createElement('canvas'); | |
| tempCanvas.width = width; | |
| tempCanvas.height = height; | |
| const tempCtx = tempCanvas.getContext('2d'); | |
| // Draw the cropped portion | |
| cropCanvas.getElement().toBlob(function(blob) { | |
| const img = new Image(); | |
| img.onload = function() { | |
| tempCtx.drawImage( | |
| img, | |
| left, top, width, height, | |
| 0, 0, width, height | |
| ); | |
| // Update the fabric image | |
| fabric.Image.fromURL(tempCanvas.toDataURL(), function(newImg) { | |
| // Calculate scale to fit in canvas | |
| const scale = Math.min(1, Math.min(500 / newImg.width, 500 / newImg.height)); | |
| newImg.scale(scale); | |
| // Center the image | |
| newImg.set({ | |
| left: (500 - newImg.width * scale) / 2, | |
| top: (500 - newImg.height * scale) / 2, | |
| originX: 'left', | |
| originY: 'top' | |
| }); | |
| // Update all canvases | |
| canvas.clear(); | |
| canvas.add(newImg); | |
| currentImage = newImg; | |
| canvas.renderAll(); | |
| cropCanvas.clear(); | |
| cropCanvas.add(newImg); | |
| cropCanvas.renderAll(); | |
| // Update original image data | |
| originalImageData = newImg.toDataURL(); | |
| // Reset cropping state | |
| isCropping = false; | |
| cropRect = null; | |
| // Switch to effects tab | |
| document.querySelector('[data-tab="effects"]').click(); | |
| }); | |
| }; | |
| img.src = URL.createObjectURL(blob); | |
| }); | |
| }); | |
| // Download functionality | |
| document.getElementById('downloadPNG').addEventListener('click', function() { | |
| if (!currentImage) return; | |
| downloadImage('png'); | |
| }); | |
| document.getElementById('downloadJPG').addEventListener('click', function() { | |
| if (!currentImage) return; | |
| downloadImage('jpeg'); | |
| }); | |
| document.getElementById('downloadWEBP').addEventListener('click', function() { | |
| if (!currentImage) return; | |
| downloadImage('webp'); | |
| }); | |
| function downloadImage(format) { | |
| if (!currentImage) return; | |
| // Create a temporary canvas for download | |
| const tempCanvas = document.createElement('canvas'); | |
| tempCanvas.width = currentImage.width * currentImage.scaleX; | |
| tempCanvas.height = currentImage.height * currentImage.scaleY; | |
| const tempCtx = tempCanvas.getContext('2d'); | |
| // Draw the current image to the temp canvas | |
| canvas.getElement().toBlob(function(blob) { | |
| const img = new Image(); | |
| img.onload = function() { | |
| tempCtx.drawImage(img, 0, 0, tempCanvas.width, tempCanvas.height); | |
| // Create download link | |
| const link = document.createElement('a'); | |
| let mimeType, extension; | |
| switch(format) { | |
| case 'png': | |
| mimeType = 'image/png'; | |
| extension = 'png'; | |
| break; | |
| case 'jpeg': | |
| mimeType = 'image/jpeg'; | |
| extension = 'jpg'; | |
| break; | |
| case 'webp': | |
| mimeType = 'image/webp'; | |
| extension = 'webp'; | |
| break; | |
| default: | |
| mimeType = 'image/png'; | |
| extension = 'png'; | |
| } | |
| link.download = `stencil-image.${extension}`; | |
| link.href = tempCanvas.toDataURL(mimeType); | |
| link.click(); | |
| }; | |
| img.src = URL.createObjectURL(blob); | |
| }); | |
| } | |
| }); | |
| </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 <a href="https://enzostvs-deepsite.hf.space" style="color: #fff;" target="_blank" >DeepSite</a> <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;"></p></body> | |
| </html> |