β 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 β