[object Object]

← 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

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 →