← 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
M data/arcade-games.jsonA public/games/dw-colorway-studio.html
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">♥</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">♥</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 →