invadrs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matthew Goodwin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # invadrs
2
+
3
+ Deterministic space-invaders-style pixel avatars from any string. Zero
4
+ dependencies. Same id in, same avatar out, forever.
5
+
6
+ ![invadrs sample avatars — the invadr() and spawn() primitives, shown across every built-in palette](assets/preview.png)
7
+
8
+ ```ts
9
+ import { invadr, spawn, dataUri } from "invadrs";
10
+
11
+ invadr("matt"); // hand-drawn creature, SVG string
12
+ spawn("matt"); // procedural creature, SVG string
13
+ invadr("matt", { palette: "sunset", size: 32 });
14
+ dataUri(invadr("matt")); // data:image/svg+xml,...
15
+ ```
16
+
17
+ ## Primitives
18
+
19
+ - `invadr(id, options?)` — one of 16 hand-drawn creatures.
20
+ - `spawn(id, options?)` — a unique procedural symmetric creature.
21
+
22
+ ## Options
23
+
24
+ `{ size?, palette?, padding?, background?, title?, resolution? }`
25
+ (`resolution` is `spawn`-only; when `size` is omitted no `width`/`height` is set,
26
+ so host CSS controls the size.)
27
+
28
+ ## Palettes & theming
29
+
30
+ Built-ins, each its own colour mood: `tokyoNight` (default, balanced rainbow),
31
+ `neon` (electric, dark-bg), `sunset` (warm), `ocean` (cool), `forest`
32
+ (greens/earth), `mono` (grayscale). Pass a name, your own `string[]`, a full
33
+ `{ colors, background }` object, or `"css-vars"` to emit `var(--accent)`… fills
34
+ that follow your theme.
35
+
36
+ ```ts
37
+ import { palettes } from "invadrs";
38
+ const brand = { colors: [...palettes.tokyoNight.colors, "#ff00aa"] };
39
+ invadr("matt", { palette: brand });
40
+ ```
41
+
42
+ ## React
43
+
44
+ ```tsx
45
+ import { Invadr, Spawn, InvadrsProvider } from "invadrs/react";
46
+
47
+ <Invadr id="matt" palette="css-vars" /> {/* hand-drawn creature */}
48
+ <Spawn id="matt" /> {/* procedural creature */}
49
+
50
+ {/* app-wide defaults; explicit props on a component win */}
51
+ <InvadrsProvider palette="tokyoNight" size={20}>…</InvadrsProvider>
52
+ ```
53
+
54
+ ## Stability contract
55
+
56
+ The hash, the order of the built-in creatures, the procedural generator, and the
57
+ `css-vars` color order are **frozen**. Changing any of them alters existing
58
+ avatars and is only ever done in a major release.
@@ -0,0 +1,166 @@
1
+ // src/hash.ts
2
+ function hashStr(s) {
3
+ let h = 2166136261;
4
+ for (let i = 0; i < s.length; i++) {
5
+ h ^= s.charCodeAt(i);
6
+ h = Math.imul(h, 16777619);
7
+ }
8
+ return h >>> 0;
9
+ }
10
+
11
+ // src/palettes.ts
12
+ var palettes = {
13
+ // balanced rainbow (default) — the Tokyo Night editor colours
14
+ tokyoNight: { colors: ["#7aa2f7", "#9ece6a", "#e0af68", "#bb9af7", "#7dcfff", "#f7768e"] },
15
+ // electric high-saturation — reads best on dark backgrounds
16
+ neon: { colors: ["#ff2e97", "#00eaff", "#39ff14", "#b026ff", "#faff00", "#ff5900"] },
17
+ // warm only — crimson, vermilion, orange, gold, rose, deep red
18
+ sunset: { colors: ["#e5383b", "#ff6b35", "#f77f00", "#ffca3a", "#ff5c8a", "#c1121f"] },
19
+ // cool only — navy, teal, sky, seafoam, blue, aqua
20
+ ocean: { colors: ["#1d3557", "#2a9d8f", "#48cae4", "#56e39f", "#4361ee", "#7bdff2"] },
21
+ // greens and earth — pine, leaf, lime-olive, fern, bark, tan
22
+ forest: { colors: ["#386641", "#6a994e", "#a7c957", "#588157", "#b08968", "#dda15e"] },
23
+ // grayscale
24
+ mono: { colors: ["#e6e6e6", "#bdbdbd", "#9e9e9e", "#757575", "#bdbdbd", "#e6e6e6"] }
25
+ };
26
+ var CSS_VARS = {
27
+ colors: ["var(--accent)", "var(--green)", "var(--amber)", "var(--purple)", "var(--cyan)", "var(--red)"]
28
+ };
29
+ function resolvePalette(input) {
30
+ if (input === void 0) return palettes.tokyoNight;
31
+ if (input === "css-vars") return CSS_VARS;
32
+ if (typeof input === "string") return palettes[input];
33
+ if (Array.isArray(input)) return { colors: input };
34
+ return input;
35
+ }
36
+ function pickColor(seed, palette) {
37
+ return palette.colors[(seed >>> 4) % palette.colors.length];
38
+ }
39
+
40
+ // src/render.ts
41
+ function escapeXml(s) {
42
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
43
+ }
44
+ function resolveCommon(seed, grid, options) {
45
+ const palette = resolvePalette(options?.palette);
46
+ return {
47
+ grid,
48
+ color: pickColor(seed, palette),
49
+ size: options?.size,
50
+ padding: options?.padding ?? 1,
51
+ background: options?.background ?? palette.background,
52
+ title: options?.title
53
+ };
54
+ }
55
+ function renderSvg(s) {
56
+ const n = s.grid.length;
57
+ const min = -s.padding;
58
+ const span = n + s.padding * 2;
59
+ const rects = [];
60
+ if (s.background) {
61
+ rects.push(`<rect x="${min}" y="${min}" width="${span}" height="${span}" fill="${s.background}"/>`);
62
+ }
63
+ for (let y = 0; y < n; y++) {
64
+ for (let x = 0; x < n; x++) {
65
+ if (s.grid[y][x]) rects.push(`<rect x="${x}" y="${y}" width="1" height="1"/>`);
66
+ }
67
+ }
68
+ const dims = s.size !== void 0 ? ` width="${s.size}" height="${s.size}"` : "";
69
+ const a11y = s.title ? `role="img"` : `aria-hidden="true"`;
70
+ const titleEl = s.title ? `<title>${escapeXml(s.title)}</title>` : "";
71
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${min} ${min} ${span} ${span}"${dims} shape-rendering="crispEdges" fill="${s.color}" ${a11y}>${titleEl}${rects.join("")}</svg>`;
72
+ }
73
+ function dataUri(svg) {
74
+ return "data:image/svg+xml," + encodeURIComponent(svg);
75
+ }
76
+
77
+ // src/invadr.ts
78
+ var INVADR_SPRITES = [
79
+ [24, 60, 126, 219, 255, 36, 90, 165],
80
+ // invader
81
+ [36, 126, 219, 255, 255, 102, 36, 66],
82
+ // crab
83
+ [60, 126, 219, 255, 255, 255, 255, 165],
84
+ // ghost
85
+ [60, 66, 165, 129, 165, 153, 66, 60],
86
+ // smiley
87
+ [24, 24, 126, 255, 255, 102, 102, 231],
88
+ // mech
89
+ [66, 36, 126, 219, 255, 165, 36, 66],
90
+ // alien
91
+ [231, 60, 126, 219, 255, 219, 126, 102],
92
+ // robot
93
+ [129, 195, 255, 219, 255, 255, 255, 165],
94
+ // cat
95
+ [60, 126, 255, 219, 255, 126, 90, 36],
96
+ // skull
97
+ [153, 126, 255, 219, 255, 255, 126, 165],
98
+ // beetle
99
+ [195, 255, 219, 255, 126, 60, 24, 36],
100
+ // owl
101
+ [36, 126, 255, 231, 255, 126, 24, 24],
102
+ // flower
103
+ [24, 60, 126, 255, 255, 126, 60, 24],
104
+ // gem
105
+ [102, 255, 255, 255, 126, 126, 60, 24],
106
+ // heart
107
+ [60, 126, 255, 255, 90, 24, 24, 60],
108
+ // mushroom
109
+ [36, 90, 255, 189, 255, 90, 165, 66]
110
+ // amoeba
111
+ ];
112
+ function byteRowsToGrid(rows) {
113
+ return rows.map((row) => {
114
+ const cells = [];
115
+ for (let x = 0; x < 8; x++) cells.push((row & 128 >> x) !== 0);
116
+ return cells;
117
+ });
118
+ }
119
+ function invadrGrid(seed) {
120
+ return byteRowsToGrid(INVADR_SPRITES[seed % INVADR_SPRITES.length]);
121
+ }
122
+ function resolveInvadr(id, options) {
123
+ const seed = hashStr(id);
124
+ return resolveCommon(seed, invadrGrid(seed), options);
125
+ }
126
+ function invadr(id, options) {
127
+ return renderSvg(resolveInvadr(id, options));
128
+ }
129
+
130
+ // src/spawn.ts
131
+ var DENSITY = 0.5;
132
+ function mulberry32(seed) {
133
+ let a = seed >>> 0;
134
+ return function() {
135
+ a = a + 1831565813 | 0;
136
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
137
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
138
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
139
+ };
140
+ }
141
+ function spawnGrid(seed, resolution) {
142
+ const n = resolution;
143
+ const half = n / 2;
144
+ const rng = mulberry32(seed);
145
+ const grid = [];
146
+ for (let y = 0; y < n; y++) {
147
+ const row = new Array(n).fill(false);
148
+ for (let x = 0; x < half; x++) {
149
+ const on = rng() < DENSITY;
150
+ row[x] = on;
151
+ row[n - 1 - x] = on;
152
+ }
153
+ grid.push(row);
154
+ }
155
+ return grid;
156
+ }
157
+ function resolveSpawn(id, options) {
158
+ const seed = hashStr(id);
159
+ const resolution = options?.resolution ?? 8;
160
+ return resolveCommon(seed, spawnGrid(seed, resolution), options);
161
+ }
162
+ function spawn(id, options) {
163
+ return renderSvg(resolveSpawn(id, options));
164
+ }
165
+
166
+ export { CSS_VARS, INVADR_SPRITES, dataUri, hashStr, invadr, palettes, pickColor, renderSvg, resolveInvadr, resolvePalette, resolveSpawn, spawn };
package/dist/index.cjs ADDED
@@ -0,0 +1,179 @@
1
+ 'use strict';
2
+
3
+ // src/hash.ts
4
+ function hashStr(s) {
5
+ let h = 2166136261;
6
+ for (let i = 0; i < s.length; i++) {
7
+ h ^= s.charCodeAt(i);
8
+ h = Math.imul(h, 16777619);
9
+ }
10
+ return h >>> 0;
11
+ }
12
+
13
+ // src/palettes.ts
14
+ var palettes = {
15
+ // balanced rainbow (default) — the Tokyo Night editor colours
16
+ tokyoNight: { colors: ["#7aa2f7", "#9ece6a", "#e0af68", "#bb9af7", "#7dcfff", "#f7768e"] },
17
+ // electric high-saturation — reads best on dark backgrounds
18
+ neon: { colors: ["#ff2e97", "#00eaff", "#39ff14", "#b026ff", "#faff00", "#ff5900"] },
19
+ // warm only — crimson, vermilion, orange, gold, rose, deep red
20
+ sunset: { colors: ["#e5383b", "#ff6b35", "#f77f00", "#ffca3a", "#ff5c8a", "#c1121f"] },
21
+ // cool only — navy, teal, sky, seafoam, blue, aqua
22
+ ocean: { colors: ["#1d3557", "#2a9d8f", "#48cae4", "#56e39f", "#4361ee", "#7bdff2"] },
23
+ // greens and earth — pine, leaf, lime-olive, fern, bark, tan
24
+ forest: { colors: ["#386641", "#6a994e", "#a7c957", "#588157", "#b08968", "#dda15e"] },
25
+ // grayscale
26
+ mono: { colors: ["#e6e6e6", "#bdbdbd", "#9e9e9e", "#757575", "#bdbdbd", "#e6e6e6"] }
27
+ };
28
+ var CSS_VARS = {
29
+ colors: ["var(--accent)", "var(--green)", "var(--amber)", "var(--purple)", "var(--cyan)", "var(--red)"]
30
+ };
31
+ function resolvePalette(input) {
32
+ if (input === void 0) return palettes.tokyoNight;
33
+ if (input === "css-vars") return CSS_VARS;
34
+ if (typeof input === "string") return palettes[input];
35
+ if (Array.isArray(input)) return { colors: input };
36
+ return input;
37
+ }
38
+ function pickColor(seed, palette) {
39
+ return palette.colors[(seed >>> 4) % palette.colors.length];
40
+ }
41
+
42
+ // src/render.ts
43
+ function escapeXml(s) {
44
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
45
+ }
46
+ function resolveCommon(seed, grid, options) {
47
+ const palette = resolvePalette(options?.palette);
48
+ return {
49
+ grid,
50
+ color: pickColor(seed, palette),
51
+ size: options?.size,
52
+ padding: options?.padding ?? 1,
53
+ background: options?.background ?? palette.background,
54
+ title: options?.title
55
+ };
56
+ }
57
+ function renderSvg(s) {
58
+ const n = s.grid.length;
59
+ const min = -s.padding;
60
+ const span = n + s.padding * 2;
61
+ const rects = [];
62
+ if (s.background) {
63
+ rects.push(`<rect x="${min}" y="${min}" width="${span}" height="${span}" fill="${s.background}"/>`);
64
+ }
65
+ for (let y = 0; y < n; y++) {
66
+ for (let x = 0; x < n; x++) {
67
+ if (s.grid[y][x]) rects.push(`<rect x="${x}" y="${y}" width="1" height="1"/>`);
68
+ }
69
+ }
70
+ const dims = s.size !== void 0 ? ` width="${s.size}" height="${s.size}"` : "";
71
+ const a11y = s.title ? `role="img"` : `aria-hidden="true"`;
72
+ const titleEl = s.title ? `<title>${escapeXml(s.title)}</title>` : "";
73
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${min} ${min} ${span} ${span}"${dims} shape-rendering="crispEdges" fill="${s.color}" ${a11y}>${titleEl}${rects.join("")}</svg>`;
74
+ }
75
+ function dataUri(svg) {
76
+ return "data:image/svg+xml," + encodeURIComponent(svg);
77
+ }
78
+
79
+ // src/invadr.ts
80
+ var INVADR_SPRITES = [
81
+ [24, 60, 126, 219, 255, 36, 90, 165],
82
+ // invader
83
+ [36, 126, 219, 255, 255, 102, 36, 66],
84
+ // crab
85
+ [60, 126, 219, 255, 255, 255, 255, 165],
86
+ // ghost
87
+ [60, 66, 165, 129, 165, 153, 66, 60],
88
+ // smiley
89
+ [24, 24, 126, 255, 255, 102, 102, 231],
90
+ // mech
91
+ [66, 36, 126, 219, 255, 165, 36, 66],
92
+ // alien
93
+ [231, 60, 126, 219, 255, 219, 126, 102],
94
+ // robot
95
+ [129, 195, 255, 219, 255, 255, 255, 165],
96
+ // cat
97
+ [60, 126, 255, 219, 255, 126, 90, 36],
98
+ // skull
99
+ [153, 126, 255, 219, 255, 255, 126, 165],
100
+ // beetle
101
+ [195, 255, 219, 255, 126, 60, 24, 36],
102
+ // owl
103
+ [36, 126, 255, 231, 255, 126, 24, 24],
104
+ // flower
105
+ [24, 60, 126, 255, 255, 126, 60, 24],
106
+ // gem
107
+ [102, 255, 255, 255, 126, 126, 60, 24],
108
+ // heart
109
+ [60, 126, 255, 255, 90, 24, 24, 60],
110
+ // mushroom
111
+ [36, 90, 255, 189, 255, 90, 165, 66]
112
+ // amoeba
113
+ ];
114
+ function byteRowsToGrid(rows) {
115
+ return rows.map((row) => {
116
+ const cells = [];
117
+ for (let x = 0; x < 8; x++) cells.push((row & 128 >> x) !== 0);
118
+ return cells;
119
+ });
120
+ }
121
+ function invadrGrid(seed) {
122
+ return byteRowsToGrid(INVADR_SPRITES[seed % INVADR_SPRITES.length]);
123
+ }
124
+ function resolveInvadr(id, options) {
125
+ const seed = hashStr(id);
126
+ return resolveCommon(seed, invadrGrid(seed), options);
127
+ }
128
+ function invadr(id, options) {
129
+ return renderSvg(resolveInvadr(id, options));
130
+ }
131
+
132
+ // src/spawn.ts
133
+ var DENSITY = 0.5;
134
+ function mulberry32(seed) {
135
+ let a = seed >>> 0;
136
+ return function() {
137
+ a = a + 1831565813 | 0;
138
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
139
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
140
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
141
+ };
142
+ }
143
+ function spawnGrid(seed, resolution) {
144
+ const n = resolution;
145
+ const half = n / 2;
146
+ const rng = mulberry32(seed);
147
+ const grid = [];
148
+ for (let y = 0; y < n; y++) {
149
+ const row = new Array(n).fill(false);
150
+ for (let x = 0; x < half; x++) {
151
+ const on = rng() < DENSITY;
152
+ row[x] = on;
153
+ row[n - 1 - x] = on;
154
+ }
155
+ grid.push(row);
156
+ }
157
+ return grid;
158
+ }
159
+ function resolveSpawn(id, options) {
160
+ const seed = hashStr(id);
161
+ const resolution = options?.resolution ?? 8;
162
+ return resolveCommon(seed, spawnGrid(seed, resolution), options);
163
+ }
164
+ function spawn(id, options) {
165
+ return renderSvg(resolveSpawn(id, options));
166
+ }
167
+
168
+ exports.CSS_VARS = CSS_VARS;
169
+ exports.INVADR_SPRITES = INVADR_SPRITES;
170
+ exports.dataUri = dataUri;
171
+ exports.hashStr = hashStr;
172
+ exports.invadr = invadr;
173
+ exports.palettes = palettes;
174
+ exports.pickColor = pickColor;
175
+ exports.renderSvg = renderSvg;
176
+ exports.resolveInvadr = resolveInvadr;
177
+ exports.resolvePalette = resolvePalette;
178
+ exports.resolveSpawn = resolveSpawn;
179
+ exports.spawn = spawn;
@@ -0,0 +1,48 @@
1
+ import { G as Grid, S as SpriteOptions, P as Palette, a as PaletteName, b as PaletteInput } from './types-C4QooPnZ.cjs';
2
+
3
+ /** A fully-resolved sprite: geometry + paint + presentation, ready to render. */
4
+ type ResolvedSprite = {
5
+ grid: Grid;
6
+ color: string;
7
+ size?: number;
8
+ padding: number;
9
+ background?: string;
10
+ title?: string;
11
+ };
12
+ /** Render a ResolvedSprite to a standalone SVG string. */
13
+ declare function renderSvg(s: ResolvedSprite): string;
14
+ /** Wrap an SVG string as a data: URI usable in `src`/`background-image`. */
15
+ declare function dataUri(svg: string): string;
16
+
17
+ /** Hand-designed 8x8 creatures (one byte per row, MSB = left pixel).
18
+ Curated so every id reads as a deliberate sprite; each is left-right
19
+ symmetric. ORDER IS FROZEN — reordering changes users' avatars. */
20
+ declare const INVADR_SPRITES: number[][];
21
+ /** Resolve a hand-drawn creature for an id (structured form). */
22
+ declare function resolveInvadr(id: string, options?: SpriteOptions): ResolvedSprite;
23
+ /** A hand-drawn creature for an id, as an SVG string. */
24
+ declare function invadr(id: string, options?: SpriteOptions): string;
25
+
26
+ /** Resolve a procedural creature for an id (structured form). */
27
+ declare function resolveSpawn(id: string, options?: SpriteOptions): ResolvedSprite;
28
+ /** A procedural symmetric creature for an id, as an SVG string. */
29
+ declare function spawn(id: string, options?: SpriteOptions): string;
30
+
31
+ /** Built-in palettes. `tokyoNight` is the default and matches mr-board.
32
+ Each palette is its own colour mood (not the same rainbow re-shaded), so a
33
+ given id looks distinct from one palette to the next. */
34
+ declare const palettes: Record<PaletteName, Palette>;
35
+ /** Palette that emits CSS custom properties instead of literal colors so the
36
+ host theme drives the sprite color. Order is frozen for mr-board parity. */
37
+ declare const CSS_VARS: Palette;
38
+ /** Resolve any PaletteInput to a concrete Palette. */
39
+ declare function resolvePalette(input?: PaletteInput): Palette;
40
+ /** Deterministically pick a color from the palette for a given seed.
41
+ Uses `seed >>> 4` so it is independent of the sprite index (`seed % n`). */
42
+ declare function pickColor(seed: number, palette: Palette): string;
43
+
44
+ /** FNV-1a string hash -> unsigned 32-bit seed. Frozen: changing this changes
45
+ every avatar, so it is a semver-major change. */
46
+ declare function hashStr(s: string): number;
47
+
48
+ export { CSS_VARS, Grid, INVADR_SPRITES, Palette, PaletteInput, PaletteName, type ResolvedSprite, SpriteOptions, dataUri, hashStr, invadr, palettes, pickColor, renderSvg, resolveInvadr, resolvePalette, resolveSpawn, spawn };
@@ -0,0 +1,48 @@
1
+ import { G as Grid, S as SpriteOptions, P as Palette, a as PaletteName, b as PaletteInput } from './types-C4QooPnZ.js';
2
+
3
+ /** A fully-resolved sprite: geometry + paint + presentation, ready to render. */
4
+ type ResolvedSprite = {
5
+ grid: Grid;
6
+ color: string;
7
+ size?: number;
8
+ padding: number;
9
+ background?: string;
10
+ title?: string;
11
+ };
12
+ /** Render a ResolvedSprite to a standalone SVG string. */
13
+ declare function renderSvg(s: ResolvedSprite): string;
14
+ /** Wrap an SVG string as a data: URI usable in `src`/`background-image`. */
15
+ declare function dataUri(svg: string): string;
16
+
17
+ /** Hand-designed 8x8 creatures (one byte per row, MSB = left pixel).
18
+ Curated so every id reads as a deliberate sprite; each is left-right
19
+ symmetric. ORDER IS FROZEN — reordering changes users' avatars. */
20
+ declare const INVADR_SPRITES: number[][];
21
+ /** Resolve a hand-drawn creature for an id (structured form). */
22
+ declare function resolveInvadr(id: string, options?: SpriteOptions): ResolvedSprite;
23
+ /** A hand-drawn creature for an id, as an SVG string. */
24
+ declare function invadr(id: string, options?: SpriteOptions): string;
25
+
26
+ /** Resolve a procedural creature for an id (structured form). */
27
+ declare function resolveSpawn(id: string, options?: SpriteOptions): ResolvedSprite;
28
+ /** A procedural symmetric creature for an id, as an SVG string. */
29
+ declare function spawn(id: string, options?: SpriteOptions): string;
30
+
31
+ /** Built-in palettes. `tokyoNight` is the default and matches mr-board.
32
+ Each palette is its own colour mood (not the same rainbow re-shaded), so a
33
+ given id looks distinct from one palette to the next. */
34
+ declare const palettes: Record<PaletteName, Palette>;
35
+ /** Palette that emits CSS custom properties instead of literal colors so the
36
+ host theme drives the sprite color. Order is frozen for mr-board parity. */
37
+ declare const CSS_VARS: Palette;
38
+ /** Resolve any PaletteInput to a concrete Palette. */
39
+ declare function resolvePalette(input?: PaletteInput): Palette;
40
+ /** Deterministically pick a color from the palette for a given seed.
41
+ Uses `seed >>> 4` so it is independent of the sprite index (`seed % n`). */
42
+ declare function pickColor(seed: number, palette: Palette): string;
43
+
44
+ /** FNV-1a string hash -> unsigned 32-bit seed. Frozen: changing this changes
45
+ every avatar, so it is a semver-major change. */
46
+ declare function hashStr(s: string): number;
47
+
48
+ export { CSS_VARS, Grid, INVADR_SPRITES, Palette, PaletteInput, PaletteName, type ResolvedSprite, SpriteOptions, dataUri, hashStr, invadr, palettes, pickColor, renderSvg, resolveInvadr, resolvePalette, resolveSpawn, spawn };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { CSS_VARS, INVADR_SPRITES, dataUri, hashStr, invadr, palettes, pickColor, renderSvg, resolveInvadr, resolvePalette, resolveSpawn, spawn } from './chunk-5SOVCTHT.js';
@@ -0,0 +1,195 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ // src/react/index.tsx
7
+
8
+ // src/hash.ts
9
+ function hashStr(s) {
10
+ let h = 2166136261;
11
+ for (let i = 0; i < s.length; i++) {
12
+ h ^= s.charCodeAt(i);
13
+ h = Math.imul(h, 16777619);
14
+ }
15
+ return h >>> 0;
16
+ }
17
+
18
+ // src/palettes.ts
19
+ var palettes = {
20
+ // balanced rainbow (default) — the Tokyo Night editor colours
21
+ tokyoNight: { colors: ["#7aa2f7", "#9ece6a", "#e0af68", "#bb9af7", "#7dcfff", "#f7768e"] },
22
+ // electric high-saturation — reads best on dark backgrounds
23
+ neon: { colors: ["#ff2e97", "#00eaff", "#39ff14", "#b026ff", "#faff00", "#ff5900"] },
24
+ // warm only — crimson, vermilion, orange, gold, rose, deep red
25
+ sunset: { colors: ["#e5383b", "#ff6b35", "#f77f00", "#ffca3a", "#ff5c8a", "#c1121f"] },
26
+ // cool only — navy, teal, sky, seafoam, blue, aqua
27
+ ocean: { colors: ["#1d3557", "#2a9d8f", "#48cae4", "#56e39f", "#4361ee", "#7bdff2"] },
28
+ // greens and earth — pine, leaf, lime-olive, fern, bark, tan
29
+ forest: { colors: ["#386641", "#6a994e", "#a7c957", "#588157", "#b08968", "#dda15e"] },
30
+ // grayscale
31
+ mono: { colors: ["#e6e6e6", "#bdbdbd", "#9e9e9e", "#757575", "#bdbdbd", "#e6e6e6"] }
32
+ };
33
+ var CSS_VARS = {
34
+ colors: ["var(--accent)", "var(--green)", "var(--amber)", "var(--purple)", "var(--cyan)", "var(--red)"]
35
+ };
36
+ function resolvePalette(input) {
37
+ if (input === void 0) return palettes.tokyoNight;
38
+ if (input === "css-vars") return CSS_VARS;
39
+ if (typeof input === "string") return palettes[input];
40
+ if (Array.isArray(input)) return { colors: input };
41
+ return input;
42
+ }
43
+ function pickColor(seed, palette) {
44
+ return palette.colors[(seed >>> 4) % palette.colors.length];
45
+ }
46
+
47
+ // src/render.ts
48
+ function resolveCommon(seed, grid, options) {
49
+ const palette = resolvePalette(options?.palette);
50
+ return {
51
+ grid,
52
+ color: pickColor(seed, palette),
53
+ size: options?.size,
54
+ padding: options?.padding ?? 1,
55
+ background: options?.background ?? palette.background,
56
+ title: options?.title
57
+ };
58
+ }
59
+
60
+ // src/invadr.ts
61
+ var INVADR_SPRITES = [
62
+ [24, 60, 126, 219, 255, 36, 90, 165],
63
+ // invader
64
+ [36, 126, 219, 255, 255, 102, 36, 66],
65
+ // crab
66
+ [60, 126, 219, 255, 255, 255, 255, 165],
67
+ // ghost
68
+ [60, 66, 165, 129, 165, 153, 66, 60],
69
+ // smiley
70
+ [24, 24, 126, 255, 255, 102, 102, 231],
71
+ // mech
72
+ [66, 36, 126, 219, 255, 165, 36, 66],
73
+ // alien
74
+ [231, 60, 126, 219, 255, 219, 126, 102],
75
+ // robot
76
+ [129, 195, 255, 219, 255, 255, 255, 165],
77
+ // cat
78
+ [60, 126, 255, 219, 255, 126, 90, 36],
79
+ // skull
80
+ [153, 126, 255, 219, 255, 255, 126, 165],
81
+ // beetle
82
+ [195, 255, 219, 255, 126, 60, 24, 36],
83
+ // owl
84
+ [36, 126, 255, 231, 255, 126, 24, 24],
85
+ // flower
86
+ [24, 60, 126, 255, 255, 126, 60, 24],
87
+ // gem
88
+ [102, 255, 255, 255, 126, 126, 60, 24],
89
+ // heart
90
+ [60, 126, 255, 255, 90, 24, 24, 60],
91
+ // mushroom
92
+ [36, 90, 255, 189, 255, 90, 165, 66]
93
+ // amoeba
94
+ ];
95
+ function byteRowsToGrid(rows) {
96
+ return rows.map((row) => {
97
+ const cells = [];
98
+ for (let x = 0; x < 8; x++) cells.push((row & 128 >> x) !== 0);
99
+ return cells;
100
+ });
101
+ }
102
+ function invadrGrid(seed) {
103
+ return byteRowsToGrid(INVADR_SPRITES[seed % INVADR_SPRITES.length]);
104
+ }
105
+ function resolveInvadr(id, options) {
106
+ const seed = hashStr(id);
107
+ return resolveCommon(seed, invadrGrid(seed), options);
108
+ }
109
+
110
+ // src/spawn.ts
111
+ var DENSITY = 0.5;
112
+ function mulberry32(seed) {
113
+ let a = seed >>> 0;
114
+ return function() {
115
+ a = a + 1831565813 | 0;
116
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
117
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
118
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
119
+ };
120
+ }
121
+ function spawnGrid(seed, resolution) {
122
+ const n = resolution;
123
+ const half = n / 2;
124
+ const rng = mulberry32(seed);
125
+ const grid = [];
126
+ for (let y = 0; y < n; y++) {
127
+ const row = new Array(n).fill(false);
128
+ for (let x = 0; x < half; x++) {
129
+ const on = rng() < DENSITY;
130
+ row[x] = on;
131
+ row[n - 1 - x] = on;
132
+ }
133
+ grid.push(row);
134
+ }
135
+ return grid;
136
+ }
137
+ function resolveSpawn(id, options) {
138
+ const seed = hashStr(id);
139
+ const resolution = options?.resolution ?? 8;
140
+ return resolveCommon(seed, spawnGrid(seed, resolution), options);
141
+ }
142
+ var InvadrsContext = react.createContext({});
143
+ function InvadrsProvider({ children, ...defaults }) {
144
+ const inherited = react.useContext(InvadrsContext);
145
+ return /* @__PURE__ */ jsxRuntime.jsx(InvadrsContext.Provider, { value: { ...inherited, ...defaults }, children });
146
+ }
147
+ function renderCells(s) {
148
+ const nodes = [];
149
+ const n = s.grid.length;
150
+ if (s.background) {
151
+ nodes.push(
152
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: -s.padding, y: -s.padding, width: n + s.padding * 2, height: n + s.padding * 2, fill: s.background }, "bg")
153
+ );
154
+ }
155
+ for (let y = 0; y < n; y++) {
156
+ for (let x = 0; x < n; x++) {
157
+ if (s.grid[y][x]) nodes.push(/* @__PURE__ */ jsxRuntime.jsx("rect", { x, y, width: 1, height: 1 }, `${x},${y}`));
158
+ }
159
+ }
160
+ return nodes;
161
+ }
162
+ function svgFrom(resolved, className) {
163
+ const n = resolved.grid.length;
164
+ const min = -resolved.padding;
165
+ const span = n + resolved.padding * 2;
166
+ const dims = resolved.size !== void 0 ? { width: resolved.size, height: resolved.size } : {};
167
+ const a11y = resolved.title ? { role: "img" } : { "aria-hidden": true };
168
+ return /* @__PURE__ */ jsxRuntime.jsxs(
169
+ "svg",
170
+ {
171
+ className,
172
+ viewBox: `${min} ${min} ${span} ${span}`,
173
+ ...dims,
174
+ shapeRendering: "crispEdges",
175
+ fill: resolved.color,
176
+ ...a11y,
177
+ children: [
178
+ resolved.title ? /* @__PURE__ */ jsxRuntime.jsx("title", { children: resolved.title }) : null,
179
+ renderCells(resolved)
180
+ ]
181
+ }
182
+ );
183
+ }
184
+ function Invadr(props) {
185
+ const { id, className, ...options } = { ...react.useContext(InvadrsContext), ...props };
186
+ return svgFrom(resolveInvadr(id, options), className);
187
+ }
188
+ function Spawn(props) {
189
+ const { id, className, ...options } = { ...react.useContext(InvadrsContext), ...props };
190
+ return svgFrom(resolveSpawn(id, options), className);
191
+ }
192
+
193
+ exports.Invadr = Invadr;
194
+ exports.InvadrsProvider = InvadrsProvider;
195
+ exports.Spawn = Spawn;
@@ -0,0 +1,23 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { S as SpriteOptions } from '../types-C4QooPnZ.cjs';
4
+
5
+ /** Props for the avatar components. `<Invadr>` and `<Spawn>` take the same
6
+ shape; the component you pick chooses the primitive (no `from` prop). */
7
+ type InvadrProps = SpriteOptions & {
8
+ id: string;
9
+ className?: string;
10
+ };
11
+ type SpawnProps = InvadrProps;
12
+ type Defaults = Partial<Omit<InvadrProps, "id">>;
13
+ /** Supply default avatar props (palette, size, padding, …) to descendants.
14
+ Explicit props on an `<Invadr>`/`<Spawn>` override these. */
15
+ declare function InvadrsProvider({ children, ...defaults }: {
16
+ children: ReactNode;
17
+ } & Defaults): react.JSX.Element;
18
+ /** A hand-drawn creature (one of 16) for an id, as inline SVG. */
19
+ declare function Invadr(props: InvadrProps): ReactNode;
20
+ /** A procedural, unique-per-id creature, as inline SVG. */
21
+ declare function Spawn(props: SpawnProps): ReactNode;
22
+
23
+ export { Invadr, type InvadrProps, InvadrsProvider, Spawn, type SpawnProps };
@@ -0,0 +1,23 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { S as SpriteOptions } from '../types-C4QooPnZ.js';
4
+
5
+ /** Props for the avatar components. `<Invadr>` and `<Spawn>` take the same
6
+ shape; the component you pick chooses the primitive (no `from` prop). */
7
+ type InvadrProps = SpriteOptions & {
8
+ id: string;
9
+ className?: string;
10
+ };
11
+ type SpawnProps = InvadrProps;
12
+ type Defaults = Partial<Omit<InvadrProps, "id">>;
13
+ /** Supply default avatar props (palette, size, padding, …) to descendants.
14
+ Explicit props on an `<Invadr>`/`<Spawn>` override these. */
15
+ declare function InvadrsProvider({ children, ...defaults }: {
16
+ children: ReactNode;
17
+ } & Defaults): react.JSX.Element;
18
+ /** A hand-drawn creature (one of 16) for an id, as inline SVG. */
19
+ declare function Invadr(props: InvadrProps): ReactNode;
20
+ /** A procedural, unique-per-id creature, as inline SVG. */
21
+ declare function Spawn(props: SpawnProps): ReactNode;
22
+
23
+ export { Invadr, type InvadrProps, InvadrsProvider, Spawn, type SpawnProps };
@@ -0,0 +1,56 @@
1
+ import { resolveInvadr, resolveSpawn } from '../chunk-5SOVCTHT.js';
2
+ import { createContext, useContext } from 'react';
3
+ import { jsx, jsxs } from 'react/jsx-runtime';
4
+
5
+ var InvadrsContext = createContext({});
6
+ function InvadrsProvider({ children, ...defaults }) {
7
+ const inherited = useContext(InvadrsContext);
8
+ return /* @__PURE__ */ jsx(InvadrsContext.Provider, { value: { ...inherited, ...defaults }, children });
9
+ }
10
+ function renderCells(s) {
11
+ const nodes = [];
12
+ const n = s.grid.length;
13
+ if (s.background) {
14
+ nodes.push(
15
+ /* @__PURE__ */ jsx("rect", { x: -s.padding, y: -s.padding, width: n + s.padding * 2, height: n + s.padding * 2, fill: s.background }, "bg")
16
+ );
17
+ }
18
+ for (let y = 0; y < n; y++) {
19
+ for (let x = 0; x < n; x++) {
20
+ if (s.grid[y][x]) nodes.push(/* @__PURE__ */ jsx("rect", { x, y, width: 1, height: 1 }, `${x},${y}`));
21
+ }
22
+ }
23
+ return nodes;
24
+ }
25
+ function svgFrom(resolved, className) {
26
+ const n = resolved.grid.length;
27
+ const min = -resolved.padding;
28
+ const span = n + resolved.padding * 2;
29
+ const dims = resolved.size !== void 0 ? { width: resolved.size, height: resolved.size } : {};
30
+ const a11y = resolved.title ? { role: "img" } : { "aria-hidden": true };
31
+ return /* @__PURE__ */ jsxs(
32
+ "svg",
33
+ {
34
+ className,
35
+ viewBox: `${min} ${min} ${span} ${span}`,
36
+ ...dims,
37
+ shapeRendering: "crispEdges",
38
+ fill: resolved.color,
39
+ ...a11y,
40
+ children: [
41
+ resolved.title ? /* @__PURE__ */ jsx("title", { children: resolved.title }) : null,
42
+ renderCells(resolved)
43
+ ]
44
+ }
45
+ );
46
+ }
47
+ function Invadr(props) {
48
+ const { id, className, ...options } = { ...useContext(InvadrsContext), ...props };
49
+ return svgFrom(resolveInvadr(id, options), className);
50
+ }
51
+ function Spawn(props) {
52
+ const { id, className, ...options } = { ...useContext(InvadrsContext), ...props };
53
+ return svgFrom(resolveSpawn(id, options), className);
54
+ }
55
+
56
+ export { Invadr, InvadrsProvider, Spawn };
@@ -0,0 +1,23 @@
1
+ /** A square pixel grid, row-major; `grid[y][x]` true = filled. */
2
+ type Grid = boolean[][];
3
+ /** A color set (and optional background) used to paint a sprite. */
4
+ type Palette = {
5
+ colors: string[];
6
+ background?: string;
7
+ };
8
+ /** Names of the built-in palettes. */
9
+ type PaletteName = "tokyoNight" | "neon" | "sunset" | "ocean" | "forest" | "mono";
10
+ /** Anything accepted for the `palette` option. `"css-vars"` emits
11
+ `var(--...)` fills so the host theme drives the color. */
12
+ type PaletteInput = PaletteName | "css-vars" | string[] | Palette;
13
+ /** Options shared by both primitives. `resolution` applies to `spawn` only. */
14
+ type SpriteOptions = {
15
+ size?: number;
16
+ palette?: PaletteInput;
17
+ padding?: number;
18
+ background?: string;
19
+ title?: string;
20
+ resolution?: number;
21
+ };
22
+
23
+ export type { Grid as G, Palette as P, SpriteOptions as S, PaletteName as a, PaletteInput as b };
@@ -0,0 +1,23 @@
1
+ /** A square pixel grid, row-major; `grid[y][x]` true = filled. */
2
+ type Grid = boolean[][];
3
+ /** A color set (and optional background) used to paint a sprite. */
4
+ type Palette = {
5
+ colors: string[];
6
+ background?: string;
7
+ };
8
+ /** Names of the built-in palettes. */
9
+ type PaletteName = "tokyoNight" | "neon" | "sunset" | "ocean" | "forest" | "mono";
10
+ /** Anything accepted for the `palette` option. `"css-vars"` emits
11
+ `var(--...)` fills so the host theme drives the color. */
12
+ type PaletteInput = PaletteName | "css-vars" | string[] | Palette;
13
+ /** Options shared by both primitives. `resolution` applies to `spawn` only. */
14
+ type SpriteOptions = {
15
+ size?: number;
16
+ palette?: PaletteInput;
17
+ padding?: number;
18
+ background?: string;
19
+ title?: string;
20
+ resolution?: number;
21
+ };
22
+
23
+ export type { Grid as G, Palette as P, SpriteOptions as S, PaletteName as a, PaletteInput as b };
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "invadrs",
3
+ "version": "0.1.0",
4
+ "description": "Deterministic space-invaders-style pixel avatars from any string.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Matthew Goodwin",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/m4ttheweric/invadrs.git"
11
+ },
12
+ "homepage": "https://github.com/m4ttheweric/invadrs#readme",
13
+ "bugs": "https://github.com/m4ttheweric/invadrs/issues",
14
+ "keywords": [
15
+ "avatar",
16
+ "identicon",
17
+ "pixel",
18
+ "pixel-art",
19
+ "sprite",
20
+ "space-invaders",
21
+ "svg",
22
+ "deterministic",
23
+ "generative",
24
+ "react"
25
+ ],
26
+ "sideEffects": false,
27
+ "files": ["dist"],
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js",
32
+ "require": "./dist/index.cjs"
33
+ },
34
+ "./react": {
35
+ "types": "./dist/react/index.d.ts",
36
+ "import": "./dist/react/index.js",
37
+ "require": "./dist/react/index.cjs"
38
+ }
39
+ },
40
+ "scripts": {
41
+ "build": "tsup",
42
+ "test": "bun test",
43
+ "prepare": "tsup"
44
+ },
45
+ "peerDependencies": {
46
+ "react": ">=18"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "react": { "optional": true }
50
+ },
51
+ "devDependencies": {
52
+ "@types/bun": "latest",
53
+ "@types/react": "^19.0.0",
54
+ "@types/react-dom": "^19.0.0",
55
+ "react": "^19.0.0",
56
+ "react-dom": "^19.0.0",
57
+ "tsup": "^8.0.0",
58
+ "typescript": "^5.4.0"
59
+ }
60
+ }