← back to Wallco Ai
Add color-dots.js: vertical strip of the pattern's screen-print ink colors (canvas-extracted) along the left edge of every pattern, included across all 17 PDP theme variants
8e674199948c79cd43b68d6903e1462ab87d9ec1 · 2026-05-29 13:17:57 -0700 · Steve
Files touched
A public/js/color-dots.jsM public/theme-variants/v1-compact.htmlM public/theme-variants/v10-stack.htmlM public/theme-variants/v11-best.htmlM public/theme-variants/v12-roomsplit.htmlM public/theme-variants/v13-roomdrawer.htmlM public/theme-variants/v14-roomzine.htmlM public/theme-variants/v15-roomsticky.htmlM public/theme-variants/v16-roommin.htmlM public/theme-variants/v17-roomshow.htmlM public/theme-variants/v2-bento.htmlM public/theme-variants/v3-tabs.htmlM public/theme-variants/v4-hero.htmlM public/theme-variants/v5-vertical.htmlM public/theme-variants/v6-drawer.htmlM public/theme-variants/v7-editorial.htmlM public/theme-variants/v8-spec.htmlM public/theme-variants/v9-config.htmlM scripts/room-left-template.htmlM server.js
Diff
commit 8e674199948c79cd43b68d6903e1462ab87d9ec1
Author: Steve <steve@designerwallcoverings.com>
Date: Fri May 29 13:17:57 2026 -0700
Add color-dots.js: vertical strip of the pattern's screen-print ink colors (canvas-extracted) along the left edge of every pattern, included across all 17 PDP theme variants
---
public/js/color-dots.js | 99 +++++++++++++++++++++++++++++++
public/theme-variants/v1-compact.html | 1 +
public/theme-variants/v10-stack.html | 1 +
public/theme-variants/v11-best.html | 1 +
public/theme-variants/v12-roomsplit.html | 1 +
public/theme-variants/v13-roomdrawer.html | 1 +
public/theme-variants/v14-roomzine.html | 1 +
public/theme-variants/v15-roomsticky.html | 1 +
public/theme-variants/v16-roommin.html | 1 +
public/theme-variants/v17-roomshow.html | 1 +
public/theme-variants/v2-bento.html | 1 +
public/theme-variants/v3-tabs.html | 1 +
public/theme-variants/v4-hero.html | 1 +
public/theme-variants/v5-vertical.html | 1 +
public/theme-variants/v6-drawer.html | 1 +
public/theme-variants/v7-editorial.html | 1 +
public/theme-variants/v8-spec.html | 1 +
public/theme-variants/v9-config.html | 1 +
scripts/room-left-template.html | 1 +
server.js | 27 +++++----
20 files changed, 131 insertions(+), 13 deletions(-)
diff --git a/public/js/color-dots.js b/public/js/color-dots.js
new file mode 100644
index 0000000..ae0da37
--- /dev/null
+++ b/public/js/color-dots.js
@@ -0,0 +1,99 @@
+/* color-dots.js — overlay a vertical strip of the pattern's screen-print ink
+ * colors along the LEFT edge of every pattern image, on every PDP theme.
+ * Same-origin design images (/designs/img/...) are canvas-readable, so we
+ * downscale + quantize to the dominant flat inks and render them as dots.
+ * Shared across all theme variants + the classic PDP (one include, no per-
+ * theme markup edits). Skips room mockups (/designs/room/) and small thumbs. */
+(function () {
+ 'use strict';
+ var DOT_COUNT = 6; // up to N ink dots
+ var MIN_W = 150; // ignore thumbnails smaller than this (rendered px)
+
+ function isPattern(img) {
+ var s = img.currentSrc || img.src || '';
+ if (!/\/designs\/img\//.test(s)) return false; // only design patterns
+ if (/\/designs\/room\//.test(s)) return false; // not room mockups
+ return true;
+ }
+
+ // Downscale to a small canvas, bucket colors (coarse quantize merges AA
+ // fringe), return the top DOT_COUNT inks by coverage, dropping near-dupes.
+ function extractInks(img) {
+ try {
+ var W = 56, H = 56;
+ var c = document.createElement('canvas'); c.width = W; c.height = H;
+ var ctx = c.getContext('2d', { willReadFrequently: true });
+ ctx.drawImage(img, 0, 0, W, H);
+ var data = ctx.getImageData(0, 0, W, H).data;
+ var buckets = {};
+ for (var i = 0; i < data.length; i += 4) {
+ var a = data[i + 3]; if (a < 128) continue;
+ var r = data[i], g = data[i + 1], b = data[i + 2];
+ var q = (Math.round(r / 24) * 24) + ',' + (Math.round(g / 24) * 24) + ',' + (Math.round(b / 24) * 24);
+ if (!buckets[q]) buckets[q] = { n: 0, r: 0, g: 0, b: 0 };
+ var bk = buckets[q]; bk.n++; bk.r += r; bk.g += g; bk.b += b;
+ }
+ var arr = Object.keys(buckets).map(function (k) {
+ var bk = buckets[k];
+ return { n: bk.n, r: Math.round(bk.r / bk.n), g: Math.round(bk.g / bk.n), b: Math.round(bk.b / bk.n) };
+ }).sort(function (a, b) { return b.n - a.n; });
+ var out = [];
+ for (var j = 0; j < arr.length && out.length < DOT_COUNT; j++) {
+ var col = arr[j];
+ var dup = out.some(function (o) {
+ return Math.abs(o.r - col.r) + Math.abs(o.g - col.g) + Math.abs(o.b - col.b) < 36;
+ });
+ if (!dup) out.push(col);
+ }
+ return out.map(function (o) {
+ return '#' + [o.r, o.g, o.b].map(function (v) { return ('0' + v.toString(16)).slice(-2); }).join('');
+ });
+ } catch (e) { return []; }
+ }
+
+ function addDots(img) {
+ if (img.dataset.cdDone) return;
+ if ((img.naturalWidth || 0) < 8) return; // not decoded yet
+ if ((img.clientWidth || img.width || 0) < MIN_W) return;
+ img.dataset.cdDone = '1';
+ var colors = extractInks(img);
+ if (!colors.length) return;
+ var host = img.parentElement; if (!host) return;
+ if (getComputedStyle(host).position === 'static') host.style.position = 'relative';
+ var col = document.createElement('div');
+ col.className = 'cd-strip';
+ col.setAttribute('aria-label', 'pattern screen-print colors');
+ col.style.cssText = 'position:absolute;left:10px;top:50%;transform:translateY(-50%);' +
+ 'display:flex;flex-direction:column;gap:9px;z-index:6;pointer-events:none';
+ colors.forEach(function (hex) {
+ var d = document.createElement('span');
+ d.title = hex; d.style.pointerEvents = 'auto';
+ d.style.cssText += 'width:18px;height:18px;border-radius:50%;background:' + hex +
+ ';box-shadow:0 0 0 2px rgba(255,255,255,.95),0 1px 5px rgba(0,0,0,.35)';
+ col.appendChild(d);
+ });
+ host.appendChild(col);
+ }
+
+ function scan() {
+ var imgs = document.images;
+ for (var i = 0; i < imgs.length; i++) {
+ var img = imgs[i];
+ if (img.dataset.cdDone || !isPattern(img)) continue;
+ if (img.complete && img.naturalWidth) addDots(img);
+ else img.addEventListener('load', function (e) { addDots(e.target); }, { once: true });
+ }
+ }
+
+ function boot() {
+ scan();
+ // re-scan on DOM changes (theme PDPs render their pattern client-side after fetch)
+ try {
+ new MutationObserver(function () { scan(); }).observe(document.body, { childList: true, subtree: true });
+ } catch (e) {}
+ window.addEventListener('resize', function () { clearTimeout(window._cdRt); window._cdRt = setTimeout(scan, 250); });
+ }
+
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
+ else boot();
+})();
diff --git a/public/theme-variants/v1-compact.html b/public/theme-variants/v1-compact.html
index 5a759fe..56ed11c 100644
--- a/public/theme-variants/v1-compact.html
+++ b/public/theme-variants/v1-compact.html
@@ -669,5 +669,6 @@ function escapeHtml(v) {
}
function escapeAttr(v) { return escapeHtml(v); }
</script>
+<script src="/js/color-dots.js" defer></script>
</body>
</html>
diff --git a/public/theme-variants/v10-stack.html b/public/theme-variants/v10-stack.html
index 8a7f508..43a57b6 100644
--- a/public/theme-variants/v10-stack.html
+++ b/public/theme-variants/v10-stack.html
@@ -349,5 +349,6 @@ function saveDesign(id) { alert('Saved #' + id + ' (stub)'); }
function escapeHtml(v) { if (v == null) return ''; return String(v).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function escapeAttr(v) { return escapeHtml(v); }
</script>
+<script src="/js/color-dots.js" defer></script>
</body>
</html>
diff --git a/public/theme-variants/v11-best.html b/public/theme-variants/v11-best.html
index 4662d09..9a19b37 100644
--- a/public/theme-variants/v11-best.html
+++ b/public/theme-variants/v11-best.html
@@ -570,5 +570,6 @@ function saveDesign(id) { alert('Saved #' + id + ' (stub)'); }
function escapeHtml(v) { if (v == null) return ''; return String(v).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function escapeAttr(v) { return escapeHtml(v); }
</script>
+<script src="/js/color-dots.js" defer></script>
</body>
</html>
diff --git a/public/theme-variants/v12-roomsplit.html b/public/theme-variants/v12-roomsplit.html
index 5e9c8ed..b57f053 100644
--- a/public/theme-variants/v12-roomsplit.html
+++ b/public/theme-variants/v12-roomsplit.html
@@ -306,5 +306,6 @@ window.copyLink = async function(){ try{ await navigator.clipboard.writeText(loc
function escapeHtml(v){ if(v==null) return ''; return String(v).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function escapeAttr(v){ return escapeHtml(v); }
</script>
+<script src="/js/color-dots.js" defer></script>
</body>
</html>
diff --git a/public/theme-variants/v13-roomdrawer.html b/public/theme-variants/v13-roomdrawer.html
index 8282a94..f5fd1eb 100644
--- a/public/theme-variants/v13-roomdrawer.html
+++ b/public/theme-variants/v13-roomdrawer.html
@@ -306,5 +306,6 @@ window.copyLink = async function(){ try{ await navigator.clipboard.writeText(loc
function escapeHtml(v){ if(v==null) return ''; return String(v).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function escapeAttr(v){ return escapeHtml(v); }
</script>
+<script src="/js/color-dots.js" defer></script>
</body>
</html>
diff --git a/public/theme-variants/v14-roomzine.html b/public/theme-variants/v14-roomzine.html
index a054bbe..f4149d4 100644
--- a/public/theme-variants/v14-roomzine.html
+++ b/public/theme-variants/v14-roomzine.html
@@ -306,5 +306,6 @@ window.copyLink = async function(){ try{ await navigator.clipboard.writeText(loc
function escapeHtml(v){ if(v==null) return ''; return String(v).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function escapeAttr(v){ return escapeHtml(v); }
</script>
+<script src="/js/color-dots.js" defer></script>
</body>
</html>
diff --git a/public/theme-variants/v15-roomsticky.html b/public/theme-variants/v15-roomsticky.html
index 99b47c3..4795d2e 100644
--- a/public/theme-variants/v15-roomsticky.html
+++ b/public/theme-variants/v15-roomsticky.html
@@ -306,5 +306,6 @@ window.copyLink = async function(){ try{ await navigator.clipboard.writeText(loc
function escapeHtml(v){ if(v==null) return ''; return String(v).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function escapeAttr(v){ return escapeHtml(v); }
</script>
+<script src="/js/color-dots.js" defer></script>
</body>
</html>
diff --git a/public/theme-variants/v16-roommin.html b/public/theme-variants/v16-roommin.html
index 4895872..cc1f4e9 100644
--- a/public/theme-variants/v16-roommin.html
+++ b/public/theme-variants/v16-roommin.html
@@ -306,5 +306,6 @@ window.copyLink = async function(){ try{ await navigator.clipboard.writeText(loc
function escapeHtml(v){ if(v==null) return ''; return String(v).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function escapeAttr(v){ return escapeHtml(v); }
</script>
+<script src="/js/color-dots.js" defer></script>
</body>
</html>
diff --git a/public/theme-variants/v17-roomshow.html b/public/theme-variants/v17-roomshow.html
index 2c2dcf6..cd4f99a 100644
--- a/public/theme-variants/v17-roomshow.html
+++ b/public/theme-variants/v17-roomshow.html
@@ -306,5 +306,6 @@ window.copyLink = async function(){ try{ await navigator.clipboard.writeText(loc
function escapeHtml(v){ if(v==null) return ''; return String(v).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function escapeAttr(v){ return escapeHtml(v); }
</script>
+<script src="/js/color-dots.js" defer></script>
</body>
</html>
diff --git a/public/theme-variants/v2-bento.html b/public/theme-variants/v2-bento.html
index 540085c..01f78c6 100644
--- a/public/theme-variants/v2-bento.html
+++ b/public/theme-variants/v2-bento.html
@@ -648,5 +648,6 @@ function escapeHtml(v) {
}
function escapeAttr(v) { return escapeHtml(v); }
</script>
+<script src="/js/color-dots.js" defer></script>
</body>
</html>
diff --git a/public/theme-variants/v3-tabs.html b/public/theme-variants/v3-tabs.html
index 4fcc4e5..6859ebc 100644
--- a/public/theme-variants/v3-tabs.html
+++ b/public/theme-variants/v3-tabs.html
@@ -678,5 +678,6 @@ function escapeHtml(v) {
}
function escapeAttr(v) { return escapeHtml(v); }
</script>
+<script src="/js/color-dots.js" defer></script>
</body>
</html>
diff --git a/public/theme-variants/v4-hero.html b/public/theme-variants/v4-hero.html
index 845bfd2..8bfb89a 100644
--- a/public/theme-variants/v4-hero.html
+++ b/public/theme-variants/v4-hero.html
@@ -598,5 +598,6 @@ function escapeHtml(v) {
}
function escapeAttr(v) { return escapeHtml(v); }
</script>
+<script src="/js/color-dots.js" defer></script>
</body>
</html>
diff --git a/public/theme-variants/v5-vertical.html b/public/theme-variants/v5-vertical.html
index cc044bf..88f8454 100644
--- a/public/theme-variants/v5-vertical.html
+++ b/public/theme-variants/v5-vertical.html
@@ -416,5 +416,6 @@ function saveDesign(id) { alert('Saved #' + id + ' (stub)'); }
function escapeHtml(v) { if (v == null) return ''; return String(v).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function escapeAttr(v) { return escapeHtml(v); }
</script>
+<script src="/js/color-dots.js" defer></script>
</body>
</html>
diff --git a/public/theme-variants/v6-drawer.html b/public/theme-variants/v6-drawer.html
index c1ead2c..65cd02a 100644
--- a/public/theme-variants/v6-drawer.html
+++ b/public/theme-variants/v6-drawer.html
@@ -398,5 +398,6 @@ function saveDesign(id) { alert('Saved #' + id + ' (stub)'); }
function escapeHtml(v) { if (v == null) return ''; return String(v).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function escapeAttr(v) { return escapeHtml(v); }
</script>
+<script src="/js/color-dots.js" defer></script>
</body>
</html>
diff --git a/public/theme-variants/v7-editorial.html b/public/theme-variants/v7-editorial.html
index 2d0ab49..dfb4072 100644
--- a/public/theme-variants/v7-editorial.html
+++ b/public/theme-variants/v7-editorial.html
@@ -349,5 +349,6 @@ function addSample() { alert('Sample added (stub)'); }
function escapeHtml(v) { if (v == null) return ''; return String(v).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function escapeAttr(v) { return escapeHtml(v); }
</script>
+<script src="/js/color-dots.js" defer></script>
</body>
</html>
diff --git a/public/theme-variants/v8-spec.html b/public/theme-variants/v8-spec.html
index 09a19a7..a67cbf2 100644
--- a/public/theme-variants/v8-spec.html
+++ b/public/theme-variants/v8-spec.html
@@ -338,5 +338,6 @@ function addSample() { alert('Sample added (stub)'); }
function escapeHtml(v) { if (v == null) return ''; return String(v).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function escapeAttr(v) { return escapeHtml(v); }
</script>
+<script src="/js/color-dots.js" defer></script>
</body>
</html>
diff --git a/public/theme-variants/v9-config.html b/public/theme-variants/v9-config.html
index 2f91a92..f5554b7 100644
--- a/public/theme-variants/v9-config.html
+++ b/public/theme-variants/v9-config.html
@@ -372,5 +372,6 @@ function addBundle() { alert('Bundle added (stub)'); }
function escapeHtml(v) { if (v == null) return ''; return String(v).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function escapeAttr(v) { return escapeHtml(v); }
</script>
+<script src="/js/color-dots.js" defer></script>
</body>
</html>
diff --git a/scripts/room-left-template.html b/scripts/room-left-template.html
index ff29d3c..6c4e0b8 100644
--- a/scripts/room-left-template.html
+++ b/scripts/room-left-template.html
@@ -290,5 +290,6 @@ window.copyLink = async function(){ try{ await navigator.clipboard.writeText(loc
function escapeHtml(v){ if(v==null) return ''; return String(v).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function escapeAttr(v){ return escapeHtml(v); }
</script>
+<script src="/js/color-dots.js" defer></script>
</body>
</html>
diff --git a/server.js b/server.js
index 69243a7..173f7fc 100644
--- a/server.js
+++ b/server.js
@@ -137,13 +137,18 @@ try { require('./src/social').mount(app); } catch (e) { console.error('Social mo
// Inject the universal share layer onto the designs grid + every design page.
// Done as a response rewrite so the 863KB server.js page templates stay untouched.
app.use((req, res, next) => {
- if (req.path === '/designs' || /^\/design\/\d+$/.test(req.path)) {
+ const isPDP = /^\/design\/\d+$/.test(req.path);
+ if (req.path === '/designs' || isPDP) {
const origSend = res.send.bind(res);
res.send = (body) => {
- if (typeof body === 'string' && body.indexOf('</body>') !== -1
- && body.indexOf('social-share.js') === -1) {
- body = body.replace('</body>',
- '<script src="/js/social-share.js" defer></script>\n</body>');
+ if (typeof body === 'string' && body.indexOf('</body>') !== -1) {
+ let inj = '';
+ if (body.indexOf('social-share.js') === -1) inj += '<script src="/js/social-share.js" defer></script>\n';
+ // color-dots on the CLASSIC PDP too (the default "theme") — left-edge
+ // screen-print ink dots, matching the 17 theme variants. PDP only (the
+ // grid isn't a theme; per-card dots would clutter it).
+ if (isPDP && body.indexOf('color-dots.js') === -1) inj += '<script src="/js/color-dots.js" defer></script>\n';
+ if (inj) body = body.replace('</body>', inj + '</body>');
}
return origSend(body);
};
@@ -6379,14 +6384,10 @@ app.get('/designs', (req, res) => {
<body>
${htmlHeader('/designs')}
<main id="grid-main">
-<!-- Theme toggle removed 2026-05-29 (Steve directive: 'i ages should not be
- superzooom on load.. revert https://wallco.ai/designs'). The theme2 CSS
- used `background:var(--card-bg)` shorthand on [class*="card"], which has
- higher specificity than the .card-img stylesheet rule and reset
- background-size from 'cover' back to 'auto' — leaving every card-img
- rendering its 1024px source at natural size inside a 256px box (=
- super-zoom). PDP /design/:id keeps its own theme toggle; this revert
- is grid-only. -->
+<!-- Theme toggle removed 2026-05-29 (Steve: revert /designs super-zoom).
+ theme2 CSS used "background" shorthand on [class*=card] which reset
+ background-size from cover to auto, making each card show its 1024px
+ source at natural size inside a 256px box. PDP keeps its own toggle. -->
${''}
${cat === 'drunk-animals' ? `
<section style="padding:48px 24px 24px;text-align:center;background:linear-gradient(180deg,#1a0f1a 0%,#2a1530 100%);color:#fff;margin-bottom:0">
← 8c1ed72 revert: theme1/theme2 toggle on /designs admin (was super-zo
·
back to Wallco Ai
·
cactus-curator: select-all button + live-animate-out + auto- 6f2666d →