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>
This commit is contained in:
parent
caeb67e886
commit
f1158e32a7
3 changed files with 255 additions and 22 deletions
|
|
@ -59,3 +59,25 @@ PDF: page 1), scales it to 240 px wide JPEG, **encrypts it with the user's key**
|
|||
{cover_ct, cover_iv}`. `GET /books/` lists `has_cover`; `GET /books/<id>/cover/`
|
||||
returns `{cover_ct, cover_iv}`. Clients cache the decrypted image locally. The
|
||||
server never sees a cover.
|
||||
|
||||
### Generated covers
|
||||
|
||||
A book without a cover (or one set to `cover: 'generated'` in its meta) gets one
|
||||
drawn by `static/js/gencover.js`: an SVG, the same on every device, never stored.
|
||||
A **concept** (motif, palettes, typeface) is chosen by score:
|
||||
|
||||
- words in the title (3), the shelf category (2) and the author (2, only at a
|
||||
name's start). A stem of five letters or more counts anywhere (German
|
||||
compounds), a shorter one at a word's start, one of three letters as a whole
|
||||
word; `^` / `$` in the list force a word's start / end.
|
||||
- the folders, by name (`FOLDER_THEMES`): the first counts 2.5 — more than an
|
||||
author or shelf word, less than a title word — further ones 1.
|
||||
|
||||
Concepts: konstruktiv (Marx), geometrie (Philosophie: rings, turning squares,
|
||||
triangle/circle/square), psyche (Psychoanalyse: two ellipses, the Borromean
|
||||
knot, a spiral), karte (Geschichte), kosmos (Science-Fiction), ornament
|
||||
(Fantasy), raster (Politik, Soziologie), schatten (Krimi: a red line, a lit
|
||||
doorway), grusel (Horror), klassiker (patterned paper with a title plate),
|
||||
sakral (Religion & Mythologie), spiel (Kinder & Jugend), code (Informatik),
|
||||
typo, wellen, botanik, berge, zellen — and bauhaus when nothing matches. A hash
|
||||
of title and author picks palette, layout and every detail.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
* 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 …); a hash of title and author picks
|
||||
* 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.
|
||||
*
|
||||
|
|
@ -38,7 +40,12 @@
|
|||
const f1 = n => Math.round(n * 10) / 10;
|
||||
|
||||
// ---- the concepts ---------------------------------------------------------------------------
|
||||
// words: stems found anywhere in the text (lower case, umlauts as they are);
|
||||
// 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";
|
||||
|
|
@ -55,28 +62,29 @@
|
|||
{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', 'real', 'voice', 'stimme', 'žižek', 'zizek', 'zupan', 'dolar', 'traum', 'dream',
|
||||
'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'],
|
||||
'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', 'geschichte', 'history', 'empire', 'imperium', 'reich', 'zerstörung', 'destruction', 'wages',
|
||||
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',
|
||||
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',
|
||||
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']]},
|
||||
|
|
@ -85,12 +93,13 @@
|
|||
'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', 'hill', 'landschaft', 'landscape',
|
||||
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', 'gen', 'evolution', 'neuro', 'zelle', 'cell', 'leben', 'life', 'tier', 'animal'],
|
||||
'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',
|
||||
|
|
@ -99,12 +108,49 @@
|
|||
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'],
|
||||
'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'],
|
||||
|
|
@ -114,13 +160,29 @@
|
|||
|
||||
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, a folder only tips the scales.
|
||||
const fields = [[norm(book.title), 3], [norm(book.shelfTag), 2],
|
||||
[norm((book.folders || (book.folder ? [book.folder] : [])).join(' ')), 1], [norm(book.author), 2]];
|
||||
// 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;
|
||||
t.words.forEach(w => fields.forEach(f => { if (f[0].indexOf(w) >= 0) score += f[1]; }));
|
||||
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);
|
||||
}
|
||||
|
|
@ -147,6 +209,29 @@
|
|||
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);
|
||||
|
|
@ -158,6 +243,27 @@
|
|||
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);
|
||||
|
|
@ -271,6 +377,13 @@
|
|||
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"/>`;
|
||||
|
|
@ -281,6 +394,98 @@
|
|||
`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++) {
|
||||
|
|
@ -301,8 +506,8 @@
|
|||
}
|
||||
|
||||
// The title's lines, for a size that fits; a word too long is broken.
|
||||
function lines(text, size, max) {
|
||||
const per = Math.max(4, Math.floor(250 / (size * 0.56)));
|
||||
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 = '';
|
||||
|
|
@ -336,11 +541,13 @@
|
|||
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.
|
||||
const size = Math.max(15, Math.min(len <= 10 ? 40 : len <= 18 ? 34 : len <= 30 ? 28 : len <= 48 ? 23 : 19,
|
||||
Math.floor(250 / (longest * 0.58))));
|
||||
const ls = lines(t, size, 5);
|
||||
// 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, ['top', 'top', 'bottom', 'plate']);
|
||||
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 = '';
|
||||
|
|
@ -353,6 +560,7 @@
|
|||
} 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) => {
|
||||
|
|
@ -360,7 +568,10 @@
|
|||
`fill="${p[1]}" letter-spacing="${upper ? 0.5 : 0}">${esc(l)}</text>`;
|
||||
});
|
||||
if (author) {
|
||||
s += `<text x="${x}" y="${f1(y0 + ls.length * lh + 22)}" font-family="${SANS}" font-size="12" letter-spacing="2" fill="${p[2]}">${esc(author.toUpperCase())}</text>`;
|
||||
// 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* diora service worker — caches the app shell for offline use.
|
||||
*/
|
||||
|
||||
const CACHE = 'diora-v123';
|
||||
const CACHE = 'diora-v124';
|
||||
const SHELL = [
|
||||
'/static/css/app.css',
|
||||
'/static/js/app/01-core.js',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue