Last active
April 24, 2026 09:10
-
-
Save mseri/b34165f91a6a2cdfd4081f3d876f7ca6 to your computer and use it in GitHub Desktop.
color checker from photo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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