Spaces:
Running
Running
| 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 = `<div class="chip-color" style="background:${color.hex}"></div>${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 | |
| ? `<span class="ok">Status: Match YES.</span> Closest: ${closest.name} ${closest.hex} (distance ${closest.distance.toFixed(1)})` | |
| : `<span class="bad">Status: Match NO.</span> Closest: ${closest.name} ${closest.hex} (distance ${closest.distance.toFixed(1)})`; | |
| }); | |
| renderPalette(); | |