← back to Dw Photo Capture
Fix grid density slider + editor preview-equals-export bake
89c56863adc158b0f68e9139a85c857525883bf9 · 2026-06-24 17:03:30 -0700 · Steve Abrams
- Density slider: set grid-template-columns directly on <main> as repeat(N,1fr)
via applyDensity(), removing reliance on var() inside repeat() (older iOS
Safari ignores that); add change-event backstop; apply on restore.
- Editor bake: clamp after each filter stage (brightness/contrast/saturation/
hue) to match CSS/SVG semantics so the saved JPEG equals the on-screen preview
(was drifting up to 67/255 on saturated pixels).
- FLAT preview now renders from bakeAdjust() pixels instead of a CSS element
filter + soft-light cast, so preview === export for the color-wheel path too
(was diverging up to 62/255). Removed now-dead curFilter().
- openEditor: add img.onerror toast so a failed Shopify proxy-image load no
longer hangs silently.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M data/build.jsonM public/index.html
Diff
commit 89c56863adc158b0f68e9139a85c857525883bf9
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jun 24 17:03:30 2026 -0700
Fix grid density slider + editor preview-equals-export bake
- Density slider: set grid-template-columns directly on <main> as repeat(N,1fr)
via applyDensity(), removing reliance on var() inside repeat() (older iOS
Safari ignores that); add change-event backstop; apply on restore.
- Editor bake: clamp after each filter stage (brightness/contrast/saturation/
hue) to match CSS/SVG semantics so the saved JPEG equals the on-screen preview
(was drifting up to 67/255 on saturated pixels).
- FLAT preview now renders from bakeAdjust() pixels instead of a CSS element
filter + soft-light cast, so preview === export for the color-wheel path too
(was diverging up to 62/255). Removed now-dead curFilter().
- openEditor: add img.onerror toast so a failed Shopify proxy-image load no
longer hangs silently.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
data/build.json | 8 ++++++--
public/index.html | 38 +++++++++++++++++++++++++-------------
2 files changed, 31 insertions(+), 15 deletions(-)
diff --git a/data/build.json b/data/build.json
index 85c4d36..99abb3d 100644
--- a/data/build.json
+++ b/data/build.json
@@ -1,8 +1,12 @@
{
- "next": 5,
+ "next": 9,
"map": {
"578af86f": 2,
"bc95fdb0": 3,
- "3a3b2a43": 4
+ "3a3b2a43": 4,
+ "deffa396": 5,
+ "468831e8": 6,
+ "cd2b7577": 7,
+ "e342dc0a": 8
}
}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index c600f7a..3a5c7be 100644
--- a/public/index.html
+++ b/public/index.html
@@ -143,8 +143,17 @@ const $=s=>document.querySelector(s), grid=$('#grid');
let ITEMS=[], filter='need';
const LS=k=>localStorage.getItem('dwphoto_'+k), setLS=(k,v)=>localStorage.setItem('dwphoto_'+k,v);
+// Density: set grid-template-columns DIRECTLY (older iOS Safari ignores var() as
+// the count inside repeat() — writing the resolved repeat(N,1fr) works everywhere).
+function applyDensity(n){
+ const c=Math.max(1,Math.min(4,parseInt(n,10)||1));
+ document.documentElement.style.setProperty('--cols',c);
+ $('#grid').style.gridTemplateColumns='repeat('+c+',1fr)';
+}
+
// restore prefs
-if(LS('dens')){$('#dens').value=LS('dens');document.documentElement.style.setProperty('--cols',LS('dens'));}
+if(LS('dens')){$('#dens').value=LS('dens');applyDensity(LS('dens'));}
+else applyDensity($('#dens').value);
if(LS('sort'))$('#sort').value=LS('sort');
if(LS('filter')){filter=LS('filter');document.querySelectorAll('.chip').forEach(c=>c.classList.toggle('on',c.dataset.f===filter));}
@@ -253,6 +262,7 @@ function openEditor(x,dataUrl){
syncLabels(); edRedraw();
$('#editor').hidden=false; document.body.style.overflow='hidden';
};
+ img.onerror=()=>toast('Could not load image to edit');
img.src=dataUrl;
}
function closeEditor(){ $('#editor').hidden=true; document.body.style.overflow=''; ES=null; }
@@ -272,7 +282,6 @@ function _hsl2rgb(h,s,l){ // h 0..1
const f=t=>{t=(t+1)%1; return t<1/6?p+(q-p)*6*t:t<1/2?q:t<2/3?p+(q-p)*(2/3-t)*6:p;};
return [f(h+1/3)*255,f(h)*255,f(h-1/3)*255];
}
-function curFilter(){ return `brightness(${+$('#edB').value}) contrast(${+$('#edC').value}) saturate(${+$('#edS').value}) hue-rotate(${+$('#edH').value}deg)`; }
// Bake the slider adjustments into real pixels (reliable on every device — no ctx.filter).
function bakeAdjust(src,w,h){
const c=document.createElement('canvas'); c.width=w; c.height=h;
@@ -285,15 +294,17 @@ function bakeAdjust(src,w,h){
0.213-cos*0.213-sn*0.787,0.715-cos*0.715+sn*0.715,0.072+cos*0.928+sn*0.072];
const castOn=ES&&ES.ts>0.04; let cr=0,cg=0,cb=0,ca=0;
if(castOn){ const t=_hsl2rgb((ES.th||0)/360,1,.5); cr=t[0];cg=t[1];cb=t[2]; ca=ES.ts*0.6; }
+ // Clamp after EACH stage to match CSS/SVG filter semantics — otherwise inter-stage
+ // overshoot (>255) gets pulled back differently and the export drifts from the preview.
for(let i=0;i<d.length;i+=4){
let r=d[i],g=d[i+1],bl=d[i+2];
- r*=b;g*=b;bl*=b; // brightness
- r=(r-128)*con+128;g=(g-128)*con+128;bl=(bl-128)*con+128; // contrast
- const L=0.2126*r+0.7152*g+0.0722*bl; // saturation
- r=L+(r-L)*sat;g=L+(g-L)*sat;bl=L+(bl-L)*sat;
- const nr=r*m[0]+g*m[1]+bl*m[2],ng=r*m[3]+g*m[4]+bl*m[5],nb=r*m[6]+g*m[7]+bl*m[8]; // hue
+ r=_cl(r*b);g=_cl(g*b);bl=_cl(bl*b); // brightness
+ r=_cl((r-128)*con+128);g=_cl((g-128)*con+128);bl=_cl((bl-128)*con+128); // contrast
+ const L=0.2126*r+0.7152*g+0.0722*bl; // saturation
+ r=_cl(L+(r-L)*sat);g=_cl(L+(g-L)*sat);bl=_cl(L+(bl-L)*sat);
+ const nr=_cl(r*m[0]+g*m[1]+bl*m[2]),ng=_cl(r*m[3]+g*m[4]+bl*m[5]),nb=_cl(r*m[6]+g*m[7]+bl*m[8]); // hue
r=nr;g=ng;bl=nb;
- if(castOn){ r=r*(1-ca)+cr*ca;g=g*(1-ca)+cg*ca;bl=bl*(1-ca)+cb*ca; } // color cast
+ if(castOn){ r=r*(1-ca)+cr*ca;g=g*(1-ca)+cg*ca;bl=bl*(1-ca)+cb*ca; } // color cast
d[i]=_cl(r);d[i+1]=_cl(g);d[i+2]=_cl(bl);
}
ctx.putImageData(id,0,0); return c;
@@ -308,11 +319,12 @@ function edRedraw(){
ctx.clearRect(0,0,cv.width,cv.height);
(ES.rollMode==='tube'?renderRoll:renderSheet)(fc,ctx,cv.width,cv.height);
} else {
- // FLAT: live preview via CSS filter (instant + iOS-reliable); cast painted in pixels
- cv.style.filter=curFilter();
+ // FLAT: preview from the SAME baked pixels we export, so what you see IS what saves
+ // (bakeAdjust is manual getImageData math — iOS-reliable, unlike ctx.filter).
+ cv.style.filter='none';
+ const fc=bakeAdjust(ES.img,ES.img.width,ES.img.height);
ctx.clearRect(0,0,cv.width,cv.height);
- ctx.drawImage(ES.img,0,0);
- if(ES.ts>0.04){ ctx.save(); ctx.globalCompositeOperation='soft-light'; ctx.globalAlpha=ES.ts*0.6; ctx.fillStyle=`hsl(${ES.th},100%,50%)`; ctx.fillRect(0,0,cv.width,cv.height); ctx.restore(); }
+ ctx.drawImage(fc,0,0);
}
}
@@ -443,7 +455,7 @@ async function skip(x){await fetch('/api/skip',{method:'POST',headers:{'Content-
$('#q').addEventListener('input',render);
$('#sort').addEventListener('change',e=>{setLS('sort',e.target.value);render();});
-$('#dens').addEventListener('input',e=>{document.documentElement.style.setProperty('--cols',e.target.value);setLS('dens',e.target.value);});
+['input','change'].forEach(ev=>$('#dens').addEventListener(ev,e=>{applyDensity(e.target.value);setLS('dens',e.target.value);}));
document.querySelectorAll('.chip').forEach(c=>c.addEventListener('click',()=>{
document.querySelectorAll('.chip').forEach(z=>z.classList.remove('on'));c.classList.add('on');filter=c.dataset.f;setLS('filter',filter);render();}));
load(); setInterval(load, 60000);
← daf466a auto-save: 2026-06-24T16:58:22 (3 files) — public/index.html
·
back to Dw Photo Capture
·
Add 'Any SKU' lookup: search all 555 Fentucci products by SK a2403b9 →