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 +21 -0
- package/README.md +128 -0
- package/dist/index.cjs +428 -0
- package/dist/index.d.cts +69 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +393 -0
- package/dist/react.cjs +55 -0
- package/dist/react.d.cts +19 -0
- package/dist/react.d.ts +19 -0
- package/dist/react.js +30 -0
- package/package.json +62 -0
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
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
});
|
package/dist/react.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|