hashvatar 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 Médhy Chabour
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,128 @@
1
+ # hashvatar
2
+
3
+ Deterministic avatars from any string — wallet address, username, UUID. **Zero dependencies**.
4
+
5
+ Two modes: **gradient** (radial blends) and **dither** (Bayer halftone + linear gradient).
6
+
7
+ ```bash
8
+ npm install hashvatar
9
+ ```
10
+
11
+ ---
12
+
13
+ ## Demo
14
+
15
+ From the repo root, build then run the demo:
16
+
17
+ ```bash
18
+ npm run build
19
+ npm run demo
20
+ ```
21
+
22
+ Then open **http://localhost:5000/demo/** in your browser. The demo loads the built bundle from `dist/`.
23
+
24
+ ---
25
+
26
+ ## Usage
27
+
28
+ ### Vanilla JS
29
+
30
+ ```js
31
+ import { createHashvatar } from "hashvatar";
32
+
33
+ const { canvas, destroy } = createHashvatar({
34
+ hash: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
35
+ size: 64,
36
+ });
37
+
38
+ document.body.appendChild(canvas);
39
+
40
+ // Stop animation later if animated:
41
+ destroy();
42
+ ```
43
+
44
+ ### React
45
+
46
+ ```tsx
47
+ import { Hashvatar } from 'hashvatar/react';
48
+
49
+ <Hashvatar hash="vitalik.eth" size={48} />
50
+ <Hashvatar hash="satoshi" size={64} mode="dither" />
51
+ <Hashvatar hash="0x742…44e" size={64} animated tones={['hotpink', '#00ff99']} />
52
+ ```
53
+
54
+ React is an optional peer dependency — only install it if you use `hashvatar/react`.
55
+
56
+ ---
57
+
58
+ ## Options
59
+
60
+ | Option | Type | Default | Description |
61
+ | ---------- | ------------------------ | ------------ | --------------------------------------------------------------------- |
62
+ | `hash` | `string` | — | Any string. Same string = same avatar. |
63
+ | `size` | `number` | `64` | Canvas size in px (square). |
64
+ | `mode` | `'gradient' \| 'dither'` | `'gradient'` | Render style. |
65
+ | `animated` | `boolean` | `false` | Animation loop. |
66
+ | `dotScale` | `number` | — | Dither cell size. If omitted, scales with canvas for consistent look. |
67
+ | `tones` | `string[]` | — | Restrict palette (hex, `oklch()`, CSS color names). |
68
+
69
+ **`createHashvatar(options)`** — returns `{ canvas, colors, destroy }`.
70
+
71
+ **`renderHashvatar(canvas, options)`** — draws into an existing canvas; returns `destroy()`.
72
+
73
+ **`hashToColors(hash, tones?, count?)`** — returns the generated OKLCH colors without rendering.
74
+
75
+ **`hashToSeeds(hash, count)`** — returns `count` deterministic numbers in `[0, 1)` from the hash (for custom rendering or seeding).
76
+
77
+ ---
78
+
79
+ ## Circle / clipping
80
+
81
+ The canvas is **square**. To show a circle:
82
+
83
+ ```css
84
+ canvas {
85
+ border-radius: 50%;
86
+ }
87
+ ```
88
+
89
+ The React component already uses `border-radius: 50%` by default. You can also use a rounded square, hexagon via `clip-path`, etc.
90
+
91
+ ---
92
+
93
+ ## Tone constraints
94
+
95
+ ```js
96
+ // Single hue family
97
+ createHashvatar({ hash, tones: ["hotpink"] });
98
+
99
+ // Multiple
100
+ createHashvatar({ hash, tones: ["#ff6b6b", "#4ecdc4"] });
101
+
102
+ // oklch
103
+ createHashvatar({ hash, tones: ["oklch(0.65 0.25 310)"] });
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Development
109
+
110
+ ```bash
111
+ npm install
112
+ npm run build # ESM + CJS + types (tsup)
113
+ npm run dev # watch & rebuild
114
+ npm run demo # serve on port 5000
115
+ ```
116
+
117
+ ---
118
+
119
+ ## Publish
120
+
121
+ ```bash
122
+ npm run build
123
+ npm publish
124
+ ```
125
+
126
+ ---
127
+
128
+ MIT — [Médhy](https://github.com/medhychabour) · [repo](https://github.com/medhychabour/hashvatar)
package/dist/index.cjs ADDED
@@ -0,0 +1,428 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ createHashvatar: () => createHashvatar,
24
+ hashToColors: () => hashToColors,
25
+ hashToSeeds: () => hashToSeeds,
26
+ oklchToCss: () => oklchToCss,
27
+ oklchToHex: () => oklchToHex,
28
+ parseTone: () => parseTone,
29
+ renderDither: () => renderDither,
30
+ renderGradient: () => renderGradient,
31
+ renderHashvatar: () => renderHashvatar
32
+ });
33
+ module.exports = __toCommonJS(src_exports);
34
+
35
+ // src/hash.ts
36
+ function fnv1a(str) {
37
+ let hash = 2166136261;
38
+ for (let i = 0; i < str.length; i++) {
39
+ hash ^= str.charCodeAt(i);
40
+ hash = hash * 16777619 >>> 0;
41
+ }
42
+ return hash;
43
+ }
44
+ function seededRng(seed) {
45
+ let s = seed;
46
+ return () => {
47
+ s |= 0;
48
+ s = s + 1831565813 | 0;
49
+ let t = Math.imul(s ^ s >>> 15, 1 | s);
50
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
51
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
52
+ };
53
+ }
54
+ function hashToSeeds(hash, count) {
55
+ const base = fnv1a(hash.toLowerCase().trim());
56
+ const rng = seededRng(base);
57
+ return Array.from({ length: count }, () => rng());
58
+ }
59
+
60
+ // src/color.ts
61
+ function hexToRgb(hex) {
62
+ const clean = hex.replace("#", "");
63
+ const full = clean.length === 3 ? clean.split("").map((c) => c + c).join("") : clean;
64
+ const n = parseInt(full, 16);
65
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
66
+ }
67
+ function linearize(c) {
68
+ const s = c / 255;
69
+ return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
70
+ }
71
+ function rgbToOklch(r, g, b) {
72
+ const rl = linearize(r), gl = linearize(g), bl = linearize(b);
73
+ const x = 0.4124564 * rl + 0.3575761 * gl + 0.1804375 * bl;
74
+ const y = 0.2126729 * rl + 0.7151522 * gl + 0.072175 * bl;
75
+ const z = 0.0193339 * rl + 0.119192 * gl + 0.9503041 * bl;
76
+ const lm = Math.cbrt(0.8189330101 * x + 0.3618667424 * y - 0.1288597137 * z);
77
+ const mm = Math.cbrt(0.0329845436 * x + 0.9293118715 * y + 0.0361456387 * z);
78
+ const sm = Math.cbrt(0.0482003018 * x + 0.2643662691 * y + 0.633851707 * z);
79
+ const L = 0.2104542553 * lm + 0.793617785 * mm - 0.0040720468 * sm;
80
+ const a = 1.9779984951 * lm - 2.428592205 * mm + 0.4505937099 * sm;
81
+ const bk = 0.0259040371 * lm + 0.7827717662 * mm - 0.808675766 * sm;
82
+ return { l: L, c: Math.sqrt(a * a + bk * bk), h: (Math.atan2(bk, a) * 180 / Math.PI + 360) % 360 };
83
+ }
84
+ function oklchToHex({ l, c, h }) {
85
+ const hRad = h * Math.PI / 180;
86
+ const a = c * Math.cos(hRad), b = c * Math.sin(hRad);
87
+ const lm = l + 0.3963377774 * a + 0.2158037573 * b;
88
+ const mm = l - 0.1055613458 * a - 0.0638541728 * b;
89
+ const sm = l - 0.0894841775 * a - 1.291485548 * b;
90
+ const L3 = lm * lm * lm, M3 = mm * mm * mm, S3 = sm * sm * sm;
91
+ const rl = 4.0767416621 * L3 - 3.3077115913 * M3 + 0.2309699292 * S3;
92
+ const gl = -1.2684380046 * L3 + 2.6097574011 * M3 - 0.3413193965 * S3;
93
+ const bl = -0.0041960863 * L3 - 0.7034186147 * M3 + 1.707614701 * S3;
94
+ const toSrgb = (v) => {
95
+ const cv = Math.max(0, Math.min(1, v));
96
+ return cv <= 31308e-7 ? cv * 12.92 : 1.055 * Math.pow(cv, 1 / 2.4) - 0.055;
97
+ };
98
+ return "#" + [rl, gl, bl].map((v) => Math.round(toSrgb(v) * 255).toString(16).padStart(2, "0")).join("");
99
+ }
100
+ function oklchToCss({ l, c, h }) {
101
+ return `oklch(${l.toFixed(3)} ${c.toFixed(3)} ${h.toFixed(1)})`;
102
+ }
103
+ function parseTone(tone) {
104
+ const t = tone.trim();
105
+ if (/^[a-zA-Z]+$/.test(t)) {
106
+ if (typeof document === "undefined") return null;
107
+ const tmp = document.createElement("canvas");
108
+ tmp.width = tmp.height = 1;
109
+ const ctx = tmp.getContext("2d");
110
+ ctx.fillStyle = t;
111
+ ctx.fillRect(0, 0, 1, 1);
112
+ const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
113
+ if (r + g + b > 0 || t.toLowerCase() === "black") return rgbToOklch(r, g, b);
114
+ return null;
115
+ }
116
+ if (t.startsWith("#") || /^[0-9a-f]{3,6}$/i.test(t)) {
117
+ const rgb = hexToRgb(t.startsWith("#") ? t : "#" + t);
118
+ return rgbToOklch(...rgb);
119
+ }
120
+ const m = t.match(/oklch\(\s*([\d.]+%?)\s+([\d.]+)\s+([\d.]+)\s*\)/i);
121
+ if (m) {
122
+ const l = m[1].endsWith("%") ? parseFloat(m[1]) / 100 : parseFloat(m[1]);
123
+ return { l, c: parseFloat(m[2]), h: parseFloat(m[3]) };
124
+ }
125
+ return null;
126
+ }
127
+ function generateColor(seed, lSeed, cSeed, tones, isSecondary = false, baseHue) {
128
+ let h, l, c;
129
+ if (tones && tones.length > 0) {
130
+ const ti = Math.floor(seed * tones.length) % tones.length;
131
+ const tone = tones[ti];
132
+ h = (tone.h + (seed * 2 - 1) * 30 + 360) % 360;
133
+ l = isSecondary ? 0.22 + lSeed * 0.18 : 0.52 + lSeed * 0.22;
134
+ c = isSecondary ? Math.max(tone.c * 0.5, 0.06) + cSeed * 0.08 : Math.max(tone.c * 0.8, 0.14) + cSeed * 0.1;
135
+ } else {
136
+ const hue = baseHue ?? seed * 360;
137
+ h = (hue + 360) % 360;
138
+ if (isSecondary) {
139
+ l = 0.18 + cSeed * 0.2;
140
+ c = 0.08 + lSeed * 0.12;
141
+ } else {
142
+ l = 0.55 + lSeed * 0.22;
143
+ c = 0.18 + cSeed * 0.18;
144
+ }
145
+ }
146
+ return { l, c: Math.min(c, 0.37), h };
147
+ }
148
+
149
+ // src/gradient.ts
150
+ var SHAPES = [
151
+ [[0.85, 0.5], [0.75, 0.18], [0.38, 0.22], [0.18, 0.52], [0.38, 0.82], [0.72, 0.78]],
152
+ [[0.22, 0.32], [0.78, 0.28], [0.82, 0.62], [0.5, 0.88], [0.18, 0.68], [0.28, 0.48]],
153
+ [[0.5, 0.12], [0.88, 0.45], [0.72, 0.88], [0.28, 0.82], [0.12, 0.42], [0.35, 0.18]],
154
+ [[0.62, 0.25], [0.9, 0.55], [0.65, 0.9], [0.25, 0.7], [0.1, 0.4], [0.35, 0.15]],
155
+ [[0.15, 0.2], [0.55, 0.08], [0.92, 0.35], [0.78, 0.75], [0.4, 0.92], [0.2, 0.6]],
156
+ [[0.45, 0.08], [0.82, 0.3], [0.7, 0.85], [0.3, 0.88], [0.08, 0.5], [0.25, 0.25]]
157
+ ];
158
+ function drawBlurredShape(ctx, path, size, tx, ty, rotate, scale, fillStyle, offsetX, offsetY) {
159
+ const cx = size / 2;
160
+ const cy = size / 2;
161
+ ctx.save();
162
+ ctx.translate(offsetX + cx, offsetY + cy);
163
+ ctx.translate(tx, ty);
164
+ ctx.rotate(rotate);
165
+ ctx.scale(scale, scale);
166
+ ctx.translate(-cx, -cy);
167
+ ctx.beginPath();
168
+ ctx.moveTo(path[0][0] * size, path[0][1] * size);
169
+ for (let i = 1; i < path.length; i++) {
170
+ ctx.lineTo(path[i][0] * size, path[i][1] * size);
171
+ }
172
+ ctx.closePath();
173
+ ctx.fillStyle = fillStyle;
174
+ ctx.fill();
175
+ ctx.restore();
176
+ }
177
+ function renderGradient(canvas, { size, colors, animated = false, seeds }) {
178
+ if (colors.length < 4) return null;
179
+ canvas.width = size;
180
+ canvas.height = size;
181
+ const ctx = canvas.getContext("2d");
182
+ const mix = seeds[0].toString() + seeds[1].toString() + seeds[2].toString() + seeds[3].toString();
183
+ const allSeeds = hashToSeeds(mix, 24);
184
+ const layers = [0, 1, 2, 3, 4, 5].map((i) => {
185
+ const j = i * 4;
186
+ return {
187
+ tx: (allSeeds[j] - 0.5) * size * 0.35,
188
+ ty: (allSeeds[j + 1] - 0.5) * size * 0.35,
189
+ rotate: (allSeeds[j + 2] - 0.5) * Math.PI * 1.2,
190
+ scale: 0.85 + allSeeds[j + 3] * 0.5
191
+ };
192
+ });
193
+ const hex0 = oklchToHex(colors[0]);
194
+ const hexColors = [oklchToHex(colors[1]), oklchToHex(colors[2]), oklchToHex(colors[3])];
195
+ const LAYER_OPTS = [
196
+ { composite: "source-over", alpha: 0.9 },
197
+ { composite: "overlay", alpha: 0.48 },
198
+ { composite: "soft-light", alpha: 0.7 },
199
+ { composite: "source-over", alpha: 0.78 },
200
+ { composite: "overlay", alpha: 0.4 },
201
+ { composite: "soft-light", alpha: 0.6 }
202
+ ];
203
+ const blur = Math.max(8, Math.round(size * 0.21));
204
+ const pad = Math.ceil(blur * 1.9);
205
+ const ROT_SPEEDS = [0.5, 0.6, 0.45, 0.55, 0.5, 0.65];
206
+ const DRIFT_AMP = size * 0.18;
207
+ const DRIFT_FREQS = [0.5, 0.45, 0.4, 0.48, 0.52, 0.38];
208
+ const DRIFT_PHASE_OFFSETS = [0, 1, 2, 0.5, 1.5, 3];
209
+ const PHASE_SPEED = 1.2;
210
+ const draw = (phase2) => {
211
+ ctx.clearRect(0, 0, size, size);
212
+ ctx.fillStyle = hex0;
213
+ ctx.fillRect(0, 0, size, size);
214
+ const w = size + pad * 2;
215
+ const h = size + pad * 2;
216
+ const offCtx = document.createElement("canvas").getContext("2d");
217
+ offCtx.canvas.width = w;
218
+ offCtx.canvas.height = h;
219
+ for (let i = 0; i < 6; i++) {
220
+ const layer = layers[i];
221
+ const rot = layer.rotate + (animated ? phase2 * ROT_SPEEDS[i] : 0);
222
+ const driftX = animated ? DRIFT_AMP * Math.sin(phase2 * DRIFT_FREQS[i] + DRIFT_PHASE_OFFSETS[i]) : 0;
223
+ const driftY = animated ? DRIFT_AMP * Math.sin(phase2 * DRIFT_FREQS[(i + 2) % 6] + DRIFT_PHASE_OFFSETS[i] * 1.3) : 0;
224
+ const scalePulse = animated ? 1 + 0.15 * Math.sin(phase2 * 0.9 + i * 0.7) : 1;
225
+ const scale = layer.scale * scalePulse;
226
+ const opts = LAYER_OPTS[i];
227
+ const hex = hexColors[i % 3];
228
+ offCtx.clearRect(0, 0, w, h);
229
+ drawBlurredShape(
230
+ offCtx,
231
+ SHAPES[i],
232
+ size,
233
+ layer.tx + driftX,
234
+ layer.ty + driftY,
235
+ rot,
236
+ scale,
237
+ hex,
238
+ pad,
239
+ pad
240
+ );
241
+ ctx.save();
242
+ ctx.filter = `blur(${blur}px)`;
243
+ ctx.globalCompositeOperation = opts.composite;
244
+ ctx.globalAlpha = opts.alpha;
245
+ ctx.drawImage(offCtx.canvas, 0, 0, w, h, -pad, -pad, w, h);
246
+ ctx.restore();
247
+ }
248
+ ctx.globalCompositeOperation = "source-over";
249
+ ctx.globalAlpha = 1;
250
+ };
251
+ if (!animated) {
252
+ draw(0);
253
+ return null;
254
+ }
255
+ let raf;
256
+ let phase = 0;
257
+ let lastTime = 0;
258
+ const tick = (now) => {
259
+ if (lastTime) phase += (now - lastTime) * 1e-3 * PHASE_SPEED;
260
+ lastTime = now;
261
+ draw(phase);
262
+ raf = requestAnimationFrame(tick);
263
+ };
264
+ raf = requestAnimationFrame(tick);
265
+ return () => cancelAnimationFrame(raf);
266
+ }
267
+
268
+ // src/dither.ts
269
+ var BAYER8 = (() => {
270
+ const m = [
271
+ [0, 32, 8, 40, 2, 34, 10, 42],
272
+ [48, 16, 56, 24, 50, 18, 58, 26],
273
+ [12, 44, 4, 36, 14, 46, 6, 38],
274
+ [60, 28, 52, 20, 62, 30, 54, 22],
275
+ [3, 35, 11, 43, 1, 33, 9, 41],
276
+ [51, 19, 59, 27, 49, 17, 57, 25],
277
+ [15, 47, 7, 39, 13, 45, 5, 37],
278
+ [63, 31, 55, 23, 61, 29, 53, 21]
279
+ ];
280
+ return m.map((r) => r.map((v) => v / 64));
281
+ })();
282
+ function hexToRgb2(hex) {
283
+ const n = parseInt(hex.replace("#", ""), 16);
284
+ return { r: n >> 16 & 255, g: n >> 8 & 255, b: n & 255 };
285
+ }
286
+ function renderDither(canvas, { size, colors, dotScale: dotScaleOpt, animated = false, seeds }) {
287
+ canvas.width = size;
288
+ canvas.height = size;
289
+ const ctx = canvas.getContext("2d");
290
+ const dotScale = dotScaleOpt ?? Math.max(2, Math.round(size / 35));
291
+ const colA = hexToRgb2(oklchToHex(colors[0]));
292
+ const colB = hexToRgb2(oklchToHex(colors[Math.min(1, colors.length - 1)]));
293
+ const baseAngle = seeds[0] * Math.PI * 2;
294
+ const falloff = 0.55 + seeds[1] * 0.25;
295
+ const swirlSpeed = 0.45;
296
+ const padding = 1;
297
+ const gridSize = Math.ceil(size / dotScale) + padding * 2;
298
+ const cellPhase = (gx, gy) => ((gx * 31 + gy * 17) * (seeds[2] * 1e3 + 1) + (seeds[3] * 1e3 | 0)) % 1e3 / 1e3 * Math.PI * 2;
299
+ const cellAmp = (gx, gy) => 0.035 + (gx * 7 + gy * 13 + seeds[2] * 50 | 0) % 55 / 1100;
300
+ const draw = (phase2) => {
301
+ const img = ctx.createImageData(size, size);
302
+ const d = img.data;
303
+ const angle = baseAngle + (animated ? phase2 * swirlSpeed : 0);
304
+ const cosA = Math.cos(angle);
305
+ const sinA = Math.sin(angle);
306
+ for (let i = 0; i < size * size * 4; i += 4) {
307
+ d[i] = colB.r;
308
+ d[i + 1] = colB.g;
309
+ d[i + 2] = colB.b;
310
+ d[i + 3] = 255;
311
+ }
312
+ for (let gy = 0; gy < gridSize; gy++) {
313
+ for (let gx = 0; gx < gridSize; gx++) {
314
+ const nx = (gx - padding + 0.5) / (gridSize - padding * 2);
315
+ const ny = (gy - padding + 0.5) / (gridSize - padding * 2);
316
+ const proj = (nx - 0.5) * cosA + (ny - 0.5) * sinA;
317
+ let drift = 0;
318
+ if (animated) {
319
+ const p = cellPhase(gx, gy);
320
+ const a = cellAmp(gx, gy);
321
+ drift = a * Math.sin(phase2 * 0.3 + p) + a * 0.55 * Math.sin(phase2 * 0.1 + p * 1.7) + 0.012 * Math.sin(phase2 * 0.2);
322
+ }
323
+ const tRaw = (proj - drift + falloff) / (falloff * 2);
324
+ const tClamp = Math.max(0, Math.min(1, tRaw));
325
+ const t = tClamp * tClamp * (3 - 2 * tClamp);
326
+ const bayer = BAYER8[gy % 8][gx % 8];
327
+ if (t > bayer) continue;
328
+ for (let py = 0; py < dotScale; py++) {
329
+ for (let px = 0; px < dotScale; px++) {
330
+ const x = (gx - padding) * dotScale + px;
331
+ const y = (gy - padding) * dotScale + py;
332
+ if (x < 0 || y < 0 || x >= size || y >= size) continue;
333
+ const idx = (y * size + x) * 4;
334
+ d[idx] = colA.r;
335
+ d[idx + 1] = colA.g;
336
+ d[idx + 2] = colA.b;
337
+ }
338
+ }
339
+ }
340
+ }
341
+ ctx.putImageData(img, 0, 0);
342
+ };
343
+ if (!animated) {
344
+ draw(0);
345
+ return null;
346
+ }
347
+ let raf;
348
+ let phase = 0;
349
+ let lastTime = 0;
350
+ const SPEED = 0.55;
351
+ const tick = (now) => {
352
+ if (lastTime) phase += (now - lastTime) * 1e-3 * SPEED;
353
+ lastTime = now;
354
+ draw(phase);
355
+ raf = requestAnimationFrame(tick);
356
+ };
357
+ raf = requestAnimationFrame(tick);
358
+ return () => cancelAnimationFrame(raf);
359
+ }
360
+
361
+ // src/index.ts
362
+ function hashToColors(hash, tones, count = 2) {
363
+ const seeds = hashToSeeds(hash, count * 3);
364
+ const parsed = tones?.map(parseTone).filter((t) => t !== null);
365
+ const toneList = parsed?.length ? parsed : void 0;
366
+ const baseHue = toneList ? void 0 : seeds[0] * 360 % 360;
367
+ return Array.from(
368
+ { length: count },
369
+ (_, i) => generateColor(seeds[i * 3], seeds[i * 3 + 1], seeds[i * 3 + 2], toneList, i > 0, baseHue)
370
+ );
371
+ }
372
+ function createHashvatar(options) {
373
+ const {
374
+ hash,
375
+ size = 64,
376
+ mode = "gradient",
377
+ animated = false,
378
+ dotScale,
379
+ tones
380
+ } = options;
381
+ const canvas = document.createElement("canvas");
382
+ const colorCount = mode === "gradient" ? 4 : 2;
383
+ const colors = hashToColors(hash, tones, colorCount);
384
+ const seeds = hashToSeeds(hash, 4);
385
+ let cancel = null;
386
+ if (mode === "dither") {
387
+ cancel = renderDither(canvas, { size, colors, dotScale, animated, seeds });
388
+ } else {
389
+ cancel = renderGradient(canvas, { size, colors, animated, seeds });
390
+ }
391
+ return {
392
+ canvas,
393
+ colors,
394
+ destroy: () => cancel?.()
395
+ };
396
+ }
397
+ function renderHashvatar(canvas, options) {
398
+ const {
399
+ hash,
400
+ size = 64,
401
+ mode = "gradient",
402
+ animated = false,
403
+ dotScale,
404
+ tones
405
+ } = options;
406
+ const colorCount = mode === "gradient" ? 4 : 2;
407
+ const colors = hashToColors(hash, tones, colorCount);
408
+ const seeds = hashToSeeds(hash, 4);
409
+ let cancel = null;
410
+ if (mode === "dither") {
411
+ cancel = renderDither(canvas, { size, colors, dotScale, animated, seeds });
412
+ } else {
413
+ cancel = renderGradient(canvas, { size, colors, animated, seeds });
414
+ }
415
+ return () => cancel?.();
416
+ }
417
+ // Annotate the CommonJS export names for ESM import in node:
418
+ 0 && (module.exports = {
419
+ createHashvatar,
420
+ hashToColors,
421
+ hashToSeeds,
422
+ oklchToCss,
423
+ oklchToHex,
424
+ parseTone,
425
+ renderDither,
426
+ renderGradient,
427
+ renderHashvatar
428
+ });
@@ -0,0 +1,69 @@
1
+ declare function hashToSeeds(hash: string, count: number): number[];
2
+
3
+ interface OklchColor {
4
+ l: number;
5
+ c: number;
6
+ h: number;
7
+ }
8
+ type ToneInput = string;
9
+ declare function oklchToHex({ l, c, h }: OklchColor): string;
10
+ declare function oklchToCss({ l, c, h }: OklchColor): string;
11
+ /**
12
+ * Parse a tone string into OklchColor.
13
+ * Supports: "#ff69b4", "ff69b4", "oklch(0.6 0.25 310)", "red", "hotpink"
14
+ * Note: CSS named color resolution requires a browser environment (uses canvas).
15
+ */
16
+ declare function parseTone(tone: string): OklchColor | null;
17
+
18
+ interface GradientOptions {
19
+ size: number;
20
+ colors: OklchColor[];
21
+ animated?: boolean;
22
+ seeds: number[];
23
+ }
24
+ declare function renderGradient(canvas: HTMLCanvasElement, { size, colors, animated, seeds }: GradientOptions): (() => void) | null;
25
+
26
+ interface DitherOptions {
27
+ size: number;
28
+ colors: OklchColor[];
29
+ dotScale?: number;
30
+ animated?: boolean;
31
+ seeds: number[];
32
+ }
33
+ declare function renderDither(canvas: HTMLCanvasElement, { size, colors, dotScale: dotScaleOpt, animated, seeds }: DitherOptions): (() => void) | null;
34
+
35
+ declare function hashToColors(hash: string, tones?: ToneInput[], count?: number): OklchColor[];
36
+ type Mode = 'gradient' | 'dither';
37
+ interface HashvatarOptions {
38
+ /** Any string: wallet address, username, UUID… */
39
+ hash: string;
40
+ /** Canvas size in px (square). Default: 64 */
41
+ size?: number;
42
+ /** Render mode. Default: 'gradient' */
43
+ mode?: Mode;
44
+ /** Enable animation. Default: false */
45
+ animated?: boolean;
46
+ /** Dot cell size for dither mode. Default: 4 */
47
+ dotScale?: number;
48
+ /**
49
+ * Restrict palette to these hue families.
50
+ * Accepts hex (#ff69b4), oklch(l c h), or CSS color names (red, hotpink…)
51
+ */
52
+ tones?: ToneInput[];
53
+ }
54
+ interface HashvatarResult {
55
+ /** The rendered canvas element */
56
+ canvas: HTMLCanvasElement;
57
+ /** The generated colors in OKLCH */
58
+ colors: OklchColor[];
59
+ /** Call to stop animation loop (no-op if not animated) */
60
+ destroy: () => void;
61
+ }
62
+ declare function createHashvatar(options: HashvatarOptions): HashvatarResult;
63
+ /**
64
+ * Convenience: render into an existing canvas element.
65
+ * Useful when you already have a <canvas> in the DOM.
66
+ */
67
+ declare function renderHashvatar(canvas: HTMLCanvasElement, options: HashvatarOptions): () => void;
68
+
69
+ export { type HashvatarOptions, type HashvatarResult, type Mode, type OklchColor, type ToneInput, createHashvatar, hashToColors, hashToSeeds, oklchToCss, oklchToHex, parseTone, renderDither, renderGradient, renderHashvatar };
@@ -0,0 +1,69 @@
1
+ declare function hashToSeeds(hash: string, count: number): number[];
2
+
3
+ interface OklchColor {
4
+ l: number;
5
+ c: number;
6
+ h: number;
7
+ }
8
+ type ToneInput = string;
9
+ declare function oklchToHex({ l, c, h }: OklchColor): string;
10
+ declare function oklchToCss({ l, c, h }: OklchColor): string;
11
+ /**
12
+ * Parse a tone string into OklchColor.
13
+ * Supports: "#ff69b4", "ff69b4", "oklch(0.6 0.25 310)", "red", "hotpink"
14
+ * Note: CSS named color resolution requires a browser environment (uses canvas).
15
+ */
16
+ declare function parseTone(tone: string): OklchColor | null;
17
+
18
+ interface GradientOptions {
19
+ size: number;
20
+ colors: OklchColor[];
21
+ animated?: boolean;
22
+ seeds: number[];
23
+ }
24
+ declare function renderGradient(canvas: HTMLCanvasElement, { size, colors, animated, seeds }: GradientOptions): (() => void) | null;
25
+
26
+ interface DitherOptions {
27
+ size: number;
28
+ colors: OklchColor[];
29
+ dotScale?: number;
30
+ animated?: boolean;
31
+ seeds: number[];
32
+ }
33
+ declare function renderDither(canvas: HTMLCanvasElement, { size, colors, dotScale: dotScaleOpt, animated, seeds }: DitherOptions): (() => void) | null;
34
+
35
+ declare function hashToColors(hash: string, tones?: ToneInput[], count?: number): OklchColor[];
36
+ type Mode = 'gradient' | 'dither';
37
+ interface HashvatarOptions {
38
+ /** Any string: wallet address, username, UUID… */
39
+ hash: string;
40
+ /** Canvas size in px (square). Default: 64 */
41
+ size?: number;
42
+ /** Render mode. Default: 'gradient' */
43
+ mode?: Mode;
44
+ /** Enable animation. Default: false */
45
+ animated?: boolean;
46
+ /** Dot cell size for dither mode. Default: 4 */
47
+ dotScale?: number;
48
+ /**
49
+ * Restrict palette to these hue families.
50
+ * Accepts hex (#ff69b4), oklch(l c h), or CSS color names (red, hotpink…)
51
+ */
52
+ tones?: ToneInput[];
53
+ }
54
+ interface HashvatarResult {
55
+ /** The rendered canvas element */
56
+ canvas: HTMLCanvasElement;
57
+ /** The generated colors in OKLCH */
58
+ colors: OklchColor[];
59
+ /** Call to stop animation loop (no-op if not animated) */
60
+ destroy: () => void;
61
+ }
62
+ declare function createHashvatar(options: HashvatarOptions): HashvatarResult;
63
+ /**
64
+ * Convenience: render into an existing canvas element.
65
+ * Useful when you already have a <canvas> in the DOM.
66
+ */
67
+ declare function renderHashvatar(canvas: HTMLCanvasElement, options: HashvatarOptions): () => void;
68
+
69
+ export { type HashvatarOptions, type HashvatarResult, type Mode, type OklchColor, type ToneInput, createHashvatar, hashToColors, hashToSeeds, oklchToCss, oklchToHex, parseTone, renderDither, renderGradient, renderHashvatar };
package/dist/index.js ADDED
@@ -0,0 +1,393 @@
1
+ // src/hash.ts
2
+ function fnv1a(str) {
3
+ let hash = 2166136261;
4
+ for (let i = 0; i < str.length; i++) {
5
+ hash ^= str.charCodeAt(i);
6
+ hash = hash * 16777619 >>> 0;
7
+ }
8
+ return hash;
9
+ }
10
+ function seededRng(seed) {
11
+ let s = seed;
12
+ return () => {
13
+ s |= 0;
14
+ s = s + 1831565813 | 0;
15
+ let t = Math.imul(s ^ s >>> 15, 1 | s);
16
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
17
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
18
+ };
19
+ }
20
+ function hashToSeeds(hash, count) {
21
+ const base = fnv1a(hash.toLowerCase().trim());
22
+ const rng = seededRng(base);
23
+ return Array.from({ length: count }, () => rng());
24
+ }
25
+
26
+ // src/color.ts
27
+ function hexToRgb(hex) {
28
+ const clean = hex.replace("#", "");
29
+ const full = clean.length === 3 ? clean.split("").map((c) => c + c).join("") : clean;
30
+ const n = parseInt(full, 16);
31
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
32
+ }
33
+ function linearize(c) {
34
+ const s = c / 255;
35
+ return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
36
+ }
37
+ function rgbToOklch(r, g, b) {
38
+ const rl = linearize(r), gl = linearize(g), bl = linearize(b);
39
+ const x = 0.4124564 * rl + 0.3575761 * gl + 0.1804375 * bl;
40
+ const y = 0.2126729 * rl + 0.7151522 * gl + 0.072175 * bl;
41
+ const z = 0.0193339 * rl + 0.119192 * gl + 0.9503041 * bl;
42
+ const lm = Math.cbrt(0.8189330101 * x + 0.3618667424 * y - 0.1288597137 * z);
43
+ const mm = Math.cbrt(0.0329845436 * x + 0.9293118715 * y + 0.0361456387 * z);
44
+ const sm = Math.cbrt(0.0482003018 * x + 0.2643662691 * y + 0.633851707 * z);
45
+ const L = 0.2104542553 * lm + 0.793617785 * mm - 0.0040720468 * sm;
46
+ const a = 1.9779984951 * lm - 2.428592205 * mm + 0.4505937099 * sm;
47
+ const bk = 0.0259040371 * lm + 0.7827717662 * mm - 0.808675766 * sm;
48
+ return { l: L, c: Math.sqrt(a * a + bk * bk), h: (Math.atan2(bk, a) * 180 / Math.PI + 360) % 360 };
49
+ }
50
+ function oklchToHex({ l, c, h }) {
51
+ const hRad = h * Math.PI / 180;
52
+ const a = c * Math.cos(hRad), b = c * Math.sin(hRad);
53
+ const lm = l + 0.3963377774 * a + 0.2158037573 * b;
54
+ const mm = l - 0.1055613458 * a - 0.0638541728 * b;
55
+ const sm = l - 0.0894841775 * a - 1.291485548 * b;
56
+ const L3 = lm * lm * lm, M3 = mm * mm * mm, S3 = sm * sm * sm;
57
+ const rl = 4.0767416621 * L3 - 3.3077115913 * M3 + 0.2309699292 * S3;
58
+ const gl = -1.2684380046 * L3 + 2.6097574011 * M3 - 0.3413193965 * S3;
59
+ const bl = -0.0041960863 * L3 - 0.7034186147 * M3 + 1.707614701 * S3;
60
+ const toSrgb = (v) => {
61
+ const cv = Math.max(0, Math.min(1, v));
62
+ return cv <= 31308e-7 ? cv * 12.92 : 1.055 * Math.pow(cv, 1 / 2.4) - 0.055;
63
+ };
64
+ return "#" + [rl, gl, bl].map((v) => Math.round(toSrgb(v) * 255).toString(16).padStart(2, "0")).join("");
65
+ }
66
+ function oklchToCss({ l, c, h }) {
67
+ return `oklch(${l.toFixed(3)} ${c.toFixed(3)} ${h.toFixed(1)})`;
68
+ }
69
+ function parseTone(tone) {
70
+ const t = tone.trim();
71
+ if (/^[a-zA-Z]+$/.test(t)) {
72
+ if (typeof document === "undefined") return null;
73
+ const tmp = document.createElement("canvas");
74
+ tmp.width = tmp.height = 1;
75
+ const ctx = tmp.getContext("2d");
76
+ ctx.fillStyle = t;
77
+ ctx.fillRect(0, 0, 1, 1);
78
+ const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
79
+ if (r + g + b > 0 || t.toLowerCase() === "black") return rgbToOklch(r, g, b);
80
+ return null;
81
+ }
82
+ if (t.startsWith("#") || /^[0-9a-f]{3,6}$/i.test(t)) {
83
+ const rgb = hexToRgb(t.startsWith("#") ? t : "#" + t);
84
+ return rgbToOklch(...rgb);
85
+ }
86
+ const m = t.match(/oklch\(\s*([\d.]+%?)\s+([\d.]+)\s+([\d.]+)\s*\)/i);
87
+ if (m) {
88
+ const l = m[1].endsWith("%") ? parseFloat(m[1]) / 100 : parseFloat(m[1]);
89
+ return { l, c: parseFloat(m[2]), h: parseFloat(m[3]) };
90
+ }
91
+ return null;
92
+ }
93
+ function generateColor(seed, lSeed, cSeed, tones, isSecondary = false, baseHue) {
94
+ let h, l, c;
95
+ if (tones && tones.length > 0) {
96
+ const ti = Math.floor(seed * tones.length) % tones.length;
97
+ const tone = tones[ti];
98
+ h = (tone.h + (seed * 2 - 1) * 30 + 360) % 360;
99
+ l = isSecondary ? 0.22 + lSeed * 0.18 : 0.52 + lSeed * 0.22;
100
+ c = isSecondary ? Math.max(tone.c * 0.5, 0.06) + cSeed * 0.08 : Math.max(tone.c * 0.8, 0.14) + cSeed * 0.1;
101
+ } else {
102
+ const hue = baseHue ?? seed * 360;
103
+ h = (hue + 360) % 360;
104
+ if (isSecondary) {
105
+ l = 0.18 + cSeed * 0.2;
106
+ c = 0.08 + lSeed * 0.12;
107
+ } else {
108
+ l = 0.55 + lSeed * 0.22;
109
+ c = 0.18 + cSeed * 0.18;
110
+ }
111
+ }
112
+ return { l, c: Math.min(c, 0.37), h };
113
+ }
114
+
115
+ // src/gradient.ts
116
+ var SHAPES = [
117
+ [[0.85, 0.5], [0.75, 0.18], [0.38, 0.22], [0.18, 0.52], [0.38, 0.82], [0.72, 0.78]],
118
+ [[0.22, 0.32], [0.78, 0.28], [0.82, 0.62], [0.5, 0.88], [0.18, 0.68], [0.28, 0.48]],
119
+ [[0.5, 0.12], [0.88, 0.45], [0.72, 0.88], [0.28, 0.82], [0.12, 0.42], [0.35, 0.18]],
120
+ [[0.62, 0.25], [0.9, 0.55], [0.65, 0.9], [0.25, 0.7], [0.1, 0.4], [0.35, 0.15]],
121
+ [[0.15, 0.2], [0.55, 0.08], [0.92, 0.35], [0.78, 0.75], [0.4, 0.92], [0.2, 0.6]],
122
+ [[0.45, 0.08], [0.82, 0.3], [0.7, 0.85], [0.3, 0.88], [0.08, 0.5], [0.25, 0.25]]
123
+ ];
124
+ function drawBlurredShape(ctx, path, size, tx, ty, rotate, scale, fillStyle, offsetX, offsetY) {
125
+ const cx = size / 2;
126
+ const cy = size / 2;
127
+ ctx.save();
128
+ ctx.translate(offsetX + cx, offsetY + cy);
129
+ ctx.translate(tx, ty);
130
+ ctx.rotate(rotate);
131
+ ctx.scale(scale, scale);
132
+ ctx.translate(-cx, -cy);
133
+ ctx.beginPath();
134
+ ctx.moveTo(path[0][0] * size, path[0][1] * size);
135
+ for (let i = 1; i < path.length; i++) {
136
+ ctx.lineTo(path[i][0] * size, path[i][1] * size);
137
+ }
138
+ ctx.closePath();
139
+ ctx.fillStyle = fillStyle;
140
+ ctx.fill();
141
+ ctx.restore();
142
+ }
143
+ function renderGradient(canvas, { size, colors, animated = false, seeds }) {
144
+ if (colors.length < 4) return null;
145
+ canvas.width = size;
146
+ canvas.height = size;
147
+ const ctx = canvas.getContext("2d");
148
+ const mix = seeds[0].toString() + seeds[1].toString() + seeds[2].toString() + seeds[3].toString();
149
+ const allSeeds = hashToSeeds(mix, 24);
150
+ const layers = [0, 1, 2, 3, 4, 5].map((i) => {
151
+ const j = i * 4;
152
+ return {
153
+ tx: (allSeeds[j] - 0.5) * size * 0.35,
154
+ ty: (allSeeds[j + 1] - 0.5) * size * 0.35,
155
+ rotate: (allSeeds[j + 2] - 0.5) * Math.PI * 1.2,
156
+ scale: 0.85 + allSeeds[j + 3] * 0.5
157
+ };
158
+ });
159
+ const hex0 = oklchToHex(colors[0]);
160
+ const hexColors = [oklchToHex(colors[1]), oklchToHex(colors[2]), oklchToHex(colors[3])];
161
+ const LAYER_OPTS = [
162
+ { composite: "source-over", alpha: 0.9 },
163
+ { composite: "overlay", alpha: 0.48 },
164
+ { composite: "soft-light", alpha: 0.7 },
165
+ { composite: "source-over", alpha: 0.78 },
166
+ { composite: "overlay", alpha: 0.4 },
167
+ { composite: "soft-light", alpha: 0.6 }
168
+ ];
169
+ const blur = Math.max(8, Math.round(size * 0.21));
170
+ const pad = Math.ceil(blur * 1.9);
171
+ const ROT_SPEEDS = [0.5, 0.6, 0.45, 0.55, 0.5, 0.65];
172
+ const DRIFT_AMP = size * 0.18;
173
+ const DRIFT_FREQS = [0.5, 0.45, 0.4, 0.48, 0.52, 0.38];
174
+ const DRIFT_PHASE_OFFSETS = [0, 1, 2, 0.5, 1.5, 3];
175
+ const PHASE_SPEED = 1.2;
176
+ const draw = (phase2) => {
177
+ ctx.clearRect(0, 0, size, size);
178
+ ctx.fillStyle = hex0;
179
+ ctx.fillRect(0, 0, size, size);
180
+ const w = size + pad * 2;
181
+ const h = size + pad * 2;
182
+ const offCtx = document.createElement("canvas").getContext("2d");
183
+ offCtx.canvas.width = w;
184
+ offCtx.canvas.height = h;
185
+ for (let i = 0; i < 6; i++) {
186
+ const layer = layers[i];
187
+ const rot = layer.rotate + (animated ? phase2 * ROT_SPEEDS[i] : 0);
188
+ const driftX = animated ? DRIFT_AMP * Math.sin(phase2 * DRIFT_FREQS[i] + DRIFT_PHASE_OFFSETS[i]) : 0;
189
+ const driftY = animated ? DRIFT_AMP * Math.sin(phase2 * DRIFT_FREQS[(i + 2) % 6] + DRIFT_PHASE_OFFSETS[i] * 1.3) : 0;
190
+ const scalePulse = animated ? 1 + 0.15 * Math.sin(phase2 * 0.9 + i * 0.7) : 1;
191
+ const scale = layer.scale * scalePulse;
192
+ const opts = LAYER_OPTS[i];
193
+ const hex = hexColors[i % 3];
194
+ offCtx.clearRect(0, 0, w, h);
195
+ drawBlurredShape(
196
+ offCtx,
197
+ SHAPES[i],
198
+ size,
199
+ layer.tx + driftX,
200
+ layer.ty + driftY,
201
+ rot,
202
+ scale,
203
+ hex,
204
+ pad,
205
+ pad
206
+ );
207
+ ctx.save();
208
+ ctx.filter = `blur(${blur}px)`;
209
+ ctx.globalCompositeOperation = opts.composite;
210
+ ctx.globalAlpha = opts.alpha;
211
+ ctx.drawImage(offCtx.canvas, 0, 0, w, h, -pad, -pad, w, h);
212
+ ctx.restore();
213
+ }
214
+ ctx.globalCompositeOperation = "source-over";
215
+ ctx.globalAlpha = 1;
216
+ };
217
+ if (!animated) {
218
+ draw(0);
219
+ return null;
220
+ }
221
+ let raf;
222
+ let phase = 0;
223
+ let lastTime = 0;
224
+ const tick = (now) => {
225
+ if (lastTime) phase += (now - lastTime) * 1e-3 * PHASE_SPEED;
226
+ lastTime = now;
227
+ draw(phase);
228
+ raf = requestAnimationFrame(tick);
229
+ };
230
+ raf = requestAnimationFrame(tick);
231
+ return () => cancelAnimationFrame(raf);
232
+ }
233
+
234
+ // src/dither.ts
235
+ var BAYER8 = (() => {
236
+ const m = [
237
+ [0, 32, 8, 40, 2, 34, 10, 42],
238
+ [48, 16, 56, 24, 50, 18, 58, 26],
239
+ [12, 44, 4, 36, 14, 46, 6, 38],
240
+ [60, 28, 52, 20, 62, 30, 54, 22],
241
+ [3, 35, 11, 43, 1, 33, 9, 41],
242
+ [51, 19, 59, 27, 49, 17, 57, 25],
243
+ [15, 47, 7, 39, 13, 45, 5, 37],
244
+ [63, 31, 55, 23, 61, 29, 53, 21]
245
+ ];
246
+ return m.map((r) => r.map((v) => v / 64));
247
+ })();
248
+ function hexToRgb2(hex) {
249
+ const n = parseInt(hex.replace("#", ""), 16);
250
+ return { r: n >> 16 & 255, g: n >> 8 & 255, b: n & 255 };
251
+ }
252
+ function renderDither(canvas, { size, colors, dotScale: dotScaleOpt, animated = false, seeds }) {
253
+ canvas.width = size;
254
+ canvas.height = size;
255
+ const ctx = canvas.getContext("2d");
256
+ const dotScale = dotScaleOpt ?? Math.max(2, Math.round(size / 35));
257
+ const colA = hexToRgb2(oklchToHex(colors[0]));
258
+ const colB = hexToRgb2(oklchToHex(colors[Math.min(1, colors.length - 1)]));
259
+ const baseAngle = seeds[0] * Math.PI * 2;
260
+ const falloff = 0.55 + seeds[1] * 0.25;
261
+ const swirlSpeed = 0.45;
262
+ const padding = 1;
263
+ const gridSize = Math.ceil(size / dotScale) + padding * 2;
264
+ const cellPhase = (gx, gy) => ((gx * 31 + gy * 17) * (seeds[2] * 1e3 + 1) + (seeds[3] * 1e3 | 0)) % 1e3 / 1e3 * Math.PI * 2;
265
+ const cellAmp = (gx, gy) => 0.035 + (gx * 7 + gy * 13 + seeds[2] * 50 | 0) % 55 / 1100;
266
+ const draw = (phase2) => {
267
+ const img = ctx.createImageData(size, size);
268
+ const d = img.data;
269
+ const angle = baseAngle + (animated ? phase2 * swirlSpeed : 0);
270
+ const cosA = Math.cos(angle);
271
+ const sinA = Math.sin(angle);
272
+ for (let i = 0; i < size * size * 4; i += 4) {
273
+ d[i] = colB.r;
274
+ d[i + 1] = colB.g;
275
+ d[i + 2] = colB.b;
276
+ d[i + 3] = 255;
277
+ }
278
+ for (let gy = 0; gy < gridSize; gy++) {
279
+ for (let gx = 0; gx < gridSize; gx++) {
280
+ const nx = (gx - padding + 0.5) / (gridSize - padding * 2);
281
+ const ny = (gy - padding + 0.5) / (gridSize - padding * 2);
282
+ const proj = (nx - 0.5) * cosA + (ny - 0.5) * sinA;
283
+ let drift = 0;
284
+ if (animated) {
285
+ const p = cellPhase(gx, gy);
286
+ const a = cellAmp(gx, gy);
287
+ drift = a * Math.sin(phase2 * 0.3 + p) + a * 0.55 * Math.sin(phase2 * 0.1 + p * 1.7) + 0.012 * Math.sin(phase2 * 0.2);
288
+ }
289
+ const tRaw = (proj - drift + falloff) / (falloff * 2);
290
+ const tClamp = Math.max(0, Math.min(1, tRaw));
291
+ const t = tClamp * tClamp * (3 - 2 * tClamp);
292
+ const bayer = BAYER8[gy % 8][gx % 8];
293
+ if (t > bayer) continue;
294
+ for (let py = 0; py < dotScale; py++) {
295
+ for (let px = 0; px < dotScale; px++) {
296
+ const x = (gx - padding) * dotScale + px;
297
+ const y = (gy - padding) * dotScale + py;
298
+ if (x < 0 || y < 0 || x >= size || y >= size) continue;
299
+ const idx = (y * size + x) * 4;
300
+ d[idx] = colA.r;
301
+ d[idx + 1] = colA.g;
302
+ d[idx + 2] = colA.b;
303
+ }
304
+ }
305
+ }
306
+ }
307
+ ctx.putImageData(img, 0, 0);
308
+ };
309
+ if (!animated) {
310
+ draw(0);
311
+ return null;
312
+ }
313
+ let raf;
314
+ let phase = 0;
315
+ let lastTime = 0;
316
+ const SPEED = 0.55;
317
+ const tick = (now) => {
318
+ if (lastTime) phase += (now - lastTime) * 1e-3 * SPEED;
319
+ lastTime = now;
320
+ draw(phase);
321
+ raf = requestAnimationFrame(tick);
322
+ };
323
+ raf = requestAnimationFrame(tick);
324
+ return () => cancelAnimationFrame(raf);
325
+ }
326
+
327
+ // src/index.ts
328
+ function hashToColors(hash, tones, count = 2) {
329
+ const seeds = hashToSeeds(hash, count * 3);
330
+ const parsed = tones?.map(parseTone).filter((t) => t !== null);
331
+ const toneList = parsed?.length ? parsed : void 0;
332
+ const baseHue = toneList ? void 0 : seeds[0] * 360 % 360;
333
+ return Array.from(
334
+ { length: count },
335
+ (_, i) => generateColor(seeds[i * 3], seeds[i * 3 + 1], seeds[i * 3 + 2], toneList, i > 0, baseHue)
336
+ );
337
+ }
338
+ function createHashvatar(options) {
339
+ const {
340
+ hash,
341
+ size = 64,
342
+ mode = "gradient",
343
+ animated = false,
344
+ dotScale,
345
+ tones
346
+ } = options;
347
+ const canvas = document.createElement("canvas");
348
+ const colorCount = mode === "gradient" ? 4 : 2;
349
+ const colors = hashToColors(hash, tones, colorCount);
350
+ const seeds = hashToSeeds(hash, 4);
351
+ let cancel = null;
352
+ if (mode === "dither") {
353
+ cancel = renderDither(canvas, { size, colors, dotScale, animated, seeds });
354
+ } else {
355
+ cancel = renderGradient(canvas, { size, colors, animated, seeds });
356
+ }
357
+ return {
358
+ canvas,
359
+ colors,
360
+ destroy: () => cancel?.()
361
+ };
362
+ }
363
+ function renderHashvatar(canvas, options) {
364
+ const {
365
+ hash,
366
+ size = 64,
367
+ mode = "gradient",
368
+ animated = false,
369
+ dotScale,
370
+ tones
371
+ } = options;
372
+ const colorCount = mode === "gradient" ? 4 : 2;
373
+ const colors = hashToColors(hash, tones, colorCount);
374
+ const seeds = hashToSeeds(hash, 4);
375
+ let cancel = null;
376
+ if (mode === "dither") {
377
+ cancel = renderDither(canvas, { size, colors, dotScale, animated, seeds });
378
+ } else {
379
+ cancel = renderGradient(canvas, { size, colors, animated, seeds });
380
+ }
381
+ return () => cancel?.();
382
+ }
383
+ export {
384
+ createHashvatar,
385
+ hashToColors,
386
+ hashToSeeds,
387
+ oklchToCss,
388
+ oklchToHex,
389
+ parseTone,
390
+ renderDither,
391
+ renderGradient,
392
+ renderHashvatar
393
+ };
package/dist/react.cjs ADDED
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/react.tsx
21
+ var react_exports = {};
22
+ __export(react_exports, {
23
+ Hashvatar: () => Hashvatar
24
+ });
25
+ module.exports = __toCommonJS(react_exports);
26
+ var import_react = require("react");
27
+ var import_index = require("./index");
28
+ var import_jsx_runtime = require("react/jsx-runtime");
29
+ function Hashvatar({ className, style, ...options }) {
30
+ const ref = (0, import_react.useRef)(null);
31
+ (0, import_react.useEffect)(() => {
32
+ if (!ref.current) return;
33
+ const destroy = (0, import_index.renderHashvatar)(ref.current, options);
34
+ return destroy;
35
+ }, [
36
+ options.hash,
37
+ options.size,
38
+ options.mode,
39
+ options.animated,
40
+ options.dotScale,
41
+ JSON.stringify(options.tones)
42
+ ]);
43
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
44
+ "canvas",
45
+ {
46
+ ref,
47
+ className,
48
+ style: { borderRadius: "50%", display: "block", ...style }
49
+ }
50
+ );
51
+ }
52
+ // Annotate the CommonJS export names for ESM import in node:
53
+ 0 && (module.exports = {
54
+ Hashvatar
55
+ });
@@ -0,0 +1,19 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { HashvatarOptions } from './index';
3
+
4
+ interface HashvatarProps extends HashvatarOptions {
5
+ /** CSS class on the <canvas> element */
6
+ className?: string;
7
+ /** Inline style */
8
+ style?: React.CSSProperties;
9
+ }
10
+ /**
11
+ * React component wrapper for hashvatar.
12
+ *
13
+ * @example
14
+ * <Hashvatar hash="vitalik.eth" size={48} mode="dither" />
15
+ * <Hashvatar hash="0xABC..." size={64} tones={['hotpink']} animated />
16
+ */
17
+ declare function Hashvatar({ className, style, ...options }: HashvatarProps): react_jsx_runtime.JSX.Element;
18
+
19
+ export { Hashvatar, type HashvatarProps };
@@ -0,0 +1,19 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { HashvatarOptions } from './index';
3
+
4
+ interface HashvatarProps extends HashvatarOptions {
5
+ /** CSS class on the <canvas> element */
6
+ className?: string;
7
+ /** Inline style */
8
+ style?: React.CSSProperties;
9
+ }
10
+ /**
11
+ * React component wrapper for hashvatar.
12
+ *
13
+ * @example
14
+ * <Hashvatar hash="vitalik.eth" size={48} mode="dither" />
15
+ * <Hashvatar hash="0xABC..." size={64} tones={['hotpink']} animated />
16
+ */
17
+ declare function Hashvatar({ className, style, ...options }: HashvatarProps): react_jsx_runtime.JSX.Element;
18
+
19
+ export { Hashvatar, type HashvatarProps };
package/dist/react.js ADDED
@@ -0,0 +1,30 @@
1
+ // src/react.tsx
2
+ import { useEffect, useRef } from "react";
3
+ import { renderHashvatar } from "./index";
4
+ import { jsx } from "react/jsx-runtime";
5
+ function Hashvatar({ className, style, ...options }) {
6
+ const ref = useRef(null);
7
+ useEffect(() => {
8
+ if (!ref.current) return;
9
+ const destroy = renderHashvatar(ref.current, options);
10
+ return destroy;
11
+ }, [
12
+ options.hash,
13
+ options.size,
14
+ options.mode,
15
+ options.animated,
16
+ options.dotScale,
17
+ JSON.stringify(options.tones)
18
+ ]);
19
+ return /* @__PURE__ */ jsx(
20
+ "canvas",
21
+ {
22
+ ref,
23
+ className,
24
+ style: { borderRadius: "50%", display: "block", ...style }
25
+ }
26
+ );
27
+ }
28
+ export {
29
+ Hashvatar
30
+ };
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "hashvatar",
3
+ "version": "0.1.0",
4
+ "description": "Deterministic avatar generation from any hash string. Zero dependencies.",
5
+ "author": "Médhy",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ },
17
+ "./react": {
18
+ "types": "./dist/react.d.ts",
19
+ "import": "./dist/react.js",
20
+ "require": "./dist/react.cjs"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "sideEffects": false,
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "dev": "tsup --watch",
30
+ "demo": "npx serve . -p 5000",
31
+ "prepublishOnly": "npm run build"
32
+ },
33
+ "devDependencies": {
34
+ "@types/react": "^18.0.0",
35
+ "react": "^18.0.0",
36
+ "tsup": "^8.0.0",
37
+ "typescript": "^5.0.0"
38
+ },
39
+ "peerDependencies": {
40
+ "react": ">=17"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "react": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "keywords": [
48
+ "avatar",
49
+ "identicon",
50
+ "jazzicon",
51
+ "web3",
52
+ "wallet",
53
+ "deterministic",
54
+ "gradient",
55
+ "dither",
56
+ "halftone"
57
+ ],
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "https://github.com/medhychabour/hashvatar"
61
+ }
62
+ }