diora-web/static/js/gencover.js
mrwnslz f1158e32a7
All checks were successful
Build and push Docker image / build (push) Successful in 18s
Test / test (push) Successful in 9m3s
Test / e2e (push) Successful in 2m29s
Generierte Cover: Ordner wählen das Motiv, neue Motive für Klassiker, Horror, Religion, Kinderbücher und Informatik
Kurze Stichwörter treffen nur noch am Wortanfang bzw. als ganzes Wort ("gen" nicht in
"Leiden", "war" nicht in "Gegenwart"), Ordnernamen werden nicht mehr nach Wörtern
durchsucht, sondern über FOLDER_THEMES einem Motiv zugeordnet. Mehr Varianten für
Philosophie, Psychoanalyse und Krimi; Großbuchstaben und lange Autorennamen laufen
nicht mehr über den Rand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 22:44:45 +02:00

607 lines
39 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Covers for books that have none (docs/library.md): drawn here, the same on every
* device, from what the book is called and where it stands — its title, author, shelf
* category and folders. Words in them point to a design concept (a motif, curated
* palettes, a typeface: Marx and Kapital to constructivism, Hegel and Dialektik to quiet
* geometry, Meer to waves, Krieg to map contours …), and so do the folders, by name
* (Klassiker to a patterned Insel-style cover, Horror to a bare tree before the moon …);
* a hash of title and author picks
* 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)
*
* Chrome-61-safe: no ?. / ??.
*/
(function () {
'use strict';
const W = 300, H = 450;
// ---- chance, the same for the same book ------------------------------------------------
function hash(s) {
let h = 2166136261;
for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); }
return h >>> 0;
}
function rng(seed) {
let a = seed >>> 0;
return function () {
a = (a + 0x6D2B79F5) >>> 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const pick = (r, list) => list[Math.floor(r() * list.length)];
const between = (r, a, b) => a + r() * (b - a);
const f1 = n => Math.round(n * 10) / 10;
// ---- the concepts ---------------------------------------------------------------------------
// words: stems in the text (lower case, umlauts as they are). One of five letters or
// more counts anywhere (German compounds: "Massenpsychologie"); a shorter one only at
// the start of a word, and one of three letters only as a word of its own (plural
// allowed) — "gen" isn't in "Leiden", "war" not in "Gegenwart" or "Wardrobe".
// A leading ^ asks for a word start whatever the length ("stern", not in "Eastern"),
// a trailing $ for a word's end as well ("king", not "Kingdom").
// palettes: [background, ink, accent, second accent], curated to sit well together.
const SERIF = "Georgia, 'Times New Roman', serif";
const SANS = "'Helvetica Neue', Helvetica, Arial, sans-serif";
const NARROW = "'Arial Narrow', 'Roboto Condensed', 'Helvetica Neue', Arial, sans-serif";
const MONO = "'Courier New', Courier, monospace";
const THEMES = [
{id: 'konstruktiv', font: NARROW, upper: true,
words: ['marx', 'kapital', 'capital', 'arbeit', 'labor', 'labour', 'revolution', 'sozialis', 'socialis', 'kommun', 'communis',
'ökonom', 'econom', 'klasse', 'class', 'gewerkschaft', 'streik', 'strike', 'partei', 'lenin', 'luxemburg', 'engels',
'proletar', 'faschis', 'fascis', 'jacobin', 'gegenstandpunkt', 'demokratie', 'herrschaft', 'massen'],
palettes: [['#efe6d2', '#1b1b1b', '#c8322b', '#1b1b1b'], ['#1d1d1d', '#f2ead7', '#d23a2c', '#e8b23a'], ['#e9dfc8', '#22252a', '#b5302a', '#7a8a8f']]},
{id: 'geometrie', font: SERIF,
words: ['philosoph', 'hegel', 'kant', 'dialekt', 'dialect', 'vernunft', 'reason', 'geist', 'spirit', 'ethik', 'ethic',
'logik', 'logic', 'metaphys', 'phänomen', 'phenomen', 'denken', 'wahrheit', 'truth', 'sein', 'nichts', 'nothing',
'pragmati', 'pessimis', 'flatland', 'dimension', 'leviathan', 'republic', 'meditation', 'treatise', 'abhandlung',
'kritik', 'critique', 'theorie', 'theory', 'begriff', 'wissen'],
palettes: [['#f1ece1', '#2b2a27', '#b08a3e', '#6f7d72'], ['#26292e', '#ece4d4', '#c9a25a', '#8a9aa8'], ['#e7e1d6', '#3a3833', '#8c5a3c', '#5d6b74']]},
{id: 'psyche', font: SERIF,
words: ['lacan', 'freud', 'psycho', 'sex', 'begehren', 'desire', 'disavowal', 'verleugn', 'unbewusst', 'unconscious',
'comedy', 'komik', 'komöd', 'parallax', 'of the real', 'voice', 'stimme', 'žižek', 'zizek', 'zupan', 'dolar', 'traum', 'dream',
'odd one', 'antigone', 'let them rot', 'klavierspielerin'],
palettes: [['#f3e3d9', '#3d2440', '#c45b6a', '#5c8a8a'], ['#2c2236', '#f2dfd2', '#e07a6f', '#7fb3ae'], ['#e8ddeb', '#2e2a3d', '#8d4f8a', '#d99a6c']]},
{id: 'wellen', font: SERIF,
words: ['meer', 'sea', 'ozean', 'ocean', 'wasser', 'water', 'insel', 'island', 'fluss', 'river', 'flut', 'flood', 'deluge',
'sintflut', 'schiff', 'ship', 'welle', 'wave', 'strand', 'küste', 'coast', 'regen', 'rain', 'venedig', 'venice'],
palettes: [['#e5eef0', '#13324a', '#2f6f8f', '#8fb8c6'], ['#0f2a3d', '#e3edf0', '#4c95b0', '#a9d3dc'], ['#efe9dc', '#1f3b4d', '#3d7a8c', '#c98a5a']]},
{id: 'karte', font: SERIF, upper: true,
words: ['krieg', 'war', 'history', 'empire', 'imperium', 'reich', 'zerstörung', 'destruction', 'wages',
'nazi', 'staat', 'state', 'nation', 'kolonial', 'colonial', 'weltkrieg', 'schlacht', 'battle', 'grenze', 'border',
'seeing like', 'world-system', 'weltsystem'],
palettes: [['#e9e1cc', '#3b3a2c', '#8a4b2a', '#6d7a4f'], ['#2f3127', '#e6dcc4', '#b5763c', '#8e9a6b'], ['#ddd5bd', '#2c2a22', '#7a3b2e', '#5f6f63']]},
{id: 'kosmos', font: SANS, upper: true,
words: ['^stern', 'star', 'kosmos', 'cosmos', 'space', 'weltraum', 'planet', 'mond', 'moon', 'dune', 'nacht', 'night',
'universum', 'universe', 'zeit', 'time', 'licht', 'light', 'galax', 'messiah', 'science fiction', 'sci-fi', 'zukunft', 'future'],
palettes: [['#141833', '#ece6d3', '#e3b65b', '#7d8bd4'], ['#0e1424', '#dfe6f2', '#d9824a', '#6fa3c4'], ['#1f1a33', '#efe7da', '#c9a8e0', '#e3c46b']]},
{id: 'ornament', font: SERIF,
words: ['fantasy', 'drache', 'dragon', 'magie', 'magic', 'prophezei', 'prophecy', 'throne', 'thron', 'könig', 'king$',
'krone', 'crown', 'schwert', 'sword', 'warrior', 'krieger', 'cats', 'katze', 'legende', 'legend', 'märchen', 'fairy',
'zauber', 'spell', 'ice and fire', 'bundle'],
palettes: [['#1e2a2a', '#efe3c4', '#c69a3c', '#8f3b46'], ['#2b1d2e', '#f0e2c8', '#d3a13f', '#3f7a6d'], ['#f0e6d0', '#2d2433', '#8c3a4a', '#2f6b65']]},
{id: 'botanik', font: SERIF,
words: ['natur', 'nature', 'wald', 'forest', 'baum', 'tree', 'garten', 'garden', 'blume', 'flower', 'blatt', 'leaf',
'frühling', 'spring', 'sommer', 'summer', 'pflanze', 'plant', 'wurzel', 'root', 'gras', 'grass', 'pilz', 'mushroom'],
palettes: [['#eef0e4', '#23301f', '#4f7a43', '#b7a35a'], ['#203024', '#e8ecdc', '#86a86b', '#d6b56a'], ['#f2ece0', '#2b3326', '#6f8f5a', '#c27c5a']]},
{id: 'berge', font: SANS, upper: true,
words: ['berg', 'mountain', 'alpen', 'alps', 'gipfel', 'peak', 'zauberberg', 'hügel', 'landschaft', 'landscape',
'schnee', 'snow', 'winter', 'eis', 'ice', 'tal', 'valley', 'wandern'],
palettes: [['#e8ecef', '#22303b', '#5b7a8f', '#a9bccb'], ['#24323d', '#eef1f2', '#7f9fb3', '#c8d6de'], ['#efe9e0', '#2e3438', '#6f8590', '#c7a27a']]},
{id: 'zellen', font: SANS,
words: ['biolog', 'gehirn', 'brain', 'mensch', 'human', 'behave', 'verhalten', 'behavio', 'sucht', 'addict', 'enough',
'körper', 'body', 'medizin', 'medic', 'genet', 'genom', 'evolution', 'neuro', 'zelle', 'cell', 'leben', 'life', 'tier',
'animal', 'species', 'darwin', 'beagle', 'natural selection'],
palettes: [['#f1ebe4', '#26323a', '#e0765c', '#4f9a94'], ['#1f2b31', '#eef0ea', '#f08a6c', '#69b3aa'], ['#eaf0ee', '#22303a', '#d4634e', '#3f8f8a']]},
{id: 'raster', font: SANS,
words: ['stadt', 'city', 'urban', 'risiko', 'risk', 'gesellschaft', 'society', 'moderne', 'modern', 'system', 'analys',
'netz', 'network', 'technik', 'techn', 'smart', 'notes', 'notiz', 'daten', 'data', 'digital', 'maschine', 'machine',
'architektur', 'architect', 'hyperpolitik', 'politik', 'politic', 'revolution of', 'end of the'],
palettes: [['#ecebe7', '#1f2328', '#d1453b', '#2f5f8f'], ['#22262b', '#ebe8e1', '#e2b33c', '#4f86b8'], ['#f0ede6', '#2a2d31', '#3f6fa0', '#d9a03a']]},
{id: 'schatten', font: NARROW, upper: true,
words: ['tod', 'death', 'mord', 'murder', 'crime', 'verbrechen', 'schatten', 'shadow', 'dunkel', 'dark', 'angst', 'fear',
'serotonin', 'blut', 'blood', 'gift', 'poison', 'detektiv', 'detective', 'kommissar', 'thriller', 'krimi', 'sherlock',
'christie', 'lupin', 'burglar', 'einbrecher'],
palettes: [['#151515', '#ecebe6', '#c2302f', '#5a5a5a'], ['#ecebe6', '#151515', '#a8241f', '#7c7c7c'], ['#1c1f24', '#e4e0d8', '#d0a04a', '#55606b']]},
{id: 'typo', font: SERIF,
words: ['roman', 'novel', 'erzählung', 'stories', 'geschichten', 'gedicht', 'poem', 'poesie', 'poetry', 'brief', 'letters',
'tagebuch', 'diary', 'molloy', 'beckett', 'manuskripte', 'literatur', 'literature', 'review', 'zeitschrift'],
palettes: [['#f2ede3', '#211f1c', '#9b3b2f', '#7d7466'], ['#24211e', '#f0e9dc', '#d39b5f', '#8f8578'], ['#e9e4f0', '#26222e', '#5b4a8a', '#a3909a']]},
// A patterned paper with a title plate, like the Insel-Bücherei.
{id: 'klassiker', font: SERIF, layouts: ['plate'], frame: true,
words: ['klassik', 'classic', 'sämtliche werke', 'gesammelte werke', 'works of'],
palettes: [['#e9dcc0', '#2a2620', '#a8452f', '#c9a86a'], ['#d9e1d6', '#22302a', '#3f6b57', '#b58a4a'], ['#e4d9e6', '#2c2433', '#6d3f73', '#c09a5a'],
['#dfe4ea', '#1f2733', '#2f557a', '#c5953f'], ['#f0e3d3', '#2d2420', '#7e2f3a', '#3f6f74']]},
{id: 'sakral', font: SERIF, upper: true,
words: ['bibel', 'bible', 'gott', 'koran', 'quran', 'edda', 'mytholog', 'myth', 'heilig', 'holy', 'sacred', 'kirche',
'church', 'religio', 'evangel', 'gospel', 'testament', 'septuaginta', 'kojiki', 'heldensage', 'sagen', 'beowulf',
'götter', 'divine', 'paradise', 'paradies', 'hölle', 'hell'],
palettes: [['#1d2440', '#efe4c8', '#c9a245', '#7d4b8a'], ['#3a1c1f', '#f1e5cc', '#d1a548', '#4f7a8a'], ['#efe6d2', '#2b2218', '#9a6b22', '#7a2e2a']]},
{id: 'grusel', font: SERIF, upper: true,
words: ['horror', 'grusel', 'gothic', 'vampir', 'dracula', 'gespenst', 'ghost', 'spuk', 'haunt', 'cthulhu', 'monster',
'frankenstein', 'sleepy hollow', 'borderland', 'poe', 'shining', 'scream', 'lovecraft'],
palettes: [['#16141a', '#e8e2d6', '#9e2a2b', '#d9d2bf', '#08070a'], ['#1d2320', '#e3e6dc', '#7d9a6a', '#c9cfb5', '#090c0a'],
['#e3dccd', '#1a1718', '#8c1c1c', '#f4efe4', '#1a1718']]},
{id: 'spiel', font: SANS,
words: ['kinder', 'children', 'kids', 'jugend', 'abenteuer', 'adventure', 'alice', 'looking-glass', 'wonderland',
'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']]},
{id: 'code', font: MONO,
words: ['program', 'comput', 'informatik', 'software', 'algorithm', 'linux', 'unix', 'python', 'javascript',
'rhcsa', 'red hat', 'interpretation of computer'],
palettes: [['#1e2229', '#e6e1d6', '#e5a93d', '#6fb3c9'], ['#f4f1ea', '#22262c', '#c2453a', '#3d7ab0'], ['#0f1a14', '#d8eadc', '#5fd08a', '#e0c068']]},
];
// Folders say what a book is when its title doesn't: by name (lower case, a part of it).
const FOLDER_THEMES = [
['marx', 'konstruktiv'], ['ökonom', 'konstruktiv'], ['ljubljana', 'psyche'], ['psychoanaly', 'psyche'],
['kritische theorie', 'geometrie'], ['philosoph', 'geometrie'], ['poststruktur', 'raster'], ['zeitdiagnos', 'raster'],
['politik', 'raster'], ['soziolog', 'raster'], ['gesellschaft', 'raster'], ['shadowrun', 'raster'],
['psycholog', 'zellen'], ['wissenschaft', 'zellen'], ['biolog', 'zellen'], ['geschicht', 'karte'],
['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'],
];
const DEFAULT = {id: 'bauhaus', font: SANS,
palettes: [['#efe9dd', '#24262a', '#d0533f', '#3f7fb0'], ['#26282d', '#efe8da', '#e3b04b', '#5b9aa8'], ['#e8ecef', '#22272c', '#c8553d', '#6b8f71'],
['#f3e9e2', '#2a2523', '#b84a62', '#4c7a8a']]};
// ---- which concept: the words, weighted by where they stand -------------------------------
function norm(s) { return String(s || '').toLowerCase(); }
const LETTER = 'a-z0-9äöüßàâçéèêëîïôûùÿñæœ';
function matcher(w, atStart) {
const start = atStart || w.charAt(0) === '^';
const whole = w.slice(-1) === '$';
const stem = w.replace(/^\^|\$$/g, '').replace(/[.*+?()[\]{}|\\]/g, '\\$&');
if (!start && !whole && stem.length >= 5) return s => s.indexOf(stem) >= 0;
const re = new RegExp('(^|[^' + LETTER + '])' + stem + (whole || stem.length <= 3 ? '(s|e|en)?($|[^' + LETTER + '])' : ''));
return s => re.test(s);
}
// In a name only at a word's start: "light" isn't in "Lightner".
THEMES.forEach(t => { t.match = t.words.map(w => matcher(w)); t.matchName = t.words.map(w => matcher(w, true)); });
function scored(book) {
// The title says most about a book; author and shelf category help. The first folder
// outweighs a word in the author or shelf category (not one in the title), further ones tip the scales.
const fields = [[norm(book.title), 3, 'match'], [norm(book.shelfTag), 2, 'match'], [norm(book.author), 2, 'matchName']];
const folders = (book.folders || (book.folder ? [book.folder] : [])).map(norm);
return THEMES.map(t => {
let score = 0;
fields.forEach(f => { if (f[0]) t[f[2]].forEach(m => { if (m(f[0])) score += f[1]; }); });
folders.forEach((f, i) => {
if (FOLDER_THEMES.some(ft => ft[1] === t.id && f.indexOf(ft[0]) >= 0)) score += i ? 1 : 2.5;
});
return {t, score};
}).filter(x => x.score > 0).sort((a, b) => b.score - a.score);
}
function theme(book) {
const s = scored(book);
return s.length ? s[0].t : DEFAULT;
}
// ---- motifs: each draws into the whole cover, the text gets its own space later -----------
const M = {
konstruktiv(r, p) {
let s = '';
const a = pick(r, [-34, -28, 28, 34, -45]);
s += `<g transform="rotate(${a} 150 225)">`;
for (let i = 0; i < 3; i++) {
const y = between(r, 60, 380), h = between(r, 14, 46);
s += `<rect x="-120" y="${f1(y)}" width="540" height="${f1(h)}" fill="${i ? p[1] : p[2]}" opacity="${i ? 0.9 : 1}"/>`;
}
s += '</g>';
s += `<circle cx="${f1(between(r, 70, 230))}" cy="${f1(between(r, 120, 320))}" r="${f1(between(r, 38, 78))}" fill="${p[2]}"/>`;
s += `<rect x="${f1(between(r, 20, 200))}" y="${f1(between(r, 40, 360))}" width="${f1(between(r, 30, 80))}" height="${f1(between(r, 30, 80))}" fill="none" stroke="${p[1]}" stroke-width="6"/>`;
return s;
},
geometrie(r, p) {
const v = r();
if (v < 0.33) { // nested squares, turning
const cx = between(r, 110, 190), cy = between(r, 220, 320), n = 5 + Math.floor(r() * 4), turn = between(r, 6, 12);
let s = '';
for (let i = n; i >= 1; i--) {
const d = i * between(r, 13, 17);
s += `<rect x="${f1(cx - d)}" y="${f1(cy - d)}" width="${f1(d * 2)}" height="${f1(d * 2)}" fill="none" stroke="${i === 2 ? p[2] : p[1]}" ` +
`stroke-width="${i === 2 ? 2.2 : 1}" opacity="${i === 2 ? 0.9 : 0.5}" transform="rotate(${f1(i * turn)} ${f1(cx)} ${f1(cy)})"/>`;
}
return s + `<circle cx="${f1(cx)}" cy="${f1(cy)}" r="${f1(between(r, 6, 11))}" fill="${p[2]}"/>`;
}
if (v < 0.6) { // triangle, circle and square, drawn
const y = between(r, 250, 330), sz = between(r, 46, 60), xs = [70, 150, 230];
const order = [0, 1, 2].sort(() => r() - 0.5), filled = Math.floor(r() * 3);
let s = `<line x1="20" y1="${f1(y + sz + 14)}" x2="${W - 20}" y2="${f1(y + sz + 14)}" stroke="${p[1]}" stroke-width="1" opacity=".5"/>`;
order.forEach((kind, i) => {
const x = xs[i], att = i === filled ? `fill="${p[2]}" stroke="none"` : `fill="none" stroke="${p[1]}" stroke-width="1.6"`;
if (kind === 0) s += `<circle cx="${x}" cy="${f1(y + sz / 2)}" r="${f1(sz / 2)}" ${att}/>`;
else if (kind === 1) s += `<rect x="${f1(x - sz / 2)}" y="${f1(y)}" width="${f1(sz)}" height="${f1(sz)}" ${att}/>`;
else s += `<path d="M${x} ${f1(y)} L${f1(x + sz / 2)} ${f1(y + sz)} L${f1(x - sz / 2)} ${f1(y + sz)} Z" ${att}/>`;
});
return s + `<circle cx="${f1(between(r, 40, 260))}" cy="${f1(y - between(r, 40, 80))}" r="3" fill="${p[3]}"/>`;
}
const cx = between(r, 100, 200), cy = between(r, 170, 300);
let s = '';
const n = 4 + Math.floor(r() * 5);
for (let i = 1; i <= n; i++) s += `<circle cx="${f1(cx)}" cy="${f1(cy)}" r="${f1(i * between(r, 14, 22))}" fill="none" stroke="${i % 3 ? p[1] : p[2]}" stroke-width="${i % 3 ? 1 : 2.4}" opacity="${i % 3 ? 0.55 : 0.9}"/>`;
const phi = 1.618;
s += `<line x1="0" y1="${f1(H / phi)}" x2="${W}" y2="${f1(H / phi)}" stroke="${p[1]}" stroke-width="1" opacity=".5"/>`;
s += `<line x1="${f1(W / phi)}" y1="0" x2="${f1(W / phi)}" y2="${H}" stroke="${p[1]}" stroke-width="1" opacity=".5"/>`;
s += `<circle cx="${f1(cx + between(r, -40, 40))}" cy="${f1(cy + between(r, -40, 40))}" r="${f1(between(r, 8, 18))}" fill="${p[2]}"/>`;
return s;
},
psyche(r, p) {
const v = r();
if (v < 0.35) { // the Borromean knot: three rings, no two linked
const cx = between(r, 125, 175), cy = between(r, 250, 300), rr = between(r, 52, 64), d = rr * 0.62;
let s = '';
[[-90, p[2], 0.75], [30, p[3], 0.6], [150, p[1], 0]].forEach(([a, c, o], i) => {
const x = cx + Math.cos(a * Math.PI / 180) * d, y = cy + Math.sin(a * Math.PI / 180) * d;
s += `<circle cx="${f1(x)}" cy="${f1(y)}" r="${f1(rr)}" fill="${c}" fill-opacity="${o}" stroke="${p[1]}" stroke-width="${i === 2 ? 2.4 : 1.2}"/>`;
});
return s;
}
if (v < 0.6) { // a spiral, over a divided ground
const cx = between(r, 120, 180), cy = between(r, 240, 300), turns = between(r, 3, 4.5), grow = between(r, 4.5, 6.5);
let d = '';
for (let k = 0; k <= turns * 36; k++) {
const a = k / 36 * Math.PI * 2, rad = grow * a;
d += (k ? ' L' : 'M') + f1(cx + Math.cos(a) * rad) + ' ' + f1(cy + Math.sin(a) * rad);
}
return `<rect x="0" y="${f1(cy)}" width="${W}" height="${f1(H - cy)}" fill="${p[3]}" opacity=".3"/>` +
`<path d="${d}" fill="none" stroke="${p[2]}" stroke-width="2.4" stroke-linecap="round"/>` +
`<circle cx="${f1(cx)}" cy="${f1(cy)}" r="4" fill="${p[1]}"/>`;
}
let s = `<rect x="${W / 2}" y="0" width="${W / 2}" height="${H}" fill="${p[3]}" opacity=".35"/>`;
const cy = between(r, 170, 290);
const rx = between(r, 60, 95), ry = between(r, 80, 130);
s += `<ellipse cx="${f1(W / 2 - rx * 0.45)}" cy="${f1(cy)}" rx="${f1(rx)}" ry="${f1(ry)}" fill="${p[2]}" opacity=".85"/>`;
s += `<ellipse cx="${f1(W / 2 + rx * 0.45)}" cy="${f1(cy)}" rx="${f1(rx)}" ry="${f1(ry)}" fill="none" stroke="${p[1]}" stroke-width="2.5"/>`;
s += `<line x1="${W / 2}" y1="40" x2="${W / 2}" y2="${H - 40}" stroke="${p[1]}" stroke-width="1" opacity=".6"/>`;
return s;
},
wellen(r, p) {
let s = '';
const n = 6 + Math.floor(r() * 5);
const amp = between(r, 6, 16), len = between(r, 60, 120);
for (let i = 0; i < n; i++) {
const y0 = 120 + i * (300 / n);
let d = `M0 ${f1(y0)}`;
for (let x = 0; x <= W; x += 10) d += ` L${x} ${f1(y0 + Math.sin((x / len) * Math.PI * 2 + i * 0.7) * amp)}`;
d += ` L${W} ${H} L0 ${H} Z`;
s += `<path d="${d}" fill="${i % 2 ? p[2] : p[3]}" opacity="${f1(0.18 + i * 0.07)}"/>`;
}
return s;
},
karte(r, p) {
let s = '';
const cx = between(r, 80, 220), cy = between(r, 160, 320);
for (let i = 1; i <= 9; i++) {
let d = '';
for (let k = 0; k <= 36; k++) {
const a = (k / 36) * Math.PI * 2;
const rad = i * 15 * (1 + 0.18 * Math.sin(a * 3 + i) + 0.1 * Math.cos(a * 5 + r()));
d += (k ? ' L' : 'M') + f1(cx + Math.cos(a) * rad) + ' ' + f1(cy + Math.sin(a) * rad * 1.15);
}
s += `<path d="${d} Z" fill="none" stroke="${i % 4 ? p[1] : p[2]}" stroke-width="${i % 4 ? 0.8 : 1.8}" opacity="${i % 4 ? 0.45 : 0.9}"/>`;
}
s += `<g stroke="${p[2]}" stroke-width="1.4"><line x1="${f1(cx)}" y1="${f1(cy - 30)}" x2="${f1(cx)}" y2="${f1(cy + 30)}"/><line x1="${f1(cx - 30)}" y1="${f1(cy)}" x2="${f1(cx + 30)}" y2="${f1(cy)}"/></g>`;
return s;
},
kosmos(r, p) {
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);
for (let i = 1; i <= 3; i++) s += `<ellipse cx="${f1(cx)}" cy="${f1(cy)}" rx="${f1(pr + i * 28)}" ry="${f1((pr + i * 28) * 0.38)}" fill="none" stroke="${p[3]}" stroke-width="1" opacity=".6" transform="rotate(-18 ${f1(cx)} ${f1(cy)})"/>`;
s += `<circle cx="${f1(cx)}" cy="${f1(cy)}" r="${f1(pr)}" fill="${p[2]}"/>`;
s += `<circle cx="${f1(cx + pr * 0.35)}" cy="${f1(cy - pr * 0.2)}" r="${f1(pr)}" fill="${p[0]}" opacity=".35"/>`;
return s;
},
ornament(r, p) {
const cx = W / 2, cy = between(r, 190, 260), n = pick(r, [8, 10, 12, 16]);
let s = `<g transform="translate(${cx} ${f1(cy)})">`;
const len = between(r, 70, 105), wid = between(r, 18, 30);
for (let i = 0; i < n; i++) {
s += `<path d="M0 0 C ${f1(wid)} ${f1(-len * 0.4)} ${f1(wid * 0.5)} ${f1(-len * 0.8)} 0 ${f1(-len)} C ${f1(-wid * 0.5)} ${f1(-len * 0.8)} ${f1(-wid)} ${f1(-len * 0.4)} 0 0 Z" ` +
`fill="${i % 2 ? p[2] : p[3]}" opacity=".85" transform="rotate(${f1(i * 360 / n)})"/>`;
}
s += `<circle r="${f1(between(r, 12, 20))}" fill="${p[1]}"/><circle r="${f1(len + 12)}" fill="none" stroke="${p[2]}" stroke-width="1.5"/></g>`;
s += `<rect x="14" y="14" width="${W - 28}" height="${H - 28}" fill="none" stroke="${p[2]}" stroke-width="1.5" opacity=".8"/>`;
return s;
},
botanik(r, p) {
let s = '';
const n = 5 + Math.floor(r() * 4);
for (let i = 0; i < n; i++) {
const x = between(r, 40, 260), y = between(r, 150, 420), len = between(r, 60, 130), a = between(r, -50, 50);
s += `<g transform="translate(${f1(x)} ${f1(y)}) rotate(${f1(a)})">` +
`<path d="M0 0 C ${f1(len * 0.35)} ${f1(-len * 0.35)} ${f1(len * 0.3)} ${f1(-len * 0.8)} 0 ${f1(-len)} C ${f1(-len * 0.3)} ${f1(-len * 0.8)} ${f1(-len * 0.35)} ${f1(-len * 0.35)} 0 0 Z" fill="${i % 3 ? p[2] : p[3]}" opacity=".8"/>` +
`<line x1="0" y1="0" x2="0" y2="${f1(-len * 0.95)}" stroke="${p[0]}" stroke-width="1.2" opacity=".7"/></g>`;
}
return s;
},
berge(r, p) {
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;
let d = `M0 ${H} L0 ${base}`;
for (let x = 0; x <= W; x += 30) d += ` L${x} ${f1(base - between(r, 10, 90 - i * 15))}`;
d += ` L${W} ${base} L${W} ${H} Z`;
s += `<path d="${d}" fill="${i % 2 ? p[1] : p[2]}" opacity="${f1(0.35 + i * 0.18)}"/>`;
}
return s;
},
zellen(r, p) {
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);
s += `<circle cx="${f1(x)}" cy="${f1(y)}" r="${f1(rad)}" fill="${i % 3 ? p[3] : p[2]}" opacity="${f1(between(r, 0.25, 0.75))}"/>`;
if (i % 4 === 0) s += `<circle cx="${f1(x)}" cy="${f1(y)}" r="${f1(rad * 0.3)}" fill="${p[1]}" opacity=".7"/>`;
}
return s;
},
raster(r, p) {
if (r() < 0.45) { // or a skyline
let s = '';
let x = 10;
while (x < W - 10) {
const w = between(r, 18, 46), h = between(r, 60, 230), v = r();
s += `<rect x="${f1(x)}" y="${f1(H - 20 - h)}" width="${f1(w)}" height="${f1(h)}" fill="${v < 0.15 ? p[2] : v < 0.25 ? p[3] : p[1]}" opacity="${v < 0.25 ? 0.95 : f1(0.12 + v * 0.18)}"/>`;
for (let wy = H - 30 - h + 12; wy < H - 32; wy += 14) {
if (r() < 0.4) s += `<rect x="${f1(x + 5)}" y="${f1(wy)}" width="${f1(Math.max(3, w - 10))}" height="3" fill="${p[0]}" opacity=".5"/>`;
}
x += w + between(r, 2, 6);
}
return s;
}
let s = '';
const cols = 4 + Math.floor(r() * 3), rows = 5 + Math.floor(r() * 3), gap = 6;
const cw = (W - 40 - gap * (cols - 1)) / cols, rh = (H - 200 - gap * (rows - 1)) / rows;
for (let i = 0; i < cols; i++) for (let j = 0; j < rows; j++) {
const v = r();
const fill = v < 0.12 ? p[2] : v < 0.2 ? p[3] : p[1];
s += `<rect x="${f1(20 + i * (cw + gap))}" y="${f1(180 + j * (rh + gap))}" width="${f1(cw)}" height="${f1(rh)}" fill="${fill}" opacity="${v < 0.2 ? 0.95 : f1(0.08 + v * 0.12)}"/>`;
}
return s;
},
schatten(r, p) {
if (r() < 0.5) { // light through a doorway, falling across the floor
const x = between(r, 90, 170), w = between(r, 44, 64), top = between(r, 150, 210), floor = between(r, 330, 360);
return `<rect x="0" y="${f1(floor)}" width="${W}" height="${f1(H - floor)}" fill="${p[3]}" opacity=".25"/>` +
`<rect x="${f1(x)}" y="${f1(top)}" width="${f1(w)}" height="${f1(floor - top)}" fill="${p[1]}" opacity=".9"/>` +
`<path d="M${f1(x)} ${f1(floor)} L${f1(x + w)} ${f1(floor)} L${f1(x + w + between(r, 60, 120))} ${H} L${f1(x - between(r, 10, 40))} ${H} Z" fill="${p[1]}" opacity=".35"/>` +
`<rect x="${f1(x + w * 0.3)}" y="${f1(floor - between(r, 60, 80))}" width="${f1(w * 0.18)}" height="${f1(between(r, 60, 80))}" fill="${p[2]}"/>`;
}
const x = between(r, 60, 240);
return `<rect x="${f1(x)}" y="0" width="${f1(between(r, 3, 8))}" height="${H}" fill="${p[2]}"/>` +
`<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) {
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>` +
`<line x1="20" y1="${f1(between(r, 120, 160))}" x2="${W - 20}" y2="${f1(between(r, 120, 160))}" stroke="${p[1]}" stroke-width="1" opacity=".5"/>`;
},
klassiker(r, p) {
// An all-over pattern (the paper), the plate for the text goes over it.
const kind = Math.floor(r() * 5), sz = pick(r, [18, 22, 26]);
let tile;
if (kind === 0) { // lattice of diamonds
tile = `<path d="M0 ${sz / 2} L${sz / 2} 0 L${sz} ${sz / 2} L${sz / 2} ${sz} Z" fill="none" stroke="${p[2]}" stroke-width="1.4"/>` +
`<circle cx="${sz / 2}" cy="${sz / 2}" r="${f1(sz / 9)}" fill="${p[3]}"/>`;
} else if (kind === 1) { // scales
tile = `<path d="M0 ${sz} A${sz / 2} ${sz / 2} 0 0 1 ${sz} ${sz} M${-sz / 2} ${sz / 2} A${sz / 2} ${sz / 2} 0 0 1 ${sz / 2} ${sz / 2} ` +
`M${sz / 2} ${sz / 2} A${sz / 2} ${sz / 2} 0 0 1 ${sz * 1.5} ${sz / 2}" fill="none" stroke="${p[2]}" stroke-width="1.3"/>`;
} else if (kind === 2) { // dots, offset rows
tile = `<circle cx="${sz / 4}" cy="${sz / 4}" r="${f1(sz / 7)}" fill="${p[2]}"/><circle cx="${sz * 0.75}" cy="${sz * 0.75}" r="${f1(sz / 7)}" fill="${p[3]}"/>`;
} else if (kind === 3) { // little stars
const c = sz / 2, a = sz / 3, b = sz / 10;
tile = `<path d="M${c} ${f1(c - a)} L${f1(c + b)} ${f1(c - b)} L${f1(c + a)} ${c} L${f1(c + b)} ${f1(c + b)} L${c} ${f1(c + a)} L${f1(c - b)} ${f1(c + b)} L${f1(c - a)} ${c} L${f1(c - b)} ${f1(c - b)} Z" fill="${p[2]}"/>` +
`<circle cx="0" cy="0" r="1.6" fill="${p[3]}"/><circle cx="${sz}" cy="0" r="1.6" fill="${p[3]}"/><circle cx="0" cy="${sz}" r="1.6" fill="${p[3]}"/><circle cx="${sz}" cy="${sz}" r="1.6" fill="${p[3]}"/>`;
} else { // stripes with a beading
tile = `<rect x="0" y="0" width="${f1(sz / 3)}" height="${sz}" fill="${p[2]}" opacity=".8"/><circle cx="${f1(sz * 0.67)}" cy="${sz / 2}" r="${f1(sz / 9)}" fill="${p[3]}"/>`;
}
return `<defs><pattern id="pt" width="${sz}" height="${sz}" patternUnits="userSpaceOnUse">${tile}</pattern></defs>` +
`<rect width="${W}" height="${H}" fill="url(#pt)" opacity=".75"/>`;
},
sakral(r, p) {
// A pointed arch with a rose window, nested; light from above.
const cx = W / 2, w = between(r, 170, 210), ys = between(r, 300, 330);
let s = `<rect width="${W}" height="${H}" fill="${p[3]}" opacity=".12"/>`;
for (let i = 0; i < 3; i++) {
const ww = w - i * 34, x0 = cx - ww / 2, x1 = cx + ww / 2, apex = ys - ww * 0.866;
s += `<path d="M${f1(x0)} ${H} L${f1(x0)} ${f1(ys)} A${f1(ww)} ${f1(ww)} 0 0 1 ${f1(cx)} ${f1(apex)} A${f1(ww)} ${f1(ww)} 0 0 1 ${f1(x1)} ${f1(ys)} L${f1(x1)} ${H}" ` +
`fill="${i === 2 ? p[2] : 'none'}" fill-opacity="${i === 2 ? 0.18 : 0}" stroke="${p[2]}" stroke-width="${i ? 1.2 : 2.4}" opacity="${i ? 0.8 : 1}"/>`;
}
const ry = ys - w * 0.3, rr = between(r, 30, 42), n = pick(r, [8, 12, 16]);
s += `<circle cx="${cx}" cy="${f1(ry)}" r="${f1(rr)}" fill="none" stroke="${p[2]}" stroke-width="1.6"/>`;
for (let i = 0; i < n; i++) {
const a = i * Math.PI * 2 / n;
s += `<line x1="${cx}" y1="${f1(ry)}" x2="${f1(cx + Math.cos(a) * rr)}" y2="${f1(ry + Math.sin(a) * rr)}" stroke="${p[2]}" stroke-width="1"/>` +
`<circle cx="${f1(cx + Math.cos(a) * rr * 0.62)}" cy="${f1(ry + Math.sin(a) * rr * 0.62)}" r="${f1(rr * 0.16)}" fill="${i % 2 ? p[3] : p[2]}" opacity=".85"/>`;
}
s += `<line x1="${cx}" y1="${f1(ry + rr)}" x2="${cx}" y2="${H}" stroke="${p[2]}" stroke-width="1.2" opacity=".8"/>`;
return s;
},
grusel(r, p) {
// A bare tree before a big moon.
const tree = p[4] || p[1];
const mx = between(r, 90, 210), my = between(r, 170, 250), mr = between(r, 70, 100);
let s = `<circle cx="${f1(mx)}" cy="${f1(my)}" r="${f1(mr)}" fill="${p[3]}" opacity=".9"/>`;
s += `<circle cx="${f1(mx + mr * 0.3)}" cy="${f1(my - mr * 0.2)}" r="${f1(mr * 0.18)}" fill="${p[0]}" opacity=".08"/>`;
let lines = '';
const branch = (x, y, a, len, w, d) => {
const x2 = x + Math.cos(a) * len, y2 = y + Math.sin(a) * len;
lines += `<line x1="${f1(x)}" y1="${f1(y)}" x2="${f1(x2)}" y2="${f1(y2)}" stroke-width="${f1(w)}"/>`;
if (d <= 0) return;
const spread = between(r, 0.3, 0.6);
branch(x2, y2, a - spread + between(r, -0.15, 0.15), len * between(r, 0.66, 0.8), w * 0.68, d - 1);
branch(x2, y2, a + spread + between(r, -0.15, 0.15), len * between(r, 0.66, 0.8), w * 0.68, d - 1);
};
const tx = between(r, 70, 230);
branch(tx, H - 30, -Math.PI / 2 + between(r, -0.15, 0.15), between(r, 85, 110), 9, 6);
s += `<g stroke="${tree}" stroke-linecap="round">${lines}</g>`;
s += `<path d="M0 ${H} L0 ${H - 34} Q${W / 2} ${f1(H - between(r, 50, 70))} ${W} ${H - 30} L${W} ${H} Z" fill="${tree}"/>`;
s += `<rect x="0" y="${H - 8}" width="${W}" height="8" fill="${p[2]}"/>`;
return s;
},
spiel(r, p) {
// Rolling hills, a sun, a few bright dots.
let s = `<circle cx="${f1(between(r, 70, 230))}" cy="${f1(between(r, 180, 240))}" r="${f1(between(r, 34, 50))}" fill="${p[2]}"/>`;
for (let i = 0; i < 3; i++) {
const cx = between(r, -40, 340), rad = between(r, 140, 220);
s += `<circle cx="${f1(cx)}" cy="${f1(H + rad * 0.45 - i * 30)}" r="${f1(rad)}" fill="${i % 2 ? p[3] : p[1]}" opacity="${f1(0.18 + i * 0.22)}"/>`;
}
for (let i = 0; i < 9; i++) {
s += `<circle cx="${f1(r() * W)}" cy="${f1(between(r, 140, 320))}" r="${f1(between(r, 3, 8))}" fill="${pick(r, [p[2], p[3]])}" opacity=".85"/>`;
}
return s;
},
code(r, p) {
// An editor's minimap: indented lines of tokens, some of them highlighted.
let s = '', indent = 0;
for (let y = 150; y < H - 24; y += 13) {
if (r() < 0.12) continue; // an empty line
indent = Math.max(0, Math.min(4, indent + pick(r, [-1, 0, 0, 1])));
let x = 26 + indent * 16;
const n = 1 + Math.floor(r() * 3);
for (let k = 0; k < n && x < W - 30; k++) {
const w = Math.min(between(r, 14, 70), W - 26 - x), v = r();
s += `<rect x="${f1(x)}" y="${y}" width="${f1(w)}" height="6" rx="3" fill="${v < 0.2 ? p[2] : v < 0.35 ? p[3] : p[1]}" opacity="${v < 0.35 ? 0.95 : 0.3}"/>`;
x += w + 6;
}
}
s += `<rect x="12" y="140" width="3" height="${H - 160}" fill="${p[2]}" opacity=".7"/>`;
return s;
},
bauhaus(r, p) {
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);
const fill = pick(r, [p[2], p[3], p[1]]);
if (kind === 0) s += `<circle cx="${f1(x)}" cy="${f1(y)}" r="${f1(size / 2)}" fill="${fill}" opacity=".9"/>`;
else if (kind === 1) s += `<rect x="${f1(x - size / 2)}" y="${f1(y - size / 2)}" width="${f1(size)}" height="${f1(size)}" fill="${fill}" opacity=".9" transform="rotate(${f1(between(r, -20, 20))} ${f1(x)} ${f1(y)})"/>`;
else s += `<path d="M${f1(x)} ${f1(y - size / 2)} L${f1(x + size / 2)} ${f1(y + size / 2)} L${f1(x - size / 2)} ${f1(y + size / 2)} Z" fill="${fill}" opacity=".9"/>`;
}
return s;
},
};
// ---- the text -------------------------------------------------------------------------------------
function esc(s) {
return String(s).replace(/[&<>"']/g, c => ({'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;'}[c]));
}
// The title's lines, for a size that fits; a word too long is broken.
function lines(text, size, max, em) {
const per = Math.max(4, Math.floor(248 / (size * (em || 0.56))));
const words = String(text).split(/\s+/).filter(Boolean);
const out = [];
let cur = '';
words.forEach(w => {
while (w.length > per) { if (cur) { out.push(cur); cur = ''; } out.push(w.slice(0, per - 1) + '‑'); w = w.slice(per - 1); }
if (!cur) cur = w;
else if ((cur + ' ' + w).length <= per) cur += ' ' + w;
else { out.push(cur); cur = w; }
});
if (cur) out.push(cur);
if (out.length > max) { out.length = max; out[max - 1] = out[max - 1].replace(/\s*\S*$/, '') + ' …'; }
return out;
}
// Only the main title ("Seeing Like a State: How …" → "Seeing Like a State").
function mainTitle(t) {
let s = String(t || '').split(/\s[-:–—•·\/]\s|:\s/)[0].trim();
// "Der Zauberberg. Erster Band" → "Der Zauberberg", but not at an initial ("George R. R. Martin").
const re = /(\S+)\.\s+(?=[A-ZÄÖÜ])/g;
let m;
while ((m = re.exec(s))) {
if (m[1].length >= 3) { s = s.slice(0, m.index + m[1].length); break; }
}
return s.length >= 3 ? s : String(t || '?');
}
function textBlock(r, th, p, book) {
const title = mainTitle(book.title);
const upper = th.upper;
const t = upper ? title.toUpperCase() : title;
const len = t.length;
const longest = t.split(/\s+/).reduce((n, w) => Math.max(n, w.length), 0);
// The longest word stays whole: the type gets smaller rather than the word broken.
// Capitals take more room than small letters (about .72 of the size against .56).
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 lh = size * 1.12;
const layout = pick(r, th.layouts || ['top', 'top', 'bottom', 'plate']);
const author = String(book.author || '').split(/[;,]/)[0].trim();
const blockH = ls.length * lh + (author ? 30 : 0) + 40;
let y0, s = '';
if (layout === 'top') {
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"/>`;
} else {
y0 = 70;
s += `<rect x="18" y="${f1(y0 - 34)}" width="${W - 36}" height="${f1(blockH)}" rx="3" fill="${p[0]}" opacity=".95"/>`;
if (th.frame) s += `<rect x="24" y="${f1(y0 - 28)}" width="${W - 48}" height="${f1(blockH - 12)}" rx="2" fill="none" stroke="${p[1]}" stroke-width="1" opacity=".6"/>`;
}
const x = 26;
ls.forEach((l, i) => {
s += `<text x="${x}" y="${f1(y0 + size * 0.85 + i * lh)}" font-family="${th.font}" font-size="${size}" font-weight="${th.font === SERIF ? 400 : 700}" ` +
`fill="${p[1]}" letter-spacing="${upper ? 0.5 : 0}">${esc(l)}</text>`;
});
if (author) {
// A long name closes up and gets smaller rather than running off the cover.
const spacing = author.length > 22 ? 1 : 2;
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;
}
// ---- the cover ---------------------------------------------------------------------------------------
function gray(hex) {
const n = parseInt(hex.slice(1), 16);
const y = Math.round(0.3 * (n >> 16) + 0.59 * ((n >> 8) & 255) + 0.11 * (n & 255));
const h = (y < 16 ? '0' : '') + y.toString(16);
return '#' + h + h + h;
}
function svg(book, opts) {
const th = theme(book);
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}${textBlock(r, th, p, book)}</svg>`;
}
const cache = new Map();
function url(book, opts) {
const key = (book.title || '') + '|' + (book.author || '') + '|' + (book.shelfTag || '') + '|' +
((book.folders || []).join(',')) + '|' + (opts && opts.gray ? 1 : 0);
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};
})();