[object Object]

← back to Dw Photo Capture

Add πŸ—ž Roll button: wrap captured pattern around a tube (cylinder shading + sheen + shadow) like the PR roll shot

b6eb1626f4ede1b0078573ef31f86d8b7f225208 Β· 2026-06-24 16:08:12 -0700 Β· Steve Abrams

Files touched

Diff

commit b6eb1626f4ede1b0078573ef31f86d8b7f225208
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jun 24 16:08:12 2026 -0700

    Add πŸ—ž Roll button: wrap captured pattern around a tube (cylinder shading + sheen + shadow) like the PR roll shot
---
 public/index.html | 78 +++++++++++++++++++++++++++++++++++++++++++------------
 1 file changed, 62 insertions(+), 16 deletions(-)

diff --git a/public/index.html b/public/index.html
index 0447db3..3ed293a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -4,8 +4,8 @@
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, maximum-scale=1">
 <meta name="apple-mobile-web-app-capable" content="yes">
-<meta name="apple-mobile-web-app-title" content="DW Photos">
-<title>DW Pattern Photo Capture</title>
+<meta name="apple-mobile-web-app-title" content="DW SKU Photos">
+<title>Mobile DW SKU Photos</title>
 <style>
   :root{ --cols: 1; --bg:#0f0e0c; --card:#1b1916; --ink:#f3efe7; --muted:#9a9184; --line:#2e2a25;
          --gold:#c8a24a; --green:#3fa06a; --red:#c0563f; }
@@ -60,6 +60,8 @@
   .ed-head{display:flex;align-items:center;gap:10px;margin-bottom:10px}
   .ed-head .t{font:600 13px/1 "SF Pro Display",sans-serif;letter-spacing:.05em;text-transform:uppercase;color:var(--gold)}
   .ed-head .s{font:600 12px/1 ui-monospace,Menlo,monospace;color:var(--muted)}
+  .roll-toggle{margin-left:auto;background:#16140f;border:1px solid var(--line);color:var(--ink);border-radius:99px;padding:7px 14px;font-size:13px;font-weight:600;cursor:pointer}
+  .roll-toggle.on{background:var(--gold);color:#1b1407;border-color:var(--gold)}
   .ed-grid{display:grid;grid-template-columns:auto 1fr;gap:14px;align-items:start}
   .ed-wheel-wrap{display:flex;flex-direction:column;align-items:center;gap:6px}
   .wheel-box{position:relative;width:128px;height:128px;touch-action:none}
@@ -81,7 +83,7 @@
 </head>
 <body>
 <header>
-  <h1>DW Pattern Photo <span>Capture</span></h1>
+  <h1>Mobile DW SKU <span>Photos</span></h1>
   <div class="bar"><i id="prog"></i></div>
   <div class="meta"><span id="count">…</span><span id="remain"></span></div>
   <div class="controls">
@@ -109,7 +111,7 @@
 <div class="editor" id="editor" hidden>
   <div class="ed-stage"><canvas id="edcanvas"></canvas></div>
   <div class="ed-panel">
-    <div class="ed-head"><span class="t">Enhance</span><span class="s" id="ed-sku"></span></div>
+    <div class="ed-head"><span class="t">Enhance</span><span class="s" id="ed-sku"></span><button class="roll-toggle" id="edRoll">πŸ—ž Roll</button></div>
     <div class="ed-grid">
       <div class="ed-wheel-wrap">
         <div class="wheel-box" id="wheelBox">
@@ -230,12 +232,14 @@ const WHEEL_R=64; // half of 128
 function openEditor(x,dataUrl){
   const img=new Image();
   img.onload=()=>{
-    ES={x,img,b:1,c:1,s:1,h:0,th:0,ts:0};
+    const flat=document.createElement('canvas'); flat.width=img.width; flat.height=img.height;
+    ES={x,img,flat,b:1,c:1,s:1,h:0,th:0,ts:0,roll:false};
     const cv=$('#edcanvas'); cv.width=img.width; cv.height=img.height;
     $('#ed-sku').textContent=x.dw_sku||'';
     // reset controls
     $('#edB').value=1; $('#edC').value=1; $('#edS').value=1; $('#edH').value=0;
     setThumb(0,0); $('#wheelVal').textContent='neutral';
+    $('#edRoll').classList.remove('on');
     syncLabels(); edRedraw();
     $('#editor').hidden=false; document.body.style.overflow='hidden';
   };
@@ -253,19 +257,56 @@ function signPct(v){const p=Math.round((+v-1)*100); return (p>0?'+':'')+p;}
 
 function scheduleRedraw(){ if(edRAF)return; edRAF=requestAnimationFrame(()=>{edRAF=0;edRedraw();}); }
 function edRedraw(){
-  if(!ES)return; const cv=$('#edcanvas'), ctx=cv.getContext('2d');
+  if(!ES)return;
+  // 1) render the enhanced FLAT image to the offscreen canvas
+  const fc=ES.flat, fx=fc.getContext('2d');
   ES.b=+$('#edB').value; ES.c=+$('#edC').value; ES.s=+$('#edS').value; ES.h=+$('#edH').value;
-  ctx.clearRect(0,0,cv.width,cv.height);
-  ctx.filter=`brightness(${ES.b}) contrast(${ES.c}) saturate(${ES.s}) hue-rotate(${ES.h}deg)`;
-  ctx.drawImage(ES.img,0,0);
-  ctx.filter='none';
+  fx.clearRect(0,0,fc.width,fc.height);
+  fx.filter=`brightness(${ES.b}) contrast(${ES.c}) saturate(${ES.s}) hue-rotate(${ES.h}deg)`;
+  fx.drawImage(ES.img,0,0);
+  fx.filter='none';
   if(ES.ts>0.04){ // color-balance tint (Photoshop-style soft-light cast)
-    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.globalAlpha=1; ctx.globalCompositeOperation='source-over';
+    fx.globalCompositeOperation='soft-light';
+    fx.globalAlpha=ES.ts*0.6; fx.fillStyle=`hsl(${ES.th},100%,50%)`;
+    fx.fillRect(0,0,fc.width,fc.height);
+    fx.globalAlpha=1; fx.globalCompositeOperation='source-over';
+  }
+  // 2) blit to the visible canvas β€” flat, or wrapped around a tube
+  const cv=$('#edcanvas'), ctx=cv.getContext('2d');
+  ctx.clearRect(0,0,cv.width,cv.height);
+  if(ES.roll) renderRoll(fc, ctx, cv.width, cv.height);
+  else ctx.drawImage(fc,0,0);
+}
+
+// Wrap the flat texture around a vertical cylinder ("roll around a tube,
+// front-to-back") with Lambertian shading + sheen + contact shadow on a dark ground.
+function renderRoll(src, o, W, H){
+  o.fillStyle='#0c0b09'; o.fillRect(0,0,W,H);          // dark studio ground
+  const cx=W/2, R=W*0.40, cov=Math.PI*0.92;            // radius + visible wrap (~166Β°)
+  const topPad=H*0.045, botPad=H*0.11, hh=H-topPad-botPad;
+  // soft contact shadow beneath the roll
+  o.save(); o.globalAlpha=.45; o.fillStyle='#000';
+  o.beginPath(); o.ellipse(cx, H-botPad*0.55, R*0.96, botPad*0.5, 0, 0, 7); o.fill(); o.restore();
+  const N=Math.min(W, 640);
+  for(let i=0;i<N;i++){
+    const sx=(i/(N-1)-0.5)*2*R;                         // screen offset from centre, βˆ’R..R
+    const cl=Math.max(-0.999,Math.min(0.999,sx/R));
+    const th=Math.asin(cl);                             // angle on the cylinder
+    const u=th/cov+0.5; if(u<0||u>1) continue;          // map to source column
+    const screenX=cx+sx, stripW=(2*R)/N+1.4;
+    o.drawImage(src, u*(src.width-1),0,1,src.height, screenX, topPad, stripW, hh);
+    const shade=Math.pow(Math.cos(th),0.7);             // bright at front β†’ dark at the curl-away edges
+    o.globalCompositeOperation='multiply';
+    o.fillStyle='rgba(0,0,0,'+(1-shade).toFixed(3)+')';
+    o.fillRect(screenX, topPad, stripW, hh);
+    o.globalCompositeOperation='source-over';
   }
+  // specular sheen down the front-left quarter
+  const sg=o.createLinearGradient(cx-R,0,cx+R,0);
+  sg.addColorStop(0,'rgba(255,255,255,0)'); sg.addColorStop(.40,'rgba(255,255,255,.18)');
+  sg.addColorStop(.52,'rgba(255,255,255,.04)'); sg.addColorStop(1,'rgba(255,255,255,0)');
+  o.globalCompositeOperation='soft-light'; o.fillStyle=sg; o.fillRect(cx-R,topPad,2*R,hh);
+  o.globalCompositeOperation='source-over';
 }
 
 // paint the color wheel once (white center β†’ full hue at rim)
@@ -299,9 +340,14 @@ wb.addEventListener('pointerdown',e=>{wb.setPointerCapture(e.pointerId);wheelPic
 wb.addEventListener('pointermove',e=>{if(e.buttons||e.pressure)wheelPick(e);});
 
 ['edB','edC','edS','edH'].forEach(id=>$('#'+id).addEventListener('input',()=>{syncLabels();scheduleRedraw();}));
+$('#edRoll').addEventListener('click',()=>{
+  if(!ES)return; ES.roll=!ES.roll;
+  $('#edRoll').classList.toggle('on',ES.roll); toast(ES.roll?'Rolled around tube':'Flat view'); edRedraw();
+});
 $('#edReset').addEventListener('click',()=>{
   $('#edB').value=1;$('#edC').value=1;$('#edS').value=1;$('#edH').value=0;
-  if(ES){ES.th=0;ES.ts=0;} setThumb(0,0); $('#wheelVal').textContent='neutral'; syncLabels(); edRedraw();
+  if(ES){ES.th=0;ES.ts=0;ES.roll=false;} setThumb(0,0); $('#wheelVal').textContent='neutral';
+  $('#edRoll').classList.remove('on'); syncLabels(); edRedraw();
 });
 $('#edCancel').addEventListener('click',()=>{toast('Edit cancelled');closeEditor();});
 $('#edUse').addEventListener('click',()=>{

← 9e8d499 auto-save: 2026-06-24T15:58:04 (2 files) β€” data/queue.json s  Β·  back to Dw Photo Capture  Β·  Roll: add Curl intensity slider + cycle Flatβ†’Tubeβ†’Sheet (cur 4b82a74 β†’