my pkgs monorepo
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(noise-avatar): new package for deterministic value-noise avatar generation

ewancroft.uk f344105c 2c4761ad

verified
+291
+67
packages/noise-avatar/README.md
··· 1 + # @ewanc26/noise-avatar 2 + 3 + Deterministic value-noise avatar generation from a string seed. Zero runtime dependencies, works in any environment with a Canvas API (browsers, jsdom). 4 + 5 + Part of the [`@ewanc26/pkgs`](https://github.com/ewanc26/pkgs) monorepo. 6 + 7 + ## Install 8 + 9 + ```bash 10 + pnpm add @ewanc26/noise-avatar 11 + ``` 12 + 13 + ## Usage 14 + 15 + ### Vanilla 16 + 17 + ```ts 18 + import { renderNoiseAvatar } from '@ewanc26/noise-avatar'; 19 + 20 + const canvas = document.querySelector('canvas'); 21 + renderNoiseAvatar(canvas, 'Alice|Subscription'); 22 + ``` 23 + 24 + ### Svelte action 25 + 26 + ```svelte 27 + <script> 28 + import { noiseAvatarAction } from '@ewanc26/noise-avatar'; 29 + let seed = 'Alice|Subscription'; 30 + </script> 31 + 32 + <canvas use:noiseAvatarAction={seed} class="rounded-full"></canvas> 33 + ``` 34 + 35 + ## API 36 + 37 + ### `renderNoiseAvatar(canvas, seed, options?)` 38 + 39 + Renders a deterministic value-noise texture onto `canvas`. Resizes the canvas to `options.displaySize` (default `64`). 40 + 41 + | Option | Type | Default | Description | 42 + |---|---|---|---| 43 + | `gridSize` | `number` | `5` | Noise grid resolution | 44 + | `displaySize` | `number` | `64` | Canvas pixel size | 45 + | `hueRange` | `number` | `60` | Hue spread in degrees around seed-derived base hue | 46 + | `saturationRange` | `[number, number]` | `[45, 70]` | Saturation min/max (%) | 47 + | `lightnessRange` | `[number, number]` | `[40, 70]` | Lightness min/max (%) | 48 + 49 + ### `noiseAvatarAction(canvas, seed, options?)` 50 + 51 + Svelte action wrapper around `renderNoiseAvatar`. Re-renders when `seed` changes via `update`. 52 + 53 + ### `hash32(str)` 54 + 55 + djb2 hash — returns an unsigned 32-bit integer. Exported for custom seed construction. 56 + 57 + ### `makePrng(seed)` 58 + 59 + Seeded LCG PRNG — returns a `() => number` producing floats in `[0, 1)`. 60 + 61 + ### `hslToRgb(h, s, l)` 62 + 63 + Converts HSL (components in `[0, 1]`) to an RGB triple (`[0, 255]` each). 64 + 65 + ## Licence 66 + 67 + AGPL-3.0-only
+41
packages/noise-avatar/package.json
··· 1 + { 2 + "name": "@ewanc26/noise-avatar", 3 + "version": "0.1.0", 4 + "description": "Deterministic value-noise avatar generation from a string seed. Zero dependencies, works in browsers and Node.js.", 5 + "author": "Ewan Croft", 6 + "license": "AGPL-3.0-only", 7 + "type": "module", 8 + "exports": { 9 + ".": { 10 + "types": "./dist/index.d.ts", 11 + "import": "./dist/index.js", 12 + "require": "./dist/index.cjs" 13 + } 14 + }, 15 + "main": "./dist/index.cjs", 16 + "module": "./dist/index.js", 17 + "types": "./dist/index.d.ts", 18 + "files": [ 19 + "dist", 20 + "README.md" 21 + ], 22 + "scripts": { 23 + "build": "tsup src/index.ts --format esm,cjs --dts --clean", 24 + "dev": "tsup src/index.ts --format esm,cjs --dts --watch", 25 + "type-check": "tsc --noEmit" 26 + }, 27 + "keywords": [ 28 + "avatar", 29 + "noise", 30 + "procedural", 31 + "canvas", 32 + "deterministic" 33 + ], 34 + "devDependencies": { 35 + "tsup": "^8.5.0", 36 + "typescript": "^5.9.3" 37 + }, 38 + "publishConfig": { 39 + "access": "public" 40 + } 41 + }
+173
packages/noise-avatar/src/index.ts
··· 1 + /** 2 + * @ewanc26/noise-avatar 3 + * 4 + * Deterministic value-noise avatar generation. 5 + * 6 + * Renders a unique, colour-rich noise texture onto an HTMLCanvasElement 7 + * from an arbitrary string seed. The same seed always produces the same image. 8 + * Zero runtime dependencies; works in any environment with a Canvas API. 9 + */ 10 + 11 + // ─── Hash ──────────────────────────────────────────────────────────────────── 12 + 13 + /** 14 + * djb2 hash — returns an unsigned 32-bit integer for any string. 15 + */ 16 + export function hash32(str: string): number { 17 + let h = 5381; 18 + for (let i = 0; i < str.length; i++) h = Math.imul(33, h) ^ str.charCodeAt(i); 19 + return h >>> 0; 20 + } 21 + 22 + // ─── PRNG ──────────────────────────────────────────────────────────────────── 23 + 24 + /** 25 + * Seeded LCG pseudo-random number generator. 26 + * Returns a function that produces floats in [0, 1). 27 + */ 28 + export function makePrng(seed: number): () => number { 29 + let s = seed >>> 0; 30 + return () => { 31 + s = (Math.imul(1664525, s) + 1013904223) >>> 0; 32 + return s / 0x100000000; 33 + }; 34 + } 35 + 36 + // ─── Colour ────────────────────────────────────────────────────────────────── 37 + 38 + /** 39 + * Convert HSL (each component in [0, 1]) to an RGB triple ([0, 255] each). 40 + */ 41 + export function hslToRgb(h: number, s: number, l: number): [number, number, number] { 42 + const a = s * Math.min(l, 1 - l); 43 + const f = (n: number) => { 44 + const k = (n + h * 12) % 12; 45 + return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)); 46 + }; 47 + return [Math.round(f(0) * 255), Math.round(f(8) * 255), Math.round(f(4) * 255)]; 48 + } 49 + 50 + // ─── Noise ─────────────────────────────────────────────────────────────────── 51 + 52 + function smoothstep(t: number): number { 53 + return t * t * (3 - 2 * t); 54 + } 55 + 56 + /** 57 + * Options for `renderNoiseAvatar`. 58 + */ 59 + export interface NoiseAvatarOptions { 60 + /** 61 + * Side length of the internal noise grid (higher = more detail). 62 + * @default 5 63 + */ 64 + gridSize?: number; 65 + /** 66 + * Width and height of the rendered canvas in pixels. 67 + * @default 64 68 + */ 69 + displaySize?: number; 70 + /** 71 + * Hue spread (degrees) around the seed-derived base hue. 72 + * @default 60 73 + */ 74 + hueRange?: number; 75 + /** 76 + * Saturation range [min, max] as percentages. 77 + * @default [45, 70] 78 + */ 79 + saturationRange?: [number, number]; 80 + /** 81 + * Lightness range [min, max] as percentages. 82 + * @default [40, 70] 83 + */ 84 + lightnessRange?: [number, number]; 85 + } 86 + 87 + /** 88 + * Render a deterministic value-noise avatar onto `canvas`. 89 + * 90 + * @param canvas Target HTMLCanvasElement (will be resized to `displaySize`). 91 + * @param seed Arbitrary string — same seed always produces the same image. 92 + * @param options Rendering options. 93 + */ 94 + export function renderNoiseAvatar( 95 + canvas: HTMLCanvasElement, 96 + seed: string, 97 + options: NoiseAvatarOptions = {} 98 + ): void { 99 + const { 100 + gridSize = 5, 101 + displaySize = 64, 102 + hueRange = 60, 103 + saturationRange = [45, 70], 104 + lightnessRange = [40, 70], 105 + } = options; 106 + 107 + canvas.width = displaySize; 108 + canvas.height = displaySize; 109 + 110 + const ctx = canvas.getContext('2d'); 111 + if (!ctx) return; 112 + 113 + const seedNum = hash32(seed); 114 + const rng = makePrng(seedNum); 115 + const baseHue = seedNum % 360; 116 + 117 + // Build value noise grid 118 + const G = gridSize + 1; 119 + const grid: number[] = Array.from({ length: G * G }, () => rng()); 120 + const gridVal = (gx: number, gy: number) => grid[gy * G + gx]; 121 + 122 + const imageData = ctx.createImageData(displaySize, displaySize); 123 + 124 + for (let py = 0; py < displaySize; py++) { 125 + for (let px = 0; px < displaySize; px++) { 126 + const fx = (px / displaySize) * gridSize; 127 + const fy = (py / displaySize) * gridSize; 128 + const gx = Math.floor(fx); 129 + const gy = Math.floor(fy); 130 + const tx = smoothstep(fx - gx); 131 + const ty = smoothstep(fy - gy); 132 + 133 + // Bilinear interpolation 134 + const v = 135 + (1 - ty) * ((1 - tx) * gridVal(gx, gy) + tx * gridVal(gx + 1, gy)) + 136 + ty * ((1 - tx) * gridVal(gx, gy + 1) + tx * gridVal(gx + 1, gy + 1)); 137 + 138 + const hue = (baseHue + v * hueRange) % 360; 139 + const sat = saturationRange[0] + v * (saturationRange[1] - saturationRange[0]); 140 + const light = lightnessRange[0] + v * (lightnessRange[1] - lightnessRange[0]); 141 + 142 + const [r, g, b] = hslToRgb(hue / 360, sat / 100, light / 100); 143 + const i = (py * displaySize + px) * 4; 144 + imageData.data[i] = r; 145 + imageData.data[i + 1] = g; 146 + imageData.data[i + 2] = b; 147 + imageData.data[i + 3] = 255; 148 + } 149 + } 150 + 151 + ctx.putImageData(imageData, 0, 0); 152 + } 153 + 154 + /** 155 + * Create a Svelte action that renders a noise avatar and updates reactively. 156 + * 157 + * @example 158 + * ```svelte 159 + * <canvas use:noiseAvatarAction={seed}></canvas> 160 + * ``` 161 + */ 162 + export function noiseAvatarAction( 163 + canvas: HTMLCanvasElement, 164 + seed: string, 165 + options?: NoiseAvatarOptions 166 + ) { 167 + renderNoiseAvatar(canvas, seed, options); 168 + return { 169 + update(newSeed: string) { 170 + renderNoiseAvatar(canvas, newSeed, options); 171 + }, 172 + }; 173 + }
+10
packages/noise-avatar/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.json", 3 + "compilerOptions": { 4 + "rootDir": "src", 5 + "outDir": "dist", 6 + "declaration": true, 7 + "declarationMap": true 8 + }, 9 + "include": ["src"] 10 + }