[object Object]

← back to CelebritySignatures

Mural QR: B+C — functional-size centre QR + short /k/<code> route

84da440736253d71e2814b034bd72b92384ff620 · 2026-06-18 17:49:42 -0700 · SteveStudio2

DTD verdict (1-1 split, broken on measured fact + argument): render the centre
QR as a functional utility mark, not a uniform signature cell — an 84px cell is
2.3px/module (dead in print) while ~165px lands at 5px/module (print-reliable).
Combined with C: server.js /k/<code> single-letter redirect map → short 47-char
URL → only 33 modules. build-collages.py drops the cell-insertion, draws a crisp
centred QR (NEAREST integer scaling) on a tasteful panel captioned SCAN TO REVEAL
NAMES. All 6 QRs decode-verified to the short URL; /k/d..t serve the named keys
(HTTP 200) locally.

NOTE: QR host is now https://celebrity.designerwallcoverings.com (public) — that
domain is NOT live yet, so scans won't resolve until it's deployed (Steve-gated).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit 84da440736253d71e2814b034bd72b92384ff620
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Thu Jun 18 17:49:42 2026 -0700

    Mural QR: B+C — functional-size centre QR + short /k/<code> route
    
    DTD verdict (1-1 split, broken on measured fact + argument): render the centre
    QR as a functional utility mark, not a uniform signature cell — an 84px cell is
    2.3px/module (dead in print) while ~165px lands at 5px/module (print-reliable).
    Combined with C: server.js /k/<code> single-letter redirect map → short 47-char
    URL → only 33 modules. build-collages.py drops the cell-insertion, draws a crisp
    centred QR (NEAREST integer scaling) on a tasteful panel captioned SCAN TO REVEAL
    NAMES. All 6 QRs decode-verified to the short URL; /k/d..t serve the named keys
    (HTTP 200) locally.
    
    NOTE: QR host is now https://celebrity.designerwallcoverings.com (public) — that
    domain is NOT live yet, so scans won't resolve until it's deployed (Steve-gated).
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 output/collage-declaration-of-independence.png | Bin 648469 -> 615467 bytes
 output/collage-hollywood.png                   | Bin 798780 -> 786375 bytes
 output/collage-movies-classic.png              | Bin 834063 -> 817862 bytes
 output/collage-politics.png                    | Bin 588463 -> 569529 bytes
 output/collage-sports.png                      | Bin 355659 -> 334063 bytes
 output/collage-tv.png                          | Bin 169526 -> 148098 bytes
 scripts/build-collages.py                      |  58 ++++++++++++++++---------
 server.js                                      |   8 ++--
 8 files changed, 43 insertions(+), 23 deletions(-)

diff --git a/output/collage-declaration-of-independence.png b/output/collage-declaration-of-independence.png
index 168520e..0cb1865 100644
Binary files a/output/collage-declaration-of-independence.png and b/output/collage-declaration-of-independence.png differ
diff --git a/output/collage-hollywood.png b/output/collage-hollywood.png
index 3def647..715d8b5 100644
Binary files a/output/collage-hollywood.png and b/output/collage-hollywood.png differ
diff --git a/output/collage-movies-classic.png b/output/collage-movies-classic.png
index dd91d64..482b3af 100644
Binary files a/output/collage-movies-classic.png and b/output/collage-movies-classic.png differ
diff --git a/output/collage-politics.png b/output/collage-politics.png
index ccf1f54..2b8fe1d 100644
Binary files a/output/collage-politics.png and b/output/collage-politics.png differ
diff --git a/output/collage-sports.png b/output/collage-sports.png
index d02524b..2a03871 100644
Binary files a/output/collage-sports.png and b/output/collage-sports.png differ
diff --git a/output/collage-tv.png b/output/collage-tv.png
index cf4e5b5..d71ac13 100644
Binary files a/output/collage-tv.png and b/output/collage-tv.png differ
diff --git a/scripts/build-collages.py b/scripts/build-collages.py
index 40648dd..19c8616 100644
--- a/scripts/build-collages.py
+++ b/scripts/build-collages.py
@@ -137,10 +137,18 @@ def fit(im):
     return im.resize((max(1, int(im.width * r)), max(1, int(im.height * r))), Image.LANCZOS)
 
 
