const softAutumn = [ { name: "Moss", hex: "#7A8450" }, { name: "Olive", hex: "#6F6B3C" }, { name: "Sage", hex: "#8A9273" }, { name: "Camel", hex: "#B08A63" }, { name: "Terracotta", hex: "#B66545" }, { name: "Rust", hex: "#9A4F2F" }, { name: "Peach", hex: "#D7A47E" }, { name: "Dusty Coral", hex: "#C9826D" }, { name: "Warm Taupe", hex: "#8E7360" }, { name: "Mushroom", hex: "#A38B78" }, { name: "Warm Navy", hex: "#3E4D58" }, { name: "Soft Teal", hex: "#5F7E78" } ]; const video = document.getElementById("video"); const startBtn = document.getElementById("startBtn"); const scanBtn = document.getElementById("scanBtn"); const statusEl = document.getElementById("status"); const sampleSwatch = document.getElementById("sampleSwatch"); const sampleHexEl = document.getElementById("sampleHex"); const paletteGrid = document.getElementById("paletteGrid"); const captureCanvas = document.getElementById("captureCanvas"); const ctx = captureCanvas.getContext("2d", { willReadFrequently: true }); let stream; function hexToRgb(hex) { const clean = hex.replace("#", ""); return { r: parseInt(clean.slice(0, 2), 16), g: parseInt(clean.slice(2, 4), 16), b: parseInt(clean.slice(4, 6), 16) }; } function rgbToHex({ r, g, b }) { return ( "#" + [r, g, b] .map((value) => value.toString(16).padStart(2, "0")) .join("") .toUpperCase() ); } function colorDistance(a, b) { return Math.sqrt( 2 * (a.r - b.r) ** 2 + 4 * (a.g - b.g) ** 2 + 3 * (a.b - b.b) ** 2 ); } function averageCenterColor() { const w = captureCanvas.width; const h = captureCanvas.height; ctx.drawImage(video, 0, 0, w, h); const data = ctx.getImageData(0, 0, w, h).data; let r = 0; let g = 0; let b = 0; let n = 0; for (let y = 10; y < 30; y++) { for (let x = 10; x < 30; x++) { const i = (y * w + x) * 4; r += data[i]; g += data[i + 1]; b += data[i + 2]; n += 1; } } return { r: Math.round(r / n), g: Math.round(g / n), b: Math.round(b / n) }; } function renderPalette() { paletteGrid.innerHTML = ""; for (const color of softAutumn) { const chip = document.createElement("div"); chip.className = "chip"; chip.innerHTML = `
${color.name}`; paletteGrid.appendChild(chip); } } startBtn.addEventListener("click", async () => { try { stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: "environment" } }, audio: false }); video.srcObject = stream; scanBtn.disabled = false; statusEl.textContent = "Status: camera ready."; } catch (error) { statusEl.textContent = `Status: camera error: ${error.message}`; } }); scanBtn.addEventListener("click", () => { if (!video.videoWidth) { statusEl.textContent = "Status: camera is not ready yet."; return; } const sample = averageCenterColor(); const sampleHex = rgbToHex(sample); let closest; for (const paletteColor of softAutumn) { const distance = colorDistance(sample, hexToRgb(paletteColor.hex)); if (!closest || distance < closest.distance) { closest = { ...paletteColor, distance }; } } const threshold = 85; const isMatch = closest.distance <= threshold; sampleSwatch.style.background = sampleHex; sampleHexEl.textContent = sampleHex; statusEl.innerHTML = isMatch ? `Status: Match YES. Closest: ${closest.name} ${closest.hex} (distance ${closest.distance.toFixed(1)})` : `Status: Match NO. Closest: ${closest.name} ${closest.hex} (distance ${closest.distance.toFixed(1)})`; }); renderPalette();