[object Object]

← back to NationalPaperHangers

Render true half-drop in the on-wall wallpaper preview

d69c43167a0f8f2f8d97048740ddf117cd4112c3 · 2026-05-18 17:11:44 -0700 · SteveStudio2

When the customer picks half-drop match, the preview now composites a
canvas 'super-tile' — two panels wide with the right column dropped by
half the vertical repeat (plus a wrap copy so it tiles seamlessly) — and
tiles that straight at double width. Falls back to straight tiling while
the image is still loading. Output is a data: URL (allowed by the page
CSP). e2e gains a half-drop assertion; suite 184 pass · 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit d69c43167a0f8f2f8d97048740ddf117cd4112c3
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Mon May 18 17:11:44 2026 -0700

    Render true half-drop in the on-wall wallpaper preview
    
    When the customer picks half-drop match, the preview now composites a
    canvas 'super-tile' — two panels wide with the right column dropped by
    half the vertical repeat (plus a wrap copy so it tiles seamlessly) — and
    tiles that straight at double width. Falls back to straight tiling while
    the image is still loading. Output is a data: URL (allowed by the page
    CSP). e2e gains a half-drop assertion; suite 184 pass · 0 fail.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 public/js/room-capture.js | 42 +++++++++++++++++++++++++++++++++++++++---
 tests/e2e-room-capture.js |  7 +++++++
 2 files changed, 46 insertions(+), 3 deletions(-)

diff --git a/public/js/room-capture.js b/public/js/room-capture.js
index abadb66..3ed3f61 100644
--- a/public/js/room-capture.js
+++ b/public/js/room-capture.js
@@ -97,9 +97,29 @@
     serialize();
   }
 
+  // Build a half-drop "super-tile": two panels wide, the right column
+  // dropped by half the vertical repeat (with a wrap copy so it tiles
+  // seamlessly). Repeated straight, this super-tile reproduces the
+  // half-drop match. Returns a data: URL (allowed by the page CSP).
+  function buildHalfDrop(img) {
+    var w = img.naturalWidth || 600, h = img.naturalHeight || 600;
+    var scale = Math.min(1, 1400 / w); // cap so the data URL stays small
+    var tw = Math.max(1, Math.round(w * scale));
+    var th = Math.max(1, Math.round(h * scale));
+    var c = document.createElement('canvas');
+    c.width = tw * 2;
+    c.height = th;
+    var ctx = c.getContext('2d');
+    ctx.drawImage(img, 0, 0, tw, th);              // left column — aligned
+    ctx.drawImage(img, tw, th / 2, tw, th);        // right column — dropped ½
+    ctx.drawImage(img, tw, th / 2 - th, tw, th);   // …wrap copy of the drop
+    return c.toDataURL('image/png');
+  }
+
   // Tile the uploaded wallpaper onto the wall box at real-world scale:
   // one panel spans (width_in/12 / wall_width_ft) of the box width; one
   // vertical repeat spans (repeat_in/12 / wall_height_ft) of its height.
+  // Half-drop swaps in the super-tile (2 panels wide) at double the width.
   function renderWallpaper(r) {
     var layer = r.el.querySelector('.rc-wallpaper-layer');
     var note = r.el.querySelector('.rc-wp-note');
@@ -114,13 +134,22 @@
       note.textContent = 'Enter the exact panel width and vertical repeat to preview at true scale.';
       return;
     }
+    var match = r.el.querySelector('.rc-wp-match').value;
     note.classList.remove('is-error');
-    note.textContent = 'Previewed at true scale — ' + wpW + '" panel, ' + wpR + '" repeat.';
+    note.textContent = 'Previewed at true scale — ' + wpW + '" panel, ' + wpR + '" repeat, '
+      + (match === 'half_drop' ? 'half-drop' : 'straight') + ' match.';
     if (!(wallW > 0) || !(wallH > 0)) { layer.style.backgroundImage = ''; return; }
     var tileWpct = ((wpW / 12) / wallW) * 100;
     var tileHpct = ((wpR / 12) / wallH) * 100;
-    layer.style.backgroundImage = 'url("' + r.wallpaperUrl + '")';
-    layer.style.backgroundSize = tileWpct.toFixed(2) + '% ' + tileHpct.toFixed(2) + '%';
+    if (match === 'half_drop' && r.wpImgReady) {
+      if (!r.halfDropUrl) r.halfDropUrl = buildHalfDrop(r.wpImg);
+      layer.style.backgroundImage = 'url("' + r.halfDropUrl + '")';
+      layer.style.backgroundSize = (tileWpct * 2).toFixed(2) + '% ' + tileHpct.toFixed(2) + '%';
+    } else {
+      // straight match — or half-drop while the image is still loading
+      layer.style.backgroundImage = 'url("' + r.wallpaperUrl + '")';
+      layer.style.backgroundSize = tileWpct.toFixed(2) + '% ' + tileHpct.toFixed(2) + '%';
+    }
   }
 
   // Corner-drag — axis-aligned rect, opposite corner stays anchored. The
@@ -220,6 +249,13 @@
       cta.textContent = 'Uploading…';
       uploadFile(f).then(function (j) {
         r.wallpaperUrl = j.url;
+        // Load the image into an <img> too — needed to composite the
+        // half-drop super-tile on a canvas. Same-origin, so not tainted.
+        r.halfDropUrl = null;
+        r.wpImgReady = false;
+        r.wpImg = new Image();
+        r.wpImg.onload = function () { r.wpImgReady = true; r.halfDropUrl = null; renderWallpaper(r); };
+        r.wpImg.src = j.url;
         cta.textContent = 'Wallpaper added ✓ — replace';
         renderWallpaper(r);
         serialize();
diff --git a/tests/e2e-room-capture.js b/tests/e2e-room-capture.js
index d39c6a4..72dcb1f 100644
--- a/tests/e2e-room-capture.js
+++ b/tests/e2e-room-capture.js
@@ -107,6 +107,13 @@ function db() { if (!_db) _db = require('../lib/db'); return _db; }
     const bg = await page.locator('.rc-wallpaper-layer').evaluate(el => el.style.backgroundImage);
     ok(/url/.test(bg), 'wallpaper tiles onto the wall at scale');
 
+    await page.locator('.rc-wp-match').selectOption('half_drop');
+    await page.waitForTimeout(400);
+    const bgHalf = await page.locator('.rc-wallpaper-layer').evaluate(el => el.style.backgroundImage);
+    ok(/^url\("data:image/.test(bgHalf), 'half-drop match composites a super-tile');
+    await page.locator('.rc-wp-match').selectOption('straight');
+    await page.waitForTimeout(150);
+
     const captures = JSON.parse(await page.locator('[data-rc-field]').inputValue());
     ok(captures.length === 1 && captures[0].room === 'Dining room'
        && captures[0].photo_url && captures[0].wall_width_ft > 0,

← 51b33df Harden the room-capture flow for mobile + touch  ·  back to NationalPaperHangers  ·  Validate booking-media uploads by magic bytes, not the spoof 3e66967 →