Art frames round 3: natural wood grain, thickness slider, no-jump layout, polish
- Oak grain is now a real feTurbulence noise field (organic wood, no drawn-on lines), with a soft light/shade for moulding roundness; miters kept. - Thickness slider (0.7–1.5×) scales rail + mat via --frame-scale, remembered locally; applies to both the page and the full-screen view. - No-jump layout: every frame — including "No frame" — reserves the same footprint (rail + mat as CSS vars), so switching never reflows the page. "No frame" is the bare art floating with a soft shadow. - Full-screen wears a thicker rail + mat so the white border keeps its proportion at large size; "No frame" still maximizes there. - Frame chips redesigned as beveled beads with a clean offset selection ring. - Subtle fading hairline divider between the title and the artwork. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
let state = $state('loading'); // loading | ready | empty
|
||||
let zoom = $state(false);
|
||||
let frame = $state('walnut');
|
||||
let thickness = $state(1); // frame-scale multiplier, 0.7–1.5
|
||||
|
||||
let who = $derived(
|
||||
art ? [art.artist || 'Unknown artist', art.date_text].filter(Boolean) : []
|
||||
@@ -24,7 +25,9 @@
|
||||
try {
|
||||
const saved = localStorage.getItem('ub_art_frame');
|
||||
if (saved && FRAMES.some((f) => f.id === saved)) frame = saved;
|
||||
} catch { /* private mode — default frame is fine */ }
|
||||
const t = parseFloat(localStorage.getItem('ub_art_thickness'));
|
||||
if (t >= 0.7 && t <= 1.5) thickness = t;
|
||||
} catch { /* private mode — defaults are fine */ }
|
||||
try {
|
||||
art = await getJSON('/api/art/today');
|
||||
state = art?.image_url ? 'ready' : 'empty';
|
||||
@@ -37,6 +40,10 @@
|
||||
frame = id;
|
||||
try { localStorage.setItem('ub_art_frame', id); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
function saveThickness() {
|
||||
try { localStorage.setItem('ub_art_thickness', String(thickness)); } catch { /* ignore */ }
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -68,10 +75,12 @@
|
||||
<h1>Daily Art</h1>
|
||||
<p>A masterwork a day, from the world's open collections.</p>
|
||||
</div>
|
||||
<div class="divider" aria-hidden="true"></div>
|
||||
|
||||
{#if state === 'ready'}
|
||||
<figure class="piece">
|
||||
<button class="frame frame--{frame}" onclick={() => (zoom = true)} aria-label="Expand artwork">
|
||||
<button class="frame frame--{frame}" style="--frame-scale:{thickness}"
|
||||
onclick={() => (zoom = true)} aria-label="Expand artwork">
|
||||
<span class="mat">
|
||||
<img src={art.image_url} alt={art.title} />
|
||||
<span class="hint">
|
||||
@@ -104,6 +113,13 @@
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if frame !== 'none'}
|
||||
<div class="thickness">
|
||||
<span class="frames-label">Thickness</span>
|
||||
<input type="range" min="0.7" max="1.5" step="0.05"
|
||||
bind:value={thickness} oninput={saveThickness} aria-label="Frame thickness" />
|
||||
</div>
|
||||
{/if}
|
||||
</figcaption>
|
||||
</figure>
|
||||
{:else if state === 'empty'}
|
||||
@@ -118,7 +134,7 @@
|
||||
|
||||
{#if zoom && art}
|
||||
<button class="lightbox" onclick={() => (zoom = false)} aria-label="Close artwork">
|
||||
<span class="frame frame--{frame} lb-frame">
|
||||
<span class="frame frame--{frame} lb-frame" style="--frame-scale:{thickness}">
|
||||
<span class="mat"><img src={art.image_url_large || art.image_url} alt={art.title} /></span>
|
||||
</span>
|
||||
<span class="lb-cap">{art.title}{#if art.artist}<span class="sep">·</span>{art.artist}{/if}</span>
|
||||
@@ -165,23 +181,35 @@
|
||||
padding: clamp(20px, 5vw, 56px); box-sizing: border-box;
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
}
|
||||
.intro { text-align: center; margin-bottom: clamp(24px, 5vw, 48px); }
|
||||
.intro { text-align: center; margin-bottom: clamp(18px, 3.5vw, 30px); }
|
||||
.intro h1 {
|
||||
font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
|
||||
font-size: clamp(2rem, 5vw, 3rem); margin: 0; letter-spacing: -0.01em;
|
||||
}
|
||||
.intro p { color: var(--muted); margin: 10px 0 0; font-size: 1.05rem; }
|
||||
|
||||
/* A quiet hairline between the title and the artwork, fading at both ends. */
|
||||
.divider {
|
||||
width: min(440px, 76%); height: 1px; margin: 0 auto clamp(24px, 4.5vw, 44px);
|
||||
background: linear-gradient(90deg, transparent, rgba(112, 123, 134, 0.30) 22%,
|
||||
rgba(112, 123, 134, 0.30) 78%, transparent);
|
||||
}
|
||||
|
||||
.piece { margin: 0; display: flex; flex-direction: column; align-items: center; max-width: 860px; }
|
||||
|
||||
/* The frame: a beveled moulding (wood/metal) around a cream mat around the art. */
|
||||
/* The frame: a beveled moulding (wood/metal) around a cream mat around the art.
|
||||
--rail / --mat are the moulding and mat widths; both scale with --frame-scale (the
|
||||
thickness slider). EVERY variant — even "No frame" — reserves the same footprint, so
|
||||
switching frames never reflows the page. */
|
||||
.frame {
|
||||
border: none; cursor: zoom-in; padding: 0; background: none;
|
||||
box-sizing: border-box;
|
||||
--rail: calc(clamp(11px, 2.3vw, 22px) * var(--frame-scale, 1));
|
||||
--mat: calc(clamp(10px, 2.4vw, 22px) * var(--frame-scale, 1));
|
||||
border: none; cursor: zoom-in; background: none;
|
||||
position: relative; line-height: 0; display: inline-block; max-width: 100%;
|
||||
border-radius: 5px;
|
||||
border-radius: 4px; padding: var(--rail);
|
||||
box-shadow: 0 24px 58px rgba(20, 30, 45, 0.20), 0 3px 10px rgba(20, 30, 45, 0.10);
|
||||
}
|
||||
.frame--walnut, .frame--oak, .frame--gold, .frame--silver { padding: clamp(11px, 2.3vw, 22px); border-radius: 4px; }
|
||||
.frame--walnut {
|
||||
background: linear-gradient(135deg, #5c3d26, #87592f 38%, #4d3220 68%, #76512f);
|
||||
box-shadow: 0 24px 58px rgba(20, 30, 45, 0.24),
|
||||
@@ -204,15 +232,15 @@
|
||||
inset 0 -3px 9px rgba(60, 70, 85, 0.42);
|
||||
}
|
||||
|
||||
/* Oak: visible wood grain along each rail + mitered (45°) corner joints, like a real
|
||||
frame cut from four lengths of moulding. --t = rail width, so the corner seams run
|
||||
exactly from the outer corner to the mat. */
|
||||
/* Oak: real wood grain from an feTurbulence noise field (organic, no drawn lines),
|
||||
topped with a soft light/shade for moulding roundness, plus mitered (45°) corner
|
||||
joints. --t = rail width, so the corner seams run from the outer corner to the mat. */
|
||||
.frame--oak {
|
||||
--t: clamp(11px, 2.3vw, 22px);
|
||||
--t: var(--rail);
|
||||
background:
|
||||
repeating-linear-gradient(95deg, rgba(74, 46, 18, 0.20) 0 1.5px, rgba(150, 104, 52, 0) 1.5px 6px),
|
||||
repeating-linear-gradient(95deg, rgba(255, 221, 170, 0.12) 0 1px, transparent 1px 4px),
|
||||
linear-gradient(135deg, #a9763f, #c89a5b 40%, #8c5d2f 70%, #b07f46);
|
||||
linear-gradient(158deg, rgba(255, 236, 205, 0.20), rgba(0, 0, 0, 0.14) 74%),
|
||||
url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='600'%20height='400'%3E%3Cfilter%20id='w'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.011%200.06'%20numOctaves='5'%20seed='17'/%3E%3CfeColorMatrix%20type='matrix'%20values='0%200%200%200.7%200.36%200%200%200%200.46%200.23%200%200%200%200.26%200.09%200%200%200%200%201'/%3E%3C/filter%3E%3Crect%20width='600'%20height='400'%20filter='url(%23w)'/%3E%3C/svg%3E") center / 100% 100% no-repeat,
|
||||
linear-gradient(135deg, #a9763f, #b07f46);
|
||||
box-shadow: 0 24px 58px rgba(20, 30, 45, 0.24),
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.10),
|
||||
inset 0 2px 3px rgba(255, 226, 185, 0.32),
|
||||
@@ -229,17 +257,19 @@
|
||||
|
||||
.mat {
|
||||
display: block; position: relative; background: #fbf8f1; border-radius: 1px;
|
||||
}
|
||||
.frame--walnut .mat, .frame--oak .mat, .frame--gold .mat, .frame--silver .mat {
|
||||
padding: clamp(10px, 2.4vw, 22px);
|
||||
padding: var(--mat);
|
||||
box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.14), inset 0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.frame img { display: block; width: 100%; height: auto; max-height: 72vh; object-fit: contain; border-radius: 1px; }
|
||||
|
||||
/* Plain mode = the clean soft-shadowed card, no moulding. */
|
||||
.frame--none { background: var(--surface); border-radius: 14px; overflow: hidden; }
|
||||
.frame--none .mat { background: none; }
|
||||
.frame--none img { border-radius: 14px; }
|
||||
/* No frame: keep the footprint (so switching never shifts the layout), drop the
|
||||
moulding + mat, and let the bare art float with a soft drop shadow. */
|
||||
.frame--none { background: transparent; box-shadow: none; }
|
||||
.frame--none .mat { background: transparent; box-shadow: none; }
|
||||
.frame--none img {
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 18px 44px rgba(20, 30, 45, 0.20), 0 4px 12px rgba(20, 30, 45, 0.10);
|
||||
}
|
||||
|
||||
/* A quiet, always-there affordance — no hover wobble. */
|
||||
.hint {
|
||||
@@ -265,20 +295,28 @@
|
||||
.more { display: inline-block; margin-left: 8px; color: var(--accent); font-weight: 600; text-decoration: none; }
|
||||
.more:hover { color: var(--accent-deep); }
|
||||
|
||||
.frames { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 9px; margin-top: 22px; }
|
||||
.frames { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 11px; margin-top: 24px; }
|
||||
.frames-label { font-size: 0.74rem; font-weight: 600; color: var(--muted); margin-right: 4px; text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
/* Frame chips read as little beveled beads; selection is a clean offset ring. */
|
||||
.swatch {
|
||||
width: 26px; height: 26px; border-radius: 50%; border: 2px solid transparent;
|
||||
cursor: pointer; padding: 0; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
|
||||
width: 30px; height: 30px; border-radius: 50%; border: none; cursor: pointer; padding: 0;
|
||||
box-shadow: inset 0 2px 3px rgba(255, 255, 255, 0.38),
|
||||
inset 0 -3px 5px rgba(0, 0, 0, 0.30), 0 1px 3px rgba(0, 0, 0, 0.22);
|
||||
outline: 2.5px solid transparent; outline-offset: 2px;
|
||||
transition: outline-color 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
.swatch.on { box-shadow: 0 0 0 2px var(--canvas), 0 0 0 4px var(--accent); }
|
||||
.swatch--walnut { background: linear-gradient(135deg, #5c3d26, #87592f); }
|
||||
.swatch--oak { background: linear-gradient(135deg, #a9763f, #c89a5b); }
|
||||
.swatch--gold { background: linear-gradient(135deg, #b88c3d, #ecd293); }
|
||||
.swatch--silver { background: linear-gradient(135deg, #a9b0ba, #edf0f3); }
|
||||
.swatch--none { background: var(--surface); border: 2px solid var(--line); box-shadow: inset 0 0 0 1px rgba(0,0,0,0.03); }
|
||||
.swatch:hover { transform: translateY(-1px); }
|
||||
.swatch.on { outline-color: var(--accent); }
|
||||
.swatch--walnut { background: linear-gradient(150deg, #6b4528, #8a5d31 55%, #4f3420); }
|
||||
.swatch--oak { background: linear-gradient(150deg, #b07f46, #cb9c5b 55%, #8c5d2f); }
|
||||
.swatch--gold { background: linear-gradient(150deg, #c79a45, #ecd293 55%, #a9772f); }
|
||||
.swatch--silver { background: linear-gradient(150deg, #aab1bb, #edf0f3 55%, #98a0ab); }
|
||||
.swatch--none { background: linear-gradient(150deg, #ffffff, #ece6da); }
|
||||
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
|
||||
|
||||
.thickness { display: flex; align-items: center; justify-content: center; gap: 12px; margin-top: 16px; }
|
||||
.thickness input[type="range"] { width: min(220px, 60vw); accent-color: var(--accent); cursor: pointer; }
|
||||
|
||||
.note { color: var(--muted); font-size: 1.05rem; margin-top: 40px; }
|
||||
|
||||
.foot {
|
||||
@@ -291,10 +329,15 @@
|
||||
background: rgba(18, 22, 28, 0.94); display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center; gap: 14px; padding: 3vmin;
|
||||
}
|
||||
/* The full-screen view wears the same frame — it makes the piece pop. The frame +
|
||||
mat eat a little room, so the image is capped to leave space for the moulding. */
|
||||
.lb-frame { cursor: zoom-out; max-width: 92vw; max-height: 90vh; box-sizing: border-box; }
|
||||
.lb-frame img { max-width: 84vw; max-height: 78vh; width: auto; height: auto; object-fit: contain; }
|
||||
/* The full-screen view wears the same frame — it makes the piece pop. Bigger rail + mat
|
||||
here so the white border keeps its proportion against the much larger image. */
|
||||
.lb-frame {
|
||||
cursor: zoom-out; max-width: 94vw; max-height: 92vh;
|
||||
--rail: calc(clamp(16px, 1.8vw, 36px) * var(--frame-scale, 1));
|
||||
--mat: calc(clamp(22px, 3.2vw, 60px) * var(--frame-scale, 1));
|
||||
}
|
||||
.lb-frame.frame--none { --rail: 0px; --mat: 0px; } /* bare art fills the screen */
|
||||
.lb-frame img { max-width: 86vw; max-height: 74vh; width: auto; height: auto; object-fit: contain; }
|
||||
.lb-cap { color: #e9e2d6; font-size: 0.9rem; }
|
||||
.lb-cap .sep { color: #9aa0a6; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user