[object Object]

← back to Dw Photo Capture

Roll: add Curl intensity slider + cycle Flat→Tube→Sheet (curled-sheet variant reveals paper's cream back, like the PR roll shot)

4b82a74e3f8b8b095b7522d09998ca4c5c69f652 · 2026-06-24 16:14:12 -0700 · Steve Abrams

Files touched

Diff

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

    Roll: add Curl intensity slider + cycle Flat→Tube→Sheet (curled-sheet variant reveals paper's cream back, like the PR roll shot)
---
 public/index.html | 64 +++++++++++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 55 insertions(+), 9 deletions(-)

diff --git a/public/index.html b/public/index.html
index 3ed293a..ea73eb6 100644
--- a/public/index.html
+++ b/public/index.html
@@ -125,6 +125,7 @@
         <div class="sl"><label>Contrast</label><input type="range" id="edC" min="0.5" max="1.6" step="0.01" value="1"><span class="v" id="edCv">0</span></div>
         <div class="sl"><label>Saturation</label><input type="range" id="edS" min="0" max="2.2" step="0.01" value="1"><span class="v" id="edSv">0</span></div>
         <div class="sl"><label>Hue</label><input type="range" id="edH" min="-180" max="180" step="1" value="0"><span class="v" id="edHv">0°</span></div>
+        <div class="sl" id="edCurlRow" style="display:none"><label>🗞 Curl</label><input type="range" id="edCurl" min="0" max="1" step="0.01" value="0.5"><span class="v" id="edCurlv">50%</span></div>
       </div>
     </div>
     <div class="ed-actions">
@@ -233,13 +234,14 @@ function openEditor(x,dataUrl){
   const img=new Image();
   img.onload=()=>{
     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};
+    ES={x,img,flat,b:1,c:1,s:1,h:0,th:0,ts:0,rollMode:'off',curl:0.5};
     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;
+    $('#edCurl').value=0.5; $('#edCurlv').textContent='50%'; $('#edCurlRow').style.display='none';
     setThumb(0,0); $('#wheelVal').textContent='neutral';
-    $('#edRoll').classList.remove('on');
+    $('#edRoll').classList.remove('on'); $('#edRoll').textContent='🗞 Roll';
     syncLabels(); edRedraw();
     $('#editor').hidden=false; document.body.style.overflow='hidden';
   };
@@ -274,7 +276,8 @@ function edRedraw(){
   // 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);
+  if(ES.rollMode==='tube') renderRoll(fc, ctx, cv.width, cv.height);
+  else if(ES.rollMode==='sheet') renderSheet(fc, ctx, cv.width, cv.height);
   else ctx.drawImage(fc,0,0);
 }
 
@@ -282,7 +285,9 @@ function edRedraw(){
 // 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 t=(ES&&ES.curl!=null)?ES.curl:0.5;             // 0 = loose/gentle  →  1 = tight tube
+  const cx=W/2, R=W*(0.52-t*0.18), cov=Math.PI*(0.62+t*0.52); // tighter ⇒ smaller radius, more wrap
+  const shExp=0.55+t*0.55;                             // tighter ⇒ deeper edge shading
   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';
@@ -295,7 +300,7 @@ function renderRoll(src, o, W, H){
     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
+    const shade=Math.pow(Math.cos(th),shExp);           // 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);
@@ -309,6 +314,37 @@ function renderRoll(src, o, W, H){
   o.globalCompositeOperation='source-over';
 }
 
+// Curled hanging SHEET (the PR roll-shot look): top faces the viewer, the sheet
+// bows forward and the bottom edge curls UNDER, revealing the paper's cream back.
+function renderSheet(src, o, W, H){
+  o.fillStyle='#0c0b09'; o.fillRect(0,0,W,H);            // dark studio ground
+  const t=(ES&&ES.curl!=null)?ES.curl:0.5;
+  const margin=W*0.07, x0=margin, drawW=W-2*margin, top=H*0.05;
+  const phiMax=Math.PI*(0.58+t*0.42);                   // how far it curls under (105°→180°)
+  const ease=1.4+t*0.9;                                  // tighter ⇒ bend concentrated at the bottom
+  const shExp=0.6+t*0.5;
+  const steps=src.height, paperLen=H*0.94, ds=paperLen/steps;
+  let y=top, minY=top;
+  for(let i=0;i<steps;i++){
+    const s=i/(steps-1), phi=Math.pow(s,ease)*phiMax;
+    const dy=Math.cos(phi)*ds, bandH=Math.max(1,Math.abs(dy)+1.3);
+    const yTop=dy>=0?y:y+dy;
+    if(phi<=Math.PI/2){                                  // front face → texture + Lambert shade
+      o.drawImage(src,0,i,src.width,1, x0,yTop,drawW,bandH);
+      o.globalCompositeOperation='multiply';
+      o.fillStyle='rgba(0,0,0,'+(1-Math.pow(Math.cos(phi),shExp)).toFixed(3)+')';
+      o.fillRect(x0,yTop,drawW,bandH); o.globalCompositeOperation='source-over';
+    } else {                                             // past vertical → cream UNDERSIDE of the paper
+      const b=Math.pow(Math.max(0,Math.cos(Math.PI-phi)),0.8), lum=Math.round(150+72*b);
+      o.fillStyle='rgb('+lum+','+(lum-10)+','+(lum-30)+')';
+      o.fillRect(x0,yTop,drawW,bandH);
+    }
+    y+=dy; if(y<minY)minY=y;
+  }
+  // soft shadow line where the curl lifts off the front
+  o.fillStyle='rgba(0,0,0,.18)'; o.fillRect(x0,minY-2,drawW,3);
+}
+
 // paint the color wheel once (white center → full hue at rim)
 (function paintWheel(){
   const w=$('#wheel'), ctx=w.getContext('2d'), R=w.width/2;
@@ -340,14 +376,24 @@ 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();}));
+$('#edCurl').addEventListener('input',e=>{
+  if(ES)ES.curl=+e.target.value; $('#edCurlv').textContent=Math.round(+e.target.value*100)+'%'; 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();
+  if(!ES)return;
+  ES.rollMode = ES.rollMode==='off'?'tube':ES.rollMode==='tube'?'sheet':'off';
+  const on=ES.rollMode!=='off';
+  $('#edRoll').classList.toggle('on',on);
+  $('#edRoll').textContent = ES.rollMode==='tube'?'🗞 Tube':ES.rollMode==='sheet'?'🗞 Sheet':'🗞 Roll';
+  $('#edCurlRow').style.display=on?'grid':'none';
+  toast(ES.rollMode==='tube'?'Rolled around tube':ES.rollMode==='sheet'?'Curled sheet':'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;ES.roll=false;} setThumb(0,0); $('#wheelVal').textContent='neutral';
-  $('#edRoll').classList.remove('on'); syncLabels(); edRedraw();
+  $('#edCurl').value=0.5; $('#edCurlv').textContent='50%'; $('#edCurlRow').style.display='none';
+  if(ES){ES.th=0;ES.ts=0;ES.rollMode='off';ES.curl=0.5;} setThumb(0,0); $('#wheelVal').textContent='neutral';
+  $('#edRoll').classList.remove('on'); $('#edRoll').textContent='🗞 Roll'; syncLabels(); edRedraw();
 });
 $('#edCancel').addEventListener('click',()=>{toast('Edit cancelled');closeEditor();});
 $('#edUse').addEventListener('click',()=>{

← b6eb162 Add 🗞 Roll button: wrap captured pattern around a tube (cyl  ·  back to Dw Photo Capture  ·  Send Cache-Control: no-store on HTML so browsers never serve 444a683 →