← 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
M public/js/room-capture.jsM tests/e2e-room-capture.js
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 →