diora-web/static/js/gencover.js
mrwnslz 89827086ba
Some checks failed
Build and push Docker image / build (push) Failing after 0s
Test / test (push) Failing after 0s
Test / e2e (push) Failing after 0s
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>
2026-10-25 12:39:57 +01:00

991 lines
67 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, 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 ?. / ??.
*/
(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 = [
// The "blue volumes": only the works of Marx and Engels (see theme()), bound in blue cloth
// with gold, the spine on the left. It sets its own text (ownText), a volume label when the
// book has a number in a series.
{id: 'blau', font: SERIF, ownText: true, words: [],
palettes: [['#22305e', '#d4b25c', '#b89a52', '#161f40'], ['#1f3554', '#cfae5e', '#a98f55', '#14233a'],
['#1b2747', '#dcbf73', '#b29a62', '#111a33']]},
{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']]},
// 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'],
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'], ['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'],
['#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);
}
// A work of Marx or Engels (by its author, or a volume of the MEW) is a blue volume,
// whatever else the words say; nothing else is.
const MARX_ENGELS = /(^|[^a-zäöü])(marx|engels)($|[^a-zäöü])/i;
const BLAU = THEMES.filter(t => t.id === 'blau')[0];
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 : opts && opts.podcast ? byId('funk') : 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) {
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);
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) {
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++) {
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) {
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);
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) {
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++) {
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) {
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;
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) {
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);
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) {
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>` +
`<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"/>`;
},
blau(r, p, book) {
const gold = p[1], dim = p[2], deep = p[3];
// Cloth: a fine weave, light from the upper left, the hinge along the spine.
let s = `<defs><pattern id="wv" width="3" height="3" patternUnits="userSpaceOnUse">` +
`<path d="M0 .5H3" stroke="${deep}" stroke-width="1" opacity=".5"/><path d="M.5 0V3" stroke="${gold}" stroke-width=".6" opacity=".06"/></pattern>` +
`<radialGradient id="lt" cx="30%" cy="18%" r="85%"><stop offset="0" stop-color="#ffffff" stop-opacity=".09"/>` +
`<stop offset="1" stop-color="#ffffff" stop-opacity="0"/></radialGradient></defs>` +
`<rect width="${W}" height="${H}" fill="url(#wv)"/><rect width="${W}" height="${H}" fill="url(#lt)"/>`;
// The spine, as on the shelf: gold bands top and bottom, a field with the volume, a groove at the hinge.
const SP = 46;
s += `<rect x="0" y="0" width="${SP}" height="${H}" fill="${deep}" opacity=".55"/>` +
`<rect x="${SP}" y="0" width="3" height="${H}" fill="${deep}"/><rect x="${SP + 3}" y="0" width="1" height="${H}" fill="${gold}" opacity=".16"/>`;
[[26, 1.6], [31, .7], [58, .7], [63, 1.6], [H - 68, 1.6], [H - 63, .7], [H - 34, .7], [H - 29, 1.6]].forEach(b => {
s += `<rect x="5" y="${b[0]}" width="${SP - 10}" height="${b[1]}" fill="${gold}"/>`;
});
s += `<path d="M${SP / 2} 40 l4 5 -4 5 -4 -5Z" fill="${gold}"/>`;
const vol = typeof book.seriesIndex === 'number' ? String(book.seriesIndex) : '';
s += `<rect x="8" y="104" width="${SP - 16}" height="44" fill="none" stroke="${gold}" stroke-width="1"/>`;
if (vol) s += `<text x="${SP / 2}" y="132" text-anchor="middle" font-family="${SERIF}" font-size="${vol.length > 3 ? 10 : 14}" fill="${gold}">${esc(vol)}</text>`;
else s += `<path d="M${SP / 2} 118 l5 6 -5 6 -5 -6Z" fill="${gold}"/>`;
s += `<rect x="12" y="170" width="${SP - 24}" height=".7" fill="${gold}" opacity=".7"/><rect x="12" y="${H - 104}" width="${SP - 24}" height=".7" fill="${gold}" opacity=".7"/>`;
const x0 = SP + 22, x1 = W - 22;
const rule = y => `<rect x="${x0}" y="${y}" width="${x1 - x0}" height="1.6" fill="${gold}"/>` +
`<rect x="${x0}" y="${y + 5}" width="${x1 - x0}" height=".8" fill="${gold}" opacity=".8"/>`;
s += rule(52) + rule(H - 64);
const cx = (x0 + x1) / 2;
const line = (t, y, size, o) => `<text x="${cx}" y="${f1(y)}" text-anchor="middle" font-family="${o && o.sans ? SANS : SERIF}" ` +
`font-size="${f1(size)}" letter-spacing="${o && o.ls != null ? o.ls : 1}" fill="${o && o.dim ? dim : gold}">${esc(t)}</text>`;
const caps = t => String(t || '').toUpperCase();
const fit = (t, max, lsp) => Math.max(7, Math.min(max, Math.floor(((x1 - x0) / Math.max(1, t.length) - lsp) / 0.72)));
const em = 0.74 * 248 / (x1 - x0); // lines() packs into 248 px; the front is narrower
const ix = typeof book.seriesIndex === 'number' ? book.seriesIndex : null;
const werke = /\bmew\b|marx.engels|werke/i.test(book.series || '');
const label = (y) => {
const n = String(ix);
return `<rect x="${f1(cx - 58)}" y="${y}" width="116" height="70" fill="none" stroke="${gold}" stroke-width="1.4"/>` +
`<rect x="${f1(cx - 54)}" y="${y + 4}" width="108" height="62" fill="none" stroke="${gold}" stroke-width=".6" opacity=".7"/>` +
line('BAND', y + 22, 10, {ls: 3, sans: true}) + line(n, y + 56, n.length > 3 ? 26 : 32, {ls: 1});
};
if (werke) {
const a = 'KARL MARX · FRIEDRICH ENGELS';
s += line(a, 92, fit(a, 11, 2), {ls: 2, sans: true});
s += line('WERKE', 160, 34, {ls: 8});
if (ix != null) s += label(190);
const t = caps(mainTitle(book.title));
const ls = lines(t, 14, 4, em);
ls.forEach((l, i) => { s += line(l, (ix != null ? 300 : 230) + i * 18, 14, {ls: 1, dim: true}); });
return s;
}
const author = caps(String(book.author || '').split(';')[0].replace(/\s*,\s*/g, ' · '));
if (author) s += line(author, 96, fit(author, 12, 2), {ls: 2, sans: true});
const t = caps(mainTitle(book.title));
const longest = t.split(/\s+/).reduce((n, w) => Math.max(n, w.length), 0);
const size = Math.max(14, Math.min(t.length <= 12 ? 28 : t.length <= 24 ? 24 : t.length <= 40 ? 20 : 17,
Math.floor((x1 - x0) / (longest * 0.74))));
const ls = lines(t, size, 5, em);
const lh = size * 1.2, h = ls.length * lh;
const y0 = (ix != null ? 200 : 228) - h / 2 + size * 0.8;
ls.forEach((l, i) => { s += line(l, y0 + i * lh, size, {ls: 1}); });
if (ix != null) s += label(296);
return s;
},
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;
},
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);
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 || '?');
}
// 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;
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, 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();
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>`;
}
// 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 ---------------------------------------------------------------------------------------
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;
}
// 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 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);
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.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, opts) => theme(book, opts).id};
})();