← back to Model Arena
Agent-driven art toy: DW Damask Kaleidoscope (frontend-developer) — 8-fold bezier damask, mouse-warp, 5 palettes, PNG export. Registered.
71faf2787e01120b266777ce78526cedc8be7a98 · 2026-07-25 11:47:12 -0700 · Steve Abrams
Files touched
M data/arcade-games.jsonA public/games/dw-content-calendar.htmlA public/games/dw-damask-kaleidoscope.html
Diff
commit 71faf2787e01120b266777ce78526cedc8be7a98
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Jul 25 11:47:12 2026 -0700
Agent-driven art toy: DW Damask Kaleidoscope (frontend-developer) — 8-fold bezier damask, mouse-warp, 5 palettes, PNG export. Registered.
---
data/arcade-games.json | 6 +
public/games/dw-content-calendar.html | 787 +++++++++++++++++++++++++++++++
public/games/dw-damask-kaleidoscope.html | 502 ++++++++++++++++++++
3 files changed, 1295 insertions(+)
diff --git a/data/arcade-games.json b/data/arcade-games.json
index 28c8f76..bd3a7b2 100644
--- a/data/arcade-games.json
+++ b/data/arcade-games.json
@@ -71,6 +71,12 @@
"title": "DW The Lookbook",
"credit": "frontend-developer",
"note": "Agent-built editorial \u2014 6 swipeable style spreads (Grandmillennial\u2192Modern Minimal), each with SVG hero + palette"
+ },
+ {
+ "slug": "dw-damask-kaleidoscope",
+ "title": "DW Damask Kaleidoscope",
+ "credit": "frontend-developer",
+ "note": "Agent-built generative-art toy \u2014 8-fold bezier damask kaleidoscope, mouse-warp, 5 palettes, PNG export"
}
]
}
\ No newline at end of file
diff --git a/public/games/dw-content-calendar.html b/public/games/dw-content-calendar.html
new file mode 100644
index 0000000..eb01c04
--- /dev/null
+++ b/public/games/dw-content-calendar.html
@@ -0,0 +1,787 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Designer Wallcoverings — Content Calendar</title>
+<style>
+/* ── tokens ── */
+:root{
+ --bg:#f4f1ea;
+ --bg2:#ede9e0;
+ --bg3:#e6e1d6;
+ --ink:#1c1a17;
+ --ink2:#4a4540;
+ --ink3:#7a746c;
+ --accent:#c9a961;
+ --accent2:#a8883e;
+ --panel:#faf8f4;
+ --hair:#d6cfc3;
+ --serif:'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif;
+ --sans:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
+ --ease:cubic-bezier(.4,0,.2,1);
+ --r:6px;
+ --shadow:0 2px 16px rgba(28,26,23,.10),0 1px 4px rgba(28,26,23,.06);
+ --panel-w:340px;
+}
+*,*::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;
+ display:flex;
+ flex-direction:column;
+}
+
+/* ── header ── */
+header{
+ background:var(--bg);
+ border-bottom:1px solid var(--hair);
+ padding:18px 28px 14px;
+ display:flex;
+ align-items:baseline;
+ gap:24px;
+ flex-wrap:wrap;
+}
+.wordmark{
+ font-family:var(--serif);
+ font-size:clamp(28px,3.5vw,44px);
+ font-weight:600;
+ letter-spacing:.18em;
+ text-transform:uppercase;
+ color:var(--ink);
+ line-height:1;
+ white-space:nowrap;
+}
+.sub{
+ font-size:11px;
+ letter-spacing:.22em;
+ text-transform:uppercase;
+ color:var(--ink3);
+ font-family:var(--sans);
+}
+
+/* ── layout shell ── */
+.shell{
+ display:flex;
+ flex:1;
+ min-height:0;
+ overflow:hidden;
+}
+.main{
+ flex:1;
+ min-width:0;
+ display:flex;
+ flex-direction:column;
+ padding:24px 20px 24px 24px;
+ gap:16px;
+ overflow-y:auto;
+}
+
+/* ── month nav ── */
+.month-nav{
+ display:flex;
+ align-items:center;
+ gap:16px;
+ flex-wrap:wrap;
+}
+.month-title{
+ font-family:var(--serif);
+ font-size:clamp(20px,2.5vw,30px);
+ font-weight:600;
+ letter-spacing:.06em;
+ color:var(--ink);
+ min-width:200px;
+}
+.nav-btn{
+ background:none;
+ border:1px solid var(--hair);
+ border-radius:var(--r);
+ width:34px;height:34px;
+ cursor:pointer;
+ display:flex;align-items:center;justify-content:center;
+ color:var(--ink2);
+ font-size:16px;
+ transition:border-color .15s var(--ease),background .15s var(--ease),color .15s var(--ease);
+}
+.nav-btn:hover{border-color:var(--accent);background:var(--accent);color:#fff}
+.today-btn{
+ background:none;border:1px solid var(--hair);border-radius:var(--r);
+ padding:6px 14px;font-size:12px;letter-spacing:.08em;text-transform:uppercase;
+ cursor:pointer;color:var(--ink2);
+ transition:all .15s var(--ease);
+ font-family:var(--sans);
+}
+.today-btn:hover{border-color:var(--accent);color:var(--accent)}
+.stats{
+ margin-left:auto;
+ display:flex;gap:18px;align-items:center;flex-wrap:wrap;
+}
+.stat{
+ font-size:12px;color:var(--ink3);
+ display:flex;align-items:center;gap:6px;
+}
+.stat strong{color:var(--ink);font-size:14px}
+
+/* ── week strip ── */
+.week-strip{
+ background:var(--bg2);
+ border:1px solid var(--hair);
+ border-radius:var(--r);
+ padding:12px 16px;
+ display:flex;
+ gap:8px;
+ overflow-x:auto;
+ flex-shrink:0;
+}
+.week-strip::-webkit-scrollbar{height:4px}
+.week-strip::-webkit-scrollbar-thumb{background:var(--hair);border-radius:2px}
+.week-day{
+ flex:0 0 auto;
+ min-width:90px;
+ background:var(--panel);
+ border:1px solid var(--hair);
+ border-radius:var(--r);
+ padding:8px 10px;
+ font-size:11px;
+}
+.week-day .wd-label{font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink3);margin-bottom:4px}
+.week-day .wd-date{font-family:var(--serif);font-size:16px;color:var(--ink);font-weight:600}
+.week-day .wd-chips{display:flex;flex-wrap:wrap;gap:3px;margin-top:5px}
+
+/* ── calendar grid ── */
+.cal-wrap{border:1px solid var(--hair);border-radius:var(--r);overflow:hidden;background:var(--panel)}
+.cal-head{
+ display:grid;grid-template-columns:repeat(7,1fr);
+ border-bottom:1px solid var(--hair);
+ background:var(--bg2);
+}
+.cal-dow{
+ padding:8px 6px;
+ font-size:11px;letter-spacing:.12em;text-transform:uppercase;
+ color:var(--ink3);text-align:center;font-family:var(--sans);
+}
+.cal-grid{
+ display:grid;
+ grid-template-columns:repeat(7,1fr);
+}
+.cal-cell{
+ border-right:1px solid var(--hair);
+ border-bottom:1px solid var(--hair);
+ min-height:110px;
+ padding:8px 6px 6px;
+ cursor:pointer;
+ position:relative;
+ transition:background .12s var(--ease);
+ display:flex;flex-direction:column;gap:4px;
+}
+.cal-cell:hover{background:var(--bg2)}
+.cal-cell.empty{background:var(--bg);cursor:default}
+.cal-cell.empty:hover{background:var(--bg)}
+.cal-cell.today .cell-num{
+ background:var(--accent);color:#fff;
+ width:22px;height:22px;border-radius:50%;
+ display:flex;align-items:center;justify-content:center;
+}
+.cal-cell.selected{background:rgba(201,169,97,.07);outline:2px solid var(--accent);outline-offset:-2px;}
+.cell-num{font-size:12px;color:var(--ink2);font-weight:500;width:22px;height:22px;display:flex;align-items:center;justify-content:center}
+.cell-chips{display:flex;flex-wrap:wrap;gap:3px;flex:1}
+.add-hint{
+ font-size:10px;color:var(--hair);
+ position:absolute;bottom:6px;right:6px;
+ opacity:0;transition:opacity .15s var(--ease);
+ pointer-events:none;
+}
+.cal-cell:not(.empty):hover .add-hint{opacity:1}
+
+/* ── chips ── */
+.chip{
+ display:inline-flex;align-items:center;gap:3px;
+ padding:3px 7px;
+ border-radius:20px;
+ font-size:10px;font-weight:600;letter-spacing:.04em;
+ cursor:pointer;
+ position:relative;
+ user-select:none;
+ border:none;
+ white-space:nowrap;
+ transition:transform .12s var(--ease),filter .12s var(--ease);
+ animation:chipPop .2s var(--ease);
+}
+.chip:hover{transform:translateY(-1px);filter:brightness(1.08)}
+@keyframes chipPop{
+ 0%{transform:scale(.6);opacity:0}
+ 70%{transform:scale(1.08)}
+ 100%{transform:scale(1);opacity:1}
+}
+.chip-reel{background:#e8d8b8;color:#6b4c18}
+.chip-carousel{background:#d4e4d8;color:#1e4d2b}
+.chip-story{background:#ddd8f0;color:#3d2e82}
+.chip-sale{background:#f0d8d8;color:#7a1f1f}
+.chip-editorial{background:#d8eaf0;color:#1a4a5c}
+.chip-sm{padding:2px 5px;font-size:9px}
+
+/* ── tooltip ── */
+[data-tip]{position:relative}
+[data-tip]::after{
+ content:attr(data-tip);
+ position:absolute;
+ bottom:calc(100% + 6px);
+ left:50%;transform:translateX(-50%);
+ background:var(--ink);color:#f4f1ea;
+ font-size:11px;font-weight:400;letter-spacing:.01em;
+ padding:5px 9px;border-radius:4px;
+ white-space:nowrap;max-width:200px;white-space:normal;
+ pointer-events:none;
+ opacity:0;transition:opacity .15s var(--ease);
+ z-index:100;
+ line-height:1.4;
+}
+[data-tip]:hover::after{opacity:1}
+
+/* ── legend ── */
+.legend{
+ display:flex;gap:12px;flex-wrap:wrap;align-items:center;
+}
+.legend-item{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--ink2);cursor:pointer}
+.legend-dot{width:10px;height:10px;border-radius:50%}
+
+/* ── right panel ── */
+.side-panel{
+ width:var(--panel-w);
+ flex-shrink:0;
+ border-left:1px solid var(--hair);
+ background:var(--panel);
+ display:flex;flex-direction:column;
+ overflow:hidden;
+ transform:translateX(var(--panel-w));
+ visibility:hidden;
+ transition:transform .28s var(--ease),visibility 0s .28s;
+}
+.side-panel.open{
+ transform:translateX(0);
+ visibility:visible;
+ transition:transform .28s var(--ease),visibility 0s 0s;
+}
+.panel-head{
+ padding:20px 20px 14px;
+ border-bottom:1px solid var(--hair);
+ display:flex;align-items:flex-start;justify-content:space-between;gap:12px;
+}
+.panel-date{font-family:var(--serif);font-size:22px;font-weight:600;color:var(--ink)}
+.panel-close{
+ background:none;border:none;cursor:pointer;
+ width:28px;height:28px;border-radius:50%;
+ display:flex;align-items:center;justify-content:center;
+ color:var(--ink3);font-size:18px;
+ transition:background .12s var(--ease),color .12s var(--ease);
+ flex-shrink:0;
+}
+.panel-close:hover{background:var(--bg3);color:var(--ink)}
+.panel-body{flex:1;overflow-y:auto;padding:16px 20px}
+.panel-body::-webkit-scrollbar{width:4px}
+.panel-body::-webkit-scrollbar-thumb{background:var(--hair);border-radius:2px}
+.post-card{
+ border:1px solid var(--hair);border-radius:var(--r);
+ margin-bottom:12px;overflow:hidden;
+}
+.post-card-head{
+ padding:8px 12px;
+ display:flex;align-items:center;justify-content:space-between;
+}
+.post-card-type{font-size:11px;font-weight:700;letter-spacing:.1em;text-transform:uppercase}
+.post-card-del{
+ background:none;border:none;cursor:pointer;
+ color:var(--ink3);font-size:16px;line-height:1;
+ padding:2px 4px;border-radius:3px;
+ transition:color .12s var(--ease),background .12s var(--ease);
+}
+.post-card-del:hover{color:#7a1f1f;background:#f0d8d8}
+.post-card-body{padding:8px 12px 12px}
+.caption-area{
+ width:100%;border:1px solid var(--hair);border-radius:4px;
+ background:var(--bg);color:var(--ink);
+ font-family:var(--sans);font-size:12px;line-height:1.5;
+ padding:7px 10px;resize:vertical;min-height:72px;
+ outline:none;transition:border-color .15s var(--ease);
+}
+.caption-area:focus{border-color:var(--accent)}
+.hashtags{
+ margin-top:6px;font-size:10px;color:var(--ink3);
+ line-height:1.5;letter-spacing:.01em;
+}
+.add-post-bar{
+ padding:12px 20px;border-top:1px solid var(--hair);
+ display:flex;gap:8px;flex-wrap:wrap;
+}
+.add-post-btn{
+ flex:1;min-width:60px;
+ padding:7px 6px;
+ border-radius:20px;font-size:10px;font-weight:700;
+ letter-spacing:.05em;text-transform:uppercase;
+ cursor:pointer;border:2px solid transparent;
+ transition:transform .12s var(--ease),filter .12s var(--ease),border-color .15s var(--ease);
+}
+.add-post-btn:hover{transform:translateY(-1px);filter:brightness(1.1)}
+.add-post-btn:active{transform:scale(.96)}
+.add-post-btn.reel{background:#e8d8b8;color:#6b4c18}
+.add-post-btn.carousel{background:#d4e4d8;color:#1e4d2b}
+.add-post-btn.story{background:#ddd8f0;color:#3d2e82}
+.add-post-btn.sale{background:#f0d8d8;color:#7a1f1f}
+.add-post-btn.editorial{background:#d8eaf0;color:#1a4a5c}
+.empty-state{
+ text-align:center;padding:32px 16px;color:var(--ink3);font-size:13px;line-height:1.6;
+}
+.empty-state span{display:block;font-size:28px;margin-bottom:8px;opacity:.5}
+
+/* ── this week strip ── */
+.strip-label{
+ font-size:11px;letter-spacing:.12em;text-transform:uppercase;
+ color:var(--ink3);margin-bottom:8px;
+}
+
+/* ── responsive ── */
+@media(max-width:800px){
+ :root{--panel-w:100vw}
+ .shell{flex-direction:column}
+ .side-panel{
+ position:fixed;bottom:0;left:0;right:0;
+ width:100%;height:60vh;
+ border-left:none;border-top:1px solid var(--hair);
+ transform:translateY(60vh);
+ }
+ .side-panel.open{transform:translateY(0)}
+ .cal-cell{min-height:80px}
+ .stats{margin-left:0}
+}
+@media(max-width:480px){
+ .main{padding:12px}
+ .cal-cell{min-height:60px;padding:4px}
+ header{padding:12px 16px 10px}
+}
+</style>
+</head>
+<body>
+
+<header>
+ <span class="wordmark">Designer Wallcoverings</span>
+ <span class="sub">Content Calendar</span>
+</header>
+
+<div class="shell">
+ <main class="main">
+ <!-- month nav -->
+ <div class="month-nav">
+ <button class="nav-btn" id="prevBtn" aria-label="Previous month">
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M10 12L6 8l4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
+ </button>
+ <div class="month-title" id="monthTitle"></div>
+ <button class="nav-btn" id="nextBtn" aria-label="Next month">
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
+ </button>
+ <button class="today-btn" id="todayBtn">Today</button>
+ <div class="stats">
+ <div class="stat"><strong id="statTotal">0</strong> posts this month</div>
+ <div id="typeStats" class="stat" style="gap:10px;flex-wrap:wrap"></div>
+ </div>
+ </div>
+
+ <!-- legend -->
+ <div class="legend" id="legend">
+ <div class="legend-item"><div class="legend-dot" style="background:#e8d8b8"></div>Reel</div>
+ <div class="legend-item"><div class="legend-dot" style="background:#d4e4d8"></div>Carousel</div>
+ <div class="legend-item"><div class="legend-dot" style="background:#ddd8f0"></div>Story</div>
+ <div class="legend-item"><div class="legend-dot" style="background:#f0d8d8"></div>Sale</div>
+ <div class="legend-item"><div class="legend-dot" style="background:#d8eaf0"></div>Editorial</div>
+ </div>
+
+ <!-- this week strip -->
+ <div>
+ <div class="strip-label">This Week at a Glance</div>
+ <div class="week-strip" id="weekStrip"></div>
+ </div>
+
+ <!-- calendar -->
+ <div class="cal-wrap">
+ <div class="cal-head" id="calHead"></div>
+ <div class="cal-grid" id="calGrid"></div>
+ </div>
+ </main>
+
+ <!-- right panel -->
+ <aside class="side-panel" id="sidePanel" aria-label="Day editor" role="complementary">
+ <div class="panel-head">
+ <div>
+ <div class="panel-date" id="panelDate">—</div>
+ <div style="font-size:11px;color:var(--ink3);margin-top:2px" id="panelDow"></div>
+ </div>
+ <button class="panel-close" id="panelClose" aria-label="Close panel">
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 2l10 10M12 2L2 12" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
+ </button>
+ </div>
+ <div class="panel-body" id="panelBody"></div>
+ <div class="add-post-bar">
+ <button class="add-post-btn reel" data-type="Reel">+ Reel</button>
+ <button class="add-post-btn carousel" data-type="Carousel">+ Carousel</button>
+ <button class="add-post-btn story" data-type="Story">+ Story</button>
+ <button class="add-post-btn sale" data-type="Sale">+ Sale</button>
+ <button class="add-post-btn editorial" data-type="Editorial">+ Editorial</button>
+ </div>
+ </aside>
+</div>
+
+<script>
+(function(){
+'use strict';
+
+/* ─── config ─── */
+const TYPES = {
+ Reel: {cls:'chip-reel', icon:'🎬', hashtags:'#reels #wallcovering #designerwallcoverings #interiordesign #luxuryhome'},
+ Carousel: {cls:'chip-carousel', icon:'🖼', hashtags:'#carousel #wallpaper #homedesign #designerwallcoverings #patterndesign'},
+ Story: {cls:'chip-story', icon:'✨', hashtags:'#stories #designerwallcoverings #behindthescenes #luxuryinteriors'},
+ Sale: {cls:'chip-sale', icon:'🏷', hashtags:'#sale #designerwallcoverings #wallcovering #limitedtime #luxurydecor'},
+ Editorial: {cls:'chip-editorial', icon:'📰', hashtags:'#editorial #designerwallcoverings #interiordesign #luxuryliving #wallpaper'},
+};
+
+const CAPTIONS = {
+ Reel: ['15-sec installation walk-through — premium grasscloth in situ','Slow pan across our new metallic linen collection','Behind the seams: how our artisans hang large-format murals'],
+ Carousel: ['Mood board: Coastal Serene palette — 5 slides','Before & after: a foyer transformation','Pattern pairing guide: 3 ways to layer texture'],
+ Story: ['Today\'s showroom pick: burnished gold foil','Quick poll — matte or shimmer?','Peek inside our latest editorial shoot'],
+ Sale: ['30% off select natural-fiber rolls — ends Sunday','Flash: sample sets $12 through midnight','Clearance spotlight: runway-ready remnants'],
+ Editorial: ['The case for bold pattern in the primary bedroom','Quiet luxury — why tone-on-tone never fails','How to choose the right wallcovering weight for high-traffic spaces'],
+};
+
+const DAYS_OF_WEEK = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
+const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
+
+/* ─── state ─── */
+const now = new Date();
+let curYear = now.getFullYear();
+let curMonth = now.getMonth();
+let selectedKey = null;
+let data = {}; // {dateKey: [{id,type,caption}]}
+
+/* ─── persistence ─── */
+function save(){ try{localStorage.setItem('dw-cal-v2', JSON.stringify(data))}catch(e){} }
+function load(){ try{const s=localStorage.getItem('dw-cal-v2');if(s)data=JSON.parse(s)}catch(e){} }
+load();
+
+function dateKey(y,m,d){ return `${y}-${String(m+1).padStart(2,'0')}-${String(d).padStart(2,'0')}` }
+function uid(){ return Math.random().toString(36).slice(2,8) }
+
+/* ─── suggestion ─── */
+function suggestCaption(type){
+ const pool = CAPTIONS[type];
+ return pool[Math.floor(Math.random()*pool.length)];
+}
+
+/* ─── render chip ─── */
+function makeChip(post, small=false){
+ const cfg = TYPES[post.type];
+ const el = document.createElement('span');
+ el.className = `chip ${cfg.cls}${small?' chip-sm':''}`;
+ el.setAttribute('data-tip', post.caption || post.type);
+ el.setAttribute('data-id', post.id);
+ el.setAttribute('tabindex','0');
+ el.setAttribute('role','button');
+ el.setAttribute('aria-label',`${post.type}: ${post.caption||'no caption'}`);
+ el.textContent = (small ? '' : cfg.icon+' ') + post.type;
+ return el;
+}
+
+/* ─── calendar render ─── */
+function renderCalendar(){
+ const head = document.getElementById('calHead');
+ const grid = document.getElementById('calGrid');
+ head.innerHTML = '';
+ grid.innerHTML = '';
+
+ DAYS_OF_WEEK.forEach(d=>{
+ const el = document.createElement('div');
+ el.className='cal-dow';
+ el.textContent=d;
+ head.appendChild(el);
+ });
+
+ const first = new Date(curYear, curMonth, 1).getDay();
+ const daysInMonth = new Date(curYear, curMonth+1, 0).getDate();
+ const total = first + daysInMonth;
+ const rows = Math.ceil(total/7)*7;
+
+ for(let i=0;i<rows;i++){
+ const day = i - first + 1;
+ const cell = document.createElement('div');
+ cell.className='cal-cell';
+
+ if(day<1||day>daysInMonth){
+ cell.classList.add('empty');
+ grid.appendChild(cell);
+ continue;
+ }
+
+ const key = dateKey(curYear, curMonth, day);
+ const isToday = day===now.getDate()&&curMonth===now.getMonth()&&curYear===now.getFullYear();
+ if(isToday) cell.classList.add('today');
+ if(key===selectedKey) cell.classList.add('selected');
+
+ cell.setAttribute('role','button');
+ cell.setAttribute('tabindex','0');
+ cell.setAttribute('aria-label',`${MONTHS[curMonth]} ${day}`);
+
+ const numEl = document.createElement('div');
+ numEl.className='cell-num';
+ numEl.textContent=day;
+ cell.appendChild(numEl);
+
+ const chipsEl = document.createElement('div');
+ chipsEl.className='cell-chips';
+ const posts = data[key]||[];
+ posts.forEach(p=>{
+ const chip = makeChip(p);
+ chip.addEventListener('click',e=>{
+ e.stopPropagation();
+ openPanel(key, day);
+ setTimeout(()=>{
+ const ta = document.querySelector(`[data-card="${p.id}"] textarea`);
+ if(ta) ta.focus();
+ },60);
+ });
+ chip.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();chip.click()}});
+ chipsEl.appendChild(chip);
+ });
+ cell.appendChild(chipsEl);
+
+ const hint = document.createElement('span');
+ hint.className='add-hint';
+ hint.textContent='+ add';
+ cell.appendChild(hint);
+
+ cell.addEventListener('click',()=>openPanel(key,day));
+ cell.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();openPanel(key,day)}});
+
+ grid.appendChild(cell);
+ }
+
+ updateStats();
+ renderWeekStrip();
+ updateMonthTitle();
+}
+
+function updateMonthTitle(){
+ document.getElementById('monthTitle').textContent = `${MONTHS[curMonth]} ${curYear}`;
+}
+
+/* ─── stats ─── */
+function updateStats(){
+ let total=0;
+ const counts={};
+ Object.keys(TYPES).forEach(t=>counts[t]=0);
+
+ // only count current month
+ for(let d=1;d<=31;d++){
+ const key=dateKey(curYear,curMonth,d);
+ if(data[key]){
+ data[key].forEach(p=>{total++;counts[p.type]=(counts[p.type]||0)+1});
+ }
+ }
+ document.getElementById('statTotal').textContent=total;
+
+ const ts = document.getElementById('typeStats');
+ ts.innerHTML='';
+ Object.entries(counts).forEach(([t,n])=>{
+ if(n===0)return;
+ const cfg=TYPES[t];
+ const sp=document.createElement('span');
+ sp.innerHTML=`<span class="chip chip-sm ${cfg.cls}">${cfg.icon} ${t} ${n}</span>`;
+ ts.appendChild(sp);
+ });
+}
+
+/* ─── week strip ─── */
+function renderWeekStrip(){
+ const strip = document.getElementById('weekStrip');
+ strip.innerHTML='';
+ // find the start of the current week (Sun)
+ const ref = new Date();
+ const dow = ref.getDay();
+ const weekStart = new Date(ref);
+ weekStart.setDate(ref.getDate()-dow);
+
+ for(let i=0;i<7;i++){
+ const d = new Date(weekStart);
+ d.setDate(weekStart.getDate()+i);
+ const key = dateKey(d.getFullYear(), d.getMonth(), d.getDate());
+ const posts = data[key]||[];
+
+ const wd = document.createElement('div');
+ wd.className='week-day';
+ wd.innerHTML=`<div class="wd-label">${DAYS_OF_WEEK[d.getDay()]}</div><div class="wd-date">${d.getDate()}</div>`;
+
+ const wChips = document.createElement('div');
+ wChips.className='wd-chips';
+ posts.forEach(p=>{
+ const c=makeChip(p,true);
+ wChips.appendChild(c);
+ });
+ if(posts.length===0){
+ const none=document.createElement('span');
+ none.style.cssText='font-size:10px;color:var(--hair)';
+ none.textContent='—';
+ wChips.appendChild(none);
+ }
+ wd.appendChild(wChips);
+ strip.appendChild(wd);
+ }
+}
+
+/* ─── panel ─── */
+function openPanel(key, day){
+ selectedKey=key;
+ const panel=document.getElementById('sidePanel');
+ const [y,m,d_]=key.split('-').map(Number);
+ const date=new Date(y,m-1,d_);
+ document.getElementById('panelDate').textContent=`${MONTHS[m-1]} ${d_}, ${y}`;
+ document.getElementById('panelDow').textContent=DAYS_OF_WEEK[date.getDay()];
+ renderPanel();
+ panel.classList.add('open');
+ // mark selected cell
+ document.querySelectorAll('.cal-cell').forEach(c=>c.classList.remove('selected'));
+ document.querySelectorAll('.cal-cell').forEach(c=>{
+ if(c.querySelector('.cell-num')?.textContent==String(d_)&&!c.classList.contains('empty')){
+ // match by day number — refine by data-key
+ }
+ });
+ // re-render to pick up selected class
+ renderCalendar();
+ selectedKey=key; // preserve after re-render
+}
+
+function closePanel(){
+ document.getElementById('sidePanel').classList.remove('open');
+ selectedKey=null;
+ document.querySelectorAll('.cal-cell.selected').forEach(c=>c.classList.remove('selected'));
+}
+
+function renderPanel(){
+ const body=document.getElementById('panelBody');
+ body.innerHTML='';
+ if(!selectedKey){return}
+ const posts=data[selectedKey]||[];
+
+ if(posts.length===0){
+ const es=document.createElement('div');
+ es.className='empty-state';
+ es.innerHTML='<span>📋</span>No posts planned yet.<br>Add a post type below.';
+ body.appendChild(es);
+ return;
+ }
+
+ posts.forEach((post,idx)=>{
+ const cfg=TYPES[post.type];
+ const card=document.createElement('div');
+ card.className='post-card';
+ card.setAttribute('data-card',post.id);
+
+ const cardHead=document.createElement('div');
+ cardHead.className='post-card-head';
+ cardHead.style.background=getComputedStyle(document.documentElement).getPropertyValue('--bg2');
+
+ const typeLabel=document.createElement('span');
+ typeLabel.className='post-card-type';
+ typeLabel.textContent=`${cfg.icon} ${post.type}`;
+
+ const delBtn=document.createElement('button');
+ delBtn.className='post-card-del';
+ delBtn.setAttribute('aria-label',`Delete ${post.type} post`);
+ delBtn.innerHTML='×';
+ delBtn.addEventListener('click',()=>{
+ const arr=data[selectedKey]||[];
+ data[selectedKey]=arr.filter(p=>p.id!==post.id);
+ save();renderPanel();renderCalendar();selectedKey=selectedKey;
+ });
+
+ cardHead.appendChild(typeLabel);
+ cardHead.appendChild(delBtn);
+
+ const cardBody=document.createElement('div');
+ cardBody.className='post-card-body';
+
+ const label=document.createElement('label');
+ label.style.cssText='font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink3);display:block;margin-bottom:4px';
+ label.textContent='Caption idea';
+
+ const ta=document.createElement('textarea');
+ ta.className='caption-area';
+ ta.setAttribute('aria-label','Caption');
+ ta.value=post.caption||'';
+ ta.placeholder='Write your caption idea here…';
+ ta.addEventListener('input',()=>{
+ post.caption=ta.value;
+ save();
+ // update chip tooltip
+ const chip=document.querySelector(`[data-id="${post.id}"]`);
+ if(chip) chip.setAttribute('data-tip',post.caption||post.type);
+ });
+
+ const hashEl=document.createElement('div');
+ hashEl.className='hashtags';
+ hashEl.textContent=cfg.hashtags;
+
+ cardBody.appendChild(label);
+ cardBody.appendChild(ta);
+ cardBody.appendChild(hashEl);
+
+ card.appendChild(cardHead);
+ card.appendChild(cardBody);
+ body.appendChild(card);
+ });
+}
+
+/* ─── add post ─── */
+document.querySelectorAll('.add-post-btn').forEach(btn=>{
+ btn.addEventListener('click',()=>{
+ if(!selectedKey)return;
+ if(!data[selectedKey]) data[selectedKey]=[];
+ const type=btn.getAttribute('data-type');
+ const post={id:uid(),type,caption:suggestCaption(type)};
+ data[selectedKey].push(post);
+ save();
+ renderPanel();
+ renderCalendar();
+ // restore panel open state
+ document.getElementById('sidePanel').classList.add('open');
+ selectedKey=selectedKey;
+ });
+});
+
+/* ─── nav ─── */
+document.getElementById('prevBtn').addEventListener('click',()=>{
+ if(curMonth===0){curMonth=11;curYear--;}else{curMonth--;}
+ renderCalendar();
+});
+document.getElementById('nextBtn').addEventListener('click',()=>{
+ if(curMonth===11){curMonth=0;curYear++;}else{curMonth++;}
+ renderCalendar();
+});
+document.getElementById('todayBtn').addEventListener('click',()=>{
+ curYear=now.getFullYear();curMonth=now.getMonth();
+ renderCalendar();
+});
+document.getElementById('panelClose').addEventListener('click',closePanel);
+
+/* ─── keyboard ─── */
+document.addEventListener('keydown',e=>{
+ if(e.key==='Escape') closePanel();
+ if(e.key==='ArrowLeft'&&(e.metaKey||e.ctrlKey)) document.getElementById('prevBtn').click();
+ if(e.key==='ArrowRight'&&(e.metaKey||e.ctrlKey)) document.getElementById('nextBtn').click();
+});
+
+/* ─── init ─── */
+renderCalendar();
+
+})();
+</script>
+</body>
+</html>
diff --git a/public/games/dw-damask-kaleidoscope.html b/public/games/dw-damask-kaleidoscope.html
new file mode 100644
index 0000000..620e836
--- /dev/null
+++ b/public/games/dw-damask-kaleidoscope.html
@@ -0,0 +1,502 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Damask Kaleidoscope — Designer Wallcoverings</title>
+<style>
+:root {
+ --bg: #0d0b09;
+ --ink: #1c1a17;
+ --accent: #c9a961;
+ --ivory: #f2ead8;
+ --serif: 'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif;
+ --sans: 'Optima','Gill Sans','Trebuchet MS',sans-serif;
+ --ease: cubic-bezier(.45,.05,.55,.95);
+}
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+html, body { width: 100%; height: 100%; overflow: hidden; background: var(--bg); }
+canvas { display: block; position: fixed; inset: 0; width: 100%; height: 100%; cursor: crosshair; }
+header {
+ position: fixed; top: 0; left: 0; z-index: 10;
+ padding: 18px 22px 14px;
+ pointer-events: none;
+}
+.wordmark {
+ font-family: var(--serif);
+ font-size: 13px;
+ font-weight: 600;
+ letter-spacing: .18em;
+ text-transform: uppercase;
+ color: var(--ivory);
+ opacity: .92;
+ line-height: 1;
+ display: block;
+ margin-bottom: 5px;
+}
+.palette-name {
+ font-family: var(--sans);
+ font-size: 10px;
+ letter-spacing: .12em;
+ text-transform: uppercase;
+ color: var(--accent);
+ opacity: .8;
+ display: block;
+}
+.hint {
+ font-family: var(--sans);
+ font-size: 9.5px;
+ letter-spacing: .08em;
+ color: var(--ivory);
+ opacity: .38;
+ display: block;
+ margin-top: 6px;
+ line-height: 1.5;
+}
+#controls {
+ position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
+ z-index: 10;
+ display: flex; align-items: center; gap: 18px;
+ background: rgba(13,11,9,.72);
+ border: 1px solid rgba(201,169,97,.22);
+ border-radius: 40px;
+ padding: 9px 20px;
+ pointer-events: auto;
+}
+.ctrl-label {
+ font-family: var(--sans);
+ font-size: 9px;
+ letter-spacing: .1em;
+ text-transform: uppercase;
+ color: var(--ivory);
+ opacity: .5;
+}
+#scale-slider {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 90px; height: 2px;
+ background: rgba(201,169,97,.35);
+ border-radius: 2px;
+ outline: none;
+ cursor: pointer;
+}
+#scale-slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 12px; height: 12px;
+ border-radius: 50%;
+ background: var(--accent);
+ cursor: pointer;
+}
+#dl-btn {
+ font-family: var(--sans);
+ font-size: 9px;
+ letter-spacing: .12em;
+ text-transform: uppercase;
+ color: var(--accent);
+ background: none;
+ border: 1px solid rgba(201,169,97,.4);
+ border-radius: 20px;
+ padding: 5px 12px;
+ cursor: pointer;
+ transition: border-color .2s, opacity .2s;
+ pointer-events: auto;
+}
+#dl-btn:hover { border-color: var(--accent); opacity: 1; }
+</style>
+</head>
+<body>
+<canvas id="c"></canvas>
+<header>
+ <span class="wordmark">Designer Wallcoverings</span>
+ <span class="palette-name" id="pal-name">Brass & Ink</span>
+ <span class="hint">click canvas · shift palette<br>move · warp symmetry</span>
+</header>
+<div id="controls">
+ <span class="ctrl-label">Scale</span>
+ <input id="scale-slider" type="range" min="0.4" max="2.2" step="0.01" value="1">
+ <button id="dl-btn">Download PNG</button>
+</div>
+<script>
+(function(){
+'use strict';
+
+// ── Palettes ──────────────────────────────────────────────────────────────────
+const PALETTES = [
+ { name: 'Brass & Ink',
+ bg: '#0d0b09', motif: ['#c9a961','#a07c3a','#e8d49a','#7a5c20','#f0e6c0'],
+ glow: 'rgba(201,169,97,0.18)', center: '#c9a961' },
+ { name: 'Celadon & Cream',
+ bg: '#0e1210', motif: ['#8fbfb0','#c2d9d0','#4a8c7a','#d8ede8','#2d6655'],
+ glow: 'rgba(143,191,176,0.16)', center: '#8fbfb0' },
+ { name: 'Aubergine & Gold',
+ bg: '#0e0b12', motif: ['#7c4d8a','#c9a961','#b07dbb','#e8d49a','#4a2060'],
+ glow: 'rgba(124,77,138,0.2)', center: '#c9a961' },
+ { name: 'Alabaster',
+ bg: '#12100e', motif: ['#d8cfbb','#b0a48a','#ede8da','#8a7d65','#f5f0e6'],
+ glow: 'rgba(216,207,187,0.12)', center: '#ede8da' },
+ { name: 'Moody Emerald',
+ bg: '#080f0b', motif: ['#2d7a50','#6ab88a','#1a4d30','#9dd4b0','#0d2b1a'],
+ glow: 'rgba(45,122,80,0.2)', center: '#6ab88a' },
+];
+
+let palIdx = 0;
+let pal = PALETTES[0];
+
+// ── Canvas setup ──────────────────────────────────────────────────────────────
+const canvas = document.getElementById('c');
+const ctx = canvas.getContext('2d');
+let W, H, CX, CY, offscreen, octx;
+
+function resize(){
+ W = canvas.width = window.innerWidth;
+ H = canvas.height = window.innerHeight;
+ CX = W/2; CY = H/2;
+ offscreen = document.createElement('canvas');
+ offscreen.width = Math.ceil(Math.max(W,H) * 0.6);
+ offscreen.height = offscreen.width;
+ octx = offscreen.getContext('2d');
+}
+resize();
+window.addEventListener('resize', resize);
+
+// ── State ─────────────────────────────────────────────────────────────────────
+let t = 0;
+let mx = 0, my = 0; // mouse offset from center, normalised -1..1
+let scale = 1;
+const FOLDS = 8; // 8-fold symmetry
+
+// ── Damask Bezier Forms ───────────────────────────────────────────────────────
+// Each "stamp" is a function(ctx, t, variant) → draws one canonical motif unit
+// All drawn in a local coord system; the kaleidoscope engine will mirror/rotate.
+
+function leaf(ctx, sx, sy, len, width, rot, col, alpha){
+ ctx.save();
+ ctx.globalAlpha = alpha;
+ ctx.strokeStyle = col;
+ ctx.fillStyle = col;
+ ctx.lineWidth = 1.2;
+ ctx.translate(sx, sy);
+ ctx.rotate(rot);
+ ctx.beginPath();
+ // Classic pointed leaf form via cubic bezier
+ ctx.moveTo(0, 0);
+ ctx.bezierCurveTo( width, -len*0.35, width*0.8, -len*0.65, 0, -len);
+ ctx.bezierCurveTo(-width*0.8, -len*0.65, -width, -len*0.35, 0, 0);
+ ctx.closePath();
+ ctx.fill();
+ // Midrib
+ ctx.globalAlpha = alpha * 0.5;
+ ctx.strokeStyle = 'rgba(0,0,0,0.4)';
+ ctx.lineWidth = 0.7;
+ ctx.beginPath();
+ ctx.moveTo(0,0);
+ ctx.lineTo(0,-len);
+ ctx.stroke();
+ ctx.restore();
+}
+
+function scroll(ctx, x, y, r, turns, col, alpha){
+ ctx.save();
+ ctx.globalAlpha = alpha;
+ ctx.strokeStyle = col;
+ ctx.lineWidth = 1.5;
+ ctx.lineCap = 'round';
+ ctx.translate(x,y);
+ ctx.beginPath();
+ // Tight inward scroll (acanthus-like)
+ const steps = 36;
+ for(let i=0; i<=steps; i++){
+ const a = (i/steps)*Math.PI*2*turns;
+ const rr = r * (1 - i/steps * 0.72);
+ const px = Math.cos(a)*rr;
+ const py = Math.sin(a)*rr;
+ if(i===0) ctx.moveTo(px,py); else ctx.lineTo(px,py);
+ }
+ ctx.stroke();
+ ctx.restore();
+}
+
+function urn(ctx, x, y, w, h, col, alpha){
+ ctx.save();
+ ctx.globalAlpha = alpha;
+ ctx.strokeStyle = col;
+ ctx.lineWidth = 1.4;
+ ctx.translate(x,y);
+ ctx.beginPath();
+ // Urn silhouette: rim → wide belly → pedestal
+ const hw = w*0.5;
+ // left side
+ ctx.moveTo(-hw*0.6, -h*0.5); // rim left
+ ctx.bezierCurveTo(-hw*0.9,-h*0.3, -hw,-h*0.1, -hw*0.85, h*0.1); // shoulder
+ ctx.bezierCurveTo(-hw*0.6, h*0.3, -hw*0.3, h*0.38, -hw*0.22, h*0.45); // waist to base
+ ctx.lineTo(-hw*0.35, h*0.5); // pedestal left
+ ctx.lineTo( hw*0.35, h*0.5); // pedestal right
+ ctx.lineTo( hw*0.22, h*0.45);
+ ctx.bezierCurveTo( hw*0.3,h*0.38, hw*0.6,h*0.3, hw*0.85,h*0.1);
+ ctx.bezierCurveTo( hw, -h*0.1, hw*0.9,-h*0.3, hw*0.6,-h*0.5);
+ ctx.lineTo(-hw*0.6, -h*0.5);
+ ctx.closePath();
+ ctx.stroke();
+ // lip
+ ctx.beginPath();
+ ctx.moveTo(-hw*0.72,-h*0.5);
+ ctx.lineTo( hw*0.72,-h*0.5);
+ ctx.stroke();
+ ctx.restore();
+}
+
+function petal(ctx, x, y, r, n, rot, col, alpha){
+ ctx.save();
+ ctx.globalAlpha = alpha;
+ ctx.strokeStyle = col;
+ ctx.fillStyle = col+'44';
+ ctx.lineWidth = 1;
+ ctx.translate(x,y);
+ ctx.rotate(rot);
+ ctx.beginPath();
+ for(let i=0;i<n;i++){
+ const a = (i/n)*Math.PI*2;
+ const na = ((i+1)/n)*Math.PI*2;
+ const ma = (a+na)/2;
+ ctx.moveTo(0,0);
+ ctx.bezierCurveTo(
+ Math.cos(a)*r*0.6, Math.sin(a)*r*0.6,
+ Math.cos(ma)*r*1.05, Math.sin(ma)*r*1.05,
+ Math.cos(na)*r*0.6, Math.sin(na)*r*0.6
+ );
+ ctx.lineTo(0,0);
+ }
+ ctx.fill();
+ ctx.stroke();
+ ctx.restore();
+}
+
+function trefoil(ctx, x, y, r, rot, col, alpha){
+ ctx.save();
+ ctx.globalAlpha = alpha;
+ ctx.strokeStyle = col;
+ ctx.lineWidth = 1.2;
+ ctx.translate(x,y);
+ ctx.rotate(rot);
+ for(let i=0;i<3;i++){
+ const a = (i/3)*Math.PI*2;
+ ctx.beginPath();
+ ctx.arc(Math.cos(a)*r*0.55, Math.sin(a)*r*0.55, r*0.45, 0, Math.PI*2);
+ ctx.stroke();
+ }
+ ctx.restore();
+}
+
+function diaper(ctx, x, y, size, col, alpha){
+ // Diaper lattice diamond
+ ctx.save();
+ ctx.globalAlpha = alpha;
+ ctx.strokeStyle = col;
+ ctx.lineWidth = 0.8;
+ ctx.translate(x,y);
+ ctx.rotate(Math.PI/4);
+ ctx.strokeRect(-size/2,-size/2,size,size);
+ ctx.restore();
+}
+
+// ── Draw one "wedge" tile (canonical 1/FOLDS slice) ─────────────────────────
+function drawWedge(oc, R, t, mouseX, mouseY, sc){
+ oc.clearRect(0,0,offscreen.width,offscreen.height);
+ const s = sc;
+ const p = pal;
+ const base = R * s;
+ const warp = 1 + mouseX * 0.22;
+ const warpY = 1 + mouseY * 0.15;
+
+ // Animate phase offsets
+ const slow = t * 0.00018;
+ const med = t * 0.00032;
+ const fast = t * 0.00055;
+
+ const c0 = p.motif[0], c1 = p.motif[1], c2 = p.motif[2],
+ c3 = p.motif[3], c4 = p.motif[4];
+
+ // Central urn — anchored at centre
+ urn(oc, 0,0, base*0.18*warp, base*0.28*warpY, c0, 0.85 + 0.12*Math.sin(slow));
+
+ // Ring of trefoils
+ for(let i=0;i<FOLDS;i++){
+ const a = (i/FOLDS)*Math.PI*2 + slow;
+ const rr = base * (0.28 + 0.04*Math.sin(med+i));
+ trefoil(oc, Math.cos(a)*rr, Math.sin(a)*rr, base*0.045, fast+i, c2,
+ 0.55 + 0.3*Math.sin(fast+i*1.3));
+ }
+
+ // Scrollwork arms — 8 arms radiating
+ for(let i=0;i<FOLDS;i++){
+ const a = (i/FOLDS)*Math.PI*2 + med*0.5;
+ const rr = base * (0.18 + 0.05*Math.sin(slow+i));
+ scroll(oc, Math.cos(a)*rr, Math.sin(a)*rr,
+ base*(0.055+0.02*Math.sin(fast+i)), 1.4 + 0.4*Math.sin(slow+i),
+ i%2===0?c0:c1, 0.7+0.2*Math.sin(fast+i));
+ }
+
+ // Leaf pairs on diagonal axes
+ for(let i=0;i<FOLDS;i++){
+ const a = (i/FOLDS)*Math.PI*2 + slow*0.7;
+ const rr = base * (0.38 + 0.06*Math.sin(med+i*0.8));
+ const leafLen = base*(0.13+0.04*Math.sin(fast+i));
+ leaf(oc, Math.cos(a)*rr, Math.sin(a)*rr, leafLen, leafLen*0.32,
+ a + Math.PI*0.5 + 0.2*Math.sin(slow+i), c1,
+ 0.65+0.25*Math.sin(med+i*1.7));
+ // smaller counter-leaf
+ leaf(oc, Math.cos(a)*rr*0.72, Math.sin(a)*rr*0.72, leafLen*0.6, leafLen*0.22,
+ a - Math.PI*0.5 + 0.15*Math.sin(fast+i), c4,
+ 0.45+0.2*Math.sin(slow+i*2.1));
+ }
+
+ // Outer petal rosettes
+ for(let i=0;i<FOLDS;i++){
+ const a = (i/FOLDS)*Math.PI*2 + Math.PI/FOLDS + fast*0.4;
+ const rr = base * (0.55 + 0.08*Math.sin(slow+i));
+ petal(oc, Math.cos(a)*rr, Math.sin(a)*rr,
+ base*(0.06+0.02*Math.sin(med+i)), 6, med+i, c3,
+ 0.6+0.3*Math.sin(fast+i*0.9));
+ }
+
+ // Diaper lattice — subtle outer ring
+ for(let i=0;i<FOLDS*2;i++){
+ const a = (i/(FOLDS*2))*Math.PI*2 + slow*0.3;
+ const rr = base*(0.7+0.05*Math.sin(med+i));
+ diaper(oc, Math.cos(a)*rr, Math.sin(a)*rr,
+ base*(0.04+0.012*Math.sin(fast+i)), c0,
+ 0.28+0.15*Math.sin(slow+i*1.4));
+ }
+
+ // Secondary scrolls mid-ring
+ for(let i=0;i<FOLDS;i++){
+ const a = (i/FOLDS)*Math.PI*2 + Math.PI/FOLDS + med;
+ const rr = base*(0.45+0.07*Math.sin(slow+i*1.2));
+ scroll(oc, Math.cos(a)*rr, Math.sin(a)*rr,
+ base*(0.04+0.015*Math.sin(fast+i)), 1.1+0.3*Math.sin(med+i),
+ i%2===0?c2:c3, 0.5+0.25*Math.sin(slow+i*2));
+ }
+
+ // Centre rosette
+ petal(oc, 0, 0, base*0.09, 12, fast, c0, 0.9+0.08*Math.sin(slow));
+ petal(oc, 0, 0, base*0.055, 8, -fast*0.7, c4, 0.7+0.2*Math.sin(med));
+}
+
+// ── Render kaleidoscope ───────────────────────────────────────────────────────
+function render(){
+ const R = Math.min(W,H)*0.5;
+ const sz = offscreen.width;
+ const hs = sz/2;
+
+ // Draw wedge tile centred at (hs,hs)
+ octx.save();
+ octx.translate(hs,hs);
+ drawWedge(octx, R, t, mx, my, scale);
+ octx.restore();
+
+ // Main canvas
+ ctx.fillStyle = pal.bg;
+ ctx.fillRect(0,0,W,H);
+
+ // Fold & stamp FOLDS times + mirror
+ ctx.save();
+ ctx.translate(CX,CY);
+
+ const wedgeAngle = (Math.PI*2)/FOLDS;
+ const slow = t * 0.00012;
+
+ for(let f=0;f<FOLDS;f++){
+ const baseA = f*wedgeAngle + slow;
+
+ // Original
+ ctx.save();
+ ctx.rotate(baseA);
+ ctx.drawImage(offscreen, -hs, -hs);
+ ctx.restore();
+
+ // Mirror (flip X)
+ ctx.save();
+ ctx.rotate(baseA);
+ ctx.scale(-1,1);
+ ctx.drawImage(offscreen, -hs, -hs);
+ ctx.restore();
+ }
+ ctx.restore();
+
+ // Gold glow at centre
+ const grd = ctx.createRadialGradient(CX,CY,0, CX,CY,R*0.22);
+ grd.addColorStop(0, pal.glow.replace('0.18','0.45').replace('0.20','0.45').replace('0.16','0.45').replace('0.12','0.45').replace('0.2)','0.45)'));
+ grd.addColorStop(0.4, pal.glow);
+ grd.addColorStop(1,'transparent');
+ ctx.fillStyle = grd;
+ ctx.fillRect(0,0,W,H);
+
+ // Vignette
+ const vig = ctx.createRadialGradient(CX,CY,R*0.45, CX,CY,R*1.1);
+ vig.addColorStop(0,'transparent');
+ vig.addColorStop(1,'rgba(0,0,0,0.78)');
+ ctx.fillStyle = vig;
+ ctx.fillRect(0,0,W,H);
+
+ // Hairline circle frame
+ ctx.save();
+ ctx.strokeStyle = pal.motif[0]+'55';
+ ctx.lineWidth = 0.7;
+ ctx.beginPath();
+ ctx.arc(CX,CY,R*0.92,0,Math.PI*2);
+ ctx.stroke();
+ ctx.beginPath();
+ ctx.arc(CX,CY,R*0.78,0,Math.PI*2);
+ ctx.stroke();
+ ctx.restore();
+}
+
+// ── Loop ──────────────────────────────────────────────────────────────────────
+function loop(ts){
+ t = ts;
+ render();
+ requestAnimationFrame(loop);
+}
+requestAnimationFrame(loop);
+
+// ── Input ─────────────────────────────────────────────────────────────────────
+function updateMouse(x,y){
+ mx = (x-CX)/(W*0.5);
+ my = (y-CY)/(H*0.5);
+}
+canvas.addEventListener('mousemove', e=>updateMouse(e.clientX,e.clientY));
+canvas.addEventListener('touchmove', e=>{
+ e.preventDefault();
+ updateMouse(e.touches[0].clientX, e.touches[0].clientY);
+},{passive:false});
+
+canvas.addEventListener('click', ()=>{
+ palIdx = (palIdx+1)%PALETTES.length;
+ pal = PALETTES[palIdx];
+ document.getElementById('pal-name').textContent = pal.name;
+ document.body.style.background = pal.bg;
+});
+canvas.addEventListener('touchend', ()=>{
+ palIdx = (palIdx+1)%PALETTES.length;
+ pal = PALETTES[palIdx];
+ document.getElementById('pal-name').textContent = pal.name;
+ document.body.style.background = pal.bg;
+});
+
+// ── Controls ──────────────────────────────────────────────────────────────────
+document.getElementById('scale-slider').addEventListener('input', e=>{
+ scale = parseFloat(e.target.value);
+});
+
+document.getElementById('dl-btn').addEventListener('click', ()=>{
+ // Render one frame to full-res temp canvas
+ const link = document.createElement('a');
+ link.download = 'dw-damask-kaleidoscope.png';
+ link.href = canvas.toDataURL('image/png');
+ link.click();
+});
+
+})();
+</script>
+</body>
+</html>
← ef2e59b Agent-driven editorial: DW The Lookbook (frontend-developer)
·
back to Model Arena
·
Agent-driven social tool: DW Content Calendar (frontend-deve b20881d →