ColourChecker / app.js
angeluget's picture
Deploy color palette checker app
beaca28 verified
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();