[object Object]

← back to Dw Photo Capture

Crop tool: add aspect-ratio presets (Free / 1:1 / 4:3 / 3:4 / 16:9) — ratio-locked, anchor-based corner resize

3832493175a0fae7019f9eb2d39b6f2f17c42e9f · 2026-06-25 09:53:50 -0700 · steve@designerwallcoverings.com

Files touched

Diff

commit 3832493175a0fae7019f9eb2d39b6f2f17c42e9f
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Thu Jun 25 09:53:50 2026 -0700

    Crop tool: add aspect-ratio presets (Free / 1:1 / 4:3 / 3:4 / 16:9) — ratio-locked, anchor-based corner resize
---
 data/build.json   |  5 +++--
 public/index.html | 54 ++++++++++++++++++++++++++++++++++++++++++------------
 2 files changed, 45 insertions(+), 14 deletions(-)

diff --git a/data/build.json b/data/build.json
index fa23cc5..ae87041 100644
--- a/data/build.json
+++ b/data/build.json
@@ -1,5 +1,5 @@
 {
-  "next": 33,
+  "next": 34,
   "map": {
     "63863152": 27,
     "578af86f": 2,
@@ -31,6 +31,7 @@
     "f4a0bf6d": 29,
     "86060b11": 30,
     "43b1741c": 31,
-    "b2f94d6e": 32
+    "b2f94d6e": 32,
+    "224e5276": 33
   }
 }
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index de92ceb..b667fba 100644
--- a/public/index.html
+++ b/public/index.html
@@ -133,6 +133,9 @@
   .cropbox i[data-h=ne]{right:-3px;top:-3px;border-left:0;border-bottom:0;cursor:nesw-resize}
   .cropbox i[data-h=sw]{left:-3px;bottom:-3px;border-right:0;border-top:0;cursor:nesw-resize}
   .cropbox i[data-h=se]{right:-3px;bottom:-3px;border-left:0;border-top:0;cursor:nwse-resize}
