← back to Model Arena
Agent-driven tool: DW Sample Box (frontend-developer) — sample-box builder, fly-to-slot, curate, request flow. Registered.
105d0ec2715028eab7c454b7e8b1e5551300cbb6 · 2026-07-25 13:16:35 -0700 · Steve Abrams
Files touched
M data/arcade-games.jsonA public/games/dw-sample-box.htmlA public/games/dw-the-big-reveal.html
Diff
commit 105d0ec2715028eab7c454b7e8b1e5551300cbb6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Jul 25 13:16:35 2026 -0700
Agent-driven tool: DW Sample Box (frontend-developer) — sample-box builder, fly-to-slot, curate, request flow. Registered.
---
data/arcade-games.json | 7 +
public/games/dw-sample-box.html | 728 ++++++++++++++++++++++++++++
public/games/dw-the-big-reveal.html | 928 ++++++++++++++++++++++++++++++++++++
3 files changed, 1663 insertions(+)
diff --git a/data/arcade-games.json b/data/arcade-games.json
index 63cc757..dd8a6dd 100644
--- a/data/arcade-games.json
+++ b/data/arcade-games.json
@@ -195,6 +195,13 @@
"credit": "frontend-developer",
"category": "Art Toys",
"note": "Agent-built toy \u2014 color a hand-drawn toile pastoral scene (50+ regions), designer palette, toile-classic preset, PNG"
+ },
+ {
+ "slug": "dw-sample-box",
+ "title": "DW Sample Box",
+ "credit": "frontend-developer",
+ "category": "Tools",
+ "note": "Agent-built commerce flow \u2014 build a sample box from 12 swatches, fly-to-slot, curate, request-your-box"
}
]
}
\ No newline at end of file
diff --git a/public/games/dw-sample-box.html b/public/games/dw-sample-box.html
new file mode 100644
index 0000000..0eaf579
--- /dev/null
+++ b/public/games/dw-sample-box.html
@@ -0,0 +1,728 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Sample Box — Designer Wallcoverings</title>
+<style>
+:root{
+ --bg:#f4f1ea;--bg2:#edeade;--ink:#1c1a17;--ink2:#4a4540;--ink3:#7a736b;
+ --accent:#c9a961;--accent2:#a8853a;--white:#fffdf8;--red:#c0392b;
+ --serif:'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif;
+ --sans:'Inter','Helvetica Neue',Arial,sans-serif;
+ --ease:cubic-bezier(.4,0,.2,1);
+ --r:6px;--shadow:0 4px 24px rgba(28,26,23,.13);
+}
+*{box-sizing:border-box;margin:0;padding:0}
+body{background:var(--bg);color:var(--ink);font-family:var(--sans);min-height:100vh;overflow-x:hidden}
+
+/* HEADER */
+header{
+ background:var(--white);border-bottom:1px solid rgba(201,169,97,.3);
+ padding:18px 32px 14px;display:flex;align-items:center;justify-content:space-between;
+ position:sticky;top:0;z-index:200;
+}
+.wordmark{
+ font-family:var(--serif);font-size:clamp(22px,3vw,40px);font-weight:400;
+ text-transform:uppercase;letter-spacing:.18em;color:var(--ink);line-height:1;
+}
+.wordmark span{display:block;font-size:.38em;letter-spacing:.32em;color:var(--accent);margin-top:2px;font-weight:400}
+.box-counter-pill{
+ display:flex;align-items:center;gap:8px;background:var(--ink);color:var(--white);
+ border-radius:24px;padding:7px 18px;font-size:.82rem;letter-spacing:.06em;cursor:pointer;
+ transition:background .2s var(--ease);font-family:var(--sans);
+}
+.box-counter-pill:hover{background:var(--accent2)}
+.pill-icon{font-size:1rem}
+
+/* LAYOUT */
+main{display:grid;grid-template-columns:1fr 340px;gap:0;min-height:calc(100vh - 73px)}
+@media(max-width:860px){main{grid-template-columns:1fr;}}
+
+/* CATALOG SIDE */
+.catalog-side{padding:28px 28px 40px;border-right:1px solid rgba(28,26,23,.09)}
+.catalog-header{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:20px}
+.catalog-title{font-family:var(--serif);font-size:2rem;font-weight:400;letter-spacing:.04em}
+.filters{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:20px}
+.filter-btn{
+ padding:5px 14px;border-radius:20px;border:1px solid rgba(28,26,23,.25);
+ background:transparent;color:var(--ink2);font-size:.78rem;letter-spacing:.05em;
+ cursor:pointer;transition:all .18s var(--ease);font-family:var(--sans);
+}
+.filter-btn.active,.filter-btn:hover{background:var(--ink);color:var(--white);border-color:var(--ink)}
+.filter-btn.color-filter.active{background:var(--accent);border-color:var(--accent);color:var(--ink)}
+
+/* SWATCH GRID */
+.swatch-grid{
+ display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:16px;
+ transition:opacity .2s var(--ease);
+}
+.swatch-card{
+ background:var(--white);border-radius:var(--r);box-shadow:0 1px 6px rgba(28,26,23,.08);
+ overflow:hidden;cursor:pointer;transition:transform .22s var(--ease),box-shadow .22s var(--ease);
+ position:relative;
+}
+.swatch-card:hover{transform:translateY(-4px) scale(1.015);box-shadow:var(--shadow)}
+.swatch-card.in-box .swatch-img::after{
+ content:'✓';position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
+ font-size:2rem;color:var(--white);background:rgba(201,169,97,.55);
+}
+.swatch-img{
+ width:100%;height:130px;position:relative;overflow:hidden;
+}
+.swatch-img svg{width:100%;height:100%;display:block}
+.swatch-info{padding:10px 11px 12px}
+.swatch-name{font-family:var(--serif);font-size:1.05rem;font-weight:400;letter-spacing:.02em;color:var(--ink);margin-bottom:2px}
+.swatch-colorway{font-size:.72rem;color:var(--ink3);letter-spacing:.06em;margin-bottom:6px;text-transform:uppercase}
+.swatch-tags{display:flex;gap:4px;flex-wrap:wrap}
+.tag{
+ font-size:.65rem;letter-spacing:.07em;text-transform:uppercase;
+ padding:2px 7px;border-radius:10px;border:1px solid rgba(28,26,23,.18);color:var(--ink2);
+}
+.tag.material{background:rgba(201,169,97,.12);border-color:rgba(201,169,97,.4);color:var(--accent2)}
+.add-btn{
+ position:absolute;bottom:42px;right:8px;
+ width:28px;height:28px;border-radius:50%;background:var(--accent);border:none;
+ color:var(--ink);font-size:1.1rem;display:flex;align-items:center;justify-content:center;
+ cursor:pointer;opacity:0;transition:opacity .18s;box-shadow:0 2px 8px rgba(0,0,0,.18);
+ line-height:1;
+}
+.swatch-card:hover .add-btn{opacity:1}
+.swatch-card.in-box .add-btn{opacity:1;background:var(--ink3);color:var(--white);font-size:.8rem}
+
+/* BOX SIDE */
+.box-side{
+ background:var(--bg2);padding:24px 20px 32px;position:sticky;top:73px;
+ height:calc(100vh - 73px);overflow-y:auto;display:flex;flex-direction:column;gap:18px;
+}
+@media(max-width:860px){.box-side{position:static;height:auto;border-top:2px solid rgba(201,169,97,.3)}}
+.box-side-title{font-family:var(--serif);font-size:1.5rem;font-weight:400;letter-spacing:.05em}
+.box-subtitle{font-size:.77rem;color:var(--ink3);letter-spacing:.05em}
+
+/* SVG BOX ILLUSTRATION */
+.box-illustration{width:100%;max-width:300px;margin:0 auto;display:block}
+
+/* SLOTS GRID */
+.slots-grid{
+ display:grid;grid-template-columns:repeat(4,1fr);gap:8px;
+}
+.slot{
+ aspect-ratio:1;border-radius:5px;border:1.5px dashed rgba(28,26,23,.25);
+ background:rgba(255,253,248,.6);overflow:hidden;position:relative;
+ transition:border-color .18s,transform .18s var(--ease);cursor:default;
+}
+.slot.filled{border-style:solid;border-color:rgba(201,169,97,.5);cursor:pointer}
+.slot.filled:hover{transform:scale(1.06);border-color:var(--accent)}
+.slot.filled::after{
+ content:'✕';position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
+ font-size:1rem;color:var(--white);background:rgba(28,26,23,.45);
+ opacity:0;transition:opacity .15s;
+}
+.slot.filled:hover::after{opacity:1}
+.slot svg{width:100%;height:100%;display:block}
+.slot-pop{animation:slotpop .35s var(--ease)}
+@keyframes slotpop{0%{transform:scale(0.5)}60%{transform:scale(1.12)}100%{transform:scale(1)}}
+
+/* BOX SUMMARY */
+.box-summary{
+ background:var(--white);border-radius:var(--r);padding:14px 16px;
+ box-shadow:0 1px 4px rgba(28,26,23,.06);
+}
+.summary-row{display:flex;justify-content:space-between;align-items:center;font-size:.82rem;margin-bottom:4px}
+.summary-row.total{font-family:var(--serif);font-size:1.1rem;border-top:1px solid rgba(28,26,23,.1);padding-top:8px;margin-top:4px}
+.summary-count{color:var(--ink3)}
+
+.box-actions{display:flex;flex-direction:column;gap:8px}
+.btn-primary{
+ width:100%;padding:13px;border-radius:var(--r);border:none;
+ background:var(--ink);color:var(--white);font-family:var(--serif);
+ font-size:1.05rem;letter-spacing:.08em;cursor:pointer;
+ transition:background .2s var(--ease);
+}
+.btn-primary:hover:not(:disabled){background:var(--accent2);color:var(--ink)}
+.btn-primary:disabled{opacity:.4;cursor:not-allowed}
+.btn-secondary{
+ width:100%;padding:10px;border-radius:var(--r);border:1.5px solid rgba(28,26,23,.25);
+ background:transparent;color:var(--ink2);font-size:.82rem;letter-spacing:.06em;
+ cursor:pointer;transition:all .18s var(--ease);font-family:var(--sans);
+}
+.btn-secondary:hover{border-color:var(--accent);color:var(--accent2)}
+.btn-clear{
+ width:100%;padding:8px;border:none;background:transparent;
+ color:var(--ink3);font-size:.75rem;cursor:pointer;letter-spacing:.04em;
+ text-decoration:underline;text-underline-offset:3px;
+}
+.btn-clear:hover{color:var(--red)}
+
+/* FLY ANIMATION */
+.fly-swatch{
+ position:fixed;border-radius:4px;overflow:hidden;z-index:9999;
+ pointer-events:none;box-shadow:0 4px 16px rgba(0,0,0,.3);
+ transition:all .55s cubic-bezier(.4,.1,.2,1);
+}
+
+/* CONFIRM OVERLAY */
+.overlay{
+ position:fixed;inset:0;background:rgba(28,26,23,.55);z-index:1000;
+ display:flex;align-items:center;justify-content:center;padding:20px;
+ visibility:hidden;opacity:0;transition:opacity .25s var(--ease),visibility .25s;
+}
+.overlay.open{visibility:visible;opacity:1}
+.overlay-card{
+ background:var(--white);border-radius:10px;max-width:520px;width:100%;
+ padding:36px 32px;box-shadow:0 20px 60px rgba(0,0,0,.22);
+ transform:translateY(20px);transition:transform .3s var(--ease);
+ max-height:90vh;overflow-y:auto;
+}
+.overlay.open .overlay-card{transform:translateY(0)}
+.overlay-title{font-family:var(--serif);font-size:2rem;font-weight:400;letter-spacing:.05em;margin-bottom:4px}
+.overlay-sub{color:var(--ink3);font-size:.82rem;margin-bottom:24px;letter-spacing:.03em}
+.overlay-samples{display:grid;grid-template-columns:repeat(auto-fill,minmax(70px,1fr));gap:10px;margin-bottom:24px}
+.overlay-sample{text-align:center}
+.overlay-swatch{width:100%;aspect-ratio:1;border-radius:4px;overflow:hidden;margin-bottom:4px;box-shadow:0 1px 4px rgba(0,0,0,.12)}
+.overlay-swatch svg{width:100%;height:100%;display:block}
+.overlay-sample-name{font-size:.65rem;color:var(--ink2);letter-spacing:.04em;line-height:1.3}
+.overlay-total{font-family:var(--serif);font-size:1.15rem;border-top:1px solid rgba(28,26,23,.12);padding-top:14px;margin-bottom:20px;display:flex;justify-content:space-between}
+.thankyou{text-align:center;padding:16px 0 8px;display:none}
+.thankyou-title{font-family:var(--serif);font-size:2.2rem;font-weight:400;letter-spacing:.06em;margin-bottom:8px;color:var(--accent2)}
+.thankyou-msg{color:var(--ink2);font-size:.88rem;line-height:1.6;letter-spacing:.02em}
+.overlay-close{position:absolute;top:14px;right:18px;background:none;border:none;font-size:1.4rem;cursor:pointer;color:var(--ink3)}
+.overlay-card{position:relative}
+
+/* HIDDEN UTILITY */
+.hidden{display:none!important}
+
+/* FILTER TRANSITION */
+.swatch-card{transition:transform .22s var(--ease),box-shadow .22s var(--ease),opacity .25s,max-height .25s}
+.swatch-card.filtered-out{opacity:0;pointer-events:none;transform:scale(.96)}
+
+/* EMPTY STATE */
+.box-empty{text-align:center;padding:16px 8px;color:var(--ink3);font-size:.82rem;line-height:1.6;letter-spacing:.03em}
+</style>
+</head>
+<body>
+
+<header>
+ <div class="wordmark">
+ Designer Wallcoverings
+ <span>Sample Box</span>
+ </div>
+ <button class="box-counter-pill" onclick="scrollToBox()" aria-label="View sample box">
+ <span class="pill-icon">■</span>
+ <span id="pill-count">0 of 8 samples</span>
+ </button>
+</header>
+
+<main>
+ <!-- CATALOG -->
+ <section class="catalog-side">
+ <div class="catalog-header">
+ <h1 class="catalog-title">Our Swatches</h1>
+ </div>
+ <div class="filters" role="group" aria-label="Filter swatches">
+ <button class="filter-btn active" data-filter="all">All</button>
+ <button class="filter-btn" data-filter="mat-woven">Woven</button>
+ <button class="filter-btn" data-filter="mat-paper">Paper</button>
+ <button class="filter-btn" data-filter="mat-grass">Grasscloth</button>
+ <button class="filter-btn" data-filter="mat-silk">Silk</button>
+ <span style="width:1px;background:rgba(28,26,23,.15);margin:0 4px"></span>
+ <button class="filter-btn color-filter" data-filter="col-neutral">Neutral</button>
+ <button class="filter-btn color-filter" data-filter="col-warm">Warm</button>
+ <button class="filter-btn color-filter" data-filter="col-cool">Cool</button>
+ <button class="filter-btn color-filter" data-filter="col-dark">Dark</button>
+ </div>
+ <div class="swatch-grid" id="swatch-grid"></div>
+ </section>
+
+ <!-- SAMPLE BOX -->
+ <aside class="box-side" id="box-side">
+ <div>
+ <h2 class="box-side-title">Your Sample Box</h2>
+ <p class="box-subtitle">Up to 8 samples · $4.25 each</p>
+ </div>
+
+ <!-- Inline SVG memo box -->
+ <svg class="box-illustration" viewBox="0 0 280 120" aria-hidden="true">
+ <!-- box body -->
+ <rect x="20" y="40" width="240" height="70" rx="6" fill="#d4c9b0" stroke="#b8a87a" stroke-width="1.5"/>
+ <!-- box lid (open, lifted back) -->
+ <path d="M20 44 Q20 10 140 10 Q260 10 260 44" fill="#e8dfc8" stroke="#b8a87a" stroke-width="1.5"/>
+ <path d="M20 44 Q20 18 140 18 Q260 18 260 44" fill="none" stroke="#c9b88a" stroke-width=".8" opacity=".6"/>
+ <!-- interior -->
+ <rect x="28" y="48" width="224" height="54" rx="3" fill="#ede3cc"/>
+ <!-- ribbon accent -->
+ <rect x="128" y="48" width="24" height="54" fill="rgba(201,169,97,.25)"/>
+ <line x1="140" y1="48" x2="140" y2="102" stroke="#c9a961" stroke-width="1" opacity=".5"/>
+ <!-- brand text on lid -->
+ <text x="140" y="30" text-anchor="middle" font-family="Georgia,serif" font-size="7" fill="#8b6f2e" letter-spacing="2.5">DESIGNER WALLCOVERINGS</text>
+ </svg>
+
+ <div class="slots-grid" id="slots-grid"></div>
+
+ <div class="box-summary">
+ <div class="summary-row">
+ <span>Samples selected</span>
+ <span class="summary-count" id="summary-count">0 of 8</span>
+ </div>
+ <div class="summary-row">
+ <span>Price per sample</span>
+ <span>$4.25</span>
+ </div>
+ <div class="summary-row total">
+ <span>Subtotal</span>
+ <span id="summary-total">$0.00</span>
+ </div>
+ </div>
+
+ <div class="box-actions">
+ <button class="btn-primary" id="request-btn" disabled onclick="openOverlay()">Request Your Box</button>
+ <button class="btn-secondary" onclick="surpriseMe()">Curate a Box for Me</button>
+ <button class="btn-clear" onclick="clearBox()">Clear box</button>
+ </div>
+
+ <div id="box-empty-msg" class="box-empty">
+ Browse the catalog and add up to 8 samples.<br>Each arrives hand-wrapped within 5–7 days.
+ </div>
+ </aside>
+</main>
+
+<!-- CONFIRM OVERLAY -->
+<div class="overlay" id="overlay" role="dialog" aria-modal="true" aria-labelledby="overlay-title">
+ <div class="overlay-card">
+ <button class="overlay-close" onclick="closeOverlay()" aria-label="Close">×</button>
+ <div id="overlay-form">
+ <h2 class="overlay-title" id="overlay-title">Your Sample Box</h2>
+ <p class="overlay-sub">A curated selection from Designer Wallcoverings</p>
+ <div class="overlay-samples" id="overlay-samples"></div>
+ <div class="overlay-total"><span>Subtotal</span><span id="overlay-total-val"></span></div>
+ <button class="btn-primary" onclick="submitBox()">Confirm & Request Samples</button>
+ </div>
+ <div class="thankyou" id="thankyou">
+ <div class="thankyou-title">Thank You</div>
+ <p class="thankyou-msg">
+ Your sample box has been received.<br>
+ Expect your hand-wrapped selection within 5–7 business days.<br><br>
+ <em style="font-family:var(--serif);font-size:1.1rem;color:var(--accent2)">— Designer Wallcoverings</em>
+ </p>
+ </div>
+ </div>
+</div>
+
+<script>
+// ── SWATCH DEFINITIONS ──────────────────────────────────────────────────────
+const SWATCHES = [
+ {
+ id:'s1',name:'Florentine Damask',colorway:'Ivory & Champagne',material:'woven',
+ matTag:'Woven Textile',color:'neutral',
+ tags:['Damask','Traditional'],
+ svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+ <rect width="100" height="100" fill="#f5eed8"/>
+ <g opacity=".22" fill="#8b6f2e">
+ ${Array.from({length:9},(_, i)=>{
+ const cx=17+((i%3)*33), cy=17+Math.floor(i/3)*33;
+ return `<ellipse cx="${cx}" cy="${cy}" rx="10" ry="14"/>
+ <ellipse cx="${cx}" cy="${cy}" rx="5" ry="8"/>
+ <circle cx="${cx}" cy="${cy}" r="3"/>
+ <line x1="${cx-10}" y1="${cy}" x2="${cx+10}" y2="${cy}" stroke="#8b6f2e" stroke-width="1"/>
+ <line x1="${cx}" y1="${cy-14}" x2="${cx}" y2="${cy+14}" stroke="#8b6f2e" stroke-width="1"/>`;
+ }).join('')}
+ </g>
+ </svg>`
+ },
+ {
+ id:'s2',name:'Pastoral Toile',colorway:'Wedgwood Blue',material:'paper',
+ matTag:'Paper',color:'cool',
+ tags:['Toile','Pastoral'],
+ svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+ <rect width="100" height="100" fill="#dce8f0"/>
+ <g stroke="#3a6080" fill="none" stroke-width=".8" opacity=".7">
+ <ellipse cx="30" cy="30" rx="12" ry="16"/><ellipse cx="30" cy="30" rx="6" ry="9"/>
+ <path d="M18 44 Q24 38 30 44 Q36 38 42 44"/>
+ <path d="M22 22 Q30 14 38 22"/>
+ <ellipse cx="70" cy="65" rx="14" ry="10"/><ellipse cx="70" cy="65" rx="7" ry="5"/>
+ <path d="M60 58 Q65 52 70 56"/>
+ <path d="M56 75 Q62 80 68 75 Q74 80 80 75"/>
+ <circle cx="50" cy="20" r="3"/><circle cx="15" cy="68" r="4"/>
+ <path d="M46 20 Q50 12 54 20" stroke-width=".6"/>
+ </g>
+ </svg>`
+ },
+ {
+ id:'s3',name:'Sea Island Grasscloth',colorway:'Natural Flax',material:'grass',
+ matTag:'Grasscloth',color:'neutral',
+ tags:['Texture','Natural'],
+ svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+ <rect width="100" height="100" fill="#c8b48a"/>
+ ${Array.from({length:28},(_,i)=>`<line x1="0" y1="${i*3.6+1}" x2="100" y2="${i*3.6+1}" stroke="#9a8260" stroke-width="${.4+Math.random()*.5}" opacity="${.3+Math.random()*.5}"/>`).join('')}
+ ${Array.from({length:12},(_,i)=>`<line x1="${i*8+1}" y1="0" x2="${i*8+1}" y2="100" stroke="#b0926a" stroke-width=".6" opacity=".2"/>`).join('')}
+ </svg>`
+ },
+ {
+ id:'s4',name:'Ming Dynasty Chinoiserie',colorway:'Rouge & Gold',material:'paper',
+ matTag:'Paper',color:'warm',
+ tags:['Chinoiserie','Scenic'],
+ svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+ <rect width="100" height="100" fill="#f7e8d2"/>
+ <g fill="none">
+ <path d="M20 70 Q25 55 30 60 Q35 50 40 55 Q45 45 50 50" stroke="#c0392b" stroke-width="1.2"/>
+ <circle cx="22" cy="72" r="4" fill="#c0392b" opacity=".7"/>
+ <circle cx="48" cy="52" r="3" fill="#c9a961" opacity=".8"/>
+ <path d="M55 30 Q60 20 65 25 Q70 15 75 20 Q80 10 85 15" stroke="#2e7d32" stroke-width="1"/>
+ <ellipse cx="60" cy="32" rx="4" ry="6" fill="#388e3c" opacity=".6"/>
+ <ellipse cx="78" cy="17" rx="3" ry="5" fill="#2e7d32" opacity=".6"/>
+ <path d="M30 10 L36 20 L30 18 L36 28" stroke="#c9a961" stroke-width="1.2" fill="none"/>
+ <rect x="27" y="22" width="9" height="7" rx="1" stroke="#c9a961" stroke-width=".8"/>
+ <path d="M10 50 Q15 45 20 48 Q25 43 30 46" stroke="#8b4513" stroke-width=".8" opacity=".5"/>
+ </g>
+ </svg>`
+ },
+ {
+ id:'s5',name:'Marrakech Trellis',colorway:'Ebony & Cream',material:'woven',
+ matTag:'Woven Textile',color:'dark',
+ tags:['Geometric','Trellis'],
+ svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+ <rect width="100" height="100" fill="#1c1a17"/>
+ ${Array.from({length:6},(_,row)=>Array.from({length:6},(_,col)=>{
+ const cx=8+col*16, cy=8+row*16;
+ return `<path d="M${cx} ${cy-7} L${cx+7} ${cy} L${cx} ${cy+7} L${cx-7} ${cy} Z" fill="none" stroke="#c9a961" stroke-width=".9"/>
+ <circle cx="${cx}" cy="${cy}" r="1.5" fill="#c9a961" opacity=".5"/>`;
+ }).join('')).join('')}
+ </svg>`
+ },
+ {
+ id:'s6',name:'Riviera Awning Stripe',colorway:'Navy & Ecru',material:'woven',
+ matTag:'Woven Textile',color:'cool',
+ tags:['Stripe','Coastal'],
+ svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+ <rect width="100" height="100" fill="#f0ece0"/>
+ ${[0,14,28,42,56,70,84].map(x=>`<rect x="${x}" y="0" width="7" height="100" fill="#1a2e4a" opacity=".85"/>`).join('')}
+ ${[7,21,35,49,63,77,91].map(x=>`<rect x="${x}" y="0" width="2" height="100" fill="#c9a961" opacity=".4"/>`).join('')}
+ </svg>`
+ },
+ {
+ id:'s7',name:'Silk Moiré',colorway:'Champagne',material:'silk',
+ matTag:'Silk',color:'neutral',
+ tags:['Moiré','Luxe'],
+ svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+ <defs>
+ <linearGradient id="moire-g" x1="0%" y1="0%" x2="100%" y2="100%">
+ <stop offset="0%" stop-color="#f0e6c8"/>
+ <stop offset="50%" stop-color="#e8d5a0"/>
+ <stop offset="100%" stop-color="#f5edd0"/>
+ </linearGradient>
+ </defs>
+ <rect width="100" height="100" fill="url(#moire-g)"/>
+ ${Array.from({length:20},(_,i)=>`<path d="M0 ${i*5} Q50 ${i*5+(i%2?3:-3)} 100 ${i*5}" fill="none" stroke="#c9a961" stroke-width=".4" opacity="${.15+.1*(i%3)}"/>`).join('')}
+ ${Array.from({length:20},(_,i)=>`<path d="M${i*5} 0 Q${i*5+(i%2?3:-3)} 50 ${i*5} 100" fill="none" stroke="#b8953a" stroke-width=".3" opacity=".12"/>`).join('')}
+ </svg>`
+ },
+ {
+ id:'s8',name:'Belgian Linen',colorway:'Warm Greige',material:'woven',
+ matTag:'Woven Textile',color:'neutral',
+ tags:['Linen','Texture'],
+ svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+ <rect width="100" height="100" fill="#d8cdb8"/>
+ ${Array.from({length:34},(_,i)=>`<line x1="0" y1="${i*3}" x2="100" y2="${i*3}" stroke="#b8a880" stroke-width="${.6+.4*(i%3==0?1:0)}" opacity="${.4+.3*(i%5==0?1:0)}"/>`).join('')}
+ ${Array.from({length:34},(_,i)=>`<line x1="${i*3}" y1="0" x2="${i*3}" y2="100" stroke="#c0b090" stroke-width="${.5+.3*(i%3==0?1:0)}" opacity="${.35+.25*(i%5==0?1:0)}"/>`).join('')}
+ </svg>`
+ },
+ {
+ id:'s9',name:'Jardin Botanique',colorway:'Forest & Ivory',material:'paper',
+ matTag:'Paper',color:'cool',
+ tags:['Botanical','Scenic'],
+ svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+ <rect width="100" height="100" fill="#e8edd8"/>
+ <g fill="none">
+ <path d="M50 90 Q50 60 50 40" stroke="#3a5a30" stroke-width="1.5"/>
+ <path d="M50 70 Q35 58 28 48" stroke="#3a5a30" stroke-width="1"/>
+ <path d="M50 60 Q65 48 72 38" stroke="#3a5a30" stroke-width="1"/>
+ <ellipse cx="28" cy="45" rx="12" ry="8" fill="#5a7a40" opacity=".7" transform="rotate(-30,28,45)"/>
+ <ellipse cx="72" cy="35" rx="12" ry="8" fill="#4a6a35" opacity=".7" transform="rotate(20,72,35)"/>
+ <ellipse cx="50" cy="35" rx="9" ry="6" fill="#6a8a50" opacity=".6"/>
+ <circle cx="20" cy="25" r="6" fill="#8aaa60" opacity=".5"/>
+ <circle cx="80" cy="20" r="5" fill="#7a9a55" opacity=".5"/>
+ <path d="M15 55 Q22 45 30 52" stroke="#5a7a40" stroke-width=".8" opacity=".6"/>
+ <circle cx="55" cy="18" r="3" fill="#c9a961" opacity=".6"/>
+ <circle cx="35" cy="15" r="2.5" fill="#d4b870" opacity=".5"/>
+ </g>
+ </svg>`
+ },
+ {
+ id:'s10',name:'Bauhaus Geometric',colorway:'Terracotta & Slate',material:'paper',
+ matTag:'Paper',color:'warm',
+ tags:['Geometric','Modern'],
+ svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+ <rect width="100" height="100" fill="#e8e0d8"/>
+ ${Array.from({length:5},(_,row)=>Array.from({length:5},(_,col)=>{
+ const shapes=[
+ `<rect x="${col*20+2}" y="${row*20+2}" width="16" height="16" fill="#c45a30" opacity=".7"/>`,
+ `<circle cx="${col*20+10}" cy="${row*20+10}" r="8" fill="#556b7a" opacity=".6"/>`,
+ `<polygon points="${col*20+10},${row*20+2} ${col*20+18},${row*20+18} ${col*20+2},${row*20+18}" fill="#8b6f2e" opacity=".5"/>`,
+ `<rect x="${col*20+4}" y="${row*20+4}" width="12" height="12" fill="none" stroke="#c45a30" stroke-width="1.5" opacity=".7"/>`,
+ `<line x1="${col*20+2}" y1="${row*20+2}" x2="${col*20+18}" y2="${row*20+18}" stroke="#556b7a" stroke-width="2" opacity=".4"/>
+ <line x1="${col*20+18}" y1="${row*20+2}" x2="${col*20+2}" y2="${row*20+18}" stroke="#556b7a" stroke-width="2" opacity=".4"/>`,
+ ];
+ return shapes[(row*5+col)%5];
+ }).join('')).join('')}
+ </svg>`
+ },
+ {
+ id:'s11',name:'Dupioni Silk',colorway:'Midnight Indigo',material:'silk',
+ matTag:'Silk',color:'dark',
+ tags:['Silk','Luxe'],
+ svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+ <defs>
+ <linearGradient id="silk-g" x1="0%" y1="0%" x2="100%" y2="0%">
+ <stop offset="0%" stop-color="#1a1a3e"/>
+ <stop offset="45%" stop-color="#2a2a5e"/>
+ <stop offset="100%" stop-color="#1a1a3e"/>
+ </linearGradient>
+ </defs>
+ <rect width="100" height="100" fill="url(#silk-g)"/>
+ ${Array.from({length:25},(_,i)=>`<line x1="0" y1="${i*4}" x2="100" y2="${i*4+2}" stroke="#6060a0" stroke-width="${.5+(i%4==0?.5:0)}" opacity="${.25+.15*(i%3==0?1:0)}"/>`).join('')}
+ ${Array.from({length:12},(_,i)=>`<line x1="${i*9}" y1="0" x2="${i*9+3}" y2="100" stroke="#4040a0" stroke-width=".4" opacity=".15"/>`).join('')}
+ <rect width="100" height="100" fill="transparent" opacity=".1"/>
+ </svg>`
+ },
+ {
+ id:'s12',name:'Suzani Ikat',colorway:'Saffron & Crimson',material:'woven',
+ matTag:'Woven Textile',color:'warm',
+ tags:['Ikat','Global'],
+ svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+ <rect width="100" height="100" fill="#f0d090"/>
+ ${Array.from({length:4},(_,row)=>Array.from({length:4},(_,col)=>{
+ const cx=12+col*25, cy=12+row*25;
+ const r=['#c0392b','#8b0000','#c9a961','#8b4513'][(row+col)%4];
+ return `<ellipse cx="${cx}" cy="${cy}" rx="9" ry="11" fill="${r}" opacity=".7"/>
+ <ellipse cx="${cx}" cy="${cy}" rx="5" ry="7" fill="rgba(255,255,255,.25)"/>
+ <circle cx="${cx}" cy="${cy}" r="2" fill="rgba(255,255,255,.5)"/>`;
+ }).join('')).join('')}
+ </svg>`
+ }
+];
+
+const MAX=8;
+const PRICE=4.25;
+let box=[];
+let activeFilter='all';
+
+// ── STORAGE ──────────────────────────────────────────────────────────────────
+function saveBox(){try{localStorage.setItem('dw-sample-box',JSON.stringify(box))}catch(e){}}
+function loadBox(){
+ try{
+ const s=localStorage.getItem('dw-sample-box');
+ if(s){
+ const ids=JSON.parse(s);
+ box=ids.filter(id=>SWATCHES.find(sw=>sw.id===id));
+ }
+ }catch(e){}
+}
+
+// ── RENDER CATALOG ────────────────────────────────────────────────────────────
+function renderCatalog(){
+ const grid=document.getElementById('swatch-grid');
+ grid.innerHTML='';
+ SWATCHES.forEach(sw=>{
+ const inBox=box.includes(sw.id);
+ const card=document.createElement('div');
+ card.className='swatch-card'+(inBox?' in-box':'');
+ card.id='card-'+sw.id;
+ card.setAttribute('tabindex','0');
+ card.setAttribute('role','button');
+ card.setAttribute('aria-label',(inBox?'Remove ':'Add ')+sw.name+' to box');
+ card.setAttribute('aria-pressed',inBox?'true':'false');
+ card.innerHTML=`
+ <div class="swatch-img" id="simg-${sw.id}">${sw.svg}</div>
+ <div class="swatch-info">
+ <div class="swatch-name">${sw.name}</div>
+ <div class="swatch-colorway">${sw.colorway}</div>
+ <div class="swatch-tags">
+ <span class="tag material">${sw.matTag}</span>
+ ${sw.tags.map(t=>`<span class="tag">${t}</span>`).join('')}
+ </div>
+ </div>
+ <button class="add-btn" title="${inBox?'Remove from box':'Add to box'}" aria-hidden="true">
+ ${inBox?'✓':'+'}
+ </button>`;
+ card.addEventListener('click',()=>toggleSwatch(sw.id,card));
+ card.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();toggleSwatch(sw.id,card);}});
+ grid.appendChild(card);
+ applyFilter(card,sw);
+ });
+}
+
+// ── FILTER ────────────────────────────────────────────────────────────────────
+function applyFilter(card,sw){
+ const f=activeFilter;
+ if(f==='all'){card.classList.remove('filtered-out');return;}
+ const matMap={'mat-woven':'woven','mat-paper':'paper','mat-grass':'grass','mat-silk':'silk'};
+ const colMap={'col-neutral':'neutral','col-warm':'warm','col-cool':'cool','col-dark':'dark'};
+ let show=true;
+ if(matMap[f]) show=(sw.material===matMap[f]);
+ if(colMap[f]) show=(sw.color===colMap[f]);
+ card.classList.toggle('filtered-out',!show);
+}
+
+document.querySelectorAll('.filter-btn').forEach(btn=>{
+ btn.addEventListener('click',()=>{
+ document.querySelectorAll('.filter-btn').forEach(b=>b.classList.remove('active'));
+ btn.classList.add('active');
+ activeFilter=btn.dataset.filter;
+ SWATCHES.forEach(sw=>{
+ const card=document.getElementById('card-'+sw.id);
+ if(card) applyFilter(card,sw);
+ });
+ });
+});
+
+// ── SLOTS ─────────────────────────────────────────────────────────────────────
+function renderSlots(){
+ const grid=document.getElementById('slots-grid');
+ grid.innerHTML='';
+ for(let i=0;i<MAX;i++){
+ const slot=document.createElement('div');
+ slot.className='slot';
+ slot.id='slot-'+i;
+ const id=box[i];
+ if(id){
+ const sw=SWATCHES.find(s=>s.id===id);
+ slot.classList.add('filled');
+ slot.innerHTML=sw.svg;
+ slot.setAttribute('title','Click to remove '+sw.name);
+ slot.setAttribute('tabindex','0');
+ slot.setAttribute('role','button');
+ slot.setAttribute('aria-label','Remove '+sw.name+' from box');
+ slot.addEventListener('click',()=>removeFromBox(id));
+ slot.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();removeFromBox(id);}});
+ } else {
+ slot.innerHTML=`<svg viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg"><text x="20" y="25" text-anchor="middle" font-size="14" fill="rgba(28,26,23,.18)">${i+1}</text></svg>`;
+ }
+ grid.appendChild(slot);
+ }
+ updateUI();
+}
+
+function updateUI(){
+ const n=box.length;
+ document.getElementById('pill-count').textContent=n+' of '+MAX+' samples';
+ document.getElementById('summary-count').textContent=n+' of '+MAX;
+ document.getElementById('summary-total').textContent='$'+(n*PRICE).toFixed(2);
+ document.getElementById('request-btn').disabled=(n===0);
+ document.getElementById('box-empty-msg').style.display=(n===0?'block':'none');
+}
+
+// ── TOGGLE ────────────────────────────────────────────────────────────────────
+function toggleSwatch(id,card){
+ if(box.includes(id)){removeFromBox(id);}
+ else{addToBox(id,card);}
+}
+
+function addToBox(id,card){
+ if(box.includes(id)||box.length>=MAX) return;
+ const slot=document.getElementById('slot-'+box.length);
+ animateFly(card,slot,id);
+}
+
+function removeFromBox(id){
+ box=box.filter(b=>b!==id);
+ saveBox();
+ renderCatalog();
+ renderSlots();
+}
+
+function clearBox(){box=[];saveBox();renderCatalog();renderSlots();}
+
+// ── FLY ANIMATION ─────────────────────────────────────────────────────────────
+function animateFly(card,targetSlot,id){
+ const srcRect=card.querySelector('.swatch-img').getBoundingClientRect();
+ const sw=SWATCHES.find(s=>s.id===id);
+
+ const fly=document.createElement('div');
+ fly.className='fly-swatch';
+ fly.style.cssText=`left:${srcRect.left}px;top:${srcRect.top}px;width:${srcRect.width}px;height:${srcRect.height}px`;
+ fly.innerHTML=sw.svg;
+ document.body.appendChild(fly);
+
+ requestAnimationFrame(()=>{
+ requestAnimationFrame(()=>{
+ const tRect=targetSlot.getBoundingClientRect();
+ fly.style.left=tRect.left+'px';
+ fly.style.top=tRect.top+'px';
+ fly.style.width=tRect.width+'px';
+ fly.style.height=tRect.height+'px';
+ fly.style.opacity='.92';
+ fly.style.borderRadius='5px';
+ });
+ });
+
+ setTimeout(()=>{
+ document.body.removeChild(fly);
+ box.push(id);
+ saveBox();
+ // pop anim
+ targetSlot.classList.add('slot-pop');
+ setTimeout(()=>targetSlot.classList.remove('slot-pop'),400);
+ renderCatalog();
+ renderSlots();
+ },570);
+}
+
+// ── SURPRISE ME ───────────────────────────────────────────────────────────────
+function surpriseMe(){
+ box=[];
+ const shuffled=[...SWATCHES].sort(()=>Math.random()-.5);
+ box=shuffled.slice(0,MAX).map(s=>s.id);
+ saveBox();
+ renderCatalog();
+ renderSlots();
+}
+
+// ── OVERLAY ───────────────────────────────────────────────────────────────────
+function openOverlay(){
+ if(box.length===0) return;
+ document.getElementById('thankyou').style.display='none';
+ document.getElementById('overlay-form').style.display='block';
+ const samples=document.getElementById('overlay-samples');
+ samples.innerHTML=box.map(id=>{
+ const sw=SWATCHES.find(s=>s.id===id);
+ return `<div class="overlay-sample">
+ <div class="overlay-swatch">${sw.svg}</div>
+ <div class="overlay-sample-name">${sw.name}<br><em style="color:var(--ink3)">${sw.colorway}</em></div>
+ </div>`;
+ }).join('');
+ document.getElementById('overlay-total-val').textContent='$'+(box.length*PRICE).toFixed(2);
+ document.getElementById('overlay').classList.add('open');
+ document.getElementById('overlay').focus();
+}
+
+function closeOverlay(){
+ document.getElementById('overlay').classList.remove('open');
+}
+
+function submitBox(){
+ document.getElementById('overlay-form').style.display='none';
+ document.getElementById('thankyou').style.display='block';
+ setTimeout(()=>{
+ box=[];saveBox();renderCatalog();renderSlots();
+ },3200);
+ setTimeout(closeOverlay,4800);
+}
+
+document.getElementById('overlay').addEventListener('click',e=>{
+ if(e.target===document.getElementById('overlay')) closeOverlay();
+});
+document.addEventListener('keydown',e=>{
+ if(e.key==='Escape') closeOverlay();
+});
+
+function scrollToBox(){
+ document.getElementById('box-side').scrollIntoView({behavior:'smooth',block:'start'});
+}
+
+// ── INIT ──────────────────────────────────────────────────────────────────────
+loadBox();
+renderCatalog();
+renderSlots();
+</script>
+</body>
+</html>
diff --git a/public/games/dw-the-big-reveal.html b/public/games/dw-the-big-reveal.html
new file mode 100644
index 0000000..4c150ba
--- /dev/null
+++ b/public/games/dw-the-big-reveal.html
@@ -0,0 +1,928 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Designer Wallcoverings — The Big Reveal</title>
+<style>
+:root{
+ --bg:#0d0b09;
+ --surface:#1a1714;
+ --ink:#f4f1ea;
+ --ink-dim:#9a9080;
+ --accent:#c9a961;
+ --accent-dim:#8a6e38;
+ --serif:'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif;
+ --sans:system-ui,-apple-system,sans-serif;
+ --ease:cubic-bezier(.4,0,.2,1);
+ --radius:4px;
+}
+*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
+html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--ink);font-family:var(--sans)}
+body{display:flex;flex-direction:column}
+
+header{
+ flex:0 0 auto;
+ display:flex;align-items:center;justify-content:space-between;
+ padding:12px 20px 10px;
+ border-bottom:1px solid rgba(201,169,97,.18);
+ background:var(--bg);
+ z-index:10;
+}
+.wordmark{
+ font-family:var(--serif);
+ font-size:clamp(22px,3.5vw,42px);
+ font-weight:400;
+ letter-spacing:.18em;
+ text-transform:uppercase;
+ color:var(--accent);
+ line-height:1;
+ user-select:none;
+}
+.header-right{display:flex;align-items:center;gap:12px}
+
+button{
+ font-family:var(--sans);
+ font-size:12px;
+ letter-spacing:.08em;
+ text-transform:uppercase;
+ border:1px solid rgba(201,169,97,.4);
+ background:transparent;
+ color:var(--accent);
+ padding:6px 14px;
+ border-radius:var(--radius);
+ cursor:pointer;
+ transition:background .2s var(--ease),color .2s var(--ease);
+}
+button:hover{background:var(--accent);color:#0d0b09}
+button.muted{opacity:.45}
+
+#game-area{
+ flex:1 1 auto;
+ display:flex;
+ flex-direction:column;
+ align-items:center;
+ justify-content:center;
+ position:relative;
+ overflow:hidden;
+ padding:0 0 8px;
+}
+
+#wall-wrap{
+ position:relative;
+ width:min(680px,96vw);
+ height:min(460px,62vh);
+ border-radius:3px;
+ overflow:hidden;
+ box-shadow:0 8px 48px rgba(0,0,0,.7);
+ cursor:crosshair;
+ touch-action:none;
+}
+#canvas-reveal,#canvas-top{
+ position:absolute;
+ top:0;left:0;
+ width:100%;height:100%;
+}
+#canvas-top{pointer-events:none}
+
+/* HUD */
+#hud{
+ width:min(680px,96vw);
+ display:flex;align-items:center;gap:14px;
+ padding:10px 0 0;
+}
+#reveal-bar-wrap{
+ flex:1;
+ height:6px;
+ background:rgba(255,255,255,.08);
+ border-radius:3px;
+ overflow:hidden;
+}
+#reveal-bar{
+ height:100%;
+ width:0%;
+ background:linear-gradient(90deg,var(--accent-dim),var(--accent));
+ border-radius:3px;
+ transition:width .12s linear;
+}
+#pct-label{
+ font-family:var(--serif);
+ font-size:16px;
+ color:var(--accent);
+ min-width:42px;
+ text-align:right;
+}
+#score-label{
+ font-size:12px;
+ color:var(--ink-dim);
+ letter-spacing:.06em;
+ white-space:nowrap;
+}
+
+/* Timer */
+#timer-wrap{
+ position:absolute;
+ top:10px;right:12px;
+ font-family:var(--serif);
+ font-size:22px;
+ color:var(--accent);
+ pointer-events:none;
+ opacity:0;
+ transition:opacity .3s;
+}
+#timer-wrap.active{opacity:1}
+
+/* Completion overlay */
+#completion{
+ position:absolute;
+ inset:0;
+ background:rgba(13,11,9,.82);
+ display:flex;flex-direction:column;
+ align-items:center;justify-content:center;
+ gap:16px;
+ visibility:hidden;
+ opacity:0;
+ transition:opacity .4s var(--ease),visibility .4s var(--ease);
+ border-radius:3px;
+ backdrop-filter:blur(4px);
+}
+#completion.show{visibility:visible;opacity:1}
+#completion-title{
+ font-family:var(--serif);
+ font-size:clamp(24px,5vw,42px);
+ letter-spacing:.12em;
+ text-transform:uppercase;
+ color:var(--accent);
+ text-align:center;
+}
+#completion-sub{
+ font-family:var(--serif);
+ font-style:italic;
+ font-size:clamp(14px,2.5vw,20px);
+ color:var(--ink-dim);
+ text-align:center;
+}
+#completion-score{
+ font-size:13px;
+ letter-spacing:.1em;
+ color:var(--accent);
+}
+.flourish{font-size:28px;color:var(--accent);animation:spin 1.4s ease-out forwards}
+@keyframes spin{from{transform:rotate(-30deg) scale(.5);opacity:0}to{transform:rotate(0) scale(1);opacity:1}}
+
+/* Score mode toggle */
+#mode-btn{border-color:rgba(201,169,97,.3)}
+#mode-btn.active{background:var(--accent-dim);color:#f4f1ea;border-color:var(--accent-dim)}
+
+/* Dust canvas */
+#dust-canvas{
+ position:absolute;inset:0;
+ pointer-events:none;
+ border-radius:3px;
+}
+</style>
+</head>
+<body>
+
+<header>
+ <div class="wordmark">Designer Wallcoverings</div>
+ <div class="header-right">
+ <span id="best-label" style="font-size:12px;color:var(--ink-dim);letter-spacing:.06em"></span>
+ <button id="mode-btn" title="Toggle timed score mode">Score Mode</button>
+ <button id="new-btn">New Wall</button>
+ <button id="mute-btn">Mute</button>
+ </div>
+</header>
+
+<div id="game-area">
+ <div id="wall-wrap">
+ <canvas id="canvas-reveal"></canvas>
+ <canvas id="canvas-top"></canvas>
+ <canvas id="dust-canvas"></canvas>
+ <div id="timer-wrap"><span id="timer-val">30</span>s</div>
+ <div id="completion">
+ <div class="flourish">✦</div>
+ <div id="completion-title">Revealed</div>
+ <div id="completion-sub">italic pattern name here</div>
+ <div id="completion-score"></div>
+ <button id="next-btn" style="margin-top:8px;padding:10px 28px;font-size:13px">Next Wall</button>
+ </div>
+ </div>
+ <div id="hud">
+ <div id="reveal-bar-wrap"><div id="reveal-bar"></div></div>
+ <div id="pct-label">0%</div>
+ <div id="score-label"></div>
+ </div>
+</div>
+
+<script>
+/* ─────────────────────────────────────────────────────────
+ WALL PAIRINGS
+ Each pairing: old layer description + new DW pattern SVG
+───────────────────────────────────────────────────────── */
+const PAIRINGS = [
+ {
+ oldName:'Faded Beige Stripe',
+ newName:'Versailles Damask',
+ newColor:'#2a1f0e',
+ accentColor:'#c9a961',
+ drawOld(ctx,W,H){drawOldStripe(ctx,W,H,'#c8b89a','#bda882','#d4c4a8')},
+ drawNew(ctx,W,H){drawDamask(ctx,W,H,'#1e140a','#c9a961','#8a6e38')}
+ },
+ {
+ oldName:'Peeling Plaster',
+ newName:'Ming Chinoiserie',
+ newColor:'#0a1a2a',
+ accentColor:'#7eb8c9',
+ drawOld(ctx,W,H){drawOldPlaster(ctx,W,H,'#c4bfb0','#b5ae9e','#d6cfc0')},
+ drawNew(ctx,W,H){drawChinoiserie(ctx,W,H,'#0d2030','#7eb8c9','#c9d8e0')}
+ },
+ {
+ oldName:'70s Floral Print',
+ newName:'Highland Grasscloth',
+ newColor:'#1a1408',
+ accentColor:'#8a9e6a',
+ drawOld(ctx,W,H){drawOldFloral(ctx,W,H,'#c2b87e','#a89d62','#d4c98e')},
+ drawNew(ctx,W,H){drawGrasscloth(ctx,W,H,'#1a1408','#8a9e6a','#6b7a4e')}
+ },
+ {
+ oldName:'Stained Textured White',
+ newName:'Toile de Jouy',
+ newColor:'#f0e8d8',
+ accentColor:'#7a4a2e',
+ drawOld(ctx,W,H){drawOldTexture(ctx,W,H,'#dfd8c8','#cfc8b4','#e8e0d0')},
+ drawNew(ctx,W,H){drawToile(ctx,W,H,'#f4ede0','#7a4a2e')}
+ }
+];
+
+/* ─── OLD LAYER RENDERERS ─── */
+function drawOldStripe(ctx,W,H,c1,c2,c3){
+ ctx.fillStyle=c1; ctx.fillRect(0,0,W,H);
+ const sw=38,fade=0.55;
+ for(let x=0;x<W;x+=sw*2){
+ ctx.fillStyle=c2; ctx.globalAlpha=fade;
+ ctx.fillRect(x,0,sw,H);
+ }
+ ctx.globalAlpha=1;
+ // grime/stain patches
+ for(let i=0;i<18;i++){
+ const gx=Math.random()*W,gy=Math.random()*H;
+ const g=ctx.createRadialGradient(gx,gy,0,gx,gy,60+Math.random()*80);
+ g.addColorStop(0,'rgba(100,80,50,0.18)');
+ g.addColorStop(1,'rgba(100,80,50,0)');
+ ctx.fillStyle=g; ctx.beginPath(); ctx.ellipse(gx,gy,70,45,Math.random()*Math.PI,0,Math.PI*2);
+ ctx.fill();
+ }
+ // age cracks
+ ctx.strokeStyle='rgba(80,60,40,0.12)'; ctx.lineWidth=1;
+ for(let i=0;i<22;i++){
+ ctx.beginPath();
+ let cx=Math.random()*W,cy=Math.random()*H;
+ ctx.moveTo(cx,cy);
+ for(let j=0;j<6;j++){cx+=Math.random()*28-14;cy+=Math.random()*18-9;ctx.lineTo(cx,cy);}
+ ctx.stroke();
+ }
+}
+function drawOldPlaster(ctx,W,H,c1,c2,c3){
+ ctx.fillStyle=c1; ctx.fillRect(0,0,W,H);
+ const id=ctx.createImageData(W,H);
+ const d=id.data;
+ for(let i=0;i<d.length;i+=4){const n=(Math.random()-.5)*22;d[i]=180+n;d[i+1]=175+n;d[i+2]=162+n;d[i+3]=255;}
+ ctx.putImageData(id,0,0);
+ // peel/crack lines
+ ctx.strokeStyle='rgba(60,50,40,0.25)'; ctx.lineWidth=1.2;
+ for(let i=0;i<30;i++){
+ ctx.beginPath();let px=Math.random()*W,py=Math.random()*H;
+ ctx.moveTo(px,py);
+ for(let j=0;j<8;j++){px+=Math.random()*24-12;py+=Math.random()*14-7;ctx.lineTo(px,py);}
+ ctx.stroke();
+ }
+ // water stains
+ for(let i=0;i<8;i++){
+ const gx=Math.random()*W,gy=Math.random()*H*0.6;
+ const g=ctx.createRadialGradient(gx,gy,10,gx,gy+40,100);
+ g.addColorStop(0,'rgba(140,120,80,0.22)');
+ g.addColorStop(1,'rgba(140,120,80,0)');
+ ctx.fillStyle=g; ctx.beginPath(); ctx.ellipse(gx,gy+40,50,80,0,0,Math.PI*2); ctx.fill();
+ }
+}
+function drawOldFloral(ctx,W,H,c1,c2,c3){
+ ctx.fillStyle=c1; ctx.fillRect(0,0,W,H);
+ // tile a tiny faded flower
+ function flower(x,y,r,col){
+ ctx.fillStyle=col; ctx.globalAlpha=0.35;
+ for(let p=0;p<6;p++){
+ const a=p/6*Math.PI*2;
+ ctx.beginPath(); ctx.ellipse(x+Math.cos(a)*r*.7,y+Math.sin(a)*r*.7,r*.55,r*.3,a,0,Math.PI*2); ctx.fill();
+ }
+ ctx.fillStyle='rgba(180,160,80,0.4)'; ctx.globalAlpha=0.4;
+ ctx.beginPath(); ctx.arc(x,y,r*.35,0,Math.PI*2); ctx.fill();
+ ctx.globalAlpha=1;
+ }
+ const cols=['#8a7040','#6a5a30','#a08050'];
+ for(let gy=0;gy<H;gy+=72){
+ for(let gx=0;gx<W;gx+=72){
+ flower(gx+(gy%144?36:0),gy,18,cols[Math.floor(Math.random()*cols.length)]);
+ }
+ }
+ // grime overlay
+ for(let i=0;i<12;i++){
+ const gx=Math.random()*W,gy=Math.random()*H;
+ const g=ctx.createRadialGradient(gx,gy,0,gx,gy,90);
+ g.addColorStop(0,'rgba(80,60,20,0.16)'); g.addColorStop(1,'rgba(80,60,20,0)');
+ ctx.fillStyle=g; ctx.beginPath(); ctx.arc(gx,gy,90,0,Math.PI*2); ctx.fill();
+ }
+}
+function drawOldTexture(ctx,W,H,c1,c2,c3){
+ ctx.fillStyle=c1; ctx.fillRect(0,0,W,H);
+ const id=ctx.createImageData(W,H);
+ const d=id.data;
+ for(let i=0;i<d.length;i+=4){
+ const n=(Math.random()-.5)*18;
+ d[i]=210+n;d[i+1]=205+n;d[i+2]=195+n;d[i+3]=255;
+ }
+ ctx.putImageData(id,0,0);
+ // stain rings
+ for(let i=0;i<14;i++){
+ const gx=Math.random()*W,gy=Math.random()*H,r=20+Math.random()*60;
+ ctx.strokeStyle=`rgba(120,100,70,${0.08+Math.random()*0.12})`;
+ ctx.lineWidth=1+Math.random()*2;
+ ctx.beginPath(); ctx.arc(gx,gy,r,0,Math.PI*2); ctx.stroke();
+ ctx.beginPath(); ctx.arc(gx,gy,r*0.6,0,Math.PI*2); ctx.stroke();
+ }
+}
+
+/* ─── NEW LAYER RENDERERS ─── */
+function drawDamask(ctx,W,H,bg,c1,c2){
+ ctx.fillStyle=bg; ctx.fillRect(0,0,W,H);
+ // gold shimmer base
+ const grad=ctx.createLinearGradient(0,0,W,H);
+ grad.addColorStop(0,'rgba(201,169,97,0.04)'); grad.addColorStop(.5,'rgba(201,169,97,0.09)'); grad.addColorStop(1,'rgba(201,169,97,0.04)');
+ ctx.fillStyle=grad; ctx.fillRect(0,0,W,H);
+ // damask motif tiling
+ const TW=110,TH=140;
+ for(let ty=0;ty<H+TH;ty+=TH){
+ for(let tx=0;tx<W+TW;tx+=TW){
+ const ox=(ty/TH%2===0)?0:TW/2;
+ damaskMotif(ctx,tx+ox,ty,TW,TH,c1,c2);
+ }
+ }
+}
+function damaskMotif(ctx,cx,cy,TW,TH,c1,c2){
+ ctx.save(); ctx.translate(cx,cy);
+ // central urn/medallion
+ ctx.strokeStyle=c1; ctx.lineWidth=1.1; ctx.fillStyle='rgba(201,169,97,0.07)';
+ // oval center
+ ctx.beginPath(); ctx.ellipse(0,0,12,18,0,0,Math.PI*2); ctx.stroke(); ctx.fill();
+ // top crown flourish
+ function petal(rx,ry,count,dist,rot){
+ for(let i=0;i<count;i++){
+ const a=rot+i/count*Math.PI*2;
+ const bx=Math.cos(a)*dist, by=Math.sin(a)*dist;
+ ctx.beginPath();
+ ctx.moveTo(0,0);
+ ctx.quadraticCurveTo(bx*.4+Math.cos(a+Math.PI/2)*rx,by*.4+Math.sin(a+Math.PI/2)*ry,bx,by);
+ ctx.quadraticCurveTo(bx*.4+Math.cos(a-Math.PI/2)*rx,by*.4+Math.sin(a-Math.PI/2)*ry,0,0);
+ ctx.fillStyle=c2; ctx.globalAlpha=0.6; ctx.fill(); ctx.globalAlpha=1;
+ }
+ }
+ petal(5,2,8,30,-Math.PI/2);
+ // acanthus scrolls top/bottom
+ for(const dy of[-38,38]){
+ ctx.beginPath();
+ ctx.moveTo(0,dy<0?-20:20);
+ ctx.bezierCurveTo(-14,dy*.6,-18,dy*.85,0,dy);
+ ctx.bezierCurveTo(18,dy*.85,14,dy*.6,0,dy<0?-20:20);
+ ctx.strokeStyle=c1; ctx.lineWidth=.9; ctx.stroke();
+ // leaf tip
+ ctx.beginPath(); ctx.arc(0,dy,3.5,0,Math.PI*2);
+ ctx.fillStyle=c1; ctx.globalAlpha=.7; ctx.fill(); ctx.globalAlpha=1;
+ }
+ // side scrolls
+ for(const dx of[-50,50]){
+ ctx.beginPath();
+ ctx.moveTo(dx<0?-14:14,0);
+ ctx.bezierCurveTo(dx*.55,-12,dx*.85,-10,dx,0);
+ ctx.bezierCurveTo(dx*.85,10,dx*.55,12,dx<0?-14:14,0);
+ ctx.strokeStyle=c1; ctx.lineWidth=.8; ctx.stroke();
+ }
+ ctx.restore();
+}
+
+function drawChinoiserie(ctx,W,H,bg,c1,c2){
+ ctx.fillStyle=bg; ctx.fillRect(0,0,W,H);
+ // subtle linen texture
+ for(let y=0;y<H;y+=3){
+ ctx.strokeStyle=`rgba(120,180,200,${0.03+Math.random()*0.02})`;
+ ctx.lineWidth=1; ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y+Math.random()*2-1); ctx.stroke();
+ }
+ // pagoda + branches tiling
+ const TW=160,TH=200;
+ for(let ty=-20;ty<H+TH;ty+=TH){
+ for(let tx=0;tx<W+TW;tx+=TW){
+ chinoisMotif(ctx,tx+TW/2,ty+TH/2,c1,c2);
+ }
+ }
+}
+function chinoisMotif(ctx,cx,cy,c1,c2){
+ ctx.save(); ctx.translate(cx,cy);
+ ctx.strokeStyle=c1; ctx.fillStyle='rgba(126,184,201,0.12)'; ctx.lineWidth=1;
+ // pagoda tower
+ for(let l=0;l<3;l++){
+ const lw=(3-l)*18,lh=12,ly=-l*28;
+ ctx.beginPath(); ctx.moveTo(-lw/2,ly); ctx.lineTo(lw/2,ly);
+ ctx.lineTo(lw/2*.8,ly-lh); ctx.lineTo(-lw/2*.8,ly-lh); ctx.closePath();
+ ctx.stroke(); ctx.fill();
+ // eave curl
+ ctx.beginPath(); ctx.moveTo(-lw/2,ly); ctx.quadraticCurveTo(-lw/2-8,ly-4,-lw/2-4,ly-10); ctx.stroke();
+ ctx.beginPath(); ctx.moveTo(lw/2,ly); ctx.quadraticCurveTo(lw/2+8,ly-4,lw/2+4,ly-10); ctx.stroke();
+ }
+ // branches left
+ function branch(sx,sy,ang,len,depth){
+ if(depth===0||len<5)return;
+ const ex=sx+Math.cos(ang)*len, ey=sy+Math.sin(ang)*len;
+ ctx.beginPath(); ctx.moveTo(sx,sy); ctx.lineTo(ex,ey);
+ ctx.strokeStyle=c1; ctx.lineWidth=depth*.5; ctx.stroke();
+ // blossom
+ if(depth===1){ctx.beginPath();ctx.arc(ex,ey,3,0,Math.PI*2);ctx.fillStyle=c2;ctx.globalAlpha=.6;ctx.fill();ctx.globalAlpha=1;}
+ branch(ex,ey,ang-0.45,len*.65,depth-1);
+ branch(ex,ey,ang+0.35,len*.6,depth-1);
+ }
+ branch(-42,30,-Math.PI/3.5,28,3);
+ branch(42,30,-Math.PI*2/3.5,28,3);
+ // bird silhouette
+ ctx.beginPath(); ctx.fillStyle=c1; ctx.globalAlpha=.55;
+ ctx.ellipse(55,-60,8,3.5,-.4,0,Math.PI*2); ctx.fill();
+ ctx.beginPath(); ctx.arc(63,-62.5,3.5,0,Math.PI*2); ctx.fill();
+ ctx.globalAlpha=1;
+ ctx.restore();
+}
+
+function drawGrasscloth(ctx,W,H,bg,c1,c2){
+ ctx.fillStyle=bg; ctx.fillRect(0,0,W,H);
+ // weave
+ const sw=4;
+ for(let y=0;y<H;y+=sw){
+ for(let x=0;x<W;x+=sw*2){
+ const ox=(Math.floor(y/sw)%2)*sw;
+ ctx.fillStyle=c1; ctx.globalAlpha=0.25+Math.random()*.15;
+ ctx.fillRect(x+ox,y,sw,sw);
+ }
+ }
+ ctx.globalAlpha=1;
+ // vertical fibers
+ for(let x=0;x<W;x+=2+Math.random()*3){
+ ctx.strokeStyle=`rgba(138,158,106,${0.06+Math.random()*0.1})`;
+ ctx.lineWidth=0.5+Math.random()*.8;
+ ctx.beginPath(); ctx.moveTo(x,0);
+ let py=0;
+ while(py<H){py+=4+Math.random()*8;ctx.lineTo(x+(Math.random()-.5)*2,py);}
+ ctx.stroke();
+ }
+ // seam lines
+ const seamW=W/3;
+ for(let s=1;s<3;s++){
+ ctx.strokeStyle='rgba(30,20,8,0.18)'; ctx.lineWidth=1.2;
+ ctx.beginPath(); ctx.moveTo(s*seamW,0); ctx.lineTo(s*seamW+Math.random()*4-2,H); ctx.stroke();
+ }
+}
+
+function drawToile(ctx,W,H,bg,ink){
+ ctx.fillStyle=bg; ctx.fillRect(0,0,W,H);
+ // subtle linen
+ const id=ctx.createImageData(W,H); const d=id.data;
+ for(let i=0;i<d.length;i+=4){const n=(Math.random()-.5)*10;d[i]=244+n;d[i+1]=237+n;d[i+2]=224+n;d[i+3]=255;}
+ ctx.putImageData(id,0,0);
+ // toile vignette scenes tiled
+ const TW=140,TH=160;
+ for(let ty=0;ty<H+TH;ty+=TH){
+ for(let tx=0;tx<W+TW;tx+=TW){
+ const ox=(ty/TH%2===0)?0:TW/2;
+ toileScene(ctx,tx+ox,ty,ink,(tx+ty)%280===0?'tree':'urn');
+ }
+ }
+}
+function toileScene(ctx,cx,cy,ink,type){
+ ctx.save(); ctx.translate(cx,cy);
+ ctx.strokeStyle=ink; ctx.fillStyle='rgba(122,74,46,0.08)'; ctx.lineWidth=0.8;
+ if(type==='urn'){
+ // classical urn
+ ctx.beginPath(); ctx.moveTo(-10,20); ctx.quadraticCurveTo(-18,0,-8,-20); ctx.lineTo(8,-20);
+ ctx.quadraticCurveTo(18,0,10,20); ctx.closePath(); ctx.stroke(); ctx.fill();
+ ctx.beginPath(); ctx.moveTo(-14,-20); ctx.lineTo(14,-20); ctx.stroke();
+ ctx.beginPath(); ctx.ellipse(0,-22,10,4,0,0,Math.PI*2); ctx.stroke();
+ // handles
+ ctx.beginPath(); ctx.moveTo(-8,-10); ctx.bezierCurveTo(-20,-10,-20,5,-8,5); ctx.stroke();
+ ctx.beginPath(); ctx.moveTo(8,-10); ctx.bezierCurveTo(20,-10,20,5,8,5); ctx.stroke();
+ // flowers from urn
+ for(let f=0;f<3;f++){
+ const a=-Math.PI/2+f*.4-.4;
+ ctx.beginPath(); ctx.moveTo(0,-22); ctx.quadraticCurveTo(Math.cos(a)*16,-22+Math.sin(a)*28,Math.cos(a)*22,-22+Math.sin(a)*40); ctx.stroke();
+ ctx.beginPath(); ctx.arc(Math.cos(a)*22,-22+Math.sin(a)*40,4,0,Math.PI*2); ctx.stroke();
+ }
+ } else {
+ // weeping willow silhouette
+ ctx.beginPath(); ctx.moveTo(0,30); ctx.lineTo(0,-35); ctx.stroke();
+ for(let b=0;b<7;b++){
+ const a=-Math.PI*.55+b*.18;
+ const bx=Math.cos(a)*30, by=Math.sin(a)*30-30;
+ ctx.beginPath(); ctx.moveTo(0,-10);
+ ctx.quadraticCurveTo(bx*.5,by*.5-10,bx,by);
+ ctx.quadraticCurveTo(bx+Math.cos(a+.4)*12,by+15,bx+Math.cos(a+.6)*8,by+28); ctx.stroke();
+ }
+ }
+ ctx.restore();
+}
+
+/* ─────────────────────────────────────────────────────────
+ AUDIO ENGINE (procedural Web Audio)
+───────────────────────────────────────────────────────── */
+let audioCtx=null, muted=false;
+function ensureAudio(){
+ if(!audioCtx) audioCtx=new(window.AudioContext||window.webkitAudioContext)();
+}
+function tearSound(intensity=0.5){
+ if(muted||!audioCtx)return;
+ const buf=audioCtx.createBuffer(1,audioCtx.sampleRate*0.06,audioCtx.sampleRate);
+ const d=buf.getChannelData(0);
+ for(let i=0;i<d.length;i++){
+ d[i]=(Math.random()*2-1)*Math.exp(-i/d.length*18)*intensity;
+ }
+ const src=audioCtx.createBufferSource();
+ src.buffer=buf;
+ const f=audioCtx.createBiquadFilter();
+ f.type='bandpass'; f.frequency.value=1800+Math.random()*600; f.Q.value=0.8;
+ const g=audioCtx.createGain(); g.gain.value=0.35;
+ src.connect(f); f.connect(g); g.connect(audioCtx.destination);
+ src.start();
+}
+function chimeSound(){
+ if(muted||!audioCtx)return;
+ const freqs=[523,659,784,1047];
+ freqs.forEach((hz,i)=>{
+ const o=audioCtx.createOscillator();
+ const g=audioCtx.createGain();
+ o.type='sine'; o.frequency.value=hz;
+ g.gain.setValueAtTime(0,audioCtx.currentTime+i*.12);
+ g.gain.linearRampToValueAtTime(0.18,audioCtx.currentTime+i*.12+.03);
+ g.gain.exponentialRampToValueAtTime(0.001,audioCtx.currentTime+i*.12+1.1);
+ o.connect(g); g.connect(audioCtx.destination);
+ o.start(audioCtx.currentTime+i*.12);
+ o.stop(audioCtx.currentTime+i*.12+1.2);
+ });
+}
+
+/* ─────────────────────────────────────────────────────────
+ DUST PARTICLES
+───────────────────────────────────────────────────────── */
+const dustCanvas=document.getElementById('dust-canvas');
+const dctx=dustCanvas.getContext('2d');
+let particles=[];
+function spawnDust(x,y,count=12){
+ for(let i=0;i<count;i++){
+ particles.push({
+ x,y,vx:(Math.random()-.5)*3.5,vy:(Math.random()-.5)*3-1.5,
+ life:1,decay:0.02+Math.random()*0.04,r:1.5+Math.random()*3,
+ hue:Math.random()>.5?'rgba(201,169,97,':'rgba(220,200,160,'
+ });
+ }
+}
+function updateDust(){
+ dctx.clearRect(0,0,dustCanvas.width,dustCanvas.height);
+ particles=particles.filter(p=>{
+ p.x+=p.vx; p.y+=p.vy; p.vy+=0.08; p.life-=p.decay;
+ if(p.life<=0)return false;
+ dctx.beginPath();
+ dctx.fillStyle=p.hue+p.life*.6+')';
+ dctx.arc(p.x,p.y,p.r*p.life,0,Math.PI*2);
+ dctx.fill();
+ return true;
+ });
+}
+
+/* ─────────────────────────────────────────────────────────
+ MAIN GAME
+───────────────────────────────────────────────────────── */
+const wrap=document.getElementById('wall-wrap');
+const revCanvas=document.getElementById('canvas-reveal');
+const topCanvas=document.getElementById('canvas-top');
+const rctx=revCanvas.getContext('2d');
+const tctx=topCanvas.getContext('2d');
+const revBar=document.getElementById('reveal-bar');
+const pctLabel=document.getElementById('pct-label');
+const scoreLabel=document.getElementById('score-label');
+const timerWrap=document.getElementById('timer-wrap');
+const timerVal=document.getElementById('timer-val');
+const completion=document.getElementById('completion');
+const completionTitle=document.getElementById('completion-title');
+const completionSub=document.getElementById('completion-sub');
+const completionScore=document.getElementById('completion-score');
+const bestLabel=document.getElementById('best-label');
+
+let W=0,H=0,pairIdx=0,peelMask=null,totalPx=0,revealedPx=0;
+let isDragging=false,lastX=0,lastY=0;
+let completed=false,peelRadius=44;
+// torn edge points for rendering
+let tornEdges=[];
+let peelProgress=0; // 0..1
+
+// Score mode
+let scoreMode=false,timerSec=30,timerInt=null,gameScore=0;
+let bestScore=parseInt(localStorage.getItem('dw_reveal_best')||'0');
+
+function resize(){
+ const rect=wrap.getBoundingClientRect();
+ W=Math.round(rect.width); H=Math.round(rect.height);
+ revCanvas.width=topCanvas.width=dustCanvas.width=W;
+ revCanvas.height=topCanvas.height=dustCanvas.height=H;
+ peelMask=new Uint8Array(W*H);
+ totalPx=W*H;
+ if(W>0&&H>0) initWall();
+}
+
+function initWall(){
+ completed=false; revealedPx=0; peelProgress=0;
+ particles=[];
+ peelMask.fill(0);
+ tornEdges=[];
+ // draw reveal (new) layer
+ rctx.clearRect(0,0,W,H);
+ PAIRINGS[pairIdx].drawNew(rctx,W,H);
+ // draw old layer on top canvas
+ tctx.clearRect(0,0,W,H);
+ PAIRINGS[pairIdx].drawOld(tctx,W,H);
+ // reset mask on top canvas — initially fully opaque old layer
+ updateTopLayer();
+ updateHUD();
+ completion.classList.remove('show');
+ revBar.style.width='0%';
+ pctLabel.textContent='0%';
+ if(scoreMode&&timerInt){clearInterval(timerInt);timerInt=null;}
+}
+
+// Track which pixels are peeled using an off-screen canvas for efficiency
+// We maintain a mask array and redraw the top canvas using compositing
+const BRUSH_STAMP=64; // max brush
+function peel(x,y,brushR){
+ brushR=Math.min(brushR,BRUSH_STAMP);
+ const x0=Math.max(0,x-brushR|0), x1=Math.min(W,x+brushR|0);
+ const y0=Math.max(0,y-brushR|0), y1=Math.min(H,y+brushR|0);
+ let newRevealed=0;
+ const r2=brushR*brushR;
+ // irregular tear edge: use elliptical brush with jagged boundary
+ for(let py=y0;py<y1;py++){
+ for(let px=x0;px<x1;px++){
+ const dx=px-x, dy=py-y;
+ // jagged: add noise based on angle
+ const ang=Math.atan2(dy,dx);
+ const noise=1+0.28*Math.sin(ang*7+px*.3+py*.4);
+ const dist2=dx*dx+dy*dy;
+ if(dist2<r2*noise*noise && peelMask[py*W+px]===0){
+ peelMask[py*W+px]=1;
+ newRevealed++;
+ }
+ }
+ }
+ return newRevealed;
+}
+
+function updateTopLayer(){
+ // Redraw old layer then cut holes via destination-out
+ tctx.clearRect(0,0,W,H);
+ PAIRINGS[pairIdx].drawOld(tctx,W,H);
+ // draw torn paper backing strip along edges — subtle cream underside
+ // Cut out peeled region
+ tctx.save();
+ tctx.globalCompositeOperation='destination-out';
+ // Build peeled region path from mask using imagedata
+ // For performance, just putImageData with alpha=0 where peeled
+ tctx.restore();
+ // More efficient: directly write alpha channel
+ const id=tctx.getImageData(0,0,W,H);
+ const d=id.data;
+ for(let i=0;i<peelMask.length;i++){
+ if(peelMask[i]===1){
+ d[i*4+3]=0;
+ }
+ }
+ tctx.putImageData(id,0,0);
+ // Draw torn paper edge effect over transition
+ drawTornEdge();
+}
+
+// Torn edge rendering: find boundary pixels and draw curled paper effect
+function drawTornEdge(){
+ // Sample boundary pixels
+ const edgePts=[];
+ const step=6;
+ for(let y=step;y<H-step;y+=step){
+ for(let x=step;x<W-step;x+=step){
+ if(peelMask[y*W+x]===1){
+ // check if any neighbor is 0
+ const hasNeighbor=(
+ peelMask[(y-step)*W+x]===0||peelMask[(y+step)*W+x]===0||
+ peelMask[y*W+(x-step)]===0||peelMask[y*W+(x+step)]===0
+ );
+ if(hasNeighbor) edgePts.push([x,y]);
+ }
+ }
+ }
+ if(edgePts.length<3)return;
+ // Draw backing paper crescent (cream-beige tinted edge) around each edge cluster
+ tctx.save();
+ for(const [ex,ey] of edgePts){
+ // Shadow cast into revealed area
+ const g=tctx.createRadialGradient(ex,ey,0,ex,ey,12);
+ g.addColorStop(0,'rgba(0,0,0,0.22)');
+ g.addColorStop(1,'rgba(0,0,0,0)');
+ tctx.fillStyle=g;
+ tctx.beginPath(); tctx.arc(ex,ey,12,0,Math.PI*2); tctx.fill();
+ }
+ // Backing paper crescent — cream underside of old wallpaper
+ for(const [ex,ey] of edgePts){
+ // Only draw on the peeled side inward a few px
+ if(peelMask[ey*W+ex]===1){
+ const g=tctx.createRadialGradient(ex,ey,0,ex,ey,7);
+ g.addColorStop(0,'rgba(245,235,210,0.9)');
+ g.addColorStop(.5,'rgba(220,205,180,0.55)');
+ g.addColorStop(1,'rgba(200,185,160,0)');
+ tctx.fillStyle=g;
+ tctx.beginPath(); tctx.arc(ex,ey,7,0,Math.PI*2); tctx.fill();
+ }
+ }
+ tctx.restore();
+}
+
+function updateHUD(){
+ const pct=Math.round(revealedPx/totalPx*100);
+ revBar.style.width=pct+'%';
+ pctLabel.textContent=pct+'%';
+ peelProgress=revealedPx/totalPx;
+}
+
+// Shimmer overlay on reveal
+let shimmerT=0;
+function drawShimmer(){
+ if(peelProgress<0.05)return;
+ tctx.save();
+ shimmerT+=0.018;
+ const sx=W/2+Math.cos(shimmerT)*W*.4;
+ const g=tctx.createLinearGradient(sx-120,0,sx+120,H);
+ g.addColorStop(0,'rgba(255,255,255,0)');
+ g.addColorStop(.5,`rgba(255,255,255,${0.04*peelProgress})`);
+ g.addColorStop(1,'rgba(255,255,255,0)');
+ tctx.globalCompositeOperation='source-over';
+ tctx.fillStyle=g;
+ // Only apply shimmer where revealed
+ // Use destination-in trick: draw shimmer only in revealed area
+ // Build temp canvas
+ tctx.restore();
+}
+
+let lastTearX=-999,lastTearY=-999;
+function onDrag(x,y){
+ if(completed)return;
+ ensureAudio();
+ const dx=x-lastTearX, dy=y-lastTearY;
+ const dist=Math.sqrt(dx*dx+dy*dy);
+ if(dist<6)return;
+ lastTearX=x; lastTearY=y;
+ // adaptive radius: faster drag = bigger brush
+ const speed=Math.min(dist,60);
+ const radius=peelRadius+speed*.22;
+ const newPx=peel(x,y,radius);
+ revealedPx=Math.min(totalPx,revealedPx+newPx);
+ updateTopLayer();
+ updateHUD();
+ // Tear sound every ~40px drag
+ if(dist>10) tearSound(0.3+Math.min(speed/60,.7)*.4);
+ // Dust puff
+ if(dist>14) spawnDust(x,y,Math.round(4+speed*.15));
+ // Score mode
+ if(scoreMode) gameScore+=Math.floor(newPx*10);
+ scoreLabel.textContent=scoreMode?'Score: '+gameScore.toLocaleString():'';
+ // Check completion
+ const pct=revealedPx/totalPx;
+ if(pct>=0.82&&!completed) triggerCompletion();
+}
+
+function triggerCompletion(){
+ completed=true;
+ chimeSound();
+ completionTitle.textContent='Revealed';
+ completionSub.textContent=PAIRINGS[pairIdx].newName;
+ let scoreStr='';
+ if(scoreMode){
+ clearInterval(timerInt); timerInt=null; timerWrap.classList.remove('active');
+ const elapsed=30-timerSec;
+ scoreStr=`Score: ${gameScore.toLocaleString()} · ${elapsed}s`;
+ if(gameScore>bestScore){
+ bestScore=gameScore;
+ localStorage.setItem('dw_reveal_best',bestScore);
+ bestLabel.textContent='Best: '+bestScore.toLocaleString();
+ scoreStr+=' · New Best!';
+ }
+ }
+ completionScore.textContent=scoreStr;
+ completion.classList.add('show');
+ // Shimmer pulse — trigger 3 bursts
+ let bursts=0;
+ function burst(){
+ if(bursts>=4||!completed)return;
+ spawnDust(W/2,H/2,30);
+ for(let i=0;i<6;i++) setTimeout(()=>spawnDust(Math.random()*W,Math.random()*H,8),i*80);
+ bursts++; setTimeout(burst,400);
+ }
+ burst();
+}
+
+/* ─── SCORE MODE ─── */
+function startScoreTimer(){
+ timerSec=30; gameScore=0; scoreLabel.textContent='Score: 0';
+ timerVal.textContent=timerSec;
+ timerWrap.classList.add('active');
+ clearInterval(timerInt);
+ timerInt=setInterval(()=>{
+ timerSec--;
+ timerVal.textContent=timerSec;
+ if(timerSec<=0){
+ clearInterval(timerInt); timerInt=null;
+ if(!completed) triggerCompletion();
+ }
+ },1000);
+}
+
+/* ─── RAF LOOP ─── */
+function loop(){
+ updateDust();
+ requestAnimationFrame(loop);
+}
+requestAnimationFrame(loop);
+
+/* ─── INPUT ─── */
+function getXY(e){
+ const rect=wrap.getBoundingClientRect();
+ const src=e.touches?e.touches[0]:e;
+ return [(src.clientX-rect.left)*(W/rect.width)|0,(src.clientY-rect.top)*(H/rect.height)|0];
+}
+wrap.addEventListener('mousedown',e=>{
+ ensureAudio();
+ isDragging=true;
+ const [x,y]=getXY(e);
+ lastTearX=x; lastTearY=y;
+ spawnDust(x,y,6);
+});
+wrap.addEventListener('mousemove',e=>{if(isDragging){const [x,y]=getXY(e);onDrag(x,y);}});
+window.addEventListener('mouseup',()=>{isDragging=false;});
+wrap.addEventListener('touchstart',e=>{
+ e.preventDefault(); ensureAudio();
+ isDragging=true;
+ const [x,y]=getXY(e);
+ lastTearX=x; lastTearY=y;
+},{passive:false});
+wrap.addEventListener('touchmove',e=>{
+ e.preventDefault();
+ if(isDragging){const [x,y]=getXY(e);onDrag(x,y);}
+},{passive:false});
+wrap.addEventListener('touchend',()=>{isDragging=false;});
+
+/* ─── BUTTONS ─── */
+document.getElementById('mute-btn').addEventListener('click',function(){
+ muted=!muted;
+ this.textContent=muted?'Unmute':'Mute';
+ this.classList.toggle('muted',muted);
+});
+const newBtn=document.getElementById('new-btn');
+const nextBtn=document.getElementById('next-btn');
+function nextWall(){
+ pairIdx=(pairIdx+1)%PAIRINGS.length;
+ initWall();
+ if(scoreMode) startScoreTimer();
+}
+newBtn.addEventListener('click',nextWall);
+nextBtn.addEventListener('click',()=>{nextWall();});
+
+document.getElementById('mode-btn').addEventListener('click',function(){
+ scoreMode=!scoreMode;
+ this.classList.toggle('active',scoreMode);
+ this.textContent=scoreMode?'Score: ON':'Score Mode';
+ if(scoreMode){
+ initWall(); startScoreTimer();
+ } else {
+ clearInterval(timerInt); timerInt=null;
+ timerWrap.classList.remove('active');
+ scoreLabel.textContent='';
+ initWall();
+ }
+});
+
+// Show best score
+if(bestScore>0) bestLabel.textContent='Best: '+bestScore.toLocaleString();
+
+/* ─── INIT ─── */
+const ro=new ResizeObserver(resize);
+ro.observe(wrap);
+resize();
+</script>
+</body>
+</html>
← 410f295 Agent-driven toy: DW Toile Coloring (frontend-developer) — f
·
back to Model Arena
·
Agent-driven toy: DW The Big Reveal (frontend-developer) — d 5d097ae →