← 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 →