Gezeichnete Cover: neue Konzepte (funk, musik, kunst, essen, sport, geld, reise), zweite Motive für die meisten; quadratisch für Hörbücher und Podcasts ohne (ladbares) Bild; sw v152
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
841ac5bed5
commit
89827086ba
5 changed files with 352 additions and 24 deletions
|
|
@ -115,8 +115,18 @@ knot, a spiral), karte (Geschichte), kosmos (Science-Fiction), ornament
|
|||
(Fantasy), raster (Politik, Soziologie), schatten (Krimi: a red line, a lit
|
||||
doorway), grusel (Horror), klassiker (patterned paper with a title plate),
|
||||
sakral (Religion & Mythologie), spiel (Kinder & Jugend), code (Informatik),
|
||||
typo, wellen, botanik, berge, zellen — and bauhaus when nothing matches. A hash
|
||||
of title and author picks palette, layout and every detail.
|
||||
typo, wellen, botanik, berge, zellen, and since 2026-10-25 funk (talk and radio: waveform,
|
||||
mast, speech bubbles), musik (record, staff), kunst (colour fields, brushstrokes), essen (plate
|
||||
on a checked cloth, citrus), sport (track, pitch), geld (price chart, coins), reise (road,
|
||||
route with compass) — and bauhaus when nothing matches (shapes, quarter-circle tiles, a
|
||||
circle crossed by diagonals). Most concepts have two or three motifs. A hash of title and
|
||||
author picks motif, palette, layout and every detail.
|
||||
|
||||
**Square** (`{square: true}`, since 2026-10-25): audiobooks and podcasts without a picture, or
|
||||
whose picture doesn't load (audiobooks.js; diora-android native-podcast.js `art()`, before the
|
||||
initial letter). 300 × 300, three title lines at most; the band of the motif shown is centred
|
||||
in what the text leaves free. Podcasts (`podcast: true`) nothing matches get funk, not
|
||||
bauhaus; a Marx/Engels audiobook gets konstruktiv (the blue volume is a binding).
|
||||
|
||||
### Export as XML (since 2026-10-24)
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@
|
|||
const shelf = document.createElement('div');
|
||||
shelf.id = 'audiobook-shelf';
|
||||
tab.appendChild(shelf);
|
||||
// A cover that doesn't load gives way to the drawn one (cover(): data-gen; shelf and book view).
|
||||
document.addEventListener('error', e => {
|
||||
const img = e.target;
|
||||
if (img.tagName === 'IMG' && img.dataset.gen && img.getAttribute('src') !== img.dataset.gen) img.src = img.dataset.gen;
|
||||
}, true);
|
||||
|
||||
// ---- helpers ------------------------------------------------------------------------
|
||||
|
||||
|
|
@ -74,11 +79,12 @@
|
|||
|
||||
function cover(url, title, cls, author) {
|
||||
const art = String(url || '').replace(/^http:/, 'https:');
|
||||
if (!art && window.DioraGenCover) { // none: one drawn from the title (gencover.js)
|
||||
return '<img class="' + cls + '" src="' + DioraGenCover.url({title: title || '', author: author || ''}) + '" alt="">';
|
||||
}
|
||||
// None, or one that doesn't load (the error listener on the shelf): one drawn from the title
|
||||
// (gencover.js), square like the others.
|
||||
const gen = window.DioraGenCover ? DioraGenCover.url({title: title || '', author: author || ''}, {square: true}) : '';
|
||||
if (!art && gen) return '<img class="' + cls + '" src="' + gen + '" alt="">';
|
||||
return art
|
||||
? '<img class="' + cls + '" src="' + esc(art) + '" alt="" loading="lazy">'
|
||||
? '<img class="' + cls + '" src="' + esc(art) + '" alt="" loading="lazy"' + (gen ? ' data-gen="' + gen + '"' : '') + '>'
|
||||
: '<span class="' + cls + ' ab-nocover">' + esc((title || '?').trim().charAt(0).toUpperCase()) + '</span>';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@
|
|||
* the palette, the layout and every detail of the motif, so no two books look alike and
|
||||
* each always looks the same. Nothing is stored or sent: it's an SVG, made when needed.
|
||||
*
|
||||
* DioraGenCover.url(book, {gray}) a data: URL (book: {title, author, shelfTag, folders})
|
||||
* DioraGenCover.theme(book) the concept chosen (for tests)
|
||||
* DioraGenCover.url(book, {gray, square, podcast}) a data: URL (book: {title, author, shelfTag, folders})
|
||||
* DioraGenCover.theme(book, opts) the concept chosen (for tests)
|
||||
*
|
||||
* square: 300 × 300 for audiobooks and podcasts (the motif's middle, the text above or below);
|
||||
* podcast: a show nothing else fits gets the radio concept rather than the plain shapes.
|
||||
*
|
||||
* Chrome-61-safe: no ?. / ??.
|
||||
*/
|
||||
|
|
@ -141,6 +144,36 @@
|
|||
'treasure', 'schatz', 'green gables', 'narnia', 'wardrobe', 'coraline', 'unendliche'],
|
||||
palettes: [['#fdf3e1', '#2d2a26', '#f2a23a', '#4fa3a5'], ['#e8f4f1', '#23343a', '#ef7b5a', '#f4c54b'], ['#2f3e6b', '#fff6e6', '#f6c445', '#ef8a6f'],
|
||||
['#f6ecf2', '#2c2433', '#e0607e', '#5bb08f']]},
|
||||
// Talk and radio: what podcasts mostly are (and their default, see theme()).
|
||||
{id: 'funk', font: SANS,
|
||||
words: ['podcast', 'radio', 'talk', 'gespräch', 'interview', 'sendung', 'show$', 'funk$', 'rundfunk', 'deutschlandfunk',
|
||||
'hörspiel', 'feature', 'on air', 'live', 'plauder', 'unterhaltung', 'conversation'],
|
||||
palettes: [['#f3ece0', '#20232a', '#e2573b', '#3f8fa0'], ['#1c2230', '#efe9df', '#f0a73c', '#7fb7c9'], ['#e9eef0', '#1f2a33', '#5a63c9', '#e9806a'],
|
||||
['#2a1f2d', '#f4e8dc', '#ef6f6c', '#f2c45a']]},
|
||||
{id: 'musik', font: SERIF,
|
||||
words: ['musik', 'music', 'jazz', 'song', 'lied', 'klang', 'sound', 'oper', 'opera', 'symphon', 'sinfon', 'konzert', 'concert',
|
||||
'rock', 'pop$', 'hip hop', 'hip-hop', 'rap$', 'techno', 'beethoven', 'mozart', 'bach$', 'album', 'vinyl', 'melod', 'harmon'],
|
||||
palettes: [['#151515', '#efe9dd', '#e0a43a', '#c4473c'], ['#f1e9dc', '#1f1d1b', '#b6382f', '#2f6c8a'], ['#20263a', '#efe6d6', '#e78f6a', '#9ec3c7']]},
|
||||
{id: 'kunst', font: SERIF,
|
||||
words: ['kunst', 'art$', 'arts$', 'malerei', 'painting', 'maler', 'painter', 'ästhetik', 'aesthetic', 'museum', 'design',
|
||||
'foto', 'photo', 'film', 'kino', 'cinema', 'bild', 'image', 'skulptur', 'galerie', 'gallery', 'avantgarde', 'avant-garde'],
|
||||
palettes: [['#efe7da', '#2a2622', '#c4492f', '#e2b04a'], ['#2a2230', '#efe5d8', '#d96a4a', '#5f8fb0'], ['#e6e9ea', '#232629', '#8a3d5c', '#e5a23e']]},
|
||||
{id: 'essen', font: SERIF,
|
||||
words: ['kochen', 'koch$', 'cook', 'küche', 'kitchen', 'rezept', 'recipe', 'essen', 'food', 'backen', 'bake', 'baking',
|
||||
'wein', 'wine', 'kaffee', 'coffee', 'genuss', 'vegan', 'gemüse', 'brot', 'bread', 'mahl', 'meal'],
|
||||
palettes: [['#f6efe2', '#2b2520', '#c8493a', '#e7b347'], ['#eef1e6', '#23301f', '#d2643c', '#6f9a54'], ['#2d2420', '#f3e9da', '#e09a3b', '#b9473a']]},
|
||||
{id: 'sport', font: NARROW, upper: true,
|
||||
words: ['sport', 'fußball', 'fussball', 'football', 'soccer', 'laufen', 'running', 'marathon', 'fahrrad', 'cycling',
|
||||
'tennis', 'fitness', 'training', 'olymp', 'spieler', 'player', 'tor$', 'goal', 'liga', 'league', 'boxen', 'boxing'],
|
||||
palettes: [['#e8efe6', '#1d2a22', '#d9483b', '#2f7a4f'], ['#1d2a3a', '#eef2f4', '#f2b23c', '#4f9fd0'], ['#f2ece2', '#22252a', '#c6462e', '#2e6aa0']]},
|
||||
{id: 'geld', font: SANS,
|
||||
words: ['geld', 'money', 'finanz', 'financ', 'börse', 'market', 'markt', 'invest', 'bank$', 'banken', 'schulden', 'debt',
|
||||
'kredit', 'credit', 'zins', 'interest', 'aktie', 'stock', 'krise', 'crisis', 'inflation', 'reichtum', 'wealth', 'kapitalis', 'wirtschaft'],
|
||||
palettes: [['#eef0e8', '#1e2a24', '#2f8a5f', '#c9a23f'], ['#1f2722', '#e9ece2', '#d9b24a', '#5fae86'], ['#f1ece3', '#24221f', '#b5462f', '#3f6f5a']]},
|
||||
{id: 'reise', font: SERIF,
|
||||
words: ['reise', 'travel', 'journey', 'unterwegs', 'zug$', 'train', 'bahn$', 'eisenbahn', 'railway', 'straße', 'road', 'fahrt',
|
||||
'atlas', 'expedition', 'entdeck', 'discover', 'wander', 'pilger', 'pilgrim', 'odyssee', 'odyssey', 'kompass', 'compass', 'route'],
|
||||
palettes: [['#f0e8d8', '#2a2a26', '#c0562f', '#3f7a8a'], ['#22303a', '#efe7d6', '#e3a04a', '#89b5bd'], ['#e7ece6', '#24302c', '#b0473a', '#d1a54a']]},
|
||||
{id: 'code', font: MONO,
|
||||
words: ['program', 'comput', 'informatik', 'software', 'algorithm', 'linux', 'unix', 'python', 'javascript',
|
||||
'rhcsa', 'red hat', 'interpretation of computer'],
|
||||
|
|
@ -156,7 +189,9 @@
|
|||
['informatik', 'code'], ['schreib', 'typo'], ['gegenwartslit', 'typo'], ['roman', 'typo'], ['zeitschrift', 'typo'],
|
||||
['religion', 'sakral'], ['mytholog', 'sakral'], ['klassiker', 'klassiker'], ['science', 'kosmos'], ['sci-fi', 'kosmos'],
|
||||
['fantasy', 'ornament'], ['krimi', 'schatten'], ['thriller', 'schatten'], ['warhammer', 'schatten'], ['horror', 'grusel'],
|
||||
['kinder', 'spiel'], ['jugend', 'spiel'],
|
||||
['kinder', 'spiel'], ['jugend', 'spiel'], ['gespräch', 'funk'], ['podcast', 'funk'], ['radio', 'funk'], ['hörspiel', 'funk'],
|
||||
['comedy', 'funk'], ['musik', 'musik'], ['music', 'musik'], ['kunst', 'kunst'], ['film', 'kunst'], ['koch', 'essen'],
|
||||
['essen', 'essen'], ['sport', 'sport'], ['finanz', 'geld'], ['wirtschaft', 'geld'], ['reise', 'reise'],
|
||||
];
|
||||
const DEFAULT = {id: 'bauhaus', font: SANS,
|
||||
palettes: [['#efe9dd', '#24262a', '#d0533f', '#3f7fb0'], ['#26282d', '#efe8da', '#e3b04b', '#5b9aa8'], ['#e8ecef', '#22272c', '#c8553d', '#6b8f71'],
|
||||
|
|
@ -198,10 +233,14 @@
|
|||
const MARX_ENGELS = /(^|[^a-zäöü])(marx|engels)($|[^a-zäöü])/i;
|
||||
const BLAU = THEMES.filter(t => t.id === 'blau')[0];
|
||||
|
||||
function theme(book) {
|
||||
const byId = id => THEMES.filter(t => t.id === id)[0];
|
||||
|
||||
function theme(book, opts) {
|
||||
// The blue volume is a book's binding: a square (audiobook, podcast) gets the red of the movement.
|
||||
if (opts && opts.square && (MARX_ENGELS.test(book.author || '') || /mew|marx.engels.werke/i.test(book.series || ''))) return byId('konstruktiv');
|
||||
if (MARX_ENGELS.test(book.author || '') || /mew|marx.engels.werke/i.test(book.series || '')) return BLAU;
|
||||
const s = scored(book).filter(x => x.t !== BLAU);
|
||||
return s.length ? s[0].t : DEFAULT;
|
||||
return s.length ? s[0].t : opts && opts.podcast ? byId('funk') : DEFAULT;
|
||||
}
|
||||
|
||||
// ---- motifs: each draws into the whole cover, the text gets its own space later -----------
|
||||
|
|
@ -285,6 +324,16 @@
|
|||
return s;
|
||||
},
|
||||
wellen(r, p) {
|
||||
if (r() < 0.4) { // the sun on the horizon, its light broken on the water
|
||||
const hy = between(r, 250, 300), sx = between(r, 90, 210), sr = between(r, 40, 62);
|
||||
let s = `<circle cx="${f1(sx)}" cy="${f1(hy)}" r="${f1(sr)}" fill="${p[2]}"/>` +
|
||||
`<rect x="0" y="${f1(hy)}" width="${W}" height="${f1(H - hy)}" fill="${p[3]}" opacity=".6"/>`;
|
||||
for (let y = hy + 10, i = 0; y < H - 10; y += 11 + i * 1.5, i++) {
|
||||
const w = sr * 2 * (1 - i * 0.05) * between(r, 0.6, 1.1);
|
||||
if (w > 6) s += `<rect x="${f1(sx - w / 2)}" y="${f1(y)}" width="${f1(w)}" height="3" rx="1.5" fill="${p[2]}" opacity="${f1(Math.max(0.2, 0.9 - i * 0.06))}"/>`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
let s = '';
|
||||
const n = 6 + Math.floor(r() * 5);
|
||||
const amp = between(r, 6, 16), len = between(r, 60, 120);
|
||||
|
|
@ -298,6 +347,20 @@
|
|||
return s;
|
||||
},
|
||||
karte(r, p) {
|
||||
if (r() < 0.4) { // a graticule, a coast, a route across it
|
||||
let s = '';
|
||||
for (let x = 30; x < W; x += 40) s += `<line x1="${x}" y1="0" x2="${x}" y2="${H}" stroke="${p[1]}" stroke-width=".6" opacity=".35"/>`;
|
||||
for (let y = 30; y < H; y += 40) s += `<line x1="0" y1="${y}" x2="${W}" y2="${y}" stroke="${p[1]}" stroke-width=".6" opacity=".35"/>`;
|
||||
const cx = between(r, 100, 200), cy = between(r, 260, 330), ph1 = r() * 6, ph2 = r() * 6;
|
||||
let d = '';
|
||||
for (let k = 0; k <= 48; k++) {
|
||||
const a = k / 48 * Math.PI * 2, rad = 70 * (1 + 0.25 * Math.sin(a * 3 + ph1) + 0.12 * Math.sin(a * 7 + ph2));
|
||||
d += (k ? ' L' : 'M') + f1(cx + Math.cos(a) * rad * 1.2) + ' ' + f1(cy + Math.sin(a) * rad);
|
||||
}
|
||||
s += `<path d="${d} Z" fill="${p[3]}" opacity=".55" stroke="${p[1]}" stroke-width="1.2"/>`;
|
||||
return s + `<path d="M${f1(between(r, 10, 60))} ${f1(between(r, 380, 430))} Q${f1(cx)} ${f1(cy - 120)} ${f1(between(r, 240, 290))} ${f1(between(r, 160, 220))}" ` +
|
||||
`fill="none" stroke="${p[2]}" stroke-width="2" stroke-dasharray="6 5"/>`;
|
||||
}
|
||||
let s = '';
|
||||
const cx = between(r, 80, 220), cy = between(r, 160, 320);
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
|
|
@ -313,6 +376,18 @@
|
|||
return s;
|
||||
},
|
||||
kosmos(r, p) {
|
||||
if (r() < 0.4) { // the moon's phases in a row, under a few stars
|
||||
let s = '';
|
||||
for (let i = 0; i < 40; i++) s += `<circle cx="${f1(r() * W)}" cy="${f1(r() * H)}" r="${f1(between(r, 0.4, 1.2))}" fill="${p[1]}" opacity="${f1(between(r, 0.3, 0.8))}"/>`;
|
||||
const y = between(r, 260, 320), rad = 22;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const x = 40 + i * 55;
|
||||
s += `<circle cx="${x}" cy="${f1(y)}" r="${rad}" fill="${p[2]}"/>`;
|
||||
if (i !== 2) s += `<circle cx="${f1(x + (i - 2) * rad * 0.9)}" cy="${f1(y)}" r="${rad}" fill="${p[0]}"/>`;
|
||||
s += `<circle cx="${x}" cy="${f1(y)}" r="${rad}" fill="none" stroke="${p[2]}" stroke-width="1" opacity=".5"/>`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
let s = '';
|
||||
for (let i = 0; i < 70; i++) s += `<circle cx="${f1(r() * W)}" cy="${f1(r() * H)}" r="${f1(between(r, 0.4, 1.5))}" fill="${p[1]}" opacity="${f1(between(r, 0.3, 0.9))}"/>`;
|
||||
const cx = between(r, 90, 210), cy = between(r, 170, 290), pr = between(r, 30, 55);
|
||||
|
|
@ -334,6 +409,22 @@
|
|||
return s;
|
||||
},
|
||||
botanik(r, p) {
|
||||
if (r() < 0.4) { // a fern frond, bending
|
||||
const x0 = between(r, 110, 190), bend = between(r, -0.6, 0.6), n = 14, pts = [];
|
||||
for (let i = 0; i <= n; i++) {
|
||||
const t = i / n;
|
||||
pts.push([x0 + Math.sin(t * 1.6) * bend * 90, H - 10 - t * 300]);
|
||||
}
|
||||
let s = `<path d="M${pts.map(q => f1(q[0]) + ' ' + f1(q[1])).join(' L')}" fill="none" stroke="${p[2]}" stroke-width="2.4" stroke-linecap="round"/>`;
|
||||
pts.slice(1, -1).forEach((q, i) => {
|
||||
const len = (1 - i / n) * between(r, 40, 60) + 8;
|
||||
[-1, 1].forEach(side => {
|
||||
s += `<path d="M${f1(q[0])} ${f1(q[1])} q${f1(side * len * 0.5)} ${f1(-len * 0.35)} ${f1(side * len)} ${f1(-len * 0.15)} ` +
|
||||
`q${f1(-side * len * 0.45)} ${f1(len * 0.25)} ${f1(-side * len)} ${f1(len * 0.15)}Z" fill="${i % 2 ? p[2] : p[3]}" opacity=".85"/>`;
|
||||
});
|
||||
});
|
||||
return s;
|
||||
}
|
||||
let s = '';
|
||||
const n = 5 + Math.floor(r() * 4);
|
||||
for (let i = 0; i < n; i++) {
|
||||
|
|
@ -345,6 +436,17 @@
|
|||
return s;
|
||||
},
|
||||
berge(r, p) {
|
||||
if (r() < 0.4) { // one peak and its snow, the sky in bands
|
||||
let s = '';
|
||||
for (let i = 0; i < 5; i++) s += `<rect x="0" y="${110 + i * 26}" width="${W}" height="13" fill="${p[3]}" opacity="${f1(0.12 + i * 0.06)}"/>`;
|
||||
const px = between(r, 100, 200), py = between(r, 170, 220), base = H - 30;
|
||||
const lx = px - between(r, 150, 220), rx = px + between(r, 150, 220);
|
||||
s += `<path d="M${f1(lx)} ${base} L${f1(px)} ${f1(py)} L${f1(rx)} ${base} Z" fill="${p[1]}" opacity=".85"/>`;
|
||||
const k = between(r, 0.22, 0.32), sl = px + (lx - px) * k, sr = px + (rx - px) * k, sy = py + (base - py) * k;
|
||||
s += `<path d="M${f1(px)} ${f1(py)} L${f1(sr)} ${f1(sy)} L${f1((px + sr) / 2)} ${f1(sy - 8)} L${f1(px)} ${f1(sy + 6)} ` +
|
||||
`L${f1((px + sl) / 2)} ${f1(sy - 6)} L${f1(sl)} ${f1(sy)} Z" fill="${p[0]}"/>`;
|
||||
return s + `<rect x="0" y="${base}" width="${W}" height="30" fill="${p[2]}"/>`;
|
||||
}
|
||||
let s = `<circle cx="${f1(between(r, 60, 240))}" cy="${f1(between(r, 120, 190))}" r="${f1(between(r, 22, 38))}" fill="${p[3]}"/>`;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const base = 260 + i * 45;
|
||||
|
|
@ -356,6 +458,17 @@
|
|||
return s;
|
||||
},
|
||||
zellen(r, p) {
|
||||
if (r() < 0.4) { // the double helix, across
|
||||
const cy = between(r, 250, 310), amp = between(r, 40, 60), len = between(r, 110, 150), ph = r() * 6;
|
||||
let a = '', b = '', rungs = '';
|
||||
for (let x = -10; x <= W + 10; x += 6) {
|
||||
const y = Math.sin(x / len * Math.PI * 2 + ph) * amp;
|
||||
a += (x > -10 ? ' L' : 'M') + x + ' ' + f1(cy + y);
|
||||
b += (x > -10 ? ' L' : 'M') + x + ' ' + f1(cy - y);
|
||||
if ((x + 10) % 18 === 0) rungs += `<line x1="${x}" y1="${f1(cy + y)}" x2="${x}" y2="${f1(cy - y)}" stroke="${pick(r, [p[2], p[3]])}" stroke-width="3" opacity=".8"/>`;
|
||||
}
|
||||
return rungs + `<path d="${a}" fill="none" stroke="${p[1]}" stroke-width="3"/><path d="${b}" fill="none" stroke="${p[2]}" stroke-width="3"/>`;
|
||||
}
|
||||
let s = '';
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const x = between(r, 20, 280), y = between(r, 140, 430), rad = between(r, 12, 42);
|
||||
|
|
@ -401,6 +514,15 @@
|
|||
`<circle cx="${f1(x)}" cy="${f1(between(r, 200, 330))}" r="${f1(between(r, 50, 90))}" fill="${p[3]}" opacity=".35"/>`;
|
||||
},
|
||||
typo(r, p, book) {
|
||||
if (r() < 0.45) { // a page of type with one line marked, a quotation mark
|
||||
let s = `<text x="${f1(between(r, 170, 210))}" y="${f1(between(r, 330, 380))}" font-family="${SERIF}" font-size="200" fill="${p[2]}" opacity=".85">“</text>`;
|
||||
const mark = 4 + Math.floor(r() * 8);
|
||||
for (let i = 0, y = 190; y < H - 30; i++, y += 15) {
|
||||
const w = i % 7 === 6 ? between(r, 60, 160) : between(r, 200, 248);
|
||||
s += `<rect x="26" y="${y}" width="${f1(w)}" height="5" fill="${i === mark ? p[2] : p[1]}" opacity="${i === mark ? 0.9 : 0.22}"/>`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
const ch = (book.title || '?').replace(/^[^A-Za-zÄÖÜäöüß0-9]+/, '').charAt(0).toUpperCase() || '?';
|
||||
return `<text x="${f1(between(r, 20, 60))}" y="${f1(between(r, 380, 430))}" font-family="${SERIF}" font-size="${f1(between(r, 240, 320))}" ` +
|
||||
`fill="${p[2]}" opacity=".9">${esc(ch)}</text>` +
|
||||
|
|
@ -560,7 +682,184 @@
|
|||
s += `<rect x="12" y="140" width="3" height="${H - 160}" fill="${p[2]}" opacity=".7"/>`;
|
||||
return s;
|
||||
},
|
||||
funk(r, p) {
|
||||
const v = r();
|
||||
if (v < 0.45) { // a voice as a waveform, mirrored about its line
|
||||
const cy = between(r, 270, 320), n = 34, gap = (W - 30) / n;
|
||||
let s = `<line x1="10" y1="${f1(cy)}" x2="${W - 10}" y2="${f1(cy)}" stroke="${p[1]}" stroke-width="1" opacity=".4"/>`;
|
||||
let level = between(r, 0.2, 0.6);
|
||||
for (let i = 0; i < n; i++) {
|
||||
level = Math.max(0.08, Math.min(1, level + between(r, -0.3, 0.3)));
|
||||
const h = level * between(r, 60, 110), c = level > 0.75 ? p[2] : i % 9 === 4 ? p[3] : p[1];
|
||||
s += `<rect x="${f1(15 + i * gap)}" y="${f1(cy - h / 2)}" width="${f1(gap * 0.55)}" height="${f1(h)}" rx="${f1(gap * 0.27)}" ` +
|
||||
`fill="${c}" opacity="${c === p[1] ? 0.75 : 1}"/>`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
if (v < 0.75) { // a mast, its waves going out
|
||||
const x = between(r, 60, 240), y = between(r, 300, 360);
|
||||
let s = '';
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
s += `<circle cx="${f1(x)}" cy="${f1(y)}" r="${i * 26}" fill="none" stroke="${i % 2 ? p[2] : p[3]}" stroke-width="${i % 2 ? 4 : 2}" opacity="${f1(1 - i * 0.12)}"/>`;
|
||||
}
|
||||
return s + `<path d="M${f1(x)} ${f1(y)} L${f1(x - 26)} ${H} M${f1(x)} ${f1(y)} L${f1(x + 26)} ${H} M${f1(x - 13)} ${f1(y + 60)} L${f1(x + 13)} ${f1(y + 60)}" ` +
|
||||
`stroke="${p[1]}" stroke-width="3" fill="none"/><circle cx="${f1(x)}" cy="${f1(y)}" r="7" fill="${p[1]}"/>`;
|
||||
}
|
||||
// two speech bubbles, one answering the other
|
||||
const bubble = (x, y, w, h, left, fill, stroke) => `<path d="M${f1(x + 14)} ${f1(y)} H${f1(x + w - 14)} Q${f1(x + w)} ${f1(y)} ${f1(x + w)} ${f1(y + 14)} ` +
|
||||
`V${f1(y + h - 14)} Q${f1(x + w)} ${f1(y + h)} ${f1(x + w - 14)} ${f1(y + h)} ` +
|
||||
(left ? `H${f1(x + 46)} L${f1(x + 22)} ${f1(y + h + 22)} L${f1(x + 30)} ${f1(y + h)}` : `H${f1(x + w - 30)} L${f1(x + w - 22)} ${f1(y + h + 22)} L${f1(x + w - 46)} ${f1(y + h)}`) +
|
||||
` H${f1(x + 14)} Q${f1(x)} ${f1(y + h)} ${f1(x)} ${f1(y + h - 14)} V${f1(y + 14)} Q${f1(x)} ${f1(y)} ${f1(x + 14)} ${f1(y)} Z" ` +
|
||||
`fill="${fill}" stroke="${stroke}" stroke-width="2.4"/>`;
|
||||
const w1 = between(r, 150, 200), w2 = between(r, 130, 180);
|
||||
return bubble(between(r, 20, 50), between(r, 190, 220), w1, between(r, 70, 90), true, p[2], 'none') +
|
||||
bubble(W - 20 - w2 - between(r, 0, 30), between(r, 310, 330), w2, between(r, 60, 80), false, 'none', p[1]);
|
||||
},
|
||||
musik(r, p) {
|
||||
if (r() < 0.5) { // a record, half out of its sleeve
|
||||
const cx = between(r, 170, 230), cy = between(r, 280, 320), R = between(r, 110, 130);
|
||||
let s = `<rect x="${f1(cx - R - 90)}" y="${f1(cy - R - 6)}" width="${f1(R * 2 + 12)}" height="${f1(R * 2 + 12)}" fill="${p[3]}" opacity=".9"/>` +
|
||||
`<circle cx="${f1(cx)}" cy="${f1(cy)}" r="${f1(R)}" fill="${p[1]}"/>`;
|
||||
for (let g = R - 8; g > R * 0.4; g -= between(r, 4, 9)) {
|
||||
s += `<circle cx="${f1(cx)}" cy="${f1(cy)}" r="${f1(g)}" fill="none" stroke="${p[0]}" stroke-width=".6" opacity=".25"/>`;
|
||||
}
|
||||
return s + `<circle cx="${f1(cx)}" cy="${f1(cy)}" r="${f1(R * 0.32)}" fill="${p[2]}"/><circle cx="${f1(cx)}" cy="${f1(cy)}" r="3" fill="${p[0]}"/>`;
|
||||
}
|
||||
// a staff with notes on it
|
||||
const y0 = between(r, 220, 280), gap = 11;
|
||||
let s = '';
|
||||
for (let k = 0; k < 5; k++) s += `<line x1="0" y1="${f1(y0 + k * gap)}" x2="${W}" y2="${f1(y0 + k * gap)}" stroke="${p[1]}" stroke-width="1.2" opacity=".7"/>`;
|
||||
for (let x = 30; x < W - 20; x += between(r, 26, 44)) {
|
||||
const y = y0 + 4 * gap - Math.floor(between(r, -3, 11)) * gap / 2, c = r() < 0.2 ? p[2] : p[1];
|
||||
s += `<ellipse cx="${f1(x)}" cy="${f1(y)}" rx="6.5" ry="4.8" fill="${c}" transform="rotate(-20 ${f1(x)} ${f1(y)})"/>` +
|
||||
`<line x1="${f1(x + 6)}" y1="${f1(y)}" x2="${f1(x + 6)}" y2="${f1(y - 34)}" stroke="${c}" stroke-width="1.6"/>`;
|
||||
}
|
||||
return s + `<rect x="0" y="${f1(y0 + 4 * gap + 60)}" width="${W}" height="${H}" fill="${p[2]}" opacity=".85"/>`;
|
||||
},
|
||||
kunst(r, p) {
|
||||
if (r() < 0.5) { // colour fields one above the other, their edges soft
|
||||
const top = between(r, 150, 180), gap = between(r, 10, 18), h1 = between(r, 110, 150);
|
||||
const field = (y, h, c, o) => `<rect x="22" y="${f1(y - 4)}" width="${W - 44}" height="${f1(h + 8)}" rx="10" fill="${c}" opacity="${f1(o * 0.25)}"/>` +
|
||||
`<rect x="26" y="${f1(y)}" width="${W - 52}" height="${f1(h)}" rx="6" fill="${c}" opacity="${o}"/>`;
|
||||
return field(top, h1, p[2], 0.95) + field(top + h1 + gap, H - 30 - (top + h1 + gap), pick(r, [p[3], p[1]]), 0.85);
|
||||
}
|
||||
// brushstrokes
|
||||
let s = '';
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const x0 = between(r, -20, 80), y0 = between(r, 180, 420), x1 = between(r, 220, 320), y1 = y0 + between(r, -80, 80);
|
||||
s += `<path d="M${f1(x0)} ${f1(y0)} Q${f1((x0 + x1) / 2)} ${f1(Math.min(y0, y1) - between(r, 20, 90))} ${f1(x1)} ${f1(y1)}" fill="none" ` +
|
||||
`stroke="${pick(r, [p[1], p[2], p[3]])}" stroke-width="${f1(between(r, 10, 34))}" stroke-linecap="round" opacity="${f1(between(r, 0.6, 0.95))}"/>`;
|
||||
}
|
||||
return s;
|
||||
},
|
||||
essen(r, p) {
|
||||
if (r() < 0.5) { // a plate on a checked cloth
|
||||
const sz = pick(r, [20, 24, 28]), cx = between(r, 120, 180), cy = between(r, 280, 320);
|
||||
return `<defs><pattern id="gi" width="${sz * 2}" height="${sz * 2}" patternUnits="userSpaceOnUse">` +
|
||||
`<rect width="${sz}" height="${sz * 2}" fill="${p[2]}" opacity=".35"/><rect width="${sz * 2}" height="${sz}" fill="${p[2]}" opacity=".35"/></pattern></defs>` +
|
||||
`<rect y="120" width="${W}" height="${H - 120}" fill="url(#gi)"/>` +
|
||||
`<circle cx="${f1(cx)}" cy="${f1(cy)}" r="92" fill="${p[0]}" stroke="${p[1]}" stroke-width="1.2"/>` +
|
||||
`<circle cx="${f1(cx)}" cy="${f1(cy)}" r="64" fill="none" stroke="${p[1]}" stroke-width="1" opacity=".5"/>` +
|
||||
`<circle cx="${f1(cx + between(r, -20, 20))}" cy="${f1(cy + between(r, -20, 20))}" r="${f1(between(r, 18, 30))}" fill="${p[3]}"/>`;
|
||||
}
|
||||
// citrus slices
|
||||
let s = '';
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const x = between(r, 40, 260), y = between(r, 180, 410), R = between(r, 30, 52), n = pick(r, [8, 9, 10]), c = pick(r, [p[2], p[3]]);
|
||||
s += `<circle cx="${f1(x)}" cy="${f1(y)}" r="${f1(R)}" fill="${c}"/><circle cx="${f1(x)}" cy="${f1(y)}" r="${f1(R * 0.86)}" fill="${p[0]}" opacity=".55"/>`;
|
||||
for (let k = 0; k < n; k++) {
|
||||
const a = k * Math.PI * 2 / n;
|
||||
s += `<line x1="${f1(x)}" y1="${f1(y)}" x2="${f1(x + Math.cos(a) * R * 0.84)}" y2="${f1(y + Math.sin(a) * R * 0.84)}" stroke="${c}" stroke-width="2"/>`;
|
||||
}
|
||||
}
|
||||
return s;
|
||||
},
|
||||
sport(r, p) {
|
||||
if (r() < 0.5) { // the bend of a running track, its lanes
|
||||
const cx = pick(r, [40, W - 40]), cy = between(r, 300, 340);
|
||||
let s = `<circle cx="${cx}" cy="${f1(cy)}" r="210" fill="${p[2]}" opacity=".9"/>`;
|
||||
for (let i = 0; i <= 6; i++) s += `<circle cx="${cx}" cy="${f1(cy)}" r="${120 + i * 15}" fill="none" stroke="${p[0]}" stroke-width="1.6"/>`;
|
||||
return s + `<circle cx="${cx}" cy="${f1(cy)}" r="118" fill="${p[3]}"/>`;
|
||||
}
|
||||
// a pitch from above
|
||||
const x0 = 30, x1 = W - 30, y0 = 150, y1 = H - 20, my = (y0 + y1) / 2;
|
||||
return `<rect x="0" y="${y0 - 14}" width="${W}" height="${H}" fill="${p[3]}" opacity=".85"/>` +
|
||||
`<g fill="none" stroke="${p[0]}" stroke-width="2"><rect x="${x0}" y="${y0}" width="${x1 - x0}" height="${y1 - y0}"/>` +
|
||||
`<line x1="${x0}" y1="${my}" x2="${x1}" y2="${my}"/><circle cx="${W / 2}" cy="${my}" r="34"/>` +
|
||||
`<rect x="${W / 2 - 60}" y="${y0}" width="120" height="44"/><rect x="${W / 2 - 60}" y="${y1 - 44}" width="120" height="44"/></g>` +
|
||||
`<circle cx="${f1(between(r, 60, 240))}" cy="${f1(between(r, y0 + 30, y1 - 30))}" r="7" fill="${p[2]}"/>`;
|
||||
},
|
||||
geld(r, p) {
|
||||
if (r() < 0.55) { // a price over time, the area under it
|
||||
let s = '';
|
||||
for (let y = 170; y < H - 20; y += 40) s += `<line x1="20" y1="${y}" x2="${W - 20}" y2="${y}" stroke="${p[1]}" stroke-width=".6" opacity=".3"/>`;
|
||||
let v = between(r, 300, 380);
|
||||
const pts = [];
|
||||
for (let x = 20; x <= W - 20; x += 10) { v = Math.max(170, Math.min(H - 30, v + between(r, -26, 22))); pts.push([x, v]); }
|
||||
const d = pts.map((q, i) => (i ? 'L' : 'M') + q[0] + ' ' + f1(q[1])).join(' '), last = pts[pts.length - 1];
|
||||
return s + `<path d="${d} L${last[0]} ${H - 20} L20 ${H - 20} Z" fill="${p[2]}" opacity=".25"/>` +
|
||||
`<path d="${d}" fill="none" stroke="${p[2]}" stroke-width="2.6" stroke-linejoin="round"/>` +
|
||||
`<circle cx="${last[0]}" cy="${f1(last[1])}" r="5" fill="${p[3]}"/>`;
|
||||
}
|
||||
// coins in stacks
|
||||
let s = '';
|
||||
const n = 4 + Math.floor(r() * 3), w = (W - 40) / n;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const cx = 20 + w * (i + 0.5), k = 2 + Math.floor(r() * 12);
|
||||
for (let j = 0; j < k; j++) {
|
||||
const y = H - 40 - j * 11;
|
||||
s += `<ellipse cx="${f1(cx)}" cy="${f1(y + 4)}" rx="${f1(w * 0.38)}" ry="7" fill="${p[1]}" opacity=".6"/>` +
|
||||
`<ellipse cx="${f1(cx)}" cy="${f1(y)}" rx="${f1(w * 0.38)}" ry="7" fill="${j === k - 1 ? p[2] : p[3]}"/>`;
|
||||
}
|
||||
}
|
||||
return s;
|
||||
},
|
||||
reise(r, p) {
|
||||
if (r() < 0.5) { // a road to the horizon
|
||||
const hy = between(r, 220, 260), vx = between(r, 110, 190);
|
||||
let s = `<rect x="0" y="${f1(hy)}" width="${W}" height="${f1(H - hy)}" fill="${p[3]}" opacity=".35"/>` +
|
||||
`<circle cx="${f1(between(r, 40, 260))}" cy="${f1(hy - between(r, 30, 70))}" r="${f1(between(r, 18, 30))}" fill="${p[2]}"/>` +
|
||||
`<path d="M${f1(vx - 2)} ${f1(hy)} L${f1(vx + 2)} ${f1(hy)} L${W - 20} ${H} L20 ${H} Z" fill="${p[1]}" opacity=".85"/>`;
|
||||
for (let t = 0.08; t < 1; t = t * 1.45 + 0.03) {
|
||||
const y = hy + (H - hy) * t, x = vx + (W / 2 - vx) * t;
|
||||
s += `<rect x="${f1(x - 1 - t * 3)}" y="${f1(y)}" width="${f1(2 + t * 6)}" height="${f1((H - hy) * t * 0.12)}" fill="${p[0]}"/>`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
// a route across, its stops, a compass
|
||||
const pts = [];
|
||||
for (let i = 0; i < 5; i++) pts.push([between(r, 30, 270), 170 + i * 60 + between(r, -15, 15)]);
|
||||
let s = `<path d="M${pts.map(q => f1(q[0]) + ' ' + f1(q[1])).join(' L')}" fill="none" stroke="${p[1]}" stroke-width="2" stroke-dasharray="2 6" stroke-linecap="round"/>`;
|
||||
pts.forEach((q, i) => {
|
||||
const end = i === pts.length - 1;
|
||||
s += `<circle cx="${f1(q[0])}" cy="${f1(q[1])}" r="${end ? 9 : 5}" fill="${end ? p[2] : p[0]}" stroke="${p[2]}" stroke-width="2"/>`;
|
||||
});
|
||||
return s + `<g transform="translate(${f1(between(r, 210, 250))} ${f1(between(r, 380, 410))}) rotate(${f1(between(r, -20, 20))})">` +
|
||||
`<circle r="26" fill="none" stroke="${p[3]}" stroke-width="1.4"/><path d="M0 -32 L6 0 L0 32 L-6 0 Z" fill="${p[3]}"/>` +
|
||||
`<path d="M-32 0 L0 5 L32 0 L0 -5 Z" fill="${p[3]}" opacity=".6"/><path d="M0 -32 L6 0 L-6 0 Z" fill="${p[2]}"/></g>`;
|
||||
},
|
||||
bauhaus(r, p) {
|
||||
const v = r();
|
||||
if (v < 0.3) { // tiles, each a quarter circle turned its own way
|
||||
const sz = (W - 40) / 4;
|
||||
let s = '';
|
||||
for (let i = 0; i < 4; i++) for (let j = 0; 170 + (j + 1) * sz <= H - 10; j++) {
|
||||
const x = 20 + i * sz, y = 170 + j * sz, corner = Math.floor(r() * 4);
|
||||
const dx = corner % 2 ? -1 : 1, dy = corner > 1 ? -1 : 1, cx = dx > 0 ? x : x + sz, cy = dy > 0 ? y : y + sz;
|
||||
s += `<rect x="${f1(x)}" y="${f1(y)}" width="${f1(sz)}" height="${f1(sz)}" fill="${pick(r, [p[0], p[3], p[1]])}" opacity=".22"/>` +
|
||||
`<path d="M${f1(cx)} ${f1(cy)} L${f1(cx + dx * sz)} ${f1(cy)} A${f1(sz)} ${f1(sz)} 0 0 ${dx * dy > 0 ? 1 : 0} ${f1(cx)} ${f1(cy + dy * sz)} Z" ` +
|
||||
`fill="${pick(r, [p[1], p[2], p[3]])}"/>`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
if (v < 0.55) { // a big circle off the edge, crossed by thin diagonals
|
||||
const cx = pick(r, [0, W]), a = cx ? -1 : 1, n = 7 + Math.floor(r() * 5);
|
||||
let s = `<circle cx="${cx}" cy="${f1(between(r, 260, 360))}" r="${f1(between(r, 120, 170))}" fill="${p[2]}"/>`;
|
||||
for (let i = 0, x = a > 0 ? -160 : 40; i < n; i++, x += between(r, 34, 44)) {
|
||||
s += `<line x1="${f1(x)}" y1="${H}" x2="${f1(x + a * 260)}" y2="150" stroke="${i % 3 ? p[1] : p[3]}" stroke-width="${i % 3 ? 1.2 : 5}" opacity=".85"/>`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
let s = '';
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const kind = Math.floor(r() * 3), x = between(r, 30, 270), y = between(r, 150, 410), size = between(r, 40, 110);
|
||||
|
|
@ -608,7 +907,8 @@
|
|||
return s.length >= 3 ? s : String(t || '?');
|
||||
}
|
||||
|
||||
function textBlock(r, th, p, book) {
|
||||
// h: the cover's height (a square's is its width); max: lines of title at most.
|
||||
function textBlock(r, th, p, book, h, max) {
|
||||
const title = mainTitle(book.title);
|
||||
const upper = th.upper;
|
||||
const t = upper ? title.toUpperCase() : title;
|
||||
|
|
@ -619,7 +919,7 @@
|
|||
const em = upper ? 0.74 : 0.58;
|
||||
const size = Math.max(14, Math.min(len <= 10 ? 40 : len <= 18 ? 34 : len <= 30 ? 28 : len <= 48 ? 23 : 19,
|
||||
Math.floor(248 / (longest * em))));
|
||||
const ls = lines(t, size, 5, upper ? 0.72 : 0.56);
|
||||
const ls = lines(t, size, max, upper ? 0.72 : 0.56);
|
||||
const lh = size * 1.12;
|
||||
const layout = pick(r, th.layouts || ['top', 'top', 'bottom', 'plate']);
|
||||
const author = String(book.author || '').split(/[;,]/)[0].trim();
|
||||
|
|
@ -629,8 +929,8 @@
|
|||
y0 = 34;
|
||||
s += `<rect x="0" y="0" width="${W}" height="${f1(blockH)}" fill="${p[0]}" opacity=".94"/>`;
|
||||
} else if (layout === 'bottom') {
|
||||
y0 = H - blockH + 20;
|
||||
s += `<rect x="0" y="${f1(H - blockH)}" width="${W}" height="${f1(blockH)}" fill="${p[0]}" opacity=".94"/>`;
|
||||
y0 = h - blockH + 20;
|
||||
s += `<rect x="0" y="${f1(h - blockH)}" width="${W}" height="${f1(blockH)}" fill="${p[0]}" opacity=".94"/>`;
|
||||
} else {
|
||||
y0 = 70;
|
||||
s += `<rect x="18" y="${f1(y0 - 34)}" width="${W - 36}" height="${f1(blockH)}" rx="3" fill="${p[0]}" opacity=".95"/>`;
|
||||
|
|
@ -647,7 +947,8 @@
|
|||
const asz = Math.max(8, Math.min(12, Math.floor((240 / author.length - spacing) / 0.7)));
|
||||
s += `<text x="${x}" y="${f1(y0 + ls.length * lh + 22)}" font-family="${SANS}" font-size="${asz}" letter-spacing="${spacing}" fill="${p[2]}">${esc(author.toUpperCase())}</text>`;
|
||||
}
|
||||
return s;
|
||||
// free: the band the text leaves to the motif (a square centres the motif in it).
|
||||
return {svg: s, free: layout === 'top' ? [blockH, h] : layout === 'bottom' ? [0, h - blockH] : [y0 - 34 + blockH, h]};
|
||||
}
|
||||
|
||||
// ---- the cover ---------------------------------------------------------------------------------------
|
||||
|
|
@ -659,24 +960,32 @@
|
|||
return '#' + h + h + h;
|
||||
}
|
||||
|
||||
// A square shows a band of the 2:3 motif: its heart (about here, the motifs keep the top of the
|
||||
// cover for the text) in the middle of what the text leaves free.
|
||||
const MOTIF_HEART = 260;
|
||||
|
||||
function svg(book, opts) {
|
||||
const th = theme(book);
|
||||
const square = !!(opts && opts.square);
|
||||
const th = theme(book, opts);
|
||||
const r = rng(hash(norm(book.title) + '|' + norm(book.author)));
|
||||
let p = pick(r, th.palettes);
|
||||
if (opts && opts.gray) p = p.map(gray);
|
||||
const motif = (M[th.id] || M.bauhaus)(r, p, book);
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">` +
|
||||
`<rect width="${W}" height="${H}" fill="${p[0]}"/>${motif}${th.ownText ? '' : textBlock(r, th, p, book)}</svg>`;
|
||||
const h = square ? W : H;
|
||||
const text = th.ownText ? {svg: '', free: [0, h]} : textBlock(r, th, p, book, h, square ? 3 : 5);
|
||||
const shift = Math.max(0, Math.min(H - h, Math.round(MOTIF_HEART - (text.free[0] + text.free[1]) / 2)));
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${h}" width="${W}" height="${h}">` +
|
||||
`<rect width="${W}" height="${h}" fill="${p[0]}"/>${square ? `<g transform="translate(0 -${shift})">${motif}</g>` : motif}${text.svg}</svg>`;
|
||||
}
|
||||
|
||||
const cache = new Map();
|
||||
function url(book, opts) {
|
||||
const key = (book.title || '') + '|' + (book.author || '') + '|' + (book.shelfTag || '') + '|' +
|
||||
((book.folders || []).join(',')) + '|' + (book.series || '') + '|' + (book.seriesIndex == null ? '' : book.seriesIndex) + '|' +
|
||||
(opts && opts.gray ? 1 : 0);
|
||||
(opts && opts.gray ? 1 : 0) + (opts && opts.square ? 's' : '') + (opts && opts.podcast ? 'p' : '');
|
||||
if (!cache.has(key)) cache.set(key, 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg(book, opts)));
|
||||
return cache.get(key);
|
||||
}
|
||||
|
||||
window.DioraGenCover = {url, svg, theme: book => theme(book).id};
|
||||
window.DioraGenCover = {url, svg, theme: (book, opts) => theme(book, opts).id};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* diora service worker — caches the app shell for offline use.
|
||||
*/
|
||||
|
||||
const CACHE = 'diora-v151';
|
||||
const CACHE = 'diora-v152';
|
||||
const SHELL = [
|
||||
'/static/css/app.css',
|
||||
'/static/js/app/01-core.js',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copied from diora-android 9a6c059 2026-10-25: web-native/static/native-podcast.js
|
||||
// Copied from diora-android 6cfec49 2026-10-25: web-native/static/native-podcast.js
|
||||
// by tools/sync_podcast_ui.py. Do not edit here: change it in diora-android
|
||||
// and copy again. The browser host is static/js/podcasts-web.js.
|
||||
|
||||
|
|
@ -402,11 +402,14 @@
|
|||
return f ? f.artwork_url : '';
|
||||
}
|
||||
|
||||
// A cover: the episode's own, else its podcast's, else a tile with the podcast's initial.
|
||||
// A cover: the episode's own, else its podcast's, else one drawn from the podcast's name
|
||||
// (diora-web gencover.js, square), else a tile with its initial.
|
||||
function art(src, size, fallback, name) {
|
||||
const box = el('div', 'nui-pod-art' + (size ? ' nui-pod-art-' + size : ''));
|
||||
const srcs = [src, fallback].filter(Boolean).map(s => s.replace(/^http:/, 'https:'))
|
||||
.filter((s, i, all) => all.indexOf(s) === i);
|
||||
const title = plain(name || '').trim();
|
||||
if (title && window.DioraGenCover) srcs.push(DioraGenCover.url({title}, {square: true, podcast: true}));
|
||||
const initial = () => {
|
||||
if (box.querySelector('img') || box.firstChild) return;
|
||||
const letter = (plain(name || '').replace(/^(the|der|die|das)\s+/i, '').match(/[0-9A-Za-zÀ-ÿ]/) || [''])[0];
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue