document.addEventListener("DOMContentLoaded", () => { // DOM Elements const video = document.getElementById("camera-stream"); const canvas = document.getElementById("overlay-canvas"); const ctx = canvas.getContext("2d", { willReadFrequently: true }); const sourceImage = document.getElementById("source-image"); // Controls const opacitySlider = document.getElementById("opacity-slider"); const opacityVal = document.getElementById("opacity-val"); const scaleSlider = document.getElementById("scale-slider"); const scaleVal = document.getElementById("scale-val"); const colorPicker = document.getElementById("color-picker"); const toleranceSlider = document.getElementById("tolerance-slider"); const toleranceVal = document.getElementById("tolerance-val"); const imageUpload = document.getElementById("image-upload"); const lockBtn = document.getElementById("lock-btn"); const unlockBtn = document.getElementById("unlock-btn"); const resetBtn = document.getElementById("reset-btn"); // State let isLocked = false; let imgX = 0; let imgY = 0; let scale = 1.0; // Touch & Drag State let isDragging = false; let startDragX = 0; let startDragY = 0; let initialPinchDistance = null; let initialScale = 1.0; let initialPinchCenter = { x: 0, y: 0 }; // Original image data for processing let originalImageData = null; // --- Camera Setup --- async function setupCamera() { try { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" }, // Prefer rear camera }); video.srcObject = stream; } catch (err) { console.error("Error accessing camera:", err); alert( "Could not access the camera. Please ensure you have granted permission.", ); } } // --- Image Processing --- function hexToRgb(hex) { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16), } : { r: 255, g: 255, b: 255 }; } function processImageColor() { if (!originalImageData) return; const targetColor = hexToRgb(colorPicker.value); const tolerance = parseInt(toleranceSlider.value, 10); // Create a copy of the original data to modify const processedData = new ImageData( new Uint8ClampedArray(originalImageData.data), originalImageData.width, originalImageData.height, ); const data = processedData.data; for (let i = 0; i < data.length; i += 4) { const r = data[i]; const g = data[i + 1]; const b = data[i + 2]; // Calculate color distance (simple Manhattan distance for speed) const rDiff = Math.abs(r - targetColor.r); const gDiff = Math.abs(g - targetColor.g); const bDiff = Math.abs(b - targetColor.b); // If the pixel is within tolerance of the target color, make it transparent if (rDiff <= tolerance && gDiff <= tolerance && bDiff <= tolerance) { data[i + 3] = 0; // Set alpha to 0 } } ctx.putImageData(processedData, 0, 0); } function initImage() { // Set canvas size to match image canvas.width = sourceImage.naturalWidth; canvas.height = sourceImage.naturalHeight; // Draw initial image ctx.drawImage(sourceImage, 0, 0); // Store original data for color processing originalImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); // Center the image initially resetPosition(); // Apply initial transparency if needed processImageColor(); } // --- Interactions & Controls --- function updateTransform() { canvas.style.transform = `translate(${imgX}px, ${imgY}px) scale(${scale})`; canvas.style.opacity = opacitySlider.value; // Sync slider with scale scaleSlider.value = scale; scaleVal.textContent = scale.toFixed(1) + "x"; } function resetPosition() { scale = 1.0; // Center based on window size and canvas size imgX = (window.innerWidth - canvas.width) / 2; // Place it slightly higher than center to leave room for controls imgY = (window.innerHeight - canvas.height) / 3; updateTransform(); } // --- Multi-Touch Gesture Logic --- function getPinchDistance(touches) { const dx = touches[0].clientX - touches[1].clientX; const dy = touches[0].clientY - touches[1].clientY; return Math.sqrt(dx * dx + dy * dy); } function getPinchCenter(touches) { return { x: (touches[0].clientX + touches[1].clientX) / 2, y: (touches[0].clientY + touches[1].clientY) / 2, }; } function startDrag(e) { if (isLocked) return; e.preventDefault(); // Prevent default touch actions like scrolling if (e.touches) { if (e.touches.length === 1) { // Single finger: Pan isDragging = true; startDragX = e.touches[0].clientX - imgX; startDragY = e.touches[0].clientY - imgY; } else if (e.touches.length === 2) { // Two fingers: Zoom & Pan isDragging = true; initialPinchDistance = getPinchDistance(e.touches); initialScale = scale; const center = getPinchCenter(e.touches); startDragX = center.x - imgX; startDragY = center.y - imgY; } } else { // Mouse drag isDragging = true; startDragX = e.clientX - imgX; startDragY = e.clientY - imgY; } } function doDrag(e) { if (!isDragging || isLocked) return; e.preventDefault(); if (e.touches) { if (e.touches.length === 1) { // Single finger: Pan imgX = e.touches[0].clientX - startDragX; imgY = e.touches[0].clientY - startDragY; } else if (e.touches.length === 2) { // Two fingers: Zoom & Pan // 1. Handle Pan (using midpoint) const center = getPinchCenter(e.touches); imgX = center.x - startDragX; imgY = center.y - startDragY; // 2. Handle Zoom if (initialPinchDistance) { const currentDistance = getPinchDistance(e.touches); const distanceRatio = currentDistance / initialPinchDistance; let newScale = initialScale * distanceRatio; // Constrain scale to reasonable limits newScale = Math.max(0.1, Math.min(newScale, 5.0)); scale = newScale; } } } else { // Mouse drag imgX = e.clientX - startDragX; imgY = e.clientY - startDragY; } updateTransform(); } function endDrag(e) { if (e.touches && e.touches.length > 0) { // If one finger remains after a pinch, reset the drag starting point so it doesn't jump isDragging = true; startDragX = e.touches[0].clientX - imgX; startDragY = e.touches[0].clientY - imgY; initialPinchDistance = null; } else { isDragging = false; initialPinchDistance = null; } } // Event Listeners for Dragging & Gestures canvas.addEventListener("mousedown", startDrag); window.addEventListener("mousemove", doDrag); window.addEventListener("mouseup", endDrag); // Use passive: false so we can call e.preventDefault() to stop page scroll canvas.addEventListener("touchstart", startDrag, { passive: false }); window.addEventListener("touchmove", doDrag, { passive: false }); window.addEventListener("touchend", endDrag); window.addEventListener("touchcancel", endDrag); // Controls Listeners opacitySlider.addEventListener("input", (e) => { opacityVal.textContent = Math.round(e.target.value * 100) + "%"; canvas.style.opacity = e.target.value; }); scaleSlider.addEventListener("input", (e) => { scale = parseFloat(e.target.value); updateTransform(); }); colorPicker.addEventListener("input", processImageColor); toleranceSlider.addEventListener("input", (e) => { toleranceVal.textContent = e.target.value; processImageColor(); }); resetBtn.addEventListener("click", resetPosition); // Lock logic lockBtn.addEventListener("click", () => { isLocked = true; document.body.classList.add("locked"); lockBtn.style.display = "none"; unlockBtn.style.display = "block"; // Explicitly show the unlock button }); unlockBtn.addEventListener("click", () => { isLocked = false; document.body.classList.remove("locked"); lockBtn.style.display = "inline-block"; unlockBtn.style.display = "none"; // Hide unlock button }); imageUpload.addEventListener("change", (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { sourceImage.onload = () => { initImage(); }; sourceImage.src = event.target.result; }; reader.readAsDataURL(file); }); // --- Initialization --- // Ensure image loads (especially useful for local dev or cached images) if (sourceImage.complete) { initImage(); } else { sourceImage.onload = initImage; } setupCamera(); });