| 1 | import {render} from './render.js' ; |
| 2 | |
| 3 | export const VERSION = 1; |
| 4 | // Ordering is part of the versioned visual contract. |
| 5 | export const FAMILIES = Object.freeze(['masks' , 'creatures' , 'botanical' , 'orbital' , 'landscapes' , 'glyphs' , 'woven' , 'machines' ]); |
| 6 | const PALETTES = [ |
| 7 | ['#142d36' , '#f5dfad' , '#ed7351' , '#65c4ae' ], |
| 8 | ['#24223c' , '#f7dfbf' , '#a89cfc' , '#f7788b' ], |
| 9 | ['#163b32' , '#f4eacb' , '#b4cf66' , '#f69c64' ], |
| 10 | ['#3a2330' , '#ffe5b9' , '#ef7689' , '#8dceba' ], |
| 11 | ['#1d304c' , '#e8eff0' , '#f4b855' , '#72b9d0' ], |
| 12 | ['#422d26' , '#f8e8c8' , '#e8a04d' , '#72afa0' ], |
| 13 | ['#283526' , '#f4edcf' , '#d3c65e' , '#ed8863' ], |
| 14 | ['#292745' , '#efe8f5' , '#d89ada' , '#8fcabc' ], |
| 15 | ['#18343c' , '#f8e4d0' , '#f88c66' , '#a7cee0' ], |
| 16 | ['#393127' , '#f4ebd8' , '#adb77a' , '#da8c6f' ], |
| 17 | ['#2c2440' , '#fbe8c2' , '#f2b54f' , '#b2a1e2' ], |
| 18 | ['#1b3537' , '#e9f0d7' , '#79c7b8' , '#e5a4aa' ] |
| 19 | ]; |
| 20 | function freeze(value) { |
| 21 | for (const child of Object.values(value)) if (child && typeof child === 'object' ) freeze(child); |
| 22 | return Object.freeze(value); |
| 23 | } |
| 24 | export function recipe(seed, {version = VERSION} = {}) { |
| 25 | if (version !== VERSION) throw new RangeError('unsupported visual version' ); |
| 26 | const stream = name => createRng(seed, name); |
| 27 | const layout = stream('layout' ), silhouette = stream('silhouette' ); |
| 28 | const features = stream('features' ), texture = stream('texture' ); |
| 29 | return freeze({ |
| 30 | version, |
| 31 | family: stream('family' ).pick(FAMILIES), |
| 32 | palette: [...stream('palette' ).pick(PALETTES)], |
| 33 | layout: {variant: layout.int(0, 3), flip: layout.int(0, 1), tilt: layout.pick([-12, 0, 12])}, |
| 34 | silhouette: {variant: silhouette.int(0, 3), width: silhouette.int(26, 38), height: silhouette.int(28, 42)}, |
| 35 | features: {variant: features.int(0, 3), count: features.int(2, 5), accent: features.int(0, 1)}, |
| 36 | texture: {variant: texture.int(0, 2), marks: texture.int(2, 4)} |
| 37 | }); |
| 38 | } |
| 39 | |
| 40 | export function avatar(seed, {size = 64, version = VERSION} = {}) { |
| 41 | if (!Number.isInteger(size) || size < 1 || size > 4096) throw new RangeError('size must be an integer from 1 to 4096' ); |
| 42 | const r = recipe(seed, {version}); |
| 43 | return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 64 64"><rect width="64" height="64" fill="${r.palette[0]}"/>${render(r)}</svg>` ; |
| 44 | } |
| 45 | |
| 46 | // Version 1: length-framed UTF-16 input, cyrb128 mixing, sfc32 stream. |
| 47 | // Noncryptographic. Every call owns its own four-word state. |
| 48 | function hash(text) { |
| 49 | let a = 1779033703, b = 3144134277, c = 1013904242, d = 2773480762; |
| 50 | for (let i = 0; i < text.length; i++) { |
| 51 | const k = text.charCodeAt(i); |
| 52 | a = b ^ Math.imul(a ^ k, 597399067); |
| 53 | b = c ^ Math.imul(b ^ k, 2869860233); |
| 54 | c = d ^ Math.imul(c ^ k, 951274213); |
| 55 | d = a ^ Math.imul(d ^ k, 2716044179); |
| 56 | } |
| 57 | a = Math.imul(c ^ (a >>> 18), 597399067); |
| 58 | b = Math.imul(d ^ (b >>> 22), 2869860233); |
| 59 | c = Math.imul(a ^ (c >>> 17), 951274213); |
| 60 | d = Math.imul(b ^ (d >>> 19), 2716044179); |
| 61 | return [(a ^ b ^ c ^ d) >>> 0, (b ^ a) >>> 0, (c ^ a) >>> 0, (d ^ a) >>> 0]; |
| 62 | } |
| 63 | const frame = (...parts) => parts.map(s => `${s.length}:${s}` ).join('' ); |
| 64 | export function createRng(seed, namespace = 'default' ) { |
| 65 | if (typeof seed !== 'string' ) throw new TypeError('seed must be a string' ); |
| 66 | if (typeof namespace !== 'string' ) throw new TypeError('namespace must be a string' ); |
| 67 | let [a, b, c, d] = hash(frame('seedmark' , '1' , seed, namespace)); |
| 68 | const rng = { |
| 69 | next() { |
| 70 | const t = ((a + b | 0) + d | 0); |
| 71 | d = d + 1 | 0; |
| 72 | a = b ^ b >>> 9; |
| 73 | b = c + (c << 3) | 0; |
| 74 | c = (c << 21 | c >>> 11) + t | 0; |
| 75 | return (t >>> 0) / 4294967296; |
| 76 | }, |
| 77 | int(min, max) { |
| 78 | const span = max - min + 1; |
| 79 | if (!Number.isSafeInteger(min) || !Number.isSafeInteger(max) || min > max || !Number.isSafeInteger(span)) { |
| 80 | throw new RangeError('integer bounds must be ordered safe integers with a safe span' ); |
| 81 | } |
| 82 | return min + Math.floor(rng.next() * span); |
| 83 | }, |
| 84 | pick(array) { |
| 85 | if (!Array.isArray(array)) throw new TypeError('pick expects an array' ); |
| 86 | if (!array.length) throw new RangeError('cannot pick from an empty array' ); |
| 87 | return array[rng.int(0, array.length - 1)]; |
| 88 | } |
| 89 | }; |
| 90 | return rng; |
| 91 | } |