Art frames: real photographic wood (CC0), Mahogany, full-screen fit + proportion fix

- Replace procedural grain with a real fine-grain wood texture (CC0, Poly Haven —
  static/textures/wood-grain.jpg). The three woods share it, recolored per species via
  CSS filter on ::before: Walnut (as-is dark), Oak (lightened/warmed), Mahogany (new —
  deepened/reddened). Miters now apply to all three woods; mat sits between texture and
  miters via z-index.
- Full-screen sizing fixed: image capped (max-height 66vh) so the bottom rail always
  fits — no more cutoff. Rail/mat dialed back to ~1:1 wood:white (was mat-heavy), only
  slightly larger than the page so the proportion matches the small view.

Texture credit (CC0, no attribution required) recorded in static/textures/CREDITS.txt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-21 17:20:27 -04:00
parent 71fb16761b
commit 90d3e8cbe6
3 changed files with 36 additions and 34 deletions
+34 -34
View File
@@ -6,6 +6,7 @@
const FRAMES = [
{ id: 'walnut', label: 'Walnut' },
{ id: 'oak', label: 'Oak' },
{ id: 'mahogany', label: 'Mahogany' },
{ id: 'gold', label: 'Gold' },
{ id: 'silver', label: 'Silver' },
{ id: 'none', label: 'No frame' },
@@ -210,13 +211,22 @@
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 {
background: linear-gradient(135deg, #5c3d26, #87592f 38%, #4d3220 68%, #76512f);
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, 190, 0.30),
inset 0 -3px 9px rgba(0, 0, 0, 0.48);
/* The three woods share one real (CC0, Poly Haven) fine-grain texture, recolored per
species with a CSS filter, with a soft bevel baked into the texture layer. Metals
stay gradient. The texture sits on ::before (behind the mat); miters on ::after. */
.frame--walnut, .frame--oak, .frame--mahogany {
background: linear-gradient(135deg, #5a3a24, #34210f); /* fallback if texture misses */
}
.frame--walnut::before, .frame--oak::before, .frame--mahogany::before {
content: ""; position: absolute; inset: 0; z-index: 0; border-radius: inherit; pointer-events: none;
background:
linear-gradient(158deg, rgba(255, 250, 240, 0.14), rgba(0, 0, 0, 0.24) 80%),
url("/textures/wood-grain.jpg") center / cover;
}
.frame--walnut::before { filter: brightness(1.08) saturate(1.05); }
.frame--oak::before { filter: brightness(1.72) contrast(0.92) saturate(1.12) sepia(0.22) hue-rotate(-6deg); }
.frame--mahogany::before { filter: brightness(0.98) contrast(1.06) saturate(1.55) sepia(0.4) hue-rotate(-16deg); }
.frame--gold {
background: linear-gradient(135deg, #b88c3d, #ecd293 42%, #a9772f 60%, #dcbd71);
box-shadow: 0 24px 58px rgba(20, 30, 45, 0.20),
@@ -232,31 +242,19 @@
inset 0 -3px 9px rgba(60, 70, 85, 0.42);
}
/* 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: var(--rail);
/* Mitered (45°) corner joints for the woods — each seam runs from the outer corner to
the mat, sized to the rail width so it tracks the thickness slider. */
.frame--walnut::after, .frame--oak::after, .frame--mahogany::after {
content: ""; position: absolute; inset: 0; z-index: 2; pointer-events: none; border-radius: inherit;
background:
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),
inset 0 -3px 9px rgba(0, 0, 0, 0.45);
}
.frame--oak::after {
content: ""; position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
background:
linear-gradient(45deg, transparent calc(50% - 0.9px), rgba(48, 28, 9, 0.6) calc(50% - 0.9px), rgba(48, 28, 9, 0.6) calc(50% + 0.1px), rgba(255, 228, 186, 0.42) calc(50% + 0.1px), rgba(255, 228, 186, 0.42) calc(50% + 1px), transparent calc(50% + 1px)) top left / var(--t) var(--t) no-repeat,
linear-gradient(135deg, transparent calc(50% - 0.9px), rgba(48, 28, 9, 0.6) calc(50% - 0.9px), rgba(48, 28, 9, 0.6) calc(50% + 0.1px), rgba(255, 228, 186, 0.42) calc(50% + 0.1px), rgba(255, 228, 186, 0.42) calc(50% + 1px), transparent calc(50% + 1px)) top right / var(--t) var(--t) no-repeat,
linear-gradient(135deg, transparent calc(50% - 0.9px), rgba(48, 28, 9, 0.6) calc(50% - 0.9px), rgba(48, 28, 9, 0.6) calc(50% + 0.1px), rgba(255, 228, 186, 0.42) calc(50% + 0.1px), rgba(255, 228, 186, 0.42) calc(50% + 1px), transparent calc(50% + 1px)) bottom left / var(--t) var(--t) no-repeat,
linear-gradient(45deg, transparent calc(50% - 0.9px), rgba(48, 28, 9, 0.6) calc(50% - 0.9px), rgba(48, 28, 9, 0.6) calc(50% + 0.1px), rgba(255, 228, 186, 0.42) calc(50% + 0.1px), rgba(255, 228, 186, 0.42) calc(50% + 1px), transparent calc(50% + 1px)) bottom right / var(--t) var(--t) no-repeat;
linear-gradient(45deg, transparent calc(50% - 0.9px), rgba(28, 16, 6, 0.7) calc(50% - 0.9px), rgba(28, 16, 6, 0.7) calc(50% + 0.1px), rgba(255, 230, 190, 0.34) calc(50% + 0.1px), rgba(255, 230, 190, 0.34) calc(50% + 1px), transparent calc(50% + 1px)) top left / var(--rail) var(--rail) no-repeat,
linear-gradient(135deg, transparent calc(50% - 0.9px), rgba(28, 16, 6, 0.7) calc(50% - 0.9px), rgba(28, 16, 6, 0.7) calc(50% + 0.1px), rgba(255, 230, 190, 0.34) calc(50% + 0.1px), rgba(255, 230, 190, 0.34) calc(50% + 1px), transparent calc(50% + 1px)) top right / var(--rail) var(--rail) no-repeat,
linear-gradient(135deg, transparent calc(50% - 0.9px), rgba(28, 16, 6, 0.7) calc(50% - 0.9px), rgba(28, 16, 6, 0.7) calc(50% + 0.1px), rgba(255, 230, 190, 0.34) calc(50% + 0.1px), rgba(255, 230, 190, 0.34) calc(50% + 1px), transparent calc(50% + 1px)) bottom left / var(--rail) var(--rail) no-repeat,
linear-gradient(45deg, transparent calc(50% - 0.9px), rgba(28, 16, 6, 0.7) calc(50% - 0.9px), rgba(28, 16, 6, 0.7) calc(50% + 0.1px), rgba(255, 230, 190, 0.34) calc(50% + 0.1px), rgba(255, 230, 190, 0.34) calc(50% + 1px), transparent calc(50% + 1px)) bottom right / var(--rail) var(--rail) no-repeat;
}
.mat {
display: block; position: relative; background: #fbf8f1; border-radius: 1px;
display: block; position: relative; z-index: 1; background: #fbf8f1; border-radius: 1px;
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);
}
@@ -309,6 +307,7 @@
.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--mahogany { background: linear-gradient(150deg, #5a241a, #7e3826 55%, #3a160e); }
.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); }
@@ -327,17 +326,18 @@
.lightbox {
position: fixed; inset: 0; z-index: 50; border: none; cursor: zoom-out;
background: rgba(18, 22, 28, 0.94); display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 14px; padding: 3vmin;
align-items: center; justify-content: center; gap: 12px; padding: 2.5vmin;
}
/* 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. */
/* The full-screen view wears the same frame. Rail + mat are only a touch larger than the
page (keeping the ~1:1 wood:white proportion), and the image is capped so the whole
framed piece — including the bottom rail — always fits on screen. */
.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));
cursor: zoom-out; max-width: 92vw;
--rail: calc(clamp(14px, 1.4vw, 26px) * var(--frame-scale, 1));
--mat: calc(clamp(16px, 1.6vw, 30px) * 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-frame img { max-width: 86vw; max-height: 66vh; width: auto; height: auto; object-fit: contain; }
.lb-cap { color: #e9e2d6; font-size: 0.9rem; }
.lb-cap .sep { color: #9aa0a6; }
</style>
+2
View File
@@ -0,0 +1,2 @@
wood-grain.jpg — "Fine Grained Wood" by Rob Tuytel, Poly Haven (https://polyhaven.com/a/fine_grained_wood)
License: CC0 (Public Domain). Free to use, no attribution required. Recolored in CSS for frame variants.
Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB