/* * 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 …); a hash of title and author picks * the palette, the layout and every detail of the motif, so no two books look alike and * each always looks the same. Nothing is stored or sent: it's an SVG, made when needed. * * DioraGenCover.url(book, {gray}) a data: URL (book: {title, author, shelfTag, folders}) * DioraGenCover.theme(book) the concept chosen (for tests) * * Chrome-61-safe: no ?. / ??. */ (function () { 'use strict'; const W = 300, H = 450; // ---- chance, the same for the same book ------------------------------------------------ function hash(s) { let h = 2166136261; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; } function rng(seed) { let a = seed >>> 0; return function () { a = (a + 0x6D2B79F5) >>> 0; let t = a; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } const pick = (r, list) => list[Math.floor(r() * list.length)]; const between = (r, a, b) => a + r() * (b - a); const f1 = n => Math.round(n * 10) / 10; // ---- the concepts --------------------------------------------------------------------------- // words: stems found anywhere in the text (lower case, umlauts as they are); // palettes: [background, ink, accent, second accent], curated to sit well together. const SERIF = "Georgia, 'Times New Roman', serif"; const SANS = "'Helvetica Neue', Helvetica, Arial, sans-serif"; const NARROW = "'Arial Narrow', 'Roboto Condensed', 'Helvetica Neue', Arial, sans-serif"; const MONO = "'Courier New', Courier, monospace"; const THEMES = [ {id: 'konstruktiv', font: NARROW, upper: true, words: ['marx', 'kapital', 'capital', 'arbeit', 'labor', 'labour', 'revolution', 'sozialis', 'socialis', 'kommun', 'communis', 'ökonom', 'econom', 'klasse', 'class', 'gewerkschaft', 'streik', 'strike', 'partei', 'lenin', 'luxemburg', 'engels', 'proletar', 'faschis', 'fascis', 'jacobin', 'gegenstandpunkt', 'demokratie', 'herrschaft', 'massen'], palettes: [['#efe6d2', '#1b1b1b', '#c8322b', '#1b1b1b'], ['#1d1d1d', '#f2ead7', '#d23a2c', '#e8b23a'], ['#e9dfc8', '#22252a', '#b5302a', '#7a8a8f']]}, {id: 'geometrie', font: SERIF, words: ['philosoph', 'hegel', 'kant', 'dialekt', 'dialect', 'vernunft', 'reason', 'geist', 'spirit', 'ethik', 'ethic', 'logik', 'logic', 'metaphys', 'phänomen', 'phenomen', 'denken', 'wahrheit', 'truth', 'sein', 'nichts', 'nothing', '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', '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'], 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', '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', 'hill', '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'], 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'], 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']]}, ]; 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(); } 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]]; return THEMES.map(t => { let score = 0; t.words.forEach(w => fields.forEach(f => { if (f[0].indexOf(w) >= 0) score += f[1]; })); return {t, score}; }).filter(x => x.score > 0).sort((a, b) => b.score - a.score); } function theme(book) { const s = scored(book); return s.length ? s[0].t : DEFAULT; } // ---- motifs: each draws into the whole cover, the text gets its own space later ----------- const M = { konstruktiv(r, p) { let s = ''; const a = pick(r, [-34, -28, 28, 34, -45]); s += ``; for (let i = 0; i < 3; i++) { const y = between(r, 60, 380), h = between(r, 14, 46); s += ``; } s += ''; s += ``; s += ``; return s; }, geometrie(r, p) { 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 += ``; const phi = 1.618; s += ``; s += ``; s += ``; return s; }, psyche(r, p) { let s = ``; const cy = between(r, 170, 290); const rx = between(r, 60, 95), ry = between(r, 80, 130); s += ``; s += ``; s += ``; return s; }, wellen(r, p) { let s = ''; const n = 6 + Math.floor(r() * 5); const amp = between(r, 6, 16), len = between(r, 60, 120); for (let i = 0; i < n; i++) { const y0 = 120 + i * (300 / n); let d = `M0 ${f1(y0)}`; for (let x = 0; x <= W; x += 10) d += ` L${x} ${f1(y0 + Math.sin((x / len) * Math.PI * 2 + i * 0.7) * amp)}`; d += ` L${W} ${H} L0 ${H} Z`; s += ``; } return s; }, karte(r, p) { let s = ''; const cx = between(r, 80, 220), cy = between(r, 160, 320); for (let i = 1; i <= 9; i++) { let d = ''; for (let k = 0; k <= 36; k++) { const a = (k / 36) * Math.PI * 2; const rad = i * 15 * (1 + 0.18 * Math.sin(a * 3 + i) + 0.1 * Math.cos(a * 5 + r())); d += (k ? ' L' : 'M') + f1(cx + Math.cos(a) * rad) + ' ' + f1(cy + Math.sin(a) * rad * 1.15); } s += ``; } s += ``; return s; }, kosmos(r, p) { let s = ''; for (let i = 0; i < 70; i++) s += ``; const cx = between(r, 90, 210), cy = between(r, 170, 290), pr = between(r, 30, 55); for (let i = 1; i <= 3; i++) s += ``; s += ``; s += ``; return s; }, ornament(r, p) { const cx = W / 2, cy = between(r, 190, 260), n = pick(r, [8, 10, 12, 16]); let s = ``; const len = between(r, 70, 105), wid = between(r, 18, 30); for (let i = 0; i < n; i++) { s += ``; } s += ``; s += ``; return s; }, botanik(r, p) { let s = ''; const n = 5 + Math.floor(r() * 4); for (let i = 0; i < n; i++) { const x = between(r, 40, 260), y = between(r, 150, 420), len = between(r, 60, 130), a = between(r, -50, 50); s += `` + `` + ``; } return s; }, berge(r, p) { let s = ``; 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 += ``; } return s; }, zellen(r, p) { let s = ''; for (let i = 0; i < 16; i++) { const x = between(r, 20, 280), y = between(r, 140, 430), rad = between(r, 12, 42); s += ``; if (i % 4 === 0) s += ``; } 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 += ``; for (let wy = H - 30 - h + 12; wy < H - 32; wy += 14) { if (r() < 0.4) s += ``; } 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 += ``; } return s; }, schatten(r, p) { const x = between(r, 60, 240); return `` + ``; }, typo(r, p, book) { const ch = (book.title || '?').replace(/^[^A-Za-zÄÖÜäöüß0-9]+/, '').charAt(0).toUpperCase() || '?'; return `${esc(ch)}` + ``; }, bauhaus(r, p) { let s = ''; for (let i = 0; i < 4; i++) { const kind = Math.floor(r() * 3), x = between(r, 30, 270), y = between(r, 150, 410), size = between(r, 40, 110); const fill = pick(r, [p[2], p[3], p[1]]); if (kind === 0) s += ``; else if (kind === 1) s += ``; else s += ``; } return s; }, }; // ---- the text ------------------------------------------------------------------------------------- function esc(s) { return String(s).replace(/[&<>"']/g, c => ({'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}[c])); } // 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))); const words = String(text).split(/\s+/).filter(Boolean); const out = []; let cur = ''; words.forEach(w => { while (w.length > per) { if (cur) { out.push(cur); cur = ''; } out.push(w.slice(0, per - 1) + '‑'); w = w.slice(per - 1); } if (!cur) cur = w; else if ((cur + ' ' + w).length <= per) cur += ' ' + w; else { out.push(cur); cur = w; } }); if (cur) out.push(cur); if (out.length > max) { out.length = max; out[max - 1] = out[max - 1].replace(/\s*\S*$/, '') + ' …'; } return out; } // Only the main title ("Seeing Like a State: How …" → "Seeing Like a State"). function mainTitle(t) { let s = String(t || '').split(/\s[-:–—•·\/]\s|:\s/)[0].trim(); // "Der Zauberberg. Erster Band" → "Der Zauberberg", but not at an initial ("George R. R. Martin"). const re = /(\S+)\.\s+(?=[A-ZÄÖÜ])/g; let m; while ((m = re.exec(s))) { if (m[1].length >= 3) { s = s.slice(0, m.index + m[1].length); break; } } return s.length >= 3 ? s : String(t || '?'); } function textBlock(r, th, p, book) { const title = mainTitle(book.title); const upper = th.upper; const t = upper ? title.toUpperCase() : title; const len = t.length; const longest = t.split(/\s+/).reduce((n, w) => Math.max(n, w.length), 0); // The longest word stays whole: the type gets smaller rather than the word broken. 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); const lh = size * 1.12; const layout = pick(r, ['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 += ``; } else if (layout === 'bottom') { y0 = H - blockH + 20; s += ``; } else { y0 = 70; s += ``; } const x = 26; ls.forEach((l, i) => { s += `${esc(l)}`; }); if (author) { s += `${esc(author.toUpperCase())}`; } return s; } // ---- the cover --------------------------------------------------------------------------------------- function gray(hex) { const n = parseInt(hex.slice(1), 16); const y = Math.round(0.3 * (n >> 16) + 0.59 * ((n >> 8) & 255) + 0.11 * (n & 255)); const h = (y < 16 ? '0' : '') + y.toString(16); return '#' + h + h + h; } function svg(book, opts) { const th = theme(book); const r = rng(hash(norm(book.title) + '|' + norm(book.author))); let p = pick(r, th.palettes); if (opts && opts.gray) p = p.map(gray); const motif = (M[th.id] || M.bauhaus)(r, p, book); return `` + `${motif}${textBlock(r, th, p, book)}`; } const cache = new Map(); function url(book, opts) { const key = (book.title || '') + '|' + (book.author || '') + '|' + (book.shelfTag || '') + '|' + ((book.folders || []).join(',')) + '|' + (opts && opts.gray ? 1 : 0); if (!cache.has(key)) cache.set(key, 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg(book, opts))); return cache.get(key); } window.DioraGenCover = {url, svg, theme: book => theme(book).id}; })();