+  .crop-presets{position:fixed;left:50%;top:max(14px,env(safe-area-inset-top));transform:translateX(-50%);display:flex;gap:7px;z-index:121}
+  .crop-presets button{background:rgba(8,7,6,.75);border:1px solid var(--line);color:var(--ink);border-radius:99px;padding:8px 13px;font-size:13px;font-weight:600;cursor:pointer}
+  .crop-presets button.on{background:var(--gold);color:#1b1407;border-color:var(--gold)}
   .crop-bar{position:fixed;left:50%;bottom:max(24px,env(safe-area-inset-bottom));transform:translateX(-50%);display:flex;gap:12px;z-index:121}
   .crop-bar button{border:none;border-radius:12px;padding:14px 26px;font-weight:700;font-size:15px;cursor:pointer}
   #cropCancel{background:#16140f;border:1px solid var(--red);color:var(--red)}
@@ -227,6 +230,13 @@
 
 <!-- crop overlay (Photoshop-style: drag handles, dim outside) -->
 <div class="cropov" id="cropov" hidden>
+  <div class="crop-presets" id="cropPresets">
+    <button class="on" data-r="0">Free</button>
+    <button data-r="1">1:1</button>
+    <button data-r="1.3333">4:3</button>
+    <button data-r="0.75">3:4</button>
+    <button data-r="1.7778">16:9</button>
+  </div>
   <div class="cropbox" id="cropbox"><i data-h="nw"></i><i data-h="ne"></i><i data-h="sw"></i><i data-h="se"></i></div>
   <div class="crop-bar"><button id="cropCancel">✕ Cancel</button><button id="cropApply">✓ Apply Crop</button></div>
 </div>
@@ -719,26 +729,46 @@ function exitCrop(){ $('#cropov').hidden=true; CROP=null; }
 function setCropBox(x,y,w,h){
   const b=$('#cropbox'); b.style.left=x+'px'; b.style.top=y+'px'; b.style.width=Math.max(30,w)+'px'; b.style.height=Math.max(30,h)+'px';
 }
+let cropRatio=0; // 0 = free, else width/height
+function setPreset(rat,btn){
+  cropRatio=+rat||0;
+  document.querySelectorAll('#cropPresets button').forEach(b=>b.classList.remove('on')); if(btn)btn.classList.add('on');
+  if(!CROP||!cropRatio)return;
+  const r=CROP.r, bb=$('#cropbox').getBoundingClientRect(), cx=bb.left+bb.width/2, cy=bb.top+bb.height/2;
+  let w=bb.width, ht=w/cropRatio;
+  if(ht>r.height*0.96){ ht=r.height*0.92; w=ht*cropRatio; }
+  if(w>r.width*0.96){ w=r.width*0.92; ht=w/cropRatio; }
+  const x=Math.max(r.left,Math.min(cx-w/2,r.right-w)), y=Math.max(r.top,Math.min(cy-ht/2,r.bottom-ht));
+  setCropBox(x,y,w,ht);
+}
 (function cropDrag(){
   const box=$('#cropbox'); let mode=null,h=null,sx,sy,ox,oy,ow,oh;
-  const clampToCanvas=()=>{ if(!CROP)return; const r=CROP.r,bb=box.getBoundingClientRect();
-    let x=Math.max(r.left,Math.min(bb.left,r.right-bb.width)), y=Math.max(r.top,Math.min(bb.top,r.bottom-bb.height));
-    let w=Math.min(bb.width,r.right-x), ht=Math.min(bb.height,r.bottom-y); setCropBox(x,y,w,ht); };
-  function down(e){ if(!CROP)return; e.preventDefault(); const t=e.touches?e.touches[0]:e; sx=t.clientX; sy=t.clientY;
+  function down(e){ if(!CROP)return; e.preventDefault(); sx=e.clientX; sy=e.clientY;
     const bb=box.getBoundingClientRect(); ox=bb.left;oy=bb.top;ow=bb.width;oh=bb.height;
     h=e.target.dataset&&e.target.dataset.h; mode=h?'resize':'move';
     document.addEventListener('pointermove',move,{passive:false}); document.addEventListener('pointerup',up); }
-  function move(e){ if(!mode||!CROP)return; e.preventDefault(); const r=CROP.r; const dx=e.clientX-sx, dy=e.clientY-sy;
-    if(mode==='move'){ setCropBox(Math.max(r.left,Math.min(ox+dx,r.right-ow)), Math.max(r.top,Math.min(oy+dy,r.bottom-oh)), ow, oh); }
-    else { let x=ox,y=oy,w=ow,ht=oh;
-      if(h.includes('w')){ x=ox+dx; w=ow-dx; } if(h.includes('e')){ w=ow+dx; }
-      if(h.includes('n')){ y=oy+dy; ht=oh-dy; } if(h.includes('s')){ ht=oh+dy; }
-      if(w<30){ if(h.includes('w')) x=ox+ow-30; w=30; } if(ht<30){ if(h.includes('n')) y=oy+oh-30; ht=30; }
-      setCropBox(x,y,w,ht); clampToCanvas(); } }
+  function move(e){ if(!mode||!CROP)return; e.preventDefault(); const r=CROP.r;
+    if(mode==='move'){ const dx=e.clientX-sx, dy=e.clientY-sy;
+      setCropBox(Math.max(r.left,Math.min(ox+dx,r.right-ow)), Math.max(r.top,Math.min(oy+dy,r.bottom-oh)), ow, oh); return; }
+    // resize: opposite corner stays anchored
+    const ax = h.includes('w') ? ox+ow : ox, ay = h.includes('n') ? oy+oh : oy;
+    let px=Math.max(r.left,Math.min(e.clientX,r.right)), py=Math.max(r.top,Math.min(e.clientY,r.bottom));
+    let w=Math.abs(px-ax), ht=Math.abs(py-ay);
+    if(cropRatio){
+      if(w/cropRatio>=ht) ht=w/cropRatio; else w=ht*cropRatio;
+      if(h.includes('e')&&ax+w>r.right){ w=r.right-ax; ht=w/cropRatio; }
+      if(h.includes('w')&&ax-w<r.left){ w=ax-r.left; ht=w/cropRatio; }
+      if(h.includes('s')&&ay+ht>r.bottom){ ht=r.bottom-ay; w=ht*cropRatio; }
+      if(h.includes('n')&&ay-ht<r.top){ ht=ay-r.top; w=ht*cropRatio; }
+    }
+    w=Math.max(30,w); ht=Math.max(30,ht);
+    const x=h.includes('w')?ax-w:ax, y=h.includes('n')?ay-ht:ay;
+    setCropBox(x,y,w,ht); }
   function up(){ mode=null; document.removeEventListener('pointermove',move); document.removeEventListener('pointerup',up); }
   box.addEventListener('pointerdown',down);
 })();
-$('#edCrop').addEventListener('click',enterCrop);
+document.querySelectorAll('#cropPresets button').forEach(b=>b.addEventListener('click',()=>setPreset(b.dataset.r,b)));
+$('#edCrop').addEventListener('click',()=>{ enterCrop(); const f=document.querySelector('#cropPresets button[data-r="0"]'); setPreset(0,f); });
 $('#cropCancel').addEventListener('click',exitCrop);
 $('#cropApply').addEventListener('click',()=>{
   if(!CROP||!ES)return; const r=CROP.r, bb=$('#cropbox').getBoundingClientRect();

← f97337a Add ✂️ Crop tool to photo editor (drag corners/move, dim out  ·  back to Dw Photo Capture  ·  Video uploads: 🎥 Video in gallery → /api/video runs the Sho 7a11139 →