-def make_qr_cell(data):
-    """Render the QR as a signature-sized square (BOX_H tall, uniform with the
-    signature cells) so it reads as 'just another signature' in the grid — no
-    bigger or more prominent than the rest."""
+# Single-letter QR codes (mirror server.js SHORT map) → short URL → low module
+# count → reliably scannable at a modest size, even printed and viewed at distance.
+CODE = {
+    "declaration-of-independence": "d", "politics": "p", "sports": "s",
+    "hollywood": "h", "movies-classic": "c", "tv": "t",
+}
+QR_PX = 180   # functional utility-mark size (~7px/module on the short /k/<code> URL)
+
+
+def make_qr(data, target_px):
+    """Render a crisp QR for `data`, scaled to ~target_px with integer px-per-module
+    (NEAREST) so the modules stay razor-sharp — critical for camera scanning."""
     qr = qrcode.QRCode(
         error_correction=qrcode.constants.ERROR_CORRECT_M,  # ~15% damage tolerance
         box_size=10, border=2,                              # built-in quiet zone
@@ -148,7 +156,10 @@ def make_qr_cell(data):
     qr.add_data(data)
     qr.make(fit=True)
     img = qr.make_image(fill_color="black", back_color="white").convert("RGB")
-    return img.resize((BOX_H, BOX_H), Image.LANCZOS)        # square, signature-sized
+    modules = qr.modules_count + 2 * qr.border
+    box = max(4, round(target_px / modules))
+    side = box * modules
+    return img.resize((side, side), Image.NEAREST)
 
 
 def render(category, cells, dropped, named, safe):
@@ -190,6 +201,26 @@ def render(category, cells, dropped, named, safe):
             tw = d.textlength(label, font=f)
             d.text((cx + (CELL_W - tw) / 2, cy + PAD + BOX_H + 4), label, font=f, fill=MUTED)
 
+    # centred QR (no-names mural only) — a functional utility mark (not a signature),
+    # sized to actually scan in print; encodes the short /k/<code> redirect URL.
+    if not named:
+        grid_h = rows_ct * cell_h
+        qr = make_qr(f"{BASE_URL}/k/{CODE.get(safe, safe)}", QR_PX)
+        cf = font(14, bold=True)
+        cap = "SCAN TO REVEAL NAMES"
+        cap_w = int(d.textlength(cap, font=cf))
+        pad_in, gap, cap_h = 16, 10, 18
+        panel_w = max(qr.width, cap_w) + pad_in * 2
+        panel_h = qr.height + gap + cap_h + pad_in * 2
+        px = (W - panel_w) // 2
+        py = HEADER_H + (grid_h - panel_h) // 2
+        d.rectangle([px + 5, py + 6, px + panel_w + 5, py + panel_h + 6], fill=(214, 210, 202))  # soft shadow
+        d.rectangle([px, py, px + panel_w, py + panel_h], fill=(255, 255, 255), outline=(150, 146, 138), width=2)
+        qx = px + (panel_w - qr.width) // 2
+        qy = py + pad_in
+        canvas.paste(qr, (qx, qy))
+        d.text((px + (panel_w - cap_w) / 2, qy + qr.height + gap), cap, font=cf, fill=INK)
+
     # footer
     d.text((PAD + 8, H - FOOTER_H + 14),
            "Uniform sizing — no signature emphasized. Deceased or public-domain only; living-celebrity rows excluded (right of publicity).",
@@ -222,22 +253,9 @@ def build(category, rows):
         print(f"[{category}] no images resolved, skipping"); return []
 
     safe = re.sub(r"[^a-z0-9]+", "-", category.lower()).strip("-")
-
-    # The mural gets the QR dropped in as just another (nameless) signature cell,
-    # centred in the grid — same size, same placement as every signature. It links
-    # to the named "key" version. render() uses the same cols formula, so inserting
-    # at this index lands the QR in the middle row/column of the laid-out grid.
-    qr_url = f"{BASE_URL}/k/{safe}"          # short → low-density → scans at sig size
-    qr_cell = ("", make_qr_cell(qr_url))
-    total = len(cells) + 1
-    cols = min(10, max(1, round(total ** 0.5)))
-    rows_ct = (total + cols - 1) // cols
-    center = min((rows_ct // 2) * cols + cols // 2, total - 1)
-    mural_cells = cells[:center] + [qr_cell] + cells[center:]
-
     made = [
-        render(category, cells, dropped, named=True, safe=safe),         # the key (QR target)
-        render(category, mural_cells, dropped, named=False, safe=safe),  # the mural (QR = a cell)
+        render(category, cells, dropped, named=True, safe=safe),    # the key (QR target)
+        render(category, cells, dropped, named=False, safe=safe),   # the mural (centre QR)
     ]
     return made
 
diff --git a/server.js b/server.js
index 610ebf6..0812131 100644
--- a/server.js
+++ b/server.js
@@ -19,10 +19,12 @@ createServer(async (req, res) => {
     let path = decodeURIComponent(req.url.split('?')[0]);
     if (path === '/') path = '/public/index.html';
     if (path === '/api/signatures') path = '/data/celebrity_signatures.json';
-    // Short QR target: /k/<cat> -> the named "key" mural. A short encoded URL keeps
-    // the signature-sized QR low-density so it scans cleanly off a printed wall.
+    // Short QR target: /k/<code> -> the named "key" mural. Single-letter codes keep
+    // the encoded URL tiny so the centre QR stays low-density and scans cleanly off a
+    // printed wall. Full-slug (/k/<cat>) still works as a fallback.
+    const SHORT = { d: 'declaration-of-independence', p: 'politics', s: 'sports', h: 'hollywood', c: 'movies-classic', t: 'tv' };
     const k = path.match(/^\/k\/([a-z0-9-]+)$/);
-    if (k) path = `/output/collage-${k[1]}-named.png`;
+    if (k) path = `/output/collage-${SHORT[k[1]] || k[1]}-named.png`;
     const file = join(ROOT, path);
     if (!file.startsWith(ROOT)) { res.writeHead(403).end('forbidden'); return; }
     const body = await readFile(file);

← ac66a5d QR points at public Kamatera host via short /k/<cat> route;  ·  back to CelebritySignatures  ·  Murals storefront: /murals sales page + wall-placement studi 9111112 →