← back to Model Arena
data/artifacts/258516d076e6/grok.html
962 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Shop by Color — Designer Wallcoverings</title>
<style>
:root {
--bg: #f4efe6;
--surface: #fbf8f2;
--ink: #1c1a17;
--muted: #8a8578;
--accent: #c9a961;
--accent-deep: #8a7340;
--line: rgba(28, 26, 23, 0.12);
--line-strong: rgba(28, 26, 23, 0.22);
--shadow: 0 12px 40px rgba(28, 26, 23, 0.08), 0 2px 8px rgba(28, 26, 23, 0.04);
--shadow-lg: 0 24px 64px rgba(28, 26, 23, 0.12), 0 4px 16px rgba(28, 26, 23, 0.06);
--ease: cubic-bezier(.22, .9, .24, 1);
--ease-out: cubic-bezier(.22, 1, .36, 1);
--ease-scene: cubic-bezier(.77, 0, .18, 1);
--serif: 'Didot', 'Bodoni MT', 'Playfair Display', 'EB Garamond', Georgia, 'Times New Roman', serif;
--sans: 'Avenir Next', 'Century Gothic', Futura, 'Segoe UI', 'Helvetica Neue', sans-serif;
--radius: 2px;
--header-h: 72px;
--hue: 32;
--sat: 42%;
--lit: 48%;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
background: var(--bg);
color: var(--ink);
font-family: var(--sans);
font-size: 16px;
line-height: 1.55;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
}
/* —— Fixed header / wordmark —— */
header.brand-bar {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 100;
height: var(--header-h);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 28px;
background: rgba(244, 239, 230, 0.92);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--line);
}
.wordmark {
font-family: var(--serif);
font-size: clamp(18px, 3.6vw, 42px);
font-weight: 400;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--ink);
line-height: 1.15;
max-width: min(72vw, 520px);
white-space: normal;
}
.header-meta {
font-family: var(--sans);
font-size: 10px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--muted);
white-space: nowrap;
}
/* —— Layout —— */
main {
max-width: 720px;
margin: 0 auto;
padding: calc(var(--header-h) + 36px) 24px 120px;
}
.hero-copy {
text-align: center;
margin-bottom: 36px;
}
.eyebrow {
font-family: var(--sans);
font-size: 11px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--accent-deep);
margin-bottom: 14px;
}
.hero-copy h1 {
font-family: var(--serif);
font-size: clamp(28px, 5vw, 50px);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.1;
color: var(--ink);
margin-bottom: 12px;
}
.hero-copy p {
font-size: 15px;
color: var(--muted);
max-width: 38ch;
margin: 0 auto;
line-height: 1.65;
}
/* —— Wheel stage —— */
.wheel-stage {
display: flex;
flex-direction: column;
align-items: center;
gap: 28px;
margin-bottom: 48px;
}
.wheel-wrap {
position: relative;
width: min(300px, 78vw);
height: min(300px, 78vw);
touch-action: none;
cursor: grab;
user-select: none;
-webkit-user-select: none;
}
.wheel-wrap:active { cursor: grabbing; }
.wheel-svg {
width: 100%;
height: 100%;
border-radius: 50%;
box-shadow: var(--shadow-lg), inset 0 0 0 1px var(--line);
filter: drop-shadow(0 8px 24px rgba(28,26,23,0.1));
}
.wheel-center {
position: absolute;
inset: 28%;
border-radius: 50%;
background: var(--surface);
border: 1px solid var(--line);
box-shadow: var(--shadow), inset 0 1px 0 rgba(255,255,255,0.7);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
pointer-events: none;
transition: background 0.5s var(--ease);
}
.swatch-live {
width: 42%;
height: 42%;
border-radius: 50%;
border: 1px solid var(--line-strong);
box-shadow: inset 0 2px 6px rgba(0,0,0,0.08);
transition: background 0.35s var(--ease);
background: hsl(var(--hue), var(--sat), var(--lit));
}
.center-label {
margin-top: 8px;
font-size: 9px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
}
.pointer-notch {
position: absolute;
top: 6px;
left: 50%;
transform: translateX(-50%);
width: 0; height: 0;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-top: 10px solid var(--ink);
z-index: 3;
filter: drop-shadow(0 1px 2px rgba(0,0,0,0.15));
pointer-events: none;
}
.hue-readout {
text-align: center;
}
.hue-name {
font-family: var(--serif);
font-size: 28px;
letter-spacing: -0.01em;
color: var(--ink);
transition: opacity 0.3s var(--ease);
}
.hue-meta {
margin-top: 6px;
font-size: 11px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
}
/* —— Filmstrip —— */
.film-section {
opacity: 0;
transform: translateY(18px);
transition: opacity 0.6s var(--ease-out), transform 0.6s var(--ease-out);
pointer-events: none;
}
.film-section.visible {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.film-head {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 16px;
padding: 0 2px;
border-bottom: 1px solid var(--line);
padding-bottom: 12px;
}
.film-head h2 {
font-family: var(--serif);
font-size: 24px;
font-weight: 400;
letter-spacing: -0.01em;
}
.film-head span {
font-size: 10px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--muted);
}
.filmstrip {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
@media (max-width: 520px) {
.filmstrip { grid-template-columns: repeat(2, 1fr); gap: 14px; }
}
.tile {
background: var(--surface);
border: 1px solid var(--line);
box-shadow: var(--shadow);
overflow: hidden;
transition: transform 0.4s var(--ease), box-shadow 0.4s var(--ease);
cursor: pointer;
position: relative;
}
.tile:hover, .tile:focus-visible {
transform: translateY(-3px);
box-shadow: var(--shadow-lg);
outline: none;
}
.tile:focus-visible { border-color: var(--accent); }
.tile-pattern {
aspect-ratio: 3 / 4;
width: 100%;
position: relative;
overflow: hidden;
}
.tile-caption {
padding: 12px 10px 14px;
border-top: 1px solid var(--line);
background: var(--surface);
}
.tile-name {
font-family: var(--serif);
font-size: 14px;
letter-spacing: 0.01em;
color: var(--ink);
line-height: 1.25;
}
.tile-family {
margin-top: 4px;
font-size: 9px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
}
/* Pattern motifs — visually distinct */
.pat-damask {
background:
radial-gradient(ellipse 40% 30% at 50% 30%, hsla(var(--h), var(--s), calc(var(--l) + 18%), 0.55) 0%, transparent 70%),
radial-gradient(ellipse 28% 40% at 50% 70%, hsla(var(--h), var(--s), calc(var(--l) + 12%), 0.4) 0%, transparent 70%),
radial-gradient(circle at 20% 20%, hsla(var(--h), var(--s), calc(var(--l) + 8%), 0.35) 0 8%, transparent 9%),
radial-gradient(circle at 80% 20%, hsla(var(--h), var(--s), calc(var(--l) + 8%), 0.35) 0 8%, transparent 9%),
radial-gradient(circle at 20% 80%, hsla(var(--h), var(--s), calc(var(--l) + 8%), 0.35) 0 8%, transparent 9%),
radial-gradient(circle at 80% 80%, hsla(var(--h), var(--s), calc(var(--l) + 8%), 0.35) 0 8%, transparent 9%),
radial-gradient(circle at 50% 50%, hsla(var(--h), var(--s), calc(var(--l) + 6%), 0.25) 0 18%, transparent 19%),
hsl(var(--h), var(--s), var(--l));
}
.pat-damask::after {
content: '';
position: absolute;
inset: 18%;
border: 1px solid hsla(var(--h), var(--s), calc(var(--l) + 22%), 0.45);
border-radius: 50% 50% 45% 45%;
box-shadow: inset 0 0 0 6px hsla(var(--h), var(--s), calc(var(--l) - 6%), 0.15);
}
.pat-stripe {
background:
repeating-linear-gradient(
90deg,
hsl(var(--h), var(--s), calc(var(--l) - 6%)) 0px,
hsl(var(--h), var(--s), calc(var(--l) - 6%)) 7px,
hsl(var(--h), var(--s), calc(var(--l) + 10%)) 7px,
hsl(var(--h), var(--s), calc(var(--l) + 10%)) 10px,
hsl(var(--h), var(--s), var(--l)) 10px,
hsl(var(--h), var(--s), var(--l)) 22px
);
}
.pat-geo {
background-color: hsl(var(--h), var(--s), var(--l));
background-image:
linear-gradient(30deg, hsla(var(--h), var(--s), calc(var(--l) - 12%), 0.55) 12%, transparent 12.5%, transparent 87%, hsla(var(--h), var(--s), calc(var(--l) - 12%), 0.55) 87.5%, hsla(var(--h), var(--s), calc(var(--l) - 12%), 0.55)),
linear-gradient(150deg, hsla(var(--h), var(--s), calc(var(--l) - 12%), 0.55) 12%, transparent 12.5%, transparent 87%, hsla(var(--h), var(--s), calc(var(--l) - 12%), 0.55) 87.5%, hsla(var(--h), var(--s), calc(var(--l) - 12%), 0.55)),
linear-gradient(30deg, hsla(var(--h), var(--s), calc(var(--l) - 12%), 0.55) 12%, transparent 12.5%, transparent 87%, hsla(var(--h), var(--s), calc(var(--l) - 12%), 0.55) 87.5%, hsla(var(--h), var(--s), calc(var(--l) - 12%), 0.55)),
linear-gradient(150deg, hsla(var(--h), var(--s), calc(var(--l) - 12%), 0.55) 12%, transparent 12.5%, transparent 87%, hsla(var(--h), var(--s), calc(var(--l) - 12%), 0.55) 87.5%, hsla(var(--h), var(--s), calc(var(--l) - 12%), 0.55)),
linear-gradient(60deg, hsla(var(--h), calc(var(--s) * 0.6), calc(var(--l) + 16%), 0.5) 25%, transparent 25.5%, transparent 75%, hsla(var(--h), calc(var(--s) * 0.6), calc(var(--l) + 16%), 0.5) 75%, hsla(var(--h), calc(var(--s) * 0.6), calc(var(--l) + 16%), 0.5)),
linear-gradient(60deg, hsla(var(--h), calc(var(--s) * 0.6), calc(var(--l) + 16%), 0.5) 25%, transparent 25.5%, transparent 75%, hsla(var(--h), calc(var(--s) * 0.6), calc(var(--l) + 16%), 0.5) 75%, hsla(var(--h), calc(var(--s) * 0.6), calc(var(--l) + 16%), 0.5));
background-size: 40px 70px;
background-position: 0 0, 0 0, 20px 35px, 20px 35px, 0 0, 20px 35px;
}
.pat-botanical {
background-color: hsl(var(--h), var(--s), var(--l));
background-image:
radial-gradient(ellipse 18px 28px at 25% 30%, hsla(var(--h), var(--s), calc(var(--l) + 16%), 0.55) 0 40%, transparent 42%),
radial-gradient(ellipse 14px 22px at 70% 55%, hsla(var(--h), var(--s), calc(var(--l) + 12%), 0.5) 0 40%, transparent 42%),
radial-gradient(ellipse 20px 14px at 55% 22%, hsla(var(--h), var(--s), calc(var(--l) - 10%), 0.4) 0 45%, transparent 47%),
radial-gradient(ellipse 12px 20px at 35% 72%, hsla(var(--h), var(--s), calc(var(--l) + 14%), 0.45) 0 40%, transparent 42%),
radial-gradient(ellipse 16px 12px at 78% 28%, hsla(var(--h), var(--s), calc(var(--l) - 8%), 0.35) 0 45%, transparent 47%),
radial-gradient(circle 3px at 50% 50%, hsla(var(--h), var(--s), calc(var(--l) + 20%), 0.6) 0 100%, transparent 100%),
radial-gradient(circle 2px at 30% 48%, hsla(var(--h), var(--s), calc(var(--l) + 18%), 0.5) 0 100%, transparent 100%),
radial-gradient(circle 2px at 68% 40%, hsla(var(--h), var(--s), calc(var(--l) + 18%), 0.5) 0 100%, transparent 100%);
background-size: 100% 100%, 100% 100%, 100% 100%, 100% 100%, 100% 100%, 48px 64px, 48px 64px, 48px 64px;
}
.pat-botanical::before {
content: '';
position: absolute;
inset: 0;
background:
repeating-linear-gradient(
0deg,
transparent 0 31px,
hsla(var(--h), var(--s), calc(var(--l) - 14%), 0.12) 31px 32px
);
pointer-events: none;
}
/* —— Share bar —— */
.share-bar {
position: fixed;
bottom: 0; left: 0; right: 0;
z-index: 90;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 14px 20px calc(14px + env(safe-area-inset-bottom));
background: rgba(251, 248, 242, 0.94);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-top: 1px solid var(--line);
}
.share-btn {
font-family: var(--sans);
font-size: 10px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--ink);
background: transparent;
border: 1px solid var(--line-strong);
padding: 11px 18px;
cursor: pointer;
transition: background 0.3s var(--ease), border-color 0.3s var(--ease), color 0.3s var(--ease);
border-radius: var(--radius);
}
.share-btn:hover, .share-btn:focus-visible {
background: var(--ink);
color: var(--bg);
border-color: var(--ink);
outline: none;
}
.share-btn.accent {
background: var(--accent);
border-color: var(--accent);
color: var(--ink);
}
.share-btn.accent:hover, .share-btn.accent:focus-visible {
background: var(--accent-deep);
border-color: var(--accent-deep);
color: var(--surface);
}
/* —— In-page toast overlay —— */
.toast {
position: fixed;
bottom: 88px;
left: 50%;
transform: translateX(-50%) translateY(12px);
background: var(--ink);
color: var(--bg);
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
padding: 12px 22px;
border-radius: var(--radius);
box-shadow: var(--shadow-lg);
opacity: 0;
pointer-events: none;
transition: opacity 0.35s var(--ease), transform 0.35s var(--ease);
z-index: 200;
white-space: nowrap;
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* Detail overlay */
.overlay {
position: fixed;
inset: 0;
z-index: 150;
background: rgba(28, 26, 23, 0.45);
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
opacity: 0;
pointer-events: none;
transition: opacity 0.4s var(--ease);
}
.overlay.open {
opacity: 1;
pointer-events: auto;
}
.overlay-card {
background: var(--surface);
border: 1px solid var(--line);
box-shadow: var(--shadow-lg);
max-width: 360px;
width: 100%;
overflow: hidden;
transform: translateY(16px) scale(0.98);
transition: transform 0.45s var(--ease-out);
}
.overlay.open .overlay-card {
transform: translateY(0) scale(1);
}
.overlay-pat {
height: 220px;
position: relative;
}
.overlay-body {
padding: 22px 24px 26px;
}
.overlay-body h3 {
font-family: var(--serif);
font-size: 28px;
font-weight: 400;
margin-bottom: 6px;
}
.overlay-body .sub {
font-size: 10px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 14px;
}
.overlay-body p {
font-size: 14px;
color: var(--muted);
line-height: 1.6;
margin-bottom: 20px;
}
.overlay-close {
font-family: var(--sans);
font-size: 10px;
letter-spacing: 0.16em;
text-transform: uppercase;
background: transparent;
border: 1px solid var(--line-strong);
color: var(--ink);
padding: 10px 16px;
cursor: pointer;
transition: background 0.3s var(--ease), color 0.3s var(--ease);
}
.overlay-close:hover {
background: var(--ink);
color: var(--bg);
}
.hint {
margin-top: 8px;
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
text-align: center;
opacity: 0.85;
}
</style>
</head>
<body>
<header class="brand-bar">
<div class="wordmark">Designer Wallcoverings</div>
<div class="header-meta">Shop by Color</div>
</header>
<main>
<section class="hero-copy">
<div class="eyebrow">Curated Atelier Collection</div>
<h1>Find your hue</h1>
<p>Turn the wheel to explore four hand-drawn motifs in each color family — named by our studio.</p>
</section>
<section class="wheel-stage">
<div class="wheel-wrap" id="wheelWrap" aria-label="Color wheel — drag or tap to select a hue">
<div class="pointer-notch" aria-hidden="true"></div>
<svg class="wheel-svg" id="wheelSvg" viewBox="0 0 200 200" role="img" aria-label="Hue spectrum wheel">
<defs>
<filter id="soft" x="-10%" y="-10%" width="120%" height="120%">
<feGaussianBlur in="SourceGraphic" stdDeviation="0.3"/>
</filter>
</defs>
<g id="segments"></g>
<circle cx="100" cy="100" r="99" fill="none" stroke="rgba(28,26,23,0.1)" stroke-width="1"/>
</svg>
<div class="wheel-center">
<div class="swatch-live" id="liveSwatch"></div>
<div class="center-label">Selected</div>
</div>
</div>
<div class="hue-readout">
<div class="hue-name" id="hueName">Champagne Linen</div>
<div class="hue-meta" id="hueMeta">Warm · Neutral family</div>
<div class="hint">Drag the wheel · Tap a pattern</div>
</div>
</section>
<section class="film-section visible" id="filmSection">
<div class="film-head">
<h2 id="filmTitle">In this family</h2>
<span>4 motifs</span>
</div>
<div class="filmstrip" id="filmstrip"></div>
</section>
</main>
<div class="share-bar">
<button type="button" class="share-btn" id="btnCopy">Copy link</button>
<button type="button" class="share-btn" id="btnSave">Save look</button>
<button type="button" class="share-btn accent" id="btnShare">Share palette</button>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<div class="overlay" id="overlay" role="dialog" aria-modal="true" aria-labelledby="ovTitle">
<div class="overlay-card">
<div class="overlay-pat" id="ovPat"></div>
<div class="overlay-body">
<h3 id="ovTitle">Pattern</h3>
<div class="sub" id="ovSub">Family</div>
<p id="ovDesc">A refined wallcovering motif from the atelier archive.</p>
<button type="button" class="overlay-close" id="ovClose">Close</button>
</div>
</div>
</div>
<script>
(function () {
const SEGMENTS = 72;
const families = [
{ h0: 8, h1: 28, name: 'Terracotta Clay', tone: 'Earthen · Warm' },
{ h0: 28, h1: 48, name: 'Champagne Linen', tone: 'Warm · Neutral family' },
{ h0: 48, h1: 72, name: 'Saffron Dusk', tone: 'Golden · Spiced' },
{ h0: 72, h1: 100, name: 'Olive Grove', tone: 'Botanical · Soft' },
{ h0: 100, h1: 145, name: 'Verdant Mist', tone: 'Garden · Cool-green' },
{ h0: 145, h1: 185, name: 'Aegean Fresco', tone: 'Coastal · Calm' },
{ h0: 185, h1: 220, name: ' indico Smoke', tone: 'Twilight · Deep' },
{ h0: 220, h1: 260, name: 'Wisteria Veil', tone: 'Dusky · Romantic' },
{ h0: 260, h1: 300, name: 'Plum Damask', tone: 'Jewel · Evening' },
{ h0: 300, h1: 340, name: 'Rosewood Blush', tone: 'Powder · Soft' },
{ h0: 340, h1: 368, name: 'Garnet Noir', tone: 'Crimson · Rich' }
];
// fix typo
families[6].name = 'Indico Smoke';
const motifs = [
{
key: 'damask',
cls: 'pat-damask',
names: ['Ornella Medallion', 'Casa Scroll', 'Palatine Crest', 'Villa Arabesque', 'Atelier Rosette'],
desc: 'A classical medallion damask — hand-scaled for grand rooms and quiet corridors alike.'
},
{
key: 'stripe',
cls: 'pat-stripe',
names: ['Regency Ribbon', 'Column Stripe', 'Harbour Line', 'Salon Ticking', 'Gallery Band'],
desc: 'Vertical rhythm in varied weights — tailored for panels, niches, and elongated walls.'
},
{
key: 'geo',
cls: 'pat-geo',
names: ['Lattice No. 4', 'Prism Field', 'Facet Trellis', 'Modulo Weave', 'Tessera Path'],
desc: 'A geometric lattice drawn from mid-century salon floors and Moroccan screens.'
},
{
key: 'botanical',
cls: 'pat-botanical',
names: ['Willow Shadow', 'Fresco Leaf', 'Meadow Trace', 'Ivy Silhouette', 'Fern Archive'],
desc: 'Soft botanical silhouettes, as if pressed from the studio’s garden folio.'
}
];
const root = document.documentElement;
const segmentsG = document.getElementById('segments');
const liveSwatch = document.getElementById('liveSwatch');
const hueName = document.getElementById('hueName');
const hueMeta = document.getElementById('hueMeta');
const filmstrip = document.getElementById('filmstrip');
const filmSection = document.getElementById('filmSection');
const wheelWrap = document.getElementById('wheelWrap');
const toast = document.getElementById('toast');
const overlay = document.getElementById('overlay');
let rotation = -20; // degrees — top of wheel is selected
let currentHue = 32;
let dragging = false;
let lastAngle = 0;
let toastTimer = null;
// Build conical-ish wheel from wedge paths
function buildWheel() {
const cx = 100, cy = 100, rOuter = 98, rInner = 42;
let html = '';
for (let i = 0; i < SEGMENTS; i++) {
const a0 = (i / SEGMENTS) * Math.PI * 2 - Math.PI / 2;
const a1 = ((i + 1) / SEGMENTS) * Math.PI * 2 - Math.PI / 2;
const h = (i / SEGMENTS) * 360;
const p = (a) => {
const xo = cx + rOuter * Math.cos(a);
const yo = cy + rOuter * Math.sin(a);
const xi = cx + rInner * Math.cos(a);
const yi = cy + rInner * Math.sin(a);
return { xo, yo, xi, yi };
};
const s = p(a0), e = p(a1);
const d = [
`M ${s.xo} ${s.yo}`,
`A ${rOuter} ${rOuter} 0 0 1 ${e.xo} ${e.yo}`,
`L ${e.xi} ${e.yi}`,
`A ${rInner} ${rInner} 0 0 0 ${s.xi} ${s.yi}`,
'Z'
].join(' ');
const sat = 48 + Math.sin((h / 360) * Math.PI * 2) * 8;
const lit = 52;
html += `<path d="${d}" fill="hsl(${h}, ${sat}%, ${lit}%)" stroke="none"/>`;
}
// inner ring hairline
html += `<circle cx="100" cy="100" r="42" fill="none" stroke="rgba(28,26,23,0.08)" stroke-width="1"/>`;
segmentsG.innerHTML = html;
applyRotation();
}
function applyRotation() {
segmentsG.setAttribute('transform', `rotate(${rotation} 100 100)`);
}
function normHue(h) {
h = h % 360;
if (h < 0) h += 360;
return h;
}
function hueFromRotation(rot) {
// pointer is at top; segment 0 (hue 0) starts at top when rot=0
// rotating wheel clockwise (positive CSS/SVG rotate) moves lower hues under pointer?
// SVG rotate positive is clockwise. Hue increases clockwise from top.
// When we rotate the group by `rotation`, the hue at top = -rotation
return normHue(-rot);
}
function familyFor(h) {
for (const f of families) {
if (h >= f.h0 && h < f.h1) return f;
// wrap for last
if (f.h1 > 360 && (h >= f.h0 || h < f.h1 - 360)) return f;
}
return families[0];
}
function satLitFor(h) {
// gentle variation so jewel hues read richer
const s = 38 + Math.round(12 * Math.abs(Math.sin((h * Math.PI) / 180)));
const l = 44 + Math.round(6 * Math.cos((h * Math.PI) / 90));
return { s, l };
}
function updateSelection(animateFilm) {
currentHue = hueFromRotation(rotation);
const { s, l } = satLitFor(currentHue);
root.style.setProperty('--hue', currentHue.toFixed(1));
root.style.setProperty('--sat', s + '%');
root.style.setProperty('--lit', l + '%');
liveSwatch.style.background = `hsl(${currentHue}, ${s}%, ${l}%)`;
const fam = familyFor(currentHue);
hueName.textContent = fam.name;
hueMeta.textContent = fam.tone;
renderFilm(currentHue, s, l, fam, animateFilm);
}
function pickName(list, hue, idx) {
const n = list.length;
return list[Math.abs(Math.round(hue / 17) + idx * 3) % n];
}
function renderFilm(h, s, l, fam, animate) {
if (animate) {
filmSection.classList.remove('visible');
}
const build = () => {
filmstrip.innerHTML = '';
// four distinct lightness/sat shifts per motif
const variants = [
{ dh: -6, ds: 4, dl: -8 },
{ dh: 4, ds: -6, dl: 6 },
{ dh: -2, ds: 10, dl: -2 },
{ dh: 8, ds: 0, dl: 10 }
];
motifs.forEach((m, i) => {
const v = variants[i];
const hh = normHue(h + v.dh);
const ss = Math.max(18, Math.min(72, s + v.ds));
const ll = Math.max(22, Math.min(72, l + v.dl));
const name = pickName(m.names, h, i);
const tile = document.createElement('article');
tile.className = 'tile';
tile.tabIndex = 0;
tile.setAttribute('role', 'button');
tile.dataset.name = name;
tile.dataset.family = fam.name;
tile.dataset.desc = m.desc;
tile.dataset.cls = m.cls;
tile.dataset.h = hh;
tile.dataset.s = ss;
tile.dataset.l = ll;
tile.innerHTML = `
<div class="tile-pattern ${m.cls}" style="--h:${hh};--s:${ss}%;--l:${ll}%;"></div>
<div class="tile-caption">
<div class="tile-name">${name}</div>
<div class="tile-family">${fam.name}</div>
</div>
`;
const open = () => openOverlay(tile.dataset);
tile.addEventListener('click', open);
tile.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); }
});
filmstrip.appendChild(tile);
});
requestAnimationFrame(() => filmSection.classList.add('visible'));
};
if (animate) {
setTimeout(build, 180);
} else {
build();
}
}
function openOverlay(data) {
const ovPat = document.getElementById('ovPat');
ovPat.className = 'overlay-pat ' + data.cls;
ovPat.style.setProperty('--h', data.h);
ovPat.style.setProperty('--s', data.s + '%');
ovPat.style.setProperty('--l', data.l + '%');
document.getElementById('ovTitle').textContent = data.name;
document.getElementById('ovSub').textContent = data.family + ' · Atelier';
document.getElementById('ovDesc').textContent = data.desc;
overlay.classList.add('open');
}
function closeOverlay() {
overlay.classList.remove('open');
}
document.getElementById('ovClose').addEventListener('click', closeOverlay);
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeOverlay(); });
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeOverlay(); });
function showToast(msg) {
toast.textContent = msg;
toast.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toast.classList.remove('show'), 2200);
}
// Pointer interaction on wheel
function angleAt(clientX, clientY) {
const rect = wheelWrap.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
return Math.atan2(clientY - cy, clientX - cx) * 180 / Math.PI;
}
function onDown(e) {
dragging = true;
const pt = e.touches ? e.touches[0] : e;
lastAngle = angleAt(pt.clientX, pt.clientY);
e.preventDefault();
}
function onMove(e) {
if (!dragging) return;
const pt = e.touches ? e.touches[0] : e;
const a = angleAt(pt.clientX, pt.clientY);
let delta = a - lastAngle;
// normalize delta
if (delta > 180) delta -= 360;
if (delta < -180) delta += 360;
rotation += delta;
lastAngle = a;
applyRotation();
updateSelection(false);
e.preventDefault();
}
function onUp() {
if (!dragging) return;
dragging = false;
// snap feel — gentle settle already continuous; refresh film with animation
updateSelection(true);
}
wheelWrap.addEventListener('mousedown', onDown);
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
wheelWrap.addEventListener('touchstart', onDown, { passive: false });
window.addEventListener('touchmove', onMove, { passive: false });
window.addEventListener('touchend', onUp);
// Click on wheel (no drag) still selects via small move; also allow click-to-jump
wheelWrap.addEventListener('click', (e) => {
if (e.detail === 0) return;
// jump so clicked hue goes to top
const a = angleAt(e.clientX, e.clientY);
// angle from center: 0 = east; top = -90
// hue under that point currently:
// position angle from top, clockwise:
const fromTop = normHue(a + 90);
const hueThere = normHue(fromTop - rotation);
// we want hueThere at top → rotation = -hueThere
// Actually simpler: rotate so the clicked angle moves to top (-90)
// delta rotation = -90 - a (in screen space)
// But our rotation is of the paint group...
// Desired: hue at top becomes hue currently at click.
const hueAtClick = normHue(fromTop + (-rotation - fromTop) + fromTop);
// hue at angle-from-top θ is: normHue(θ - rotation) with θ=fromTop
const hClick = normHue(fromTop - rotation);
rotation = -hClick;
applyRotation();
updateSelection(true);
});
// Share actions
document.getElementById('btnCopy').addEventListener('click', () => {
const text = `Designer Wallcoverings — ${hueName.textContent} palette`;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(() => showToast('Link copied')).catch(() => showToast('Copied · ' + hueName.textContent));
} else {
showToast('Saved · ' + hueName.textContent);
}
});
document.getElementById('btnSave').addEventListener('click', () => {
showToast('Look saved to atelier');
});
document.getElementById('btnShare').addEventListener('click', () => {
const fam = familyFor(currentHue);
const payload = `Shop by Color · ${fam.name} — Designer Wallcoverings`;
if (navigator.share) {
navigator.share({ title: 'Designer Wallcoverings', text: payload }).catch(() => showToast('Palette ready to share'));
} else if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(payload).then(() => showToast('Palette copied')).catch(() => showToast('Palette ready'));
} else {
showToast('Palette · ' + fam.name);
}
});
// Init
buildWheel();
updateSelection(false);
})();
</script>
</body>
</html>