mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
feat(client): update book cover placeholder with vibrant lattice design
This commit is contained in:
@@ -80,36 +80,6 @@ const authorDisplay = computed(() => {
|
||||
return a.length > max ? `${a.slice(0, max - 1)}\u2026` : a
|
||||
})
|
||||
|
||||
// Precomputed corner diamond point strings for the outer frame.
|
||||
const cornerDiamondPoints = computed(() => {
|
||||
const { w, h, m } = vb.value
|
||||
const r = 3
|
||||
const corners = [
|
||||
{ cx: m, cy: m },
|
||||
{ cx: w - m, cy: m },
|
||||
{ cx: w - m, cy: h - m },
|
||||
{ cx: m, cy: h - m },
|
||||
]
|
||||
return corners.map(({ cx, cy }) => `${cx},${cy - r} ${cx + r},${cy} ${cx},${cy + r} ${cx - r},${cy}`)
|
||||
})
|
||||
|
||||
// Divider: two lines flanking a center diamond at y=236.
|
||||
const divider = computed(() => {
|
||||
const { cx, w, m } = vb.value
|
||||
const y = 236
|
||||
const gap = 6
|
||||
const r = 3.5
|
||||
const edgePad = props.isAudio ? m + 50 : m + 15
|
||||
return {
|
||||
lx1: edgePad,
|
||||
lx2: cx - gap,
|
||||
rx1: cx + gap,
|
||||
rx2: w - edgePad,
|
||||
pts: `${cx},${y - r} ${cx + r},${y} ${cx},${y + r} ${cx - r},${y}`,
|
||||
y,
|
||||
}
|
||||
})
|
||||
|
||||
// Clip path to keep all text safely within the inner frame.
|
||||
// Generated once per instance so each SVG on the page has a unique ID.
|
||||
const clipId = Math.random().toString(36).slice(2, 8)
|
||||
@@ -118,43 +88,62 @@ const clipId = Math.random().toString(36).slice(2, 8)
|
||||
<template>
|
||||
<svg :viewBox="`0 0 ${vb.w} ${vb.h}`" xmlns="http://www.w3.org/2000/svg" class="absolute inset-0 w-full h-full" preserveAspectRatio="xMidYMid meet">
|
||||
<defs>
|
||||
<linearGradient :id="`grad-${clipId}`" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" :stop-color="p.from" />
|
||||
<stop offset="100%" :stop-color="p.to" />
|
||||
</linearGradient>
|
||||
<clipPath :id="clipId">
|
||||
<rect :x="vb.m + 5" :y="vb.m + 5" :width="vb.w - (vb.m + 5) * 2" :height="vb.h - (vb.m + 5) * 2" />
|
||||
<rect :x="vb.m + 15" :y="vb.m + 15" :width="vb.w - (vb.m + 15) * 2" :height="vb.h - (vb.m + 15) * 2" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect :width="vb.w" :height="vb.h" :fill="p.from" />
|
||||
<rect :width="vb.w" :height="vb.h" :fill="`url(#grad-${clipId})`" />
|
||||
|
||||
<!-- Outer decorative frame -->
|
||||
<rect :x="vb.m" :y="vb.m" :width="vb.w - vb.m * 2" :height="vb.h - vb.m * 2" fill="none" :stroke="p.accent" stroke-width="1" opacity="0.5" />
|
||||
<!-- Variation 9: Diamond Lattice -->
|
||||
<g :stroke="p.accent" fill="none" opacity="0.15" stroke-width="0.5">
|
||||
<path v-for="i in 10" :key="`d1-${i}`" :d="`M ${-50 + i * 40},0 L ${vb.w + 50 + i * 40},${vb.h}`" />
|
||||
<path v-for="i in 10" :key="`d2-${i}`" :d="`M ${vb.w + 50 - i * 40},0 L ${-50 - i * 40},${vb.h}`" />
|
||||
|
||||
<!-- Inner frame -->
|
||||
<rect
|
||||
:x="vb.m + 5"
|
||||
:y="vb.m + 5"
|
||||
:width="vb.w - (vb.m + 5) * 2"
|
||||
:height="vb.h - (vb.m + 5) * 2"
|
||||
fill="none"
|
||||
:stroke="p.accent"
|
||||
stroke-width="0.4"
|
||||
opacity="0.25"
|
||||
/>
|
||||
<!-- Small accent diamonds at intersections -->
|
||||
<g opacity="0.4" :fill="p.accent" stroke="none">
|
||||
<template v-for="x in 4" :key="`x-${x}`">
|
||||
<rect
|
||||
v-for="y in 6"
|
||||
:key="`y-${y}`"
|
||||
:x="(vb.w / 5) * x - 1.5"
|
||||
:y="(vb.h / 7) * y - 1.5"
|
||||
width="3"
|
||||
height="3"
|
||||
transform="rotate(45)"
|
||||
style="transform-origin: center"
|
||||
/>
|
||||
</template>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Corner diamond ornaments -->
|
||||
<polygon v-for="(pts, i) in cornerDiamondPoints" :key="i" :points="pts" :fill="p.accent" opacity="0.65" />
|
||||
<!-- Decorative Internal Frame -->
|
||||
<g :stroke="p.accent" fill="none" opacity="0.6">
|
||||
<rect :x="vb.m + 5" :y="vb.m + 5" :width="vb.w - (vb.m + 5) * 2" :height="vb.h - (vb.m + 5) * 2" stroke-width="0.8" />
|
||||
<path :d="`M ${vb.m},${vb.m + 15} V ${vb.m} H ${vb.m + 15}`" stroke-width="2" />
|
||||
<path :d="`M ${vb.w - vb.m},${vb.m + 15} V ${vb.m} H ${vb.w - vb.m - 15}`" stroke-width="2" />
|
||||
<path :d="`M ${vb.w - vb.m},${vb.h - vb.m - 15} V ${vb.h - vb.m} H ${vb.w - vb.m - 15}`" stroke-width="2" />
|
||||
<path :d="`M ${vb.m},${vb.h - vb.m - 15} V ${vb.h - vb.m} H ${vb.m + 15}`" stroke-width="2" />
|
||||
</g>
|
||||
|
||||
<!-- Text clipped to the inner frame area -->
|
||||
<!-- Text Layer -->
|
||||
<g :clip-path="`url(#${clipId})`">
|
||||
<!-- Title -->
|
||||
<text :x="vb.cx" :y="titleY" :font-size="fsize" font-weight="900" font-family="inherit" text-anchor="middle" :fill="p.color">
|
||||
<text :x="vb.cx" :y="titleY" :font-size="fsize" font-weight="800" font-family="inherit" text-anchor="middle" :fill="p.color">
|
||||
<tspan v-for="(line, i) in lines" :key="i" :x="vb.cx" :dy="i === 0 ? 0 : lh">{{ line }}</tspan>
|
||||
</text>
|
||||
|
||||
<!-- Divider: line — diamond — line -->
|
||||
<line :x1="divider.lx1" :y1="divider.y" :x2="divider.lx2" :y2="divider.y" :stroke="p.accent" stroke-width="0.8" opacity="0.65" />
|
||||
<polygon :points="divider.pts" :fill="p.accent" opacity="0.65" />
|
||||
<line :x1="divider.rx1" :y1="divider.y" :x2="divider.rx2" :y2="divider.y" :stroke="p.accent" stroke-width="0.8" opacity="0.65" />
|
||||
<!-- Lattice Divider -->
|
||||
<g :fill="p.accent" opacity="0.8">
|
||||
<circle :cx="vb.cx" :cy="236" r="3" />
|
||||
<line :x1="vb.cx - 35" :y1="236" :x2="vb.cx - 8" :y2="236" :stroke="p.accent" stroke-width="1" />
|
||||
<line :x1="vb.cx + 8" :y1="236" :x2="vb.cx + 35" :y2="236" :stroke="p.accent" stroke-width="1" />
|
||||
</g>
|
||||
|
||||
<!-- Author -->
|
||||
<text
|
||||
@@ -162,10 +151,11 @@ const clipId = Math.random().toString(36).slice(2, 8)
|
||||
:x="vb.cx"
|
||||
:y="authorY"
|
||||
:font-size="authorFsize"
|
||||
font-weight="500"
|
||||
font-weight="600"
|
||||
font-family="inherit"
|
||||
text-anchor="middle"
|
||||
:fill="p.textMuted"
|
||||
style="letter-spacing: 0.05em"
|
||||
>
|
||||
{{ authorDisplay }}
|
||||
</text>
|
||||
|
||||
@@ -83,10 +83,10 @@ describe('bookCoverPalette', () => {
|
||||
expect(hues.size).toBeGreaterThan(10)
|
||||
})
|
||||
|
||||
it('always produces dark backgrounds', () => {
|
||||
it('always produces vibrant backgrounds', () => {
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const p = bookCoverPalette(`seed-${i}`)
|
||||
expect(p.from).toMatch(/^oklch\(0\.[1-3]/)
|
||||
expect(p.from).toMatch(/^oklch\(0\.[4-7]/)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -21,22 +21,26 @@ export function bookCoverPalette(seed: string): BookCoverPalette {
|
||||
const n2 = (n >>> 8) & 0xff
|
||||
const n3 = (n >>> 16) & 0xff
|
||||
|
||||
const bgL = 0.14 + (n2 % 10) * 0.018
|
||||
const bgC = 0.06 + (n3 % 8) * 0.015
|
||||
const from = `oklch(${bgL.toFixed(3)} ${bgC.toFixed(3)} ${Math.round(hue)})`
|
||||
const toL = Math.min(0.35, bgL + 0.06)
|
||||
const toC = Math.max(0.02, bgC - 0.02)
|
||||
const to = `oklch(${toL.toFixed(3)} ${toC.toFixed(3)} ${Math.round((hue + 20) % 360)})`
|
||||
// Base Lightness: 0.45 to 0.75 (Clean and vibrant)
|
||||
const baseL = 0.45 + (n2 % 6) * 0.05
|
||||
const baseC = 0.12 + (n3 % 5) * 0.02
|
||||
const from = `oklch(${baseL.toFixed(3)} ${baseC.toFixed(3)} ${Math.round(hue)})`
|
||||
|
||||
const color = `oklch(0.99 0.025 ${Math.round(hue)})`
|
||||
// Gradient: 20% lightness drop
|
||||
const toL = baseL - 0.2
|
||||
const to = `oklch(${toL.toFixed(3)} ${baseC.toFixed(3)} ${Math.round(hue)})`
|
||||
|
||||
const accentL = 0.7 + (n2 % 7) * 0.03
|
||||
const accentC = 0.14 + (n3 % 6) * 0.025
|
||||
const accent = `oklch(${accentL.toFixed(3)} ${accentC.toFixed(3)} ${Math.round(hue)})`
|
||||
const textMuted = `oklch(${(accentL + 0.1).toFixed(3)} ${Math.max(0.04, accentC - 0.08).toFixed(3)} ${Math.round(hue)})`
|
||||
// Text: Purest tint
|
||||
const color = `oklch(0.99 0.01 ${Math.round(hue)})`
|
||||
|
||||
// Accent: Brightest highlight
|
||||
const accentL = Math.min(0.95, baseL + 0.35)
|
||||
const accent = `oklch(${accentL.toFixed(3)} ${Math.max(0.02, baseC - 0.08).toFixed(3)} ${Math.round(hue)})`
|
||||
|
||||
const textMuted = `oklch(0.9 0.02 ${Math.round(hue)})`
|
||||
|
||||
return {
|
||||
gradient: `linear-gradient(150deg, ${from} 0%, ${to} 100%)`,
|
||||
gradient: `linear-gradient(135deg, ${from} 0%, ${to} 100%)`,
|
||||
from,
|
||||
to,
|
||||
color,
|
||||
|
||||
Reference in New Issue
Block a user