OverlayChecker / index.html
YaroslavLinder's picture
Update index.html
c92993c verified
Raw
History Blame Contribute Delete
13 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stencil Overlay Tool</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Checkerboard background for transparency visualization */
.checkerboard {
background-color: #e5e5f7;
background-image: repeating-linear-gradient(45deg, #d5d5f2 25%, transparent 25%, transparent 75%, #d5d5f2 75%, #d5d5f2), repeating-linear-gradient(45deg, #d5d5f2 25%, #e5e5f7 25%, #e5e5f7 75%, #d5d5f2 75%, #d5d5f2);
background-position: 0 0, 10px 10px;
background-size: 20px 20px;
}
</style>
</head>
<body class="bg-gray-100 min-h-screen p-8 text-gray-800">
<div class="max-w-5xl mx-auto space-y-6">
<!-- Header -->
<div class="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<h1 class="text-2xl font-bold mb-2">Stencil Overlay Composer</h1>
<p class="text-sm text-gray-600">Upload a background image and a stencil. The stencil will have its white background removed using luminance-to-alpha conversion.</p>
</div>
<!-- Controls -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Uploads -->
<div class="bg-white p-6 rounded-xl shadow-sm border border-gray-200 space-y-4">
<h2 class="font-semibold text-lg border-b pb-2">1. Upload Images</h2>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Base Image (Background)</label>
<input type="file" id="baseInput" accept="image/*" class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Stencil Image (Overlay)</label>
<input type="file" id="stencilInput" accept="image/*" class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100"/>
</div>
</div>
<!-- Settings -->
<div class="bg-white p-6 rounded-xl shadow-sm border border-gray-200 space-y-4">
<h2 class="font-semibold text-lg border-b pb-2">2. Adjustments</h2>
<div>
<div class="flex justify-between">
<label for="color" class="block text-sm font-medium text-gray-700">Overlay Color</label>
<span id="colorVal" class="text-sm text-gray-500">#ff0000</span>
</div>
<input type="color" id="color" value="#ff0000" class="w-full h-8 mt-1 rounded cursor-pointer">
</div>
<div>
<div class="flex justify-between">
<label for="gamma" class="block text-sm font-medium text-gray-700">Gamma (Line Thickness)</label>
<span id="gammaVal" class="text-sm text-gray-500">0.9</span>
</div>
<input type="range" id="gamma" min="0.1" max="3.0" step="0.1" value="0.9" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<p class="text-xs text-gray-400 mt-1">&lt; 1 thickens lines, &gt; 1 thins them.</p>
</div>
<div>
<div class="flex justify-between">
<label for="opacity" class="block text-sm font-medium text-gray-700">Stencil Global Opacity</label>
<span id="opacityVal" class="text-sm text-gray-500">100%</span>
</div>
<input type="range" id="opacity" min="0" max="100" step="1" value="100" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
</div>
</div>
</div>
<!-- Preview Area -->
<div class="bg-white p-6 rounded-xl shadow-sm border border-gray-200 flex flex-col items-center">
<h2 class="font-semibold text-lg border-b pb-4 w-full mb-4">Result</h2>
<!-- Dimension Info & Warning -->
<div id="imageInfo" class="hidden w-full flex-col items-center mb-4 text-sm bg-gray-50 p-3 rounded-lg border border-gray-200">
<div class="flex space-x-6 text-gray-600">
<p>Background: <span id="bgDims" class="font-mono font-medium">N/A</span></p>
<p>Stencil: <span id="stencilDims" class="font-mono font-medium">N/A</span></p>
</div>
<p id="dimWarning" class="hidden mt-2 text-red-600 font-bold bg-red-50 px-3 py-1 rounded border border-red-200">
⚠️ Warning: Image dimensions do not match. The stencil will be stretched.
</p>
</div>
<div id="canvasContainer" class="checkerboard rounded shadow-inner overflow-hidden max-w-full">
<canvas id="mainCanvas" class="max-w-full h-auto"></canvas>
</div>
<div id="placeholder" class="py-12 text-gray-400 italic">
Upload both images to see the result
</div>
<button id="downloadBtn" class="mt-4 hidden bg-gray-800 text-white px-6 py-2 rounded-lg hover:bg-black transition-colors">
Download Result
</button>
</div>
</div>
<script>
// State
const state = {
baseImage: null,
stencilImage: null,
gamma: 0.9,
opacity: 1.0,
color: '#ff0000'
};
// DOM Elements
const baseInput = document.getElementById('baseInput');
const stencilInput = document.getElementById('stencilInput');
const colorPicker = document.getElementById('color');
const colorVal = document.getElementById('colorVal');
const gammaSlider = document.getElementById('gamma');
const gammaVal = document.getElementById('gammaVal');
const opacitySlider = document.getElementById('opacity');
const opacityVal = document.getElementById('opacityVal');
const canvas = document.getElementById('mainCanvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const downloadBtn = document.getElementById('downloadBtn');
const placeholder = document.getElementById('placeholder');
const canvasContainer = document.getElementById('canvasContainer');
// New DOM Elements for Dimensions
const imageInfo = document.getElementById('imageInfo');
const bgDims = document.getElementById('bgDims');
const stencilDims = document.getElementById('stencilDims');
const dimWarning = document.getElementById('dimWarning');
// Event Listeners
baseInput.addEventListener('change', (e) => handleImageUpload(e, 'baseImage'));
stencilInput.addEventListener('change', (e) => handleImageUpload(e, 'stencilImage'));
colorPicker.addEventListener('input', (e) => {
state.color = e.target.value;
colorVal.textContent = state.color;
render();
});
gammaSlider.addEventListener('input', (e) => {
state.gamma = parseFloat(e.target.value);
gammaVal.textContent = state.gamma;
render();
});
opacitySlider.addEventListener('input', (e) => {
state.opacity = parseInt(e.target.value) / 100;
opacityVal.textContent = e.target.value + '%';
render();
});
downloadBtn.addEventListener('click', () => {
const link = document.createElement('a');
link.download = 'stencil-overlay.png';
link.href = canvas.toDataURL();
link.click();
});
// Image Loading Logic
function handleImageUpload(event, key) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
state[key] = img;
render();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
// Helper to convert Hex to RGB
function hexToRgb(hex) {
const bigint = parseInt(hex.slice(1), 16);
return {
r: (bigint >> 16) & 255,
g: (bigint >> 8) & 255,
b: bigint & 255
};
}
// Logic Ported from the provided Node.js snippet
function processStencil(img, width, height, gamma, hexColor) {
// Create a temporary canvas to read pixel data
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d');
// Draw the raw stencil image
tempCtx.drawImage(img, 0, 0, width, height);
// Get raw pixel data
const imageData = tempCtx.getImageData(0, 0, width, height);
const data = imageData.data;
// Get target RGB values
const targetColor = hexToRgb(hexColor);
// --- CORE ALGORITHM START ---
// Adapted from sharp logic to Canvas API
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const a = data[i + 3];
// Calculate luminance (perceived brightness)
const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
// Convert luminance to alpha: bright = transparent, dark = opaque
// Gamma curve allows fine-tuning
const alphaFactor = 1 - Math.pow(luminance, gamma);
const alpha = Math.round(alphaFactor * (a / 255) * 255);
// Apply the target RGB color instead of clearing it to black
data[i] = targetColor.r;
data[i + 1] = targetColor.g;
data[i + 2] = targetColor.b;
data[i + 3] = alpha;
}
// --- CORE ALGORITHM END ---
tempCtx.putImageData(imageData, 0, 0);
return tempCanvas;
}
// Main Render Loop
function render() {
// Update Dimension Text & Validation
if (state.baseImage || state.stencilImage) {
imageInfo.classList.remove('hidden');
imageInfo.classList.add('flex');
if (state.baseImage) bgDims.textContent = `${state.baseImage.width}x${state.baseImage.height}px`;
if (state.stencilImage) stencilDims.textContent = `${state.stencilImage.width}x${state.stencilImage.height}px`;
// Check for mismatch
if (state.baseImage && state.stencilImage) {
if (state.baseImage.width !== state.stencilImage.width || state.baseImage.height !== state.stencilImage.height) {
dimWarning.classList.remove('hidden');
} else {
dimWarning.classList.add('hidden');
}
}
}
if (!state.baseImage) return;
// Hide placeholder, show canvas/button
placeholder.style.display = 'none';
canvas.style.display = 'block';
// Set canvas size to match base image
canvas.width = state.baseImage.width;
canvas.height = state.baseImage.height;
// 1. Draw Base Image
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(state.baseImage, 0, 0);
if (state.stencilImage) {
// 2. Process Stencil (Remove Background)
const processedStencil = processStencil(
state.stencilImage,
canvas.width,
canvas.height,
state.gamma,
state.color
);
// 3. Draw Processed Stencil Overlay
ctx.globalAlpha = state.opacity;
ctx.drawImage(processedStencil, 0, 0);
ctx.globalAlpha = 1.0; // Reset
downloadBtn.classList.remove('hidden');
}
}
</script>
</body>
</html>