[object Object]

← back to Model Arena

Agent-driven tool: DW Colorway Studio (frontend-developer) — pattern across 10 colorways, compare grid, room vignette. Registered.

6caed3a64d616c54f0ab811bb1d45e222faac9e6 · 2026-07-25 12:54:03 -0700 · Steve Abrams

Files touched

Diff

commit 6caed3a64d616c54f0ab811bb1d45e222faac9e6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Jul 25 12:54:03 2026 -0700

    Agent-driven tool: DW Colorway Studio (frontend-developer) — pattern across 10 colorways, compare grid, room vignette. Registered.
---
 data/arcade-games.json               |   7 +
 public/games/dw-colorway-studio.html | 693 +++++++++++++++++++++++++++++++++++
 2 files changed, 700 insertions(+)

diff --git a/data/arcade-games.json b/data/arcade-games.json
index 16bd7c0..d5b72bb 100644
--- a/data/arcade-games.json
+++ b/data/arcade-games.json
@@ -167,6 +167,13 @@
       "credit": "frontend-developer",
       "category": "Tools",
       "note": "Agent-built scene composer \u2014 place hand-painted chinoiserie motifs (birds, branches, pagodas) on silk grounds, PNG export"
+    },
+    {
+      "slug": "dw-colorway-studio",
+      "title": "DW Colorway Studio",
+      "credit": "frontend-developer",
+      "category": "Tools",
+      "note": "Agent-built tool \u2014 one pattern across 10 named colorways, live recolor + compare grid + room vignette + favorites"
     }
   ]
 }
\ No newline at end of file
diff --git a/public/games/dw-colorway-studio.html b/public/games/dw-colorway-studio.html
new file mode 100644
index 0000000..0efe380
--- /dev/null
+++ b/public/games/dw-colorway-studio.html
@@ -0,0 +1,693 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Colorway Studio — Designer Wallcoverings</title>
+<style>
+:root{
+  --bg:#f4f1ea;--ink:#1c1a17;--ink-dim:#6b6056;--ink-faint:#c4bdb2;
+  --accent:#c9a961;--accent-dim:#a0813a;--panel:#ede9e1;--panel2:#e6e1d6;
+  --white:#faf8f4;--border:#d8d1c4;
+  --serif:'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif;
+  --sans:'Gill Sans','Gill Sans MT','Optima',Optima,sans-serif;
+  --ease:cubic-bezier(.4,0,.2,1);--ease-spring:cubic-bezier(.34,1.56,.64,1);
+  --r:6px;
+}
+*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
+html{font-size:16px;scroll-behavior:smooth}
+body{background:var(--bg);color:var(--ink);font-family:var(--sans);min-height:100vh;overflow-x:hidden}
+
+/* ── HEADER ── */
+header{
+  display:flex;align-items:center;justify-content:space-between;
+  padding:.9rem 2.4rem .9rem 2rem;
+  border-bottom:1px solid var(--border);background:var(--bg);
+  position:sticky;top:0;z-index:50;
+}
+.wordmark{
+  font-family:var(--serif);font-size:clamp(26px,3vw,42px);
+  font-weight:400;text-transform:uppercase;letter-spacing:.18em;
+  color:var(--ink);line-height:1;white-space:nowrap;
+}
+.wordmark span{display:block;font-size:.42em;letter-spacing:.32em;color:var(--accent);margin-top:.2em;font-style:italic;text-transform:none}
+.header-tag{font-family:var(--serif);font-size:.9rem;letter-spacing:.12em;color:var(--ink-dim);font-style:italic}
+
+/* ── LAYOUT ── */
+.studio{display:grid;grid-template-columns:1fr 320px;min-height:calc(100vh - 70px);gap:0}
+@media(max-width:900px){.studio{grid-template-columns:1fr}}
+
+/* ── HERO ── */
+.hero-wrap{display:flex;flex-direction:column;padding:2rem 2rem 1.5rem 2.4rem;gap:1.2rem}
+.hero-controls{display:flex;align-items:center;gap:1.2rem;flex-wrap:wrap}
+.hero-controls label{font-family:var(--serif);font-size:.85rem;letter-spacing:.1em;color:var(--ink-dim);text-transform:uppercase}
+.select-wrap{position:relative;display:inline-flex;align-items:center}
+.select-wrap::after{content:'▾';position:absolute;right:.7rem;pointer-events:none;color:var(--accent);font-size:.8rem}
+select{
+  appearance:none;background:var(--white);border:1px solid var(--border);
+  border-radius:var(--r);padding:.45rem 2.2rem .45rem .9rem;
+  font-family:var(--sans);font-size:.85rem;color:var(--ink);
+  cursor:pointer;transition:border-color .2s var(--ease);
+}
+select:focus{outline:2px solid var(--accent);outline-offset:2px;border-color:var(--accent)}
+.mode-btns{display:flex;gap:.5rem;margin-left:auto}
+.mode-btn{
+  padding:.45rem 1rem;border:1px solid var(--border);border-radius:var(--r);
+  background:var(--white);font-family:var(--serif);font-size:.85rem;
+  letter-spacing:.06em;color:var(--ink-dim);cursor:pointer;
+  transition:all .2s var(--ease);
+}
+.mode-btn.active,.mode-btn:hover{background:var(--accent);border-color:var(--accent);color:var(--white)}
+
+/* ── SINGLE HERO ── */
+.hero-single{display:block}
+.hero-compare{display:none}
+
+.hero-canvas{
+  position:relative;width:100%;aspect-ratio:16/9;
+  border-radius:8px;overflow:hidden;background:var(--panel);
+  box-shadow:0 4px 32px rgba(28,26,23,.12);
+}
+.hero-svg-wrap{
+  position:absolute;inset:0;transition:opacity .45s var(--ease);
+}
+.hero-svg-wrap.fade-out{opacity:0}
+.hero-svg-wrap svg{width:100%;height:100%}
+
+.caption{
+  font-family:var(--serif);font-size:1.1rem;letter-spacing:.06em;
+  color:var(--ink-dim);font-style:italic;text-align:center;
+  padding:.6rem 0 .2rem;
+}
+.caption strong{color:var(--ink);font-weight:600;font-style:normal}
+
+/* ── COMPARE GRID ── */
+.compare-grid{
+  display:grid;grid-template-columns:repeat(3,1fr);gap:.8rem;
+  padding:.3rem 0;
+}
+.compare-cell{
+  position:relative;border-radius:6px;overflow:hidden;
+  aspect-ratio:4/3;cursor:pointer;background:var(--panel);
+  box-shadow:0 2px 10px rgba(28,26,23,.09);
+  transition:transform .2s var(--ease),box-shadow .2s var(--ease);
+}
+.compare-cell:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(28,26,23,.15)}
+.compare-cell svg{width:100%;height:100%;display:block}
+.compare-cell-label{
+  position:absolute;bottom:0;left:0;right:0;
+  background:linear-gradient(to top,rgba(28,26,23,.7),transparent);
+  color:#fff;font-family:var(--serif);font-size:.8rem;letter-spacing:.08em;
+  padding:.5rem .7rem .4rem;font-style:italic;
+}
+.compare-heart{
+  position:absolute;top:.5rem;right:.5rem;font-size:1.1rem;cursor:pointer;
+  transition:transform .2s var(--ease-spring);z-index:2;
+  text-shadow:0 1px 4px rgba(0,0,0,.25);
+}
+.compare-heart.active{transform:scale(1.4)}
+
+/* ── ROOM VIGNETTE ── */
+.room-vignette{
+  position:relative;width:100%;aspect-ratio:4/3;border-radius:8px;
+  overflow:hidden;background:#e8e2d8;margin-top:.2rem;
+  box-shadow:0 2px 14px rgba(28,26,23,.1);
+}
+.room-vignette svg{width:100%;height:100%}
+
+/* ── SIDEBAR ── */
+.sidebar{
+  border-left:1px solid var(--border);background:var(--white);
+  display:flex;flex-direction:column;overflow-y:auto;
+  padding:1.6rem 1.4rem 2rem;gap:1.6rem;
+}
+@media(max-width:900px){.sidebar{border-left:none;border-top:1px solid var(--border)}}
+.sidebar-section-title{
+  font-family:var(--serif);font-size:.78rem;text-transform:uppercase;
+  letter-spacing:.2em;color:var(--ink-dim);margin-bottom:.8rem;border-bottom:1px solid var(--border);padding-bottom:.5rem;
+}
+
+/* ── COLORWAY RAIL ── */
+.colorway-rail{display:flex;flex-direction:column;gap:.5rem}
+.cw-chip{
+  display:flex;align-items:center;gap:.8rem;padding:.6rem .8rem;
+  border-radius:var(--r);cursor:pointer;border:2px solid transparent;
+  transition:all .18s var(--ease);position:relative;
+}
+.cw-chip:hover{background:var(--panel)}
+.cw-chip.active{border-color:var(--accent);background:var(--panel)}
+.cw-chip:hover .cw-ring,.cw-chip.active .cw-ring{box-shadow:0 0 0 2px var(--accent)}
+.cw-swatch{
+  width:34px;height:34px;border-radius:50%;flex-shrink:0;
+  border:2px solid rgba(28,26,23,.1);transition:transform .18s var(--ease-spring);
+}
+.cw-chip:hover .cw-swatch{transform:scale(1.12)}
+.cw-info{flex:1;min-width:0}
+.cw-name{font-family:var(--serif);font-size:1rem;letter-spacing:.04em;color:var(--ink);line-height:1.1}
+.cw-hex{font-size:.72rem;color:var(--ink-dim);letter-spacing:.06em;font-family:monospace}
+.cw-heart{
+  font-size:1.1rem;cursor:pointer;transition:transform .2s var(--ease-spring),color .2s;
+  color:var(--ink-faint);padding:.2rem;
+}
+.cw-heart.active{color:#c0392b;transform:scale(1.25)}
+
+/* ── FAVORITES ── */
+.fav-bar{display:flex;flex-wrap:wrap;gap:.5rem}
+.fav-tag{
+  display:flex;align-items:center;gap:.35rem;padding:.25rem .6rem;
+  background:var(--panel);border-radius:20px;font-family:var(--serif);
+  font-size:.78rem;color:var(--ink);letter-spacing:.04em;
+  border:1px solid var(--border);
+}
+.fav-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
+
+/* ── DOWNLOAD BTN ── */
+.dl-btn{
+  display:flex;align-items:center;justify-content:center;gap:.5rem;
+  width:100%;padding:.65rem 1rem;
+  background:var(--ink);color:var(--white);border:none;border-radius:var(--r);
+  font-family:var(--serif);font-size:.95rem;letter-spacing:.08em;cursor:pointer;
+  transition:background .2s var(--ease);
+}
+.dl-btn:hover{background:var(--accent)}
+.dl-btn svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
+
+/* ── COMPARE COUNT ── */
+.compare-count{font-size:.78rem;color:var(--ink-dim);font-family:var(--serif);font-style:italic;margin-top:.3rem}
+
+/* stagger anim */
+@keyframes slideUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
+.compare-cell{animation:slideUp .3s var(--ease) both}
+.compare-cell:nth-child(2){animation-delay:.05s}
+.compare-cell:nth-child(3){animation-delay:.1s}
+.compare-cell:nth-child(4){animation-delay:.15s}
+.compare-cell:nth-child(5){animation-delay:.2s}
+.compare-cell:nth-child(6){animation-delay:.25s}
+.compare-cell:nth-child(7){animation-delay:.3s}
+.compare-cell:nth-child(8){animation-delay:.35s}
+.compare-cell:nth-child(9){animation-delay:.4s}
+
+@keyframes heartPop{0%{transform:scale(1)}40%{transform:scale(1.5)}70%{transform:scale(.9)}100%{transform:scale(1.25)}}
+.cw-heart.popping{animation:heartPop .35s var(--ease-spring) forwards}
+.compare-heart.popping{animation:heartPop .35s var(--ease-spring) forwards}
+</style>
+</head>
+<body>
+
+<header>
+  <div class="wordmark">Designer Wallcoverings<span>Colorway Studio</span></div>
+  <div class="header-tag">See every pattern in every light</div>
+</header>
+
+<div class="studio">
+  <!-- MAIN -->
+  <div class="hero-wrap">
+    <div class="hero-controls">
+      <label>Pattern</label>
+      <div class="select-wrap">
+        <select id="patternSel">
+          <option value="damask">Damask</option>
+          <option value="toile">Toile</option>
+          <option value="trellis">Trellis</option>
+          <option value="chinoiserie">Chinoiserie</option>
+          <option value="grasscloth">Grasscloth</option>
+          <option value="stripe">Stripe</option>
+        </select>
+      </div>
+      <div class="mode-btns">
+        <button class="mode-btn active" id="btnSingle">Single</button>
+        <button class="mode-btn" id="btnCompare">Compare</button>
+      </div>
+    </div>
+
+    <!-- SINGLE VIEW -->
+    <div class="hero-single" id="heroSingle">
+      <div class="hero-canvas" id="heroCanvas">
+        <div class="hero-svg-wrap" id="heroSvgWrap"></div>
+      </div>
+      <div class="caption" id="caption">Now viewing: <strong id="capPattern">Damask</strong> in <strong id="capColorway">Alabaster</strong></div>
+    </div>
+
+    <!-- COMPARE VIEW -->
+    <div class="hero-compare" id="heroCompare">
+      <div class="compare-grid" id="compareGrid"></div>
+    </div>
+
+    <!-- ROOM VIGNETTE -->
+    <div style="margin-top:.6rem">
+      <div class="sidebar-section-title" style="font-size:.72rem;letter-spacing:.18em;color:var(--ink-dim);margin-bottom:.5rem">ROOM VIGNETTE</div>
+      <div class="room-vignette" id="roomVignette"></div>
+    </div>
+  </div>
+
+  <!-- SIDEBAR -->
+  <div class="sidebar">
+    <div>
+      <div class="sidebar-section-title">Colorways</div>
+      <div class="colorway-rail" id="colorwayRail"></div>
+    </div>
+
+    <div>
+      <div class="sidebar-section-title">Favorites</div>
+      <div class="fav-bar" id="favBar"><span style="font-family:var(--serif);font-size:.85rem;color:var(--ink-dim);font-style:italic">Heart a colorway to save it</span></div>
+    </div>
+
+    <div>
+      <div class="sidebar-section-title">Download Tile</div>
+      <button class="dl-btn" id="dlBtn">
+        <svg viewBox="0 0 24 24"><polyline points="8 17 12 21 16 17"/><line x1="12" y1="21" x2="12" y2="8"/><path d="M20.88 18.09A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.29"/></svg>
+        Download Current Tile
+      </button>
+    </div>
+  </div>
+</div>
+
+<script>
+// ── COLORWAYS ──────────────────────────────────────────────────────────────
+const COLORWAYS = [
+  {id:'alabaster', name:'Alabaster', bg:'#f5f0e8', fg:'#c9c0b0', accent:'#bdb4a6'},
+  {id:'celadon',   name:'Celadon',   bg:'#dce8dc', fg:'#8aab8a', accent:'#6b9068'},
+  {id:'blush',     name:'Blush',     bg:'#f2ddd6', fg:'#d4967f', accent:'#b87460'},
+  {id:'ink',       name:'Ink',       bg:'#1c1f2b', fg:'#3a4060', accent:'#7a8099'},
+  {id:'brass',     name:'Brass',     bg:'#ede0c0', fg:'#c9a961', accent:'#9a7c3a'},
+  {id:'aubergine', name:'Aubergine', bg:'#3b2040', fg:'#6e3d6e', accent:'#c8a0c8'},
+  {id:'sage',      name:'Sage',      bg:'#dde4d8', fg:'#8da882', accent:'#5e7a58'},
+  {id:'terracotta',name:'Terracotta',bg:'#e8c8b0', fg:'#c07848', accent:'#8a4820'},
+  {id:'midnight',  name:'Midnight',  bg:'#0f1824', fg:'#1e3048', accent:'#4a7899'},
+  {id:'oyster',    name:'Oyster',    bg:'#e8e0d0', fg:'#c0b494', accent:'#9a8c70'},
+];
+
+// ── PATTERN GENERATORS ────────────────────────────────────────────────────
+function makeDamask(bg, fg, accent, w=400, h=400) {
+  const hw=w/2,hh=h/2;
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">
+  <defs>
+    <pattern id="dp" x="0" y="0" width="${hw}" height="${hh}" patternUnits="userSpaceOnUse">
+      <rect width="${hw}" height="${hh}" fill="${bg}"/>
+      <!-- center medallion -->
+      <ellipse cx="${hw/2}" cy="${hh/2}" rx="${hw*.28}" ry="${hh*.38}" fill="none" stroke="${fg}" stroke-width="1.5"/>
+      <ellipse cx="${hw/2}" cy="${hh/2}" rx="${hw*.16}" ry="${hh*.22}" fill="${fg}" opacity=".18"/>
+      <!-- petal top -->
+      <path d="M${hw/2},${hh*.12} C${hw*.38},${hh*.18} ${hw*.38},${hh*.3} ${hw/2},${hh*.36} C${hw*.62},${hh*.3} ${hw*.62},${hh*.18} ${hw/2},${hh*.12}Z" fill="${fg}" opacity=".55"/>
+      <!-- petal bottom -->
+      <path d="M${hw/2},${hh*.64} C${hw*.38},${hh*.7} ${hw*.38},${hh*.82} ${hw/2},${hh*.88} C${hw*.62},${hh*.82} ${hw*.62},${hh*.7} ${hw/2},${hh*.64}Z" fill="${fg}" opacity=".55"/>
+      <!-- petal left -->
+      <path d="M${hw*.12},${hh/2} C${hw*.18},${hh*.38} ${hw*.3},${hh*.38} ${hw*.36},${hh/2} C${hw*.3},${hh*.62} ${hw*.18},${hh*.62} ${hw*.12},${hh/2}Z" fill="${fg}" opacity=".55"/>
+      <!-- petal right -->
+      <path d="M${hw*.64},${hh/2} C${hw*.7},${hh*.38} ${hw*.82},${hh*.38} ${hw*.88},${hh/2} C${hw*.82},${hh*.62} ${hw*.7},${hh*.62} ${hw*.64},${hh/2}Z" fill="${fg}" opacity=".55"/>
+      <!-- diamond corners -->
+      <path d="M${hw/2},${hh*.04} L${hw*.56},${hh*.1} L${hw/2},${hh*.16} L${hw*.44},${hh*.1}Z" fill="${accent}" opacity=".7"/>
+      <path d="M${hw/2},${hh*.84} L${hw*.56},${hh*.9} L${hw/2},${hh*.96} L${hw*.44},${hh*.9}Z" fill="${accent}" opacity=".7"/>
+      <path d="M${hw*.04},${hh/2} L${hw*.1},${hh*.56} L${hw*.16},${hh/2} L${hw*.1},${hh*.44}Z" fill="${accent}" opacity=".7"/>
+      <path d="M${hw*.84},${hh/2} L${hw*.9},${hh*.56} L${hw*.96},${hh/2} L${hw*.9},${hh*.44}Z" fill="${accent}" opacity=".7"/>
+      <!-- corner florets -->
+      <circle cx="0" cy="0" r="${hw*.07}" fill="${fg}" opacity=".3"/>
+      <circle cx="${hw}" cy="0" r="${hw*.07}" fill="${fg}" opacity=".3"/>
+      <circle cx="0" cy="${hh}" r="${hw*.07}" fill="${fg}" opacity=".3"/>
+      <circle cx="${hw}" cy="${hh}" r="${hw*.07}" fill="${fg}" opacity=".3"/>
+      <!-- scroll lines -->
+      <path d="M${hw*.2},${hh*.2} Q${hw*.3},${hh*.08} ${hw/2},${hh*.12}" fill="none" stroke="${accent}" stroke-width="1" opacity=".5"/>
+      <path d="M${hw*.8},${hh*.2} Q${hw*.7},${hh*.08} ${hw/2},${hh*.12}" fill="none" stroke="${accent}" stroke-width="1" opacity=".5"/>
+      <path d="M${hw*.2},${hh*.8} Q${hw*.3},${hh*.92} ${hw/2},${hh*.88}" fill="none" stroke="${accent}" stroke-width="1" opacity=".5"/>
+      <path d="M${hw*.8},${hh*.8} Q${hw*.7},${hh*.92} ${hw/2},${hh*.88}" fill="none" stroke="${accent}" stroke-width="1" opacity=".5"/>
+    </pattern>
+  </defs>
+  <rect width="${w}" height="${h}" fill="url(#dp)"/>
+</svg>`;
+}
+
+function makeToile(bg, fg, accent, w=400, h=400) {
+  const u=w/4;
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">
+  <defs>
+    <pattern id="tp" x="0" y="0" width="${u*2}" height="${u*2}" patternUnits="userSpaceOnUse">
+      <rect width="${u*2}" height="${u*2}" fill="${bg}"/>
+      <!-- urn motif top-left -->
+      <path d="M${u*.5},${u*.9} Q${u*.3},${u*.7} ${u*.4},${u*.5} L${u*.45},${u*.35} L${u*.55},${u*.35} L${u*.6},${u*.5} Q${u*.7},${u*.7} ${u*.5},${u*.9}Z" fill="${fg}" opacity=".4"/>
+      <ellipse cx="${u*.5}" cy="${u*.32}" rx="${u*.16}" ry="${u*.08}" fill="${fg}" opacity=".4"/>
+      <!-- branch left -->
+      <path d="M${u*.5},${u*.7} Q${u*.15},${u*.6} ${u*.1},${u*.4}" fill="none" stroke="${fg}" stroke-width="${u*.03}" opacity=".35"/>
+      <path d="M${u*.1},${u*.4} Q${u*.05},${u*.2} ${u*.2},${u*.15}" fill="none" stroke="${fg}" stroke-width="${u*.025}" opacity=".35"/>
+      <!-- branch right -->
+      <path d="M${u*.5},${u*.7} Q${u*.85},${u*.6} ${u*.9},${u*.4}" fill="none" stroke="${fg}" stroke-width="${u*.03}" opacity=".35"/>
+      <path d="M${u*.9},${u*.4} Q${u*.95},${u*.2} ${u*.8},${u*.15}" fill="none" stroke="${fg}" stroke-width="${u*.025}" opacity=".35"/>
+      <!-- leaves -->
+      <ellipse cx="${u*.18}" cy="${u*.34}" rx="${u*.1}" ry="${u*.06}" transform="rotate(-30,${u*.18},${u*.34})" fill="${fg}" opacity=".3"/>
+      <ellipse cx="${u*.82}" cy="${u*.34}" rx="${u*.1}" ry="${u*.06}" transform="rotate(30,${u*.82},${u*.34})" fill="${fg}" opacity=".3"/>
+      <!-- bird bottom-right -->
+      <path d="M${u*1.5},${u*1.55} Q${u*1.6},${u*1.45} ${u*1.75},${u*1.5} Q${u*1.6},${u*1.6} ${u*1.5},${u*1.55}Z" fill="${fg}" opacity=".5"/>
+      <circle cx="${u*1.48}" cy="${u*1.54}" r="${u*.04}" fill="${fg}" opacity=".6"/>
+      <path d="M${u*1.5},${u*1.55} Q${u*1.42},${u*1.38} ${u*1.38},${u*1.45}" fill="none" stroke="${fg}" stroke-width="${u*.025}" opacity=".5"/>
+      <path d="M${u*1.5},${u*1.55} Q${u*1.38},${u*1.6} ${u*1.3},${u*1.58}" fill="none" stroke="${fg}" stroke-width="${u*.025}" opacity=".5"/>
+      <!-- dots -->
+      <circle cx="${u*.5}" cy="${u*.08}" r="${u*.04}" fill="${accent}" opacity=".45"/>
+      <circle cx="${u*1.5}" cy="${u*1.08}" r="${u*.04}" fill="${accent}" opacity=".45"/>
+      <circle cx="${u*.08}" cy="${u*1.5}" r="${u*.04}" fill="${accent}" opacity=".45"/>
+      <circle cx="${u*1.92}" cy="${u*.5}" r="${u*.04}" fill="${accent}" opacity=".45"/>
+    </pattern>
+  </defs>
+  <rect width="${w}" height="${h}" fill="url(#tp)"/>
+</svg>`;
+}
+
+function makeTrellis(bg, fg, accent, w=400, h=400) {
+  const s=w/5;
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">
+  <defs>
+    <pattern id="trp" x="0" y="0" width="${s}" height="${s}" patternUnits="userSpaceOnUse">
+      <rect width="${s}" height="${s}" fill="${bg}"/>
+      <!-- diamond grid -->
+      <path d="M${s/2},0 L${s},${s/2} L${s/2},${s} L0,${s/2}Z" fill="none" stroke="${fg}" stroke-width="1.2" opacity=".6"/>
+      <!-- inner diamond -->
+      <path d="M${s/2},${s*.15} L${s*.85},${s/2} L${s/2},${s*.85} L${s*.15},${s/2}Z" fill="none" stroke="${fg}" stroke-width=".7" opacity=".35"/>
+      <!-- center dot with petals -->
+      <circle cx="${s/2}" cy="${s/2}" r="${s*.1}" fill="${fg}" opacity=".5"/>
+      <circle cx="${s/2}" cy="${s/2}" r="${s*.05}" fill="${accent}" opacity=".8"/>
+      <!-- corner dots -->
+      <circle cx="0" cy="0" r="${s*.06}" fill="${fg}" opacity=".4"/>
+      <circle cx="${s}" cy="0" r="${s*.06}" fill="${fg}" opacity=".4"/>
+      <circle cx="0" cy="${s}" r="${s*.06}" fill="${fg}" opacity=".4"/>
+      <circle cx="${s}" cy="${s}" r="${s*.06}" fill="${fg}" opacity=".4"/>
+      <!-- mid-edge accents -->
+      <circle cx="${s/2}" cy="0" r="${s*.04}" fill="${accent}" opacity=".5"/>
+      <circle cx="${s/2}" cy="${s}" r="${s*.04}" fill="${accent}" opacity=".5"/>
+      <circle cx="0" cy="${s/2}" r="${s*.04}" fill="${accent}" opacity=".5"/>
+      <circle cx="${s}" cy="${s/2}" r="${s*.04}" fill="${accent}" opacity=".5"/>
+    </pattern>
+  </defs>
+  <rect width="${w}" height="${h}" fill="url(#trp)"/>
+</svg>`;
+}
+
+function makeChinoiserie(bg, fg, accent, w=400, h=400) {
+  const u=w/2;
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">
+  <defs>
+    <pattern id="cp" x="0" y="0" width="${u}" height="${u}" patternUnits="userSpaceOnUse">
+      <rect width="${u}" height="${u}" fill="${bg}"/>
+      <!-- pagoda silhouette -->
+      <path d="M${u*.5},${u*.15} L${u*.56},${u*.25} L${u*.52},${u*.25} L${u*.57},${u*.38} L${u*.52},${u*.38} L${u*.58},${u*.55} L${u*.42},${u*.55} L${u*.48},${u*.38} L${u*.43},${u*.38} L${u*.48},${u*.25} L${u*.44},${u*.25}Z" fill="${fg}" opacity=".45"/>
+      <!-- roof flourishes -->
+      <path d="M${u*.35},${u*.26} Q${u*.42},${u*.22} ${u*.44},${u*.25}" fill="none" stroke="${fg}" stroke-width="1" opacity=".4"/>
+      <path d="M${u*.65},${u*.26} Q${u*.58},${u*.22} ${u*.56},${u*.25}" fill="none" stroke="${fg}" stroke-width="1" opacity=".4"/>
+      <!-- cherry blossom branch -->
+      <path d="M${u*.18},${u*.9} Q${u*.25},${u*.65} ${u*.38},${u*.55}" fill="none" stroke="${fg}" stroke-width="${u*.025}" opacity=".5"/>
+      <path d="M${u*.25},${u*.7} Q${u*.12},${u*.55} ${u*.08},${u*.42}" fill="none" stroke="${fg}" stroke-width="${u*.02}" opacity=".4"/>
+      <!-- blossoms -->
+      ${[{x:.1,y:.38},{x:.07,y:.45},{x:.22,y:.63},{x:.32,y:.52}].map(p=>`
+      <circle cx="${u*p.x}" cy="${u*p.y}" r="${u*.06}" fill="${fg}" opacity=".25"/>
+      <circle cx="${u*p.x}" cy="${u*p.y}" r="${u*.03}" fill="${accent}" opacity=".55"/>
+      `).join('')}
+      <!-- mountain horizon -->
+      <path d="M0,${u*.92} Q${u*.15},${u*.76} ${u*.3},${u*.85} Q${u*.45},${u*.74} ${u*.55},${u*.82} Q${u*.7},${u*.68} ${u},${u*.8} L${u},${u} L0,${u}Z" fill="${fg}" opacity=".12"/>
+      <!-- border -->
+      <rect x="2" y="2" width="${u-4}" height="${u-4}" fill="none" stroke="${fg}" stroke-width=".8" opacity=".18"/>
+    </pattern>
+  </defs>
+  <rect width="${w}" height="${h}" fill="url(#cp)"/>
+</svg>`;
+}
+
+function makeGrasscloth(bg, fg, accent, w=400, h=400) {
+  const rows=24, cols=16;
+  const cw=w/cols, ch=h/rows;
+  let lines='';
+  for(let r=0;r<rows;r++){
+    for(let c=0;c<cols;c++){
+      const x=c*cw, y=r*ch;
+      const offset=((r+c)%3)*.3;
+      lines+=`<rect x="${x+offset}" y="${y}" width="${cw*.85}" height="${ch*.92}" fill="${fg}" opacity="${.12+((r*7+c*13)%5)*.04}" rx=".5"/>`;
+    }
+  }
+  // weft threads
+  for(let r=0;r<=rows;r++){
+    lines+=`<line x1="0" y1="${r*ch}" x2="${w}" y2="${r*ch}" stroke="${bg}" stroke-width=".9" opacity=".6"/>`;
+  }
+  for(let c=0;c<=cols;c+=3){
+    lines+=`<line x1="${c*cw}" y1="0" x2="${c*cw}" y2="${h}" stroke="${accent}" stroke-width=".4" opacity=".18"/>`;
+  }
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">
+  <rect width="${w}" height="${h}" fill="${bg}"/>
+  ${lines}
+</svg>`;
+}
+
+function makeStripe(bg, fg, accent, w=400, h=400) {
+  const n=10, sw=w/n;
+  let stripes='';
+  for(let i=0;i<n;i++){
+    const x=i*sw;
+    if(i%4===0) stripes+=`<rect x="${x}" y="0" width="${sw}" height="${h}" fill="${fg}" opacity=".55"/>`;
+    else if(i%4===2) stripes+=`<rect x="${x}" y="0" width="${sw}" height="${h}" fill="${accent}" opacity=".35"/>`;
+    else stripes+=`<rect x="${x}" y="0" width="${sw}" height="${h}" fill="${fg}" opacity=".1"/>`;
+  }
+  // pinstripes
+  for(let i=0;i<=n;i++){
+    stripes+=`<line x1="${i*sw}" y1="0" x2="${i*sw}" y2="${h}" stroke="${fg}" stroke-width=".6" opacity=".3"/>`;
+  }
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">
+  <rect width="${w}" height="${h}" fill="${bg}"/>
+  ${stripes}
+</svg>`;
+}
+
+const PATTERNS = {
+  damask:{label:'Damask',fn:makeDamask},
+  toile:{label:'Toile',fn:makeToile},
+  trellis:{label:'Trellis',fn:makeTrellis},
+  chinoiserie:{label:'Chinoiserie',fn:makeChinoiserie},
+  grasscloth:{label:'Grasscloth',fn:makeGrasscloth},
+  stripe:{label:'Stripe',fn:makeStripe},
+};
+
+// ── ROOM VIGNETTE ─────────────────────────────────────────────────────────
+function makeRoom(bg, fg, accent) {
+  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300">
+  <!-- wall: tiled pattern background using a small repeat -->
+  <defs>
+    <pattern id="wallpat" x="0" y="0" width="60" height="60" patternUnits="userSpaceOnUse">
+      <rect width="60" height="60" fill="${bg}"/>
+      <ellipse cx="30" cy="30" rx="8" ry="11" fill="none" stroke="${fg}" stroke-width="1.2" opacity=".5"/>
+      <circle cx="30" cy="30" r="3.5" fill="${fg}" opacity=".3"/>
+      <path d="M30,19 C27,22 27,26 30,28 C33,26 33,22 30,19Z" fill="${fg}" opacity=".4"/>
+      <path d="M30,41 C27,38 27,34 30,32 C33,34 33,38 30,41Z" fill="${fg}" opacity=".4"/>
+      <circle cx="0" cy="0" r="3" fill="${fg}" opacity=".2"/>
+      <circle cx="60" cy="0" r="3" fill="${fg}" opacity=".2"/>
+      <circle cx="0" cy="60" r="3" fill="${fg}" opacity=".2"/>
+      <circle cx="60" cy="60" r="3" fill="${fg}" opacity=".2"/>
+    </pattern>
+  </defs>
+  <!-- rear wall -->
+  <rect x="0" y="0" width="400" height="220" fill="url(#wallpat)"/>
+  <!-- floor -->
+  <path d="M0,220 L400,220 L400,300 L0,300Z" fill="#d4c8b4"/>
+  <!-- floor boards -->
+  <line x1="0" y1="240" x2="400" y2="240" stroke="#bfb4a0" stroke-width="1.5"/>
+  <line x1="0" y1="262" x2="400" y2="262" stroke="#bfb4a0" stroke-width="1.5"/>
+  <line x1="0" y1="284" x2="400" y2="284" stroke="#bfb4a0" stroke-width="1.5"/>
+  <!-- sofa -->
+  <rect x="80" y="195" width="220" height="60" rx="4" fill="${accent}" opacity=".7"/>
+  <rect x="76" y="188" width="228" height="22" rx="5" fill="${accent}" opacity=".85"/>
+  <!-- sofa legs -->
+  <rect x="95" y="254" width="12" height="14" rx="2" fill="#7a6850"/>
+  <rect x="273" y="254" width="12" height="14" rx="2" fill="#7a6850"/>
+  <!-- cushions -->
+  <rect x="90" y="191" width="65" height="55" rx="4" fill="${fg}" opacity=".25"/>
+  <rect x="165" y="191" width="65" height="55" rx="4" fill="${fg}" opacity=".2"/>
+  <rect x="240" y="191" width="55" height="55" rx="4" fill="${fg}" opacity=".25"/>
+  <!-- side table -->
+  <rect x="315" y="215" width="55" height="8" rx="2" fill="#8a7558"/>
+  <rect x="328" y="223" width="8" height="35" rx="2" fill="#8a7558"/>
+  <rect x="348" y="223" width="8" height="35" rx="2" fill="#8a7558"/>
+  <!-- lamp -->
+  <path d="M340,170 L330,215 L370,215Z" fill="#e8e0cc" opacity=".85"/>
+  <rect x="348" y="215" width="5" height="35" rx="1" fill="#7a6850"/>
+  <!-- wainscot rail -->
+  <rect x="0" y="185" width="400" height="4" fill="${fg}" opacity=".3"/>
+  <!-- baseboard -->
+  <rect x="0" y="216" width="400" height="8" fill="#c8c0b0"/>
+  <!-- picture frame -->
+  <rect x="270" y="60" width="80" height="60" rx="2" fill="none" stroke="${fg}" stroke-width="2.5" opacity=".5"/>
+  <rect x="276" y="66" width="68" height="48" rx="1" fill="${fg}" opacity=".08"/>
+</svg>`;
+}
+
+// ── STATE ─────────────────────────────────────────────────────────────────
+let currentPattern = 'damask';
+let currentCwId = 'alabaster';
+let mode = 'single'; // 'single'|'compare'
+let favorites = new Set(JSON.parse(localStorage.getItem('dw-favs')||'[]'));
+let compareFavs = new Set(JSON.parse(localStorage.getItem('dw-cfavs')||'[]'));
+
+function getCw(id){ return COLORWAYS.find(c=>c.id===id) }
+function getPatFn(){ return PATTERNS[currentPattern].fn }
+
+// ── RENDER HERO ───────────────────────────────────────────────────────────
+function renderHero(cwId, animate=true){
+  const cw = getCw(cwId);
+  const wrap = document.getElementById('heroSvgWrap');
+  if(animate){ wrap.classList.add('fade-out'); }
+  setTimeout(()=>{
+    wrap.innerHTML = getPatFn()(cw.bg, cw.fg, cw.accent, 800, 450);
+    wrap.classList.remove('fade-out');
+  }, animate?200:0);
+  document.getElementById('capPattern').textContent = PATTERNS[currentPattern].label;
+  document.getElementById('capColorway').textContent = cw.name;
+  renderRoom(cwId);
+}
+
+function renderRoom(cwId){
+  const cw = getCw(cwId);
+  document.getElementById('roomVignette').innerHTML = makeRoom(cw.bg, cw.fg, cw.accent);
+}
+
+// ── RENDER COMPARE ────────────────────────────────────────────────────────
+function renderCompare(){
+  const grid = document.getElementById('compareGrid');
+  grid.innerHTML = '';
+  COLORWAYS.forEach((cw,i)=>{
+    const cell = document.createElement('div');
+    cell.className = 'compare-cell';
+    cell.style.animationDelay = (i*0.05)+'s';
+    const svg = getPatFn()(cw.bg, cw.fg, cw.accent, 300, 225);
+    const isFav = compareFavs.has(cw.id);
+    cell.innerHTML = `${svg}<div class="compare-cell-label">${cw.name}</div>
+      <div class="compare-heart${isFav?' active':''}" data-cw="${cw.id}" title="Favorite">&#9829;</div>`;
+    cell.querySelector('svg').addEventListener('click',()=>{
+      setColorway(cw.id, true);
+      setMode('single');
+    });
+    cell.querySelector('.compare-heart').addEventListener('click',(e)=>{
+      e.stopPropagation();
+      toggleFavCompare(cw.id, cell.querySelector('.compare-heart'));
+    });
+    grid.appendChild(cell);
+  });
+}
+
+// ── RENDER COLORWAY RAIL ──────────────────────────────────────────────────
+function renderRail(){
+  const rail = document.getElementById('colorwayRail');
+  rail.innerHTML = '';
+  COLORWAYS.forEach(cw=>{
+    const chip = document.createElement('div');
+    chip.className = 'cw-chip'+(cw.id===currentCwId?' active':'');
+    chip.dataset.cw = cw.id;
+    const isFav = favorites.has(cw.id);
+    chip.innerHTML = `
+      <div class="cw-swatch" style="background:${cw.bg};border-color:${cw.fg}20"></div>
+      <div class="cw-info"><div class="cw-name">${cw.name}</div><div class="cw-hex">${cw.bg}</div></div>
+      <div class="cw-heart${isFav?' active':''}" data-cw="${cw.id}" title="Favorite">&#9829;</div>`;
+    chip.addEventListener('click', ()=>{ setColorway(cw.id, true); });
+    chip.querySelector('.cw-heart').addEventListener('click',(e)=>{
+      e.stopPropagation();
+      toggleFav(cw.id, chip.querySelector('.cw-heart'));
+    });
+    rail.appendChild(chip);
+  });
+}
+
+// ── FAVORITES ─────────────────────────────────────────────────────────────
+function renderFavBar(){
+  const bar = document.getElementById('favBar');
+  if(favorites.size===0){
+    bar.innerHTML = '<span style="font-family:var(--serif);font-size:.85rem;color:var(--ink-dim);font-style:italic">Heart a colorway to save it</span>';
+    return;
+  }
+  bar.innerHTML = '';
+  favorites.forEach(id=>{
+    const cw = getCw(id);
+    const tag = document.createElement('div');
+    tag.className='fav-tag';
+    tag.innerHTML=`<div class="fav-dot" style="background:${cw.bg};border:1px solid ${cw.fg}50"></div>${cw.name}`;
+    tag.style.cursor='pointer';
+    tag.title='Click to view';
+    tag.addEventListener('click',()=>{ setColorway(id,true); if(mode==='compare') setMode('single'); });
+    bar.appendChild(tag);
+  });
+}
+
+function toggleFav(id, el){
+  if(favorites.has(id)){favorites.delete(id);}
+  else {favorites.add(id); el.classList.add('popping'); setTimeout(()=>el.classList.remove('popping'),400);}
+  el.classList.toggle('active', favorites.has(id));
+  localStorage.setItem('dw-favs', JSON.stringify([...favorites]));
+  renderFavBar();
+}
+
+function toggleFavCompare(id, el){
+  if(compareFavs.has(id)){compareFavs.delete(id); if(favorites.has(id)) favorites.delete(id);}
+  else {
+    compareFavs.add(id); favorites.add(id);
+    el.classList.add('popping'); setTimeout(()=>el.classList.remove('popping'),400);
+  }
+  el.classList.toggle('active', compareFavs.has(id));
+  localStorage.setItem('dw-cfavs', JSON.stringify([...compareFavs]));
+  localStorage.setItem('dw-favs', JSON.stringify([...favorites]));
+  renderRail();
+  renderFavBar();
+}
+
+// ── SET COLORWAY ──────────────────────────────────────────────────────────
+function setColorway(id, animate=false){
+  currentCwId = id;
+  document.querySelectorAll('.cw-chip').forEach(c=>c.classList.toggle('active', c.dataset.cw===id));
+  renderHero(id, animate);
+}
+
+// ── SET PATTERN ───────────────────────────────────────────────────────────
+function setPattern(p){
+  currentPattern = p;
+  if(mode==='compare') renderCompare();
+  else renderHero(currentCwId, true);
+}
+
+// ── SET MODE ──────────────────────────────────────────────────────────────
+function setMode(m){
+  mode = m;
+  document.getElementById('heroSingle').style.display = m==='single'?'block':'none';
+  document.getElementById('heroCompare').style.display = m==='compare'?'block':'none';
+  document.getElementById('btnSingle').classList.toggle('active', m==='single');
+  document.getElementById('btnCompare').classList.toggle('active', m==='compare');
+  if(m==='compare') renderCompare();
+  else renderHero(currentCwId, false);
+}
+
+// ── DOWNLOAD ──────────────────────────────────────────────────────────────
+document.getElementById('dlBtn').addEventListener('click',()=>{
+  const cw = getCw(currentCwId);
+  const svgStr = getPatFn()(cw.bg, cw.fg, cw.accent, 600, 600);
+  const blob = new Blob([svgStr],{type:'image/svg+xml'});
+  const url = URL.createObjectURL(blob);
+  const a = document.createElement('a');
+  a.href = url;
+  a.download = `dw-${currentPattern}-${currentCwId}.svg`;
+  a.click();
+  setTimeout(()=>URL.revokeObjectURL(url),1000);
+});
+
+// ── CONTROLS ──────────────────────────────────────────────────────────────
+document.getElementById('patternSel').addEventListener('change', e=>setPattern(e.target.value));
+document.getElementById('btnSingle').addEventListener('click',()=>setMode('single'));
+document.getElementById('btnCompare').addEventListener('click',()=>setMode('compare'));
+
+// Keyboard nav
+document.addEventListener('keydown', e=>{
+  if(e.key==='ArrowDown'||e.key==='ArrowRight'){
+    const idx=COLORWAYS.findIndex(c=>c.id===currentCwId);
+    setColorway(COLORWAYS[(idx+1)%COLORWAYS.length].id, true);
+    renderRail();
+  } else if(e.key==='ArrowUp'||e.key==='ArrowLeft'){
+    const idx=COLORWAYS.findIndex(c=>c.id===currentCwId);
+    setColorway(COLORWAYS[(idx-1+COLORWAYS.length)%COLORWAYS.length].id, true);
+    renderRail();
+  }
+});
+
+// ── INIT ──────────────────────────────────────────────────────────────────
+renderRail();
+renderHero('alabaster', false);
+renderFavBar();
+</script>
+</body>
+</html>

← 4dd0e69 Agent-driven tool: DW Chinoiserie Studio (frontend-developer  ·  back to Model Arena  ·  Agent-driven art toy: DW Motif Spinner (frontend-developer) a0778d8 →