| 1 | import { avatar, recipe, FAMILIES, VERSION } from '../src/index.js' ; |
| 2 | |
| 3 | const $ = selector => document.querySelector(selector); |
| 4 | const params = new URL(location.href).searchParams; |
| 5 | let seed = params.get('seed' ) ?? 'seedmark' ; |
| 6 | let selected = params.get('selected' ) ?? seed; |
| 7 | let batch = 0, samples = [], view = 'gallery' ; |
| 8 | $('#seed' ).value = seed; |
| 9 | for (const family of FAMILIES) { |
| 10 | const option = document.createElement('option' ); option.value = family; option.textContent = family; $('#family' ).append(option); |
| 11 | } |
| 12 | function image(text, size) { |
| 13 | const el = document.createElement('span' ); el.className = 'avatar' ; |
| 14 | el.style.width = `${size}px` ; el.style.height = `${size}px` ; |
| 15 | el.innerHTML = avatar(text, { size, version: VERSION }); |
| 16 | el.setAttribute('aria-hidden' , 'true' ); return el; |
| 17 | } |
| 18 | function syncURL() { |
| 19 | const url = new URL(location.href); url.searchParams.set('seed' , seed); |
| 20 | if (selected !== seed) url.searchParams.set('selected' , selected); else url.searchParams.delete('selected' ); |
| 21 | try { history.replaceState(null, '' , url); } catch {} |
| 22 | } |
| 23 | function colors(value) { |
| 24 | if (typeof value === 'string' ) return /^#[0-9a-f]{3,8}$/i.test(value) ? [value] : []; |
| 25 | if (!value || typeof value !== 'object' ) return []; |
| 26 | return Object.values(value).flatMap(colors); |
| 27 | } |
| 28 | function select(text) { |
| 29 | selected = text; const r = recipe(text, { version: VERSION }); |
| 30 | $('#selected' ).innerHTML = avatar(text, { size: 164, version: VERSION }); |
| 31 | $('#selected-seed' ).textContent = text === '' ? '(empty string)' : text; |
| 32 | $('#selected-family' ).textContent = r.family; $('#selected-version' ).textContent = String(r.version); |
| 33 | $('#recipe' ).textContent = JSON.stringify(r, null, 2); |
| 34 | $('#scale-preview' ).replaceChildren(...[24, 32, 48, 64].map(size => { |
| 35 | const item = document.createElement('div' ); item.className = 'scale-item' ; |
| 36 | const label = document.createElement('small' ); label.textContent = `${size}` ; |
| 37 | item.append(image(text, size), label); return item; |
| 38 | })); |
| 39 | $('#palette' ).replaceChildren(...[...new Set(colors(r.palette))].map(color => { |
| 40 | const swatch = document.createElement('span' ); swatch.style.backgroundColor = color; swatch.title = color; return swatch; |
| 41 | })); |
| 42 | for (const button of document.querySelectorAll('.tile' )) button.setAttribute('aria-pressed' , button.dataset.seed === selected); |
| 43 | $('#status' ).textContent = '' ; syncURL(); |
| 44 | } |
| 45 | function makeSamples() { |
| 46 | samples = Array.from({ length: 128 }, (_, i) => { |
| 47 | const text = i === 0 && batch === 0 ? seed : `${seed}/${batch}:${i}` ; |
| 48 | return { seed: text, family: recipe(text, { version: VERSION }).family }; |
| 49 | }); |
| 50 | } |
| 51 | function drawGallery() { |
| 52 | const family = $('#family' ).value, size = Number($('#size' ).value); |
| 53 | const filtered = samples.filter(s => family === 'all' || s.family === family); |
| 54 | $('#gallery' ).style.setProperty('--size' , `${size}px` ); |
| 55 | $('#gallery' ).replaceChildren(...filtered.map(s => { |
| 56 | const button = document.createElement('button' ); button.className = 'tile' ; button.dataset.seed = s.seed; |
| 57 | button.title = `${s.family}: ${s.seed}` ; button.setAttribute('aria-label' , `Select ${s.family} avatar for ${s.seed || 'empty seed'}` ); |
| 58 | button.setAttribute('aria-pressed' , s.seed === selected); |
| 59 | const code = document.createElement('code' ); code.textContent = s.seed || '(empty)' ; |
| 60 | button.append(image(s.seed, size), code); button.onclick = () => select(s.seed); return button; |
| 61 | })); |
| 62 | $('#batch-note' ).textContent = `${filtered.length} avatars / batch ${batch + 1}` ; |
| 63 | } |
| 64 | const lines = [ |
| 65 | [0, 'Anyone tried the new avatar module yet?' ], |
| 66 | [1, 'This is me. Apparently I’m a very specific arrangement of shapes.' ], |
| 67 | [2, 'I found you in the gallery before I read your name.' ], |
| 68 | [0, 'That’s the test. Same person, every time.' ], |
| 69 | [3, 'Checking how these hold up at chat size.' ], |
| 70 | [1, 'Try the circle crop too.' ], |
| 71 | [2, 'And light mode against dark mode.' ], |
| 72 | [3, 'The actual seed can be any text.' ], |
| 73 | [0, 'No initials required.' ] |
| 74 | ]; |
| 75 | function drawChat() { |
| 76 | const names = ['Rowan' , 'Alex' , 'Kit' , 'Sol' ]; |
| 77 | const label = document.createElement('p' ); label.className = 'chat-label' ; label.textContent = 'Example conversation' ; |
| 78 | $('#chat' ).replaceChildren(label, ...lines.map(([person, text], index) => { |
| 79 | const identity = samples[person]; const row = document.createElement('article' ); row.className = 'message' ; |
| 80 | const button = document.createElement('button' ); button.setAttribute('aria-label' , `Inspect ${names[person]}'s avatar` ); |
| 81 | button.append(image(identity.seed, 40)); button.onclick = () => select(identity.seed); |
| 82 | const body = document.createElement('div' ), name = document.createElement('h3' ), time = document.createElement('time' ), p = document.createElement('p' ); |
| 83 | name.textContent = names[person]; time.textContent = `12:${String(index + 10).padStart(2, '0')}` ; name.append(time); p.textContent = text; |
| 84 | body.append(name, p); row.append(button, body); return row; |
| 85 | })); |
| 86 | } |
| 87 | function setView(next) { |
| 88 | view = next; $('#gallery' ).hidden = next !== 'gallery' ; $('#chat' ).hidden = next !== 'chat' ; |
| 89 | $('#grid-tab' ).setAttribute('aria-pressed' , next === 'gallery' ); $('#chat-tab' ).setAttribute('aria-pressed' , next === 'chat' ); |
| 90 | $('#family' ).disabled = next === 'chat' ; $('#size' ).disabled = next === 'chat' ; |
| 91 | } |
| 92 | $('#seed-form' ).onsubmit = event => { event.preventDefault(); seed = $('#seed' ).value; batch = 0; makeSamples(); drawGallery(); drawChat(); select(seed); }; |
| 93 | $('#shuffle' ).onclick = () => { |
| 94 | const bytes = crypto.getRandomValues(new Uint8Array(12)); |
| 95 | $('#seed' ).value = [...bytes].map(n => n.toString(16).padStart(2, '0' )).join('' ); |
| 96 | $('#seed-form' ).requestSubmit(); |
| 97 | }; |
| 98 | $('#family' ).onchange = drawGallery; $('#size' ).onchange = drawGallery; |
| 99 | $('#crop' ).onchange = () => document.body.classList.toggle('circle' , $('#crop' ).value === 'circle' ); |
| 100 | $('#more' ).onclick = () => { batch++; makeSamples(); drawGallery(); drawChat(); }; |
| 101 | $('#grid-tab' ).onclick = () => setView('gallery' ); $('#chat-tab' ).onclick = () => setView('chat' ); |
| 102 | $('#theme' ).onclick = () => document.body.classList.toggle('dark' ); |
| 103 | async function copy(text, success) { |
| 104 | try { await navigator.clipboard.writeText(text); $('#status' ).textContent = success; } |
| 105 | catch { $('#status' ).textContent = 'Clipboard unavailable. Download the SVG, or copy the seed from above.' ; } |
| 106 | } |
| 107 | $('#copy-svg' ).onclick = () => copy(avatar(selected, { size: 256, version: VERSION }), 'SVG copied.' ); |
| 108 | $('#copy-link' ).onclick = () => { syncURL(); copy(location.href, 'Seed link copied.' ); }; |
| 109 | $('#download-svg' ).onclick = () => { |
| 110 | const url = URL.createObjectURL(new Blob([avatar(selected, { size: 256, version: VERSION })], { type: 'image/svg+xml' })); |
| 111 | const a = document.createElement('a' ); a.href = url; a.download = 'seedmark.svg' ; a.click(); |
| 112 | setTimeout(() => URL.revokeObjectURL(url), 1000); $('#status' ).textContent = 'SVG downloaded.' ; |
| 113 | }; |
| 114 | makeSamples(); drawGallery(); drawChat(); select(selected); |
| 115 | Object.defineProperty(window, '__seedmark' , { value: Object.freeze({ snapshot: () => Object.freeze({ seed, selected, batch, view, family: recipe(selected).family, count: document.querySelectorAll('.tile' ).length, version: VERSION }) }) }); |