stencilbuddy / index.html
Hrm. Kmdalton
Add 2 files
dc20b9e verified
<!DOCTYPE html>
<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>