Skip to content

Instantly share code, notes, and snippets.

@mseri
Last active April 24, 2026 09:10
Show Gist options
  • Select an option

  • Save mseri/b34165f91a6a2cdfd4081f3d876f7ca6 to your computer and use it in GitHub Desktop.

Select an option

Save mseri/b34165f91a6a2cdfd4081f3d876f7ca6 to your computer and use it in GitHub Desktop.
color checker from photo
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#111;color:#f0f0f0;font-family:system-ui;padding:12px;max-width:500px;margin:0 auto}
h2{font-size:17px;margin-bottom:10px}
.btns{display:flex;gap:8px;margin-bottom:10px}
button{flex:1;padding:10px;border:none;border-radius:8px;font-size:14px;cursor:pointer;color:#fff;-webkit-appearance:none}
#btnCam{background:#2563eb}#btnFile{background:#059669}
#msg{font-size:13px;margin-bottom:8px;min-height:18px;color:#f87171}
#hint{text-align:center;padding:30px 0;color:#555;font-size:14px}
#viewWrap{display:none;width:100%;background:#000;position:relative;border-radius:10px;overflow:hidden;display:flex;align-items:center;justify-content:center}
video,#ic{display:block;max-width:100%;max-height:55vh;width:auto;height:auto}
#ov{position:absolute;top:0;left:0;pointer-events:none;border-radius:10px}
#result{margin-top:12px}
.row{display:flex;align-items:center;gap:10px;padding:8px 10px;background:#1e1e1e;border-radius:8px;margin-bottom:6px}
.name{font-size:14px;font-weight:500}
.code{font-size:12px;font-family:monospace;color:#888}
.lbl{font-size:11px;color:#666;text-transform:uppercase;letter-spacing:1px;margin:8px 0 4px}
</style>
</head>
<body>
<h2>🎨 Colour Picker</h2>
<div class="btns">
<button id="btnCam">📷 Camera</button>
<button id="btnFile">🖼 Photo</button>
<input id="fileIn" type="file" accept="image/*" style="display:none">
</div>
<div id="msg"></div>
<div id="hint">Open camera or load a photo, then tap to sample.</div>
<div id="viewWrap">
<video id="vid" playsinline muted autoplay></video>
<canvas id="ic" style="display:none"></canvas>
<canvas id="ov"></canvas>
</div>
<div id="result"></div>
<script>
const C=[
["Black",0,0,0],["White",255,255,255],["Red",255,0,0],["Lime",0,255,0],["Blue",0,0,255],
["Yellow",255,255,0],["Cyan",0,255,255],["Magenta",255,0,255],["Silver",192,192,192],
["Gray",128,128,128],["Maroon",128,0,0],["Olive",128,128,0],["Green",0,128,0],
["Purple",128,0,128],["Teal",0,128,128],["Navy",0,0,128],["Orange",255,165,0],
["Coral",255,127,80],["Salmon",250,128,114],["Gold",255,215,0],["Khaki",240,230,140],
["Crimson",220,20,60],["Tomato",255,99,71],["OrangeRed",255,69,0],["Pink",255,192,203],
["HotPink",255,105,180],["DeepPink",255,20,147],["Violet",238,130,238],["Orchid",218,112,214],
["Plum",221,160,221],["BlueViolet",138,43,226],["Indigo",75,0,130],["SlateBlue",106,90,205],
["RoyalBlue",65,105,225],["DodgerBlue",30,144,255],["SkyBlue",135,206,235],
["LightBlue",173,216,230],["SteelBlue",70,130,180],["CornflowerBlue",100,149,237],
["MidnightBlue",25,25,112],["Aquamarine",127,255,212],["Turquoise",64,224,208],
["LightSeaGreen",32,178,170],["SeaGreen",46,139,87],["SpringGreen",0,255,127],
["LawnGreen",124,252,0],["LimeGreen",50,205,50],["YellowGreen",154,205,50],
["ForestGreen",34,139,34],["DarkGreen",0,100,0],["DarkCyan",0,139,139],
["PaleGreen",152,251,152],["OliveDrab",107,142,35],["Sienna",160,82,45],
["Brown",165,42,42],["SaddleBrown",139,69,19],["Chocolate",210,105,30],
["Peru",205,133,63],["Goldenrod",218,165,32],["Tan",210,180,140],
["BurlyWood",222,184,135],["Wheat",245,222,179],["SandyBrown",244,164,96],
["RosyBrown",188,143,143],["Beige",245,245,220],["Snow",255,250,250],
["Lavender",230,230,250],["Gainsboro",220,220,220],["LightGray",211,211,211],
["DarkGray",169,169,169],["DimGray",105,105,105],["SlateGray",112,128,144],
["DarkSlateGray",47,79,79],["Charcoal",54,69,79],["Amber",255,191,0],
["Rust",183,65,14],["Burgundy",128,0,32],["Scarlet",255,36,0],
["FireBrick",178,34,34],["Raspberry",227,11,93],["Rose",255,0,127],
["Ruby",155,17,30],["Wine",114,47,55],["Mauve",224,176,255],["Lilac",200,162,200],
["Periwinkle",204,204,255],["Cobalt",0,71,171],["Jade",0,168,107],
["Emerald",80,200,120],["Mint",62,180,137],["Sage",188,184,138],
["Pistachio",147,197,114],["Avocado",86,130,3],["Ochre",204,119,34],
["Mustard",255,219,88],["Saffron",244,196,48],["Straw",228,217,111],
["Ecru",194,178,128],["Taupe",72,60,50],["Sepia",112,66,20],
["Mahogany",192,64,0],["Coffee",111,78,55],["Copper",184,115,51],
["Bronze",205,127,50],["Champagne",247,231,206],["Peach",255,203,164],
["TerraCotta",226,114,91],["TiffanyBlue",10,186,181],["Eggplant",97,64,81],
["Grape",111,45,168],["Heather",183,135,206],["Apricot",251,206,177],
["Cream",255,253,208],["Fawn",229,170,112],["MediumPurple",147,112,219],
["Chartreuse",127,255,0],["Cerulean",0,123,167],["Umber",99,81,71]
];
function dist(r,g,b,r2,g2,b2){const dr=r-r2,dg=g-g2,db=b-b2;return 2*dr*dr+4*dg*dg+3*db*db;}
function closest(r,g,b){
return C.map(c=>({name:c[0],r:c[1],g:c[2],b:c[3],d:dist(r,g,b,c[1],c[2],c[3])}))
.sort((a,b)=>a.d-b.d).slice(0,5);
}
function hex(r,g,b){return '#'+[r,g,b].map(v=>v.toString(16).padStart(2,'0')).join('');}
function avgPx(ctx,x,y,w,h){
x=Math.max(0,x|0); y=Math.max(0,y|0); w=Math.max(1,w|0); h=Math.max(1,h|0);
const d=ctx.getImageData(x,y,w,h).data;
let r=0,g=0,b=0,n=d.length/4;
for(let i=0;i<d.length;i+=4){r+=d[i];g+=d[i+1];b+=d[i+2];}
return[r/n|0,g/n|0,b/n|0];
}
let mode="idle", stream=null;
const vid=document.getElementById("vid");
const ic=document.getElementById("ic");
const ov=document.getElementById("ov");
const wrap=document.getElementById("viewWrap");
const msg=document.getElementById("msg");
const resultEl=document.getElementById("result");
function setMsg(t,isErr){msg.textContent=t;msg.style.color=isErr?"#f87171":"#6ee7b7";}
function stopCam(){if(stream){stream.getTracks().forEach(t=>t.stop());stream=null;}}
function showWrap(){
wrap.style.display="block";
document.getElementById("hint").style.display="none";
// Size overlay after layout settles
requestAnimationFrame(()=>requestAnimationFrame(sizeOv));
}
function sizeOv(){
const src=mode==="camera"?vid:ic;
const w=src.offsetWidth, h=src.offsetHeight;
if(!w||!h)return;
ov.width=w; ov.height=h;
ov.style.width=w+"px"; ov.style.height=h+"px";
ov.style.top="0"; ov.style.left="0";
}
document.getElementById("btnCam").onclick=async()=>{
stopCam(); resultEl.innerHTML=""; setMsg("Requesting camera…",false);
try{
stream=await navigator.mediaDevices.getUserMedia({video:{facingMode:{ideal:"environment"},width:{ideal:1280}}});
vid.srcObject=stream;
vid.style.display="block"; ic.style.display="none";
mode="camera"; showWrap();
vid.addEventListener("playing",()=>{sizeOv();setMsg("Tap anywhere to sample",false);},{once:true});
}catch(e){setMsg("Camera error: "+e.message,true);}
};
document.getElementById("btnFile").onclick=()=>document.getElementById("fileIn").click();
document.getElementById("fileIn").onchange=e=>{
const f=e.target.files[0]; if(!f)return;
stopCam(); resultEl.innerHTML="";
const url=URL.createObjectURL(f);
const img=new Image();
img.onload=()=>{
const mw=Math.min(460,window.innerWidth-24);
const mh=window.innerHeight*0.55;
const scale=Math.min(mw/img.width,mh/img.height,1);
ic.width=img.width*scale|0; ic.height=img.height*scale|0;
ic.style.width=""; ic.style.height=""; // let CSS max-width/max-height control display size
ic.getContext("2d").drawImage(img,0,0,ic.width,ic.height);
vid.style.display="none"; ic.style.display="block";
mode="image"; showWrap();
setMsg("Tap anywhere to sample",false);
};
img.onerror=()=>setMsg("Failed to load image",true);
img.src=url;
};
function drawMarker(cx,cy){
sizeOv();
const ctx=ov.getContext("2d");
ctx.clearRect(0,0,ov.width,ov.height);
const r=12, lw=2;
ctx.strokeStyle="#fff"; ctx.lineWidth=lw; ctx.setLineDash([]);
ctx.strokeRect(cx-r,cy-r,r*2,r*2);
ctx.beginPath();
[[cx-r-6,cy,cx-r,cy],[cx+r,cy,cx+r+6,cy],
[cx,cy-r-6,cx,cy-r],[cx,cy+r,cx,cy+r+6]].forEach(([x1,y1,x2,y2])=>{
ctx.moveTo(x1,y1); ctx.lineTo(x2,y2);
});
ctx.stroke();
}
function renderResult(r,g,b){
const h=hex(r,g,b), cols=closest(r,g,b);
let html=`<div class="lbl">Sampled</div>
<div class="row">
<div style="width:38px;height:38px;border-radius:7px;border:1px solid #444;background:${h};flex-shrink:0"></div>
<div><div class="name">${cols[0].name}</div><div class="code">${h} · rgb(${r},${g},${b})</div></div>
</div><div class="lbl">5 closest</div>`;
cols.forEach(c=>{
const ch=hex(c.r,c.g,c.b);
html+=`<div class="row">
<div style="width:30px;height:30px;border-radius:5px;border:1px solid #444;background:${ch};flex-shrink:0"></div>
<div><div class="name">${c.name}</div><div class="code">${ch} · rgb(${c.r},${c.g},${c.b})</div></div>
</div>`;
});
resultEl.innerHTML=html;
}
function onTap(e){
if(mode==="idle")return;
e.preventDefault(); e.stopPropagation();
sizeOv();
const rect=wrap.getBoundingClientRect();
const touch=e.changedTouches?e.changedTouches[0]:(e.touches?e.touches[0]:e);
const cx=touch.clientX-rect.left;
const cy=touch.clientY-rect.top;
drawMarker(cx,cy);
const BOX=10;
let r,g,b;
if(mode==="image"){
[r,g,b]=avgPx(ic.getContext("2d"),cx-BOX,cy-BOX,BOX*2,BOX*2);
} else {
const vw=vid.videoWidth||1, vh=vid.videoHeight||1;
const dw=vid.offsetWidth||1, dh=vid.offsetHeight||1;
const tmp=document.createElement("canvas");
tmp.width=vw; tmp.height=vh;
tmp.getContext("2d").drawImage(vid,0,0);
[r,g,b]=avgPx(tmp.getContext("2d"),(cx-BOX)*vw/dw,(cy-BOX)*vh/dh,BOX*2*vw/dw,BOX*2*vh/dh);
}
renderResult(r,g,b);
}
// Attach to the wrap div — covers both video and image canvas
wrap.addEventListener("touchend", onTap, {passive:false});
wrap.addEventListener("click", onTap);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment