@apollo-deploy/avatars 0.2.2
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 +114 -0
- package/dist/index.cjs +344 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +106 -0
- package/dist/index.d.ts +106 -0
- package/dist/index.js +335 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Outpace Studios
|
|
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,114 @@
|
|
|
1
|
+
<a href="https://avatars.outpacestudios.com">
|
|
2
|
+
<img src="https://avatars.outpacestudios.com/meta.jpg" alt="@apollo-deploy/avatars, by Outpace Studios" width="100%" />
|
|
3
|
+
</a>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">@apollo-deploy/avatars</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
Generative gradient avatars for React. A unique gradient for every seed, with no stored images and no network.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/@apollo-deploy/avatars"><img src="https://img.shields.io/npm/v/@apollo-deploy/avatars?color=000&labelColor=000" alt="npm version" /></a>
|
|
13
|
+
<a href="https://bundlephobia.com/package/@apollo-deploy/avatars"><img src="https://img.shields.io/badge/gzipped-2.3_kB-000?labelColor=000" alt="gzipped size" /></a>
|
|
14
|
+
<img src="https://img.shields.io/badge/dependencies-0-000?labelColor=000" alt="zero dependencies" />
|
|
15
|
+
<img src="https://img.shields.io/badge/types-included-000?labelColor=000" alt="types included" />
|
|
16
|
+
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-000?labelColor=000" alt="license" /></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<a href="https://avatars.outpacestudios.com"><b>Live playground →</b></a> ·
|
|
21
|
+
<a href="https://avatars.outpacestudios.com/docs"><b>Docs →</b></a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
Give it any string or number (a user id, an email, a username) and it paints a
|
|
27
|
+
unique, good-looking gradient on a `<canvas>`. The same seed always yields the
|
|
28
|
+
same gradient, so you get stable avatars with **nothing to store and nothing to
|
|
29
|
+
fetch**. The gradient engine is bundled in, so this is the only thing you install.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm i @apollo-deploy/avatars # or: pnpm add / yarn add / bun add
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`react >= 18` is the only peer dependency. Works with React 18 and 19.
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { GradientAvatar } from "@apollo-deploy/avatars";
|
|
43
|
+
|
|
44
|
+
function UserAvatar({ user }) {
|
|
45
|
+
return <GradientAvatar seed={user.id} size={40} />;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
That's the whole API surface for most apps. A few more:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
<GradientAvatar seed="jane@example.com" size={96} /> {/* circle (default) */}
|
|
53
|
+
<GradientAvatar seed="jane@example.com" size={96} radius={16} /> {/* rounded square */}
|
|
54
|
+
<GradientAvatar seed="jane@example.com" size={96} radius={0} /> {/* square */}
|
|
55
|
+
<GradientAvatar seed={42} size={64} className="ring-2 ring-white/10" />
|
|
56
|
+
<GradientAvatar seed="jane@example.com" size={96} animate /> {/* living gradient */}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Why @apollo-deploy/avatars
|
|
60
|
+
|
|
61
|
+
- **Deterministic.** Same seed, same gradient, every time. A user id or email *is* the avatar; you never store or migrate an image.
|
|
62
|
+
- **No images, no network.** Rendered at runtime on a `<canvas>`. No CDN, no requests, no broken `<img>` links, no upload pipeline.
|
|
63
|
+
- **Tiny & zero-dependency.** ~2.3 kB gzipped; `react` is the only peer.
|
|
64
|
+
- **Actually pretty.** Soft mesh gradients, not blocky identicons.
|
|
65
|
+
- **Any size, any shape.** Circles, rounded squares, hard squares: your call.
|
|
66
|
+
- **Exports anywhere.** Built-in helpers turn a seed into a data URL, a `Blob`, or a full-resolution image for downloads and clipboard.
|
|
67
|
+
- **Typed.** Ships with TypeScript declarations.
|
|
68
|
+
|
|
69
|
+
## Props
|
|
70
|
+
|
|
71
|
+
| Prop | Type | Default | Description |
|
|
72
|
+
|------|------|---------|-------------|
|
|
73
|
+
| `seed` | `string \| number` | None | Any value; each unique seed is a unique gradient. |
|
|
74
|
+
| `size` | `number` | `32` | Rendered size in pixels. |
|
|
75
|
+
| `radius` | `number \| string` | `"9999px"` | Corner radius. Number = pixels, string = any CSS length. Defaults to a full circle; pass `0` for a square. |
|
|
76
|
+
| `animate` | `boolean` | `false` | Slowly drift the gradient. The canvas is still painted once; the motion is a GPU-only CSS transform on the cached layer, so there's no per-frame JS, no repaint, and no extra memory. Respects `prefers-reduced-motion`. |
|
|
77
|
+
| `animationDuration` | `number` | `16` | Seconds per drift cycle. Slower is subtler and cheaper. Only applies when `animate` is on. |
|
|
78
|
+
| `className` | `string` | None | Extra classes on the wrapper `<span>`. |
|
|
79
|
+
| `style` | `CSSProperties` | None | Extra inline styles merged onto the wrapper. |
|
|
80
|
+
|
|
81
|
+
## Beyond React: the engine
|
|
82
|
+
|
|
83
|
+
The framework-agnostic engine is re-exported, so you can generate gradients
|
|
84
|
+
without rendering a component, handy for an `<img src>`, a download button, or
|
|
85
|
+
a server-rendered preview.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { gradientToDataURL, generatePalette } from "@apollo-deploy/avatars";
|
|
89
|
+
|
|
90
|
+
// A 512×512 PNG data URL. Drop straight into an <img>.
|
|
91
|
+
const src = gradientToDataURL("jane@example.com", { size: 512 });
|
|
92
|
+
|
|
93
|
+
// Just the colors behind a seed.
|
|
94
|
+
const { colors, harmony } = generatePalette("jane@example.com");
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
| Helper | Description |
|
|
98
|
+
|--------|-------------|
|
|
99
|
+
| `drawMeshGradient(ctx, seed, size)` | Paint the raw mesh into a 2D canvas context. |
|
|
100
|
+
| `renderGradient(canvas, seed, options?)` | Render a seed into a canvas with the signature soft blur. |
|
|
101
|
+
| `gradientToDataURL(seed, options?)` | Render and return a data URL. |
|
|
102
|
+
| `gradientToBlob(seed, options?)` | Render and resolve a `Blob` (e.g. for the clipboard). |
|
|
103
|
+
| `generatePalette(seed)` | The colors and harmony rule behind a seed. |
|
|
104
|
+
| `seedFromString(input)` / `toSeed(seed)` | The deterministic hashing that turns any value into a numeric seed. |
|
|
105
|
+
|
|
106
|
+
Types `GradientPalette`, `Harmony`, `RenderOptions`, and `ExportOptions` are exported too.
|
|
107
|
+
|
|
108
|
+
## Playground
|
|
109
|
+
|
|
110
|
+
Type any seed and watch the gradient at **[avatars.outpacestudios.com](https://avatars.outpacestudios.com)**, then copy it to your clipboard or download a 2000×2000 image. Full docs at **[/docs](https://avatars.outpacestudios.com/docs)**.
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
[MIT](./LICENSE), free to use. By [Outpace Studios](https://outpacestudios.com).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
// src/index.tsx
|
|
7
|
+
|
|
8
|
+
// src/engine.ts
|
|
9
|
+
var HARMONY_TYPES = [
|
|
10
|
+
"analogous",
|
|
11
|
+
"triadic",
|
|
12
|
+
"splitComplementary",
|
|
13
|
+
"tetradic",
|
|
14
|
+
"complementary"
|
|
15
|
+
];
|
|
16
|
+
var GOLDEN_ANGLE = 137.5;
|
|
17
|
+
var DEFAULT_BLUR_FRACTION = 0.06;
|
|
18
|
+
function seededRandom(seed) {
|
|
19
|
+
let s = seed;
|
|
20
|
+
return () => {
|
|
21
|
+
s += 1831565813;
|
|
22
|
+
let t = s;
|
|
23
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
24
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
25
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function hslToHex(h, s, l) {
|
|
29
|
+
h = (h % 360 + 360) % 360;
|
|
30
|
+
s = Math.max(0, Math.min(100, s)) / 100;
|
|
31
|
+
l = Math.max(0, Math.min(100, l)) / 100;
|
|
32
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
33
|
+
const x = c * (1 - Math.abs(h / 60 % 2 - 1));
|
|
34
|
+
const m = l - c / 2;
|
|
35
|
+
let r = 0;
|
|
36
|
+
let g = 0;
|
|
37
|
+
let b = 0;
|
|
38
|
+
if (h < 60) {
|
|
39
|
+
r = c;
|
|
40
|
+
g = x;
|
|
41
|
+
} else if (h < 120) {
|
|
42
|
+
r = x;
|
|
43
|
+
g = c;
|
|
44
|
+
} else if (h < 180) {
|
|
45
|
+
g = c;
|
|
46
|
+
b = x;
|
|
47
|
+
} else if (h < 240) {
|
|
48
|
+
g = x;
|
|
49
|
+
b = c;
|
|
50
|
+
} else if (h < 300) {
|
|
51
|
+
r = x;
|
|
52
|
+
b = c;
|
|
53
|
+
} else {
|
|
54
|
+
r = c;
|
|
55
|
+
b = x;
|
|
56
|
+
}
|
|
57
|
+
const toHex = (n) => {
|
|
58
|
+
const hex = Math.round((n + m) * 255).toString(16);
|
|
59
|
+
return hex.length === 1 ? `0${hex}` : hex;
|
|
60
|
+
};
|
|
61
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
|
|
62
|
+
}
|
|
63
|
+
function harmonyHues(baseHue, harmony) {
|
|
64
|
+
switch (harmony) {
|
|
65
|
+
case "analogous":
|
|
66
|
+
return [baseHue, baseHue + 30, baseHue + 60, baseHue - 30];
|
|
67
|
+
case "triadic":
|
|
68
|
+
return [baseHue, baseHue + 120, baseHue + 240];
|
|
69
|
+
case "splitComplementary":
|
|
70
|
+
return [baseHue, baseHue + 150, baseHue + 210];
|
|
71
|
+
case "tetradic":
|
|
72
|
+
return [baseHue, baseHue + 90, baseHue + 180, baseHue + 270];
|
|
73
|
+
case "complementary":
|
|
74
|
+
return [baseHue, baseHue + 180, baseHue + 20, baseHue + 200];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function seedFromString(input) {
|
|
78
|
+
let h = 2166136261 >>> 0;
|
|
79
|
+
for (let i = 0; i < input.length; i++) {
|
|
80
|
+
h ^= input.charCodeAt(i);
|
|
81
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
82
|
+
}
|
|
83
|
+
h ^= h >>> 16;
|
|
84
|
+
h = Math.imul(h, 2146121005) >>> 0;
|
|
85
|
+
h ^= h >>> 15;
|
|
86
|
+
h = Math.imul(h, 2221713035) >>> 0;
|
|
87
|
+
h ^= h >>> 16;
|
|
88
|
+
return h >>> 0;
|
|
89
|
+
}
|
|
90
|
+
function toSeed(seed) {
|
|
91
|
+
if (typeof seed === "number") return seed;
|
|
92
|
+
return seedFromString(seed);
|
|
93
|
+
}
|
|
94
|
+
function generatePalette(seed) {
|
|
95
|
+
const s = toSeed(seed);
|
|
96
|
+
const random = seededRandom(s);
|
|
97
|
+
const baseHue = s * GOLDEN_ANGLE % 360;
|
|
98
|
+
const harmonyIndex = Math.floor(random() * HARMONY_TYPES.length);
|
|
99
|
+
const harmony = HARMONY_TYPES[harmonyIndex];
|
|
100
|
+
const hues = harmonyHues(baseHue, harmony);
|
|
101
|
+
const colors = hues.map((hue) => {
|
|
102
|
+
const saturation = 75 + random() * 25;
|
|
103
|
+
const lightness = 50 + random() * 20;
|
|
104
|
+
return hslToHex(hue, saturation, lightness);
|
|
105
|
+
});
|
|
106
|
+
return { seed: s, colors, harmony };
|
|
107
|
+
}
|
|
108
|
+
function drawMeshGradient(ctx, seed, size) {
|
|
109
|
+
const s = toSeed(seed);
|
|
110
|
+
const { colors } = generatePalette(s);
|
|
111
|
+
const random = seededRandom(s * 12345);
|
|
112
|
+
ctx.fillStyle = colors[0];
|
|
113
|
+
ctx.fillRect(0, 0, size, size);
|
|
114
|
+
const numSpots = 8 + Math.floor(random() * 5);
|
|
115
|
+
const spots = [];
|
|
116
|
+
for (let i = 0; i < numSpots; i++) {
|
|
117
|
+
const angle = random() * Math.PI * 2;
|
|
118
|
+
const distance = random() * size * 0.4;
|
|
119
|
+
const centerX = size / 2 + Math.cos(angle) * distance;
|
|
120
|
+
const centerY = size / 2 + Math.sin(angle) * distance;
|
|
121
|
+
spots.push({
|
|
122
|
+
x: centerX + (random() - 0.5) * size * 0.3,
|
|
123
|
+
y: centerY + (random() - 0.5) * size * 0.3,
|
|
124
|
+
radius: size * (0.3 + random() * 0.4),
|
|
125
|
+
color: colors[i % colors.length]
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
spots.sort((a, b) => b.radius - a.radius);
|
|
129
|
+
ctx.globalCompositeOperation = "source-over";
|
|
130
|
+
for (const spot of spots) {
|
|
131
|
+
const g = ctx.createRadialGradient(
|
|
132
|
+
spot.x,
|
|
133
|
+
spot.y,
|
|
134
|
+
0,
|
|
135
|
+
spot.x,
|
|
136
|
+
spot.y,
|
|
137
|
+
spot.radius
|
|
138
|
+
);
|
|
139
|
+
g.addColorStop(0, `${spot.color}FF`);
|
|
140
|
+
g.addColorStop(0.3, `${spot.color}DD`);
|
|
141
|
+
g.addColorStop(0.6, `${spot.color}88`);
|
|
142
|
+
g.addColorStop(1, `${spot.color}00`);
|
|
143
|
+
ctx.fillStyle = g;
|
|
144
|
+
ctx.fillRect(0, 0, size, size);
|
|
145
|
+
}
|
|
146
|
+
const hx = size * 0.3 + random() * size * 0.2;
|
|
147
|
+
const hy = size * 0.3 + random() * size * 0.2;
|
|
148
|
+
const hg = ctx.createRadialGradient(hx, hy, 0, hx, hy, size * 0.3);
|
|
149
|
+
hg.addColorStop(0, "rgba(255,255,255,0.15)");
|
|
150
|
+
hg.addColorStop(1, "rgba(255,255,255,0)");
|
|
151
|
+
ctx.fillStyle = hg;
|
|
152
|
+
ctx.fillRect(0, 0, size, size);
|
|
153
|
+
}
|
|
154
|
+
function blurFor(size, blur) {
|
|
155
|
+
if (blur === 0) return 0;
|
|
156
|
+
return blur ?? Math.round(size * DEFAULT_BLUR_FRACTION);
|
|
157
|
+
}
|
|
158
|
+
function renderGradient(canvas, seed, options = {}) {
|
|
159
|
+
const size = canvas.width;
|
|
160
|
+
const blur = blurFor(size, options.blur);
|
|
161
|
+
const ctx = canvas.getContext("2d");
|
|
162
|
+
if (!ctx) return;
|
|
163
|
+
if (blur <= 0) {
|
|
164
|
+
ctx.clearRect(0, 0, size, size);
|
|
165
|
+
drawMeshGradient(ctx, seed, size);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const scratch = createCanvas(size, size);
|
|
169
|
+
const sctx = scratch.getContext("2d");
|
|
170
|
+
if (!sctx) return;
|
|
171
|
+
drawMeshGradient(sctx, seed, size);
|
|
172
|
+
const scaleUp = 1 + blur / size * 4;
|
|
173
|
+
const dw = size * scaleUp;
|
|
174
|
+
const offset = (dw - size) / 2;
|
|
175
|
+
ctx.clearRect(0, 0, size, size);
|
|
176
|
+
if (supportsCanvasFilter()) {
|
|
177
|
+
ctx.filter = `blur(${blur}px)`;
|
|
178
|
+
ctx.drawImage(scratch, -offset, -offset, dw, dw);
|
|
179
|
+
ctx.filter = "none";
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const factor = Math.max(2, Math.min(16, blur / 2));
|
|
183
|
+
const sw = Math.max(1, Math.round(size / factor));
|
|
184
|
+
const small = createCanvas(sw, sw);
|
|
185
|
+
const smallCtx = small.getContext("2d");
|
|
186
|
+
if (!smallCtx) {
|
|
187
|
+
ctx.drawImage(scratch, -offset, -offset, dw, dw);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
smallCtx.imageSmoothingEnabled = true;
|
|
191
|
+
smallCtx.imageSmoothingQuality = "high";
|
|
192
|
+
smallCtx.drawImage(scratch, 0, 0, sw, sw);
|
|
193
|
+
ctx.imageSmoothingEnabled = true;
|
|
194
|
+
ctx.imageSmoothingQuality = "high";
|
|
195
|
+
ctx.drawImage(small, -offset, -offset, dw, dw);
|
|
196
|
+
}
|
|
197
|
+
var canvasFilterSupport = null;
|
|
198
|
+
function supportsCanvasFilter() {
|
|
199
|
+
if (canvasFilterSupport !== null) return canvasFilterSupport;
|
|
200
|
+
const probe = createCanvas(1, 1).getContext(
|
|
201
|
+
"2d"
|
|
202
|
+
);
|
|
203
|
+
if (!probe) {
|
|
204
|
+
canvasFilterSupport = false;
|
|
205
|
+
return canvasFilterSupport;
|
|
206
|
+
}
|
|
207
|
+
probe.filter = "blur(1px)";
|
|
208
|
+
canvasFilterSupport = probe.filter === "blur(1px)";
|
|
209
|
+
return canvasFilterSupport;
|
|
210
|
+
}
|
|
211
|
+
function createCanvas(w, h) {
|
|
212
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
213
|
+
return new OffscreenCanvas(w, h);
|
|
214
|
+
}
|
|
215
|
+
const c = document.createElement("canvas");
|
|
216
|
+
c.width = w;
|
|
217
|
+
c.height = h;
|
|
218
|
+
return c;
|
|
219
|
+
}
|
|
220
|
+
function gradientToDataURL(seed, options = {}) {
|
|
221
|
+
const { size = 512, type = "image/png", quality = 0.92 } = options;
|
|
222
|
+
const canvas = document.createElement("canvas");
|
|
223
|
+
canvas.width = size;
|
|
224
|
+
canvas.height = size;
|
|
225
|
+
renderGradient(canvas, seed, options);
|
|
226
|
+
return canvas.toDataURL(type, quality);
|
|
227
|
+
}
|
|
228
|
+
function gradientToBlob(seed, options = {}) {
|
|
229
|
+
const { size = 512, type = "image/png", quality = 0.92 } = options;
|
|
230
|
+
const canvas = document.createElement("canvas");
|
|
231
|
+
canvas.width = size;
|
|
232
|
+
canvas.height = size;
|
|
233
|
+
renderGradient(canvas, seed, options);
|
|
234
|
+
return new Promise((resolve) => canvas.toBlob(resolve, type, quality));
|
|
235
|
+
}
|
|
236
|
+
var RENDER_SIZE = 256;
|
|
237
|
+
var BLUR_FRACTION = 0.06;
|
|
238
|
+
var ANIMATED_CLASS = "oa-avatar-canvas--animated";
|
|
239
|
+
var STYLE_ELEMENT_ID = "oa-avatar-keyframes";
|
|
240
|
+
var KEYFRAMES_CSS = `
|
|
241
|
+
@keyframes oa-avatar-drift {
|
|
242
|
+
0% { transform: scale(1.25) translate(0%, 0%) rotate(0deg); }
|
|
243
|
+
20% { transform: scale(1.30) translate(-3%, 2%) rotate(8deg); }
|
|
244
|
+
40% { transform: scale(1.22) translate(3%, 3%) rotate(-6deg); }
|
|
245
|
+
60% { transform: scale(1.28) translate(2%, -3%) rotate(5deg); }
|
|
246
|
+
80% { transform: scale(1.24) translate(-2%, -2%) rotate(-4deg); }
|
|
247
|
+
100% { transform: scale(1.25) translate(0%, 0%) rotate(0deg); }
|
|
248
|
+
}
|
|
249
|
+
.${ANIMATED_CLASS} {
|
|
250
|
+
transform-origin: center;
|
|
251
|
+
will-change: transform;
|
|
252
|
+
animation-name: oa-avatar-drift;
|
|
253
|
+
animation-timing-function: ease-in-out;
|
|
254
|
+
animation-iteration-count: infinite;
|
|
255
|
+
animation-direction: alternate;
|
|
256
|
+
}
|
|
257
|
+
@media (prefers-reduced-motion: reduce) {
|
|
258
|
+
.${ANIMATED_CLASS} {
|
|
259
|
+
animation: none;
|
|
260
|
+
transform: none;
|
|
261
|
+
will-change: auto;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
`;
|
|
265
|
+
var stylesInjected = false;
|
|
266
|
+
function ensureAnimationStyles() {
|
|
267
|
+
if (stylesInjected || typeof document === "undefined") return;
|
|
268
|
+
if (document.getElementById(STYLE_ELEMENT_ID)) {
|
|
269
|
+
stylesInjected = true;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const el = document.createElement("style");
|
|
273
|
+
el.id = STYLE_ELEMENT_ID;
|
|
274
|
+
el.textContent = KEYFRAMES_CSS;
|
|
275
|
+
document.head.appendChild(el);
|
|
276
|
+
stylesInjected = true;
|
|
277
|
+
}
|
|
278
|
+
function GradientAvatar({
|
|
279
|
+
seed,
|
|
280
|
+
size = 32,
|
|
281
|
+
radius = "9999px",
|
|
282
|
+
animate = false,
|
|
283
|
+
animationDuration = 16,
|
|
284
|
+
className,
|
|
285
|
+
style
|
|
286
|
+
}) {
|
|
287
|
+
const canvasRef = react.useRef(null);
|
|
288
|
+
react.useEffect(() => {
|
|
289
|
+
const canvas = canvasRef.current;
|
|
290
|
+
if (!canvas) return;
|
|
291
|
+
const ctx = canvas.getContext("2d");
|
|
292
|
+
if (!ctx) return;
|
|
293
|
+
ctx.clearRect(0, 0, RENDER_SIZE, RENDER_SIZE);
|
|
294
|
+
drawMeshGradient(ctx, seed, RENDER_SIZE);
|
|
295
|
+
}, [seed]);
|
|
296
|
+
react.useEffect(() => {
|
|
297
|
+
if (animate) ensureAnimationStyles();
|
|
298
|
+
}, [animate]);
|
|
299
|
+
const blurPx = Math.max(1, Math.round(size * BLUR_FRACTION));
|
|
300
|
+
const canvasStyle = {
|
|
301
|
+
width: "100%",
|
|
302
|
+
height: "100%",
|
|
303
|
+
display: "block",
|
|
304
|
+
filter: `blur(${blurPx}px)`
|
|
305
|
+
};
|
|
306
|
+
if (animate) {
|
|
307
|
+
canvasStyle.animationDuration = `${animationDuration}s`;
|
|
308
|
+
}
|
|
309
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
310
|
+
"span",
|
|
311
|
+
{
|
|
312
|
+
className,
|
|
313
|
+
style: {
|
|
314
|
+
display: "inline-block",
|
|
315
|
+
overflow: "hidden",
|
|
316
|
+
borderRadius: radius,
|
|
317
|
+
width: size,
|
|
318
|
+
height: size,
|
|
319
|
+
...style
|
|
320
|
+
},
|
|
321
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
322
|
+
"canvas",
|
|
323
|
+
{
|
|
324
|
+
ref: canvasRef,
|
|
325
|
+
width: RENDER_SIZE,
|
|
326
|
+
height: RENDER_SIZE,
|
|
327
|
+
className: animate ? ANIMATED_CLASS : void 0,
|
|
328
|
+
style: canvasStyle
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
exports.GradientAvatar = GradientAvatar;
|
|
336
|
+
exports.drawMeshGradient = drawMeshGradient;
|
|
337
|
+
exports.generatePalette = generatePalette;
|
|
338
|
+
exports.gradientToBlob = gradientToBlob;
|
|
339
|
+
exports.gradientToDataURL = gradientToDataURL;
|
|
340
|
+
exports.renderGradient = renderGradient;
|
|
341
|
+
exports.seedFromString = seedFromString;
|
|
342
|
+
exports.toSeed = toSeed;
|
|
343
|
+
//# sourceMappingURL=index.cjs.map
|
|
344
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/engine.ts","../src/index.tsx"],"names":["useRef","useEffect","jsx"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Gradient engine for @apollo-deploy/avatars.\n *\n * Framework-agnostic mesh-gradient avatar generator. Every seed (string or\n * number) deterministically produces a unique gradient — no stored images,\n * no network. Pure palette/RNG core plus optional Canvas2D render helpers.\n */\n\nexport type Harmony =\n\t| \"analogous\"\n\t| \"triadic\"\n\t| \"splitComplementary\"\n\t| \"tetradic\"\n\t| \"complementary\";\n\nexport interface GradientPalette {\n\t/** The numeric seed the palette was derived from. */\n\tseed: number;\n\t/** Hex color stops used to paint the mesh. */\n\tcolors: string[];\n\t/** Which color-harmony rule produced the hues. */\n\tharmony: Harmony;\n}\n\nconst HARMONY_TYPES: Harmony[] = [\n\t\"analogous\",\n\t\"triadic\",\n\t\"splitComplementary\",\n\t\"tetradic\",\n\t\"complementary\",\n];\n\nconst GOLDEN_ANGLE = 137.5;\n\n/** Default blur radius as a fraction of the rendered dimension. */\nexport const DEFAULT_BLUR_FRACTION = 0.06;\n\nfunction seededRandom(seed: number): () => number {\n\tlet s = seed;\n\treturn () => {\n\t\ts += 0x6d2b79f5;\n\t\tlet t = s;\n\t\tt = Math.imul(t ^ (t >>> 15), t | 1);\n\t\tt ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n\t\treturn ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n\t};\n}\n\nfunction hslToHex(h: number, s: number, l: number): string {\n\th = ((h % 360) + 360) % 360;\n\ts = Math.max(0, Math.min(100, s)) / 100;\n\tl = Math.max(0, Math.min(100, l)) / 100;\n\n\tconst c = (1 - Math.abs(2 * l - 1)) * s;\n\tconst x = c * (1 - Math.abs(((h / 60) % 2) - 1));\n\tconst m = l - c / 2;\n\n\tlet r = 0;\n\tlet g = 0;\n\tlet b = 0;\n\tif (h < 60) {\n\t\tr = c;\n\t\tg = x;\n\t} else if (h < 120) {\n\t\tr = x;\n\t\tg = c;\n\t} else if (h < 180) {\n\t\tg = c;\n\t\tb = x;\n\t} else if (h < 240) {\n\t\tg = x;\n\t\tb = c;\n\t} else if (h < 300) {\n\t\tr = x;\n\t\tb = c;\n\t} else {\n\t\tr = c;\n\t\tb = x;\n\t}\n\n\tconst toHex = (n: number) => {\n\t\tconst hex = Math.round((n + m) * 255).toString(16);\n\t\treturn hex.length === 1 ? `0${hex}` : hex;\n\t};\n\treturn `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();\n}\n\nfunction harmonyHues(baseHue: number, harmony: Harmony): number[] {\n\tswitch (harmony) {\n\t\tcase \"analogous\":\n\t\t\treturn [baseHue, baseHue + 30, baseHue + 60, baseHue - 30];\n\t\tcase \"triadic\":\n\t\t\treturn [baseHue, baseHue + 120, baseHue + 240];\n\t\tcase \"splitComplementary\":\n\t\t\treturn [baseHue, baseHue + 150, baseHue + 210];\n\t\tcase \"tetradic\":\n\t\t\treturn [baseHue, baseHue + 90, baseHue + 180, baseHue + 270];\n\t\tcase \"complementary\":\n\t\t\treturn [baseHue, baseHue + 180, baseHue + 20, baseHue + 200];\n\t}\n}\n\n/**\n * Stable string → 32-bit unsigned hash (FNV-1a + bit-mixing avalanche).\n * Uses the full uint32 range as a seed so similar strings diverge fully.\n */\nexport function seedFromString(input: string): number {\n\tlet h = 2166136261 >>> 0;\n\tfor (let i = 0; i < input.length; i++) {\n\t\th ^= input.charCodeAt(i);\n\t\th = Math.imul(h, 16777619) >>> 0;\n\t}\n\th ^= h >>> 16;\n\th = Math.imul(h, 0x7feb352d) >>> 0;\n\th ^= h >>> 15;\n\th = Math.imul(h, 0x846ca68b) >>> 0;\n\th ^= h >>> 16;\n\treturn h >>> 0;\n}\n\n/** Normalize a string or number seed to the numeric seed used internally. */\nexport function toSeed(seed: number | string): number {\n\tif (typeof seed === \"number\") return seed;\n\treturn seedFromString(seed);\n}\n\n/** Derive the deterministic color palette for a seed. */\nexport function generatePalette(seed: number | string): GradientPalette {\n\tconst s = toSeed(seed);\n\tconst random = seededRandom(s);\n\tconst baseHue = (s * GOLDEN_ANGLE) % 360;\n\tconst harmonyIndex = Math.floor(random() * HARMONY_TYPES.length);\n\tconst harmony = HARMONY_TYPES[harmonyIndex];\n\tconst hues = harmonyHues(baseHue, harmony);\n\tconst colors = hues.map((hue) => {\n\t\tconst saturation = 75 + random() * 25;\n\t\tconst lightness = 50 + random() * 20;\n\t\treturn hslToHex(hue, saturation, lightness);\n\t});\n\treturn { seed: s, colors, harmony };\n}\n\n/**\n * Minimal Canvas2D context surface the renderer needs. Both\n * `HTMLCanvasElement` and `OffscreenCanvas` 2D contexts satisfy it.\n */\nexport type GradientContext = {\n\tfillStyle: string | CanvasGradient | CanvasPattern;\n\tglobalCompositeOperation: GlobalCompositeOperation;\n\tfillRect(x: number, y: number, w: number, h: number): void;\n\tcreateRadialGradient(\n\t\tx0: number,\n\t\ty0: number,\n\t\tr0: number,\n\t\tx1: number,\n\t\ty1: number,\n\t\tr1: number,\n\t): CanvasGradient;\n};\n\n/**\n * Draw the mesh gradient for `seed` into `ctx` at `size` x `size`.\n * The caller is responsible for any blur — apply `filter: blur(…)` on the\n * displayed canvas (≈6% of the rendered dimension) for the signature look,\n * or use {@link renderGradient} / {@link gradientToDataURL} which bake it in.\n */\nexport function drawMeshGradient(\n\tctx: GradientContext,\n\tseed: number | string,\n\tsize: number,\n): void {\n\tconst s = toSeed(seed);\n\tconst { colors } = generatePalette(s);\n\tconst random = seededRandom(s * 12345);\n\n\tctx.fillStyle = colors[0];\n\tctx.fillRect(0, 0, size, size);\n\n\tconst numSpots = 8 + Math.floor(random() * 5);\n\tconst spots: Array<{ x: number; y: number; radius: number; color: string }> =\n\t\t[];\n\n\tfor (let i = 0; i < numSpots; i++) {\n\t\tconst angle = random() * Math.PI * 2;\n\t\tconst distance = random() * size * 0.4;\n\t\tconst centerX = size / 2 + Math.cos(angle) * distance;\n\t\tconst centerY = size / 2 + Math.sin(angle) * distance;\n\t\tspots.push({\n\t\t\tx: centerX + (random() - 0.5) * size * 0.3,\n\t\t\ty: centerY + (random() - 0.5) * size * 0.3,\n\t\t\tradius: size * (0.3 + random() * 0.4),\n\t\t\tcolor: colors[i % colors.length],\n\t\t});\n\t}\n\n\tspots.sort((a, b) => b.radius - a.radius);\n\n\tctx.globalCompositeOperation = \"source-over\";\n\tfor (const spot of spots) {\n\t\tconst g = ctx.createRadialGradient(\n\t\t\tspot.x,\n\t\t\tspot.y,\n\t\t\t0,\n\t\t\tspot.x,\n\t\t\tspot.y,\n\t\t\tspot.radius,\n\t\t);\n\t\tg.addColorStop(0, `${spot.color}FF`);\n\t\tg.addColorStop(0.3, `${spot.color}DD`);\n\t\tg.addColorStop(0.6, `${spot.color}88`);\n\t\tg.addColorStop(1, `${spot.color}00`);\n\t\tctx.fillStyle = g;\n\t\tctx.fillRect(0, 0, size, size);\n\t}\n\n\tconst hx = size * 0.3 + random() * size * 0.2;\n\tconst hy = size * 0.3 + random() * size * 0.2;\n\tconst hg = ctx.createRadialGradient(hx, hy, 0, hx, hy, size * 0.3);\n\thg.addColorStop(0, \"rgba(255,255,255,0.15)\");\n\thg.addColorStop(1, \"rgba(255,255,255,0)\");\n\tctx.fillStyle = hg;\n\tctx.fillRect(0, 0, size, size);\n}\n\nexport interface RenderOptions {\n\t/**\n\t * Blur radius in pixels. Defaults to ~6% of the canvas size for the\n\t * signature soft look. Pass `0` to disable.\n\t */\n\tblur?: number;\n}\n\nfunction blurFor(size: number, blur?: number): number {\n\tif (blur === 0) return 0;\n\treturn blur ?? Math.round(size * DEFAULT_BLUR_FRACTION);\n}\n\n/**\n * Render a seed's gradient into an existing canvas, baking in the soft blur.\n * Draws at the canvas's current `width`/`height`. Browser/OffscreenCanvas only.\n */\nexport function renderGradient(\n\tcanvas: HTMLCanvasElement | OffscreenCanvas,\n\tseed: number | string,\n\toptions: RenderOptions = {},\n): void {\n\tconst size = canvas.width;\n\tconst blur = blurFor(size, options.blur);\n\n\tconst ctx = canvas.getContext(\"2d\") as CanvasRenderingContext2D | null;\n\tif (!ctx) return;\n\n\tif (blur <= 0) {\n\t\tctx.clearRect(0, 0, size, size);\n\t\tdrawMeshGradient(ctx, seed, size);\n\t\treturn;\n\t}\n\n\t// Draw the raw mesh on a scratch canvas, then composite it back with blur\n\t// scaled up slightly so the soft edges fall outside the frame (no ring).\n\tconst scratch = createCanvas(size, size);\n\tconst sctx = scratch.getContext(\"2d\") as CanvasRenderingContext2D | null;\n\tif (!sctx) return;\n\tdrawMeshGradient(sctx, seed, size);\n\n\tconst scaleUp = 1 + (blur / size) * 4;\n\tconst dw = size * scaleUp;\n\tconst offset = (dw - size) / 2;\n\tctx.clearRect(0, 0, size, size);\n\tif (supportsCanvasFilter()) {\n\t\tctx.filter = `blur(${blur}px)`;\n\t\tctx.drawImage(scratch as CanvasImageSource, -offset, -offset, dw, dw);\n\t\tctx.filter = \"none\";\n\t\treturn;\n\t}\n\t// 2D-canvas `filter` is a silent no-op on Safari < 17: approximate the\n\t// gaussian by bouncing through a small canvas. Bilinear resampling on the\n\t// way down and back up smears by roughly the downscale factor, which is\n\t// plenty for a mesh that is already smooth gradients.\n\tconst factor = Math.max(2, Math.min(16, blur / 2));\n\tconst sw = Math.max(1, Math.round(size / factor));\n\tconst small = createCanvas(sw, sw);\n\tconst smallCtx = small.getContext(\"2d\") as CanvasRenderingContext2D | null;\n\tif (!smallCtx) {\n\t\tctx.drawImage(scratch as CanvasImageSource, -offset, -offset, dw, dw);\n\t\treturn;\n\t}\n\tsmallCtx.imageSmoothingEnabled = true;\n\tsmallCtx.imageSmoothingQuality = \"high\";\n\tsmallCtx.drawImage(scratch as CanvasImageSource, 0, 0, sw, sw);\n\tctx.imageSmoothingEnabled = true;\n\tctx.imageSmoothingQuality = \"high\";\n\tctx.drawImage(small as CanvasImageSource, -offset, -offset, dw, dw);\n}\n\n/* Engines that honor 2D-canvas `filter` echo an assigned value back from the\n * property; Safari < 17 ignores the assignment. Probed once, then cached. */\nlet canvasFilterSupport: boolean | null = null;\nfunction supportsCanvasFilter(): boolean {\n\tif (canvasFilterSupport !== null) return canvasFilterSupport;\n\tconst probe = createCanvas(1, 1).getContext(\n\t\t\"2d\",\n\t) as CanvasRenderingContext2D | null;\n\tif (!probe) {\n\t\tcanvasFilterSupport = false;\n\t\treturn canvasFilterSupport;\n\t}\n\tprobe.filter = \"blur(1px)\";\n\tcanvasFilterSupport = probe.filter === \"blur(1px)\";\n\treturn canvasFilterSupport;\n}\n\nfunction createCanvas(\n\tw: number,\n\th: number,\n): HTMLCanvasElement | OffscreenCanvas {\n\tif (typeof OffscreenCanvas !== \"undefined\") {\n\t\treturn new OffscreenCanvas(w, h);\n\t}\n\tconst c = document.createElement(\"canvas\");\n\tc.width = w;\n\tc.height = h;\n\treturn c;\n}\n\nexport interface ExportOptions extends RenderOptions {\n\t/** Output pixel dimensions (square). Default: 512. */\n\tsize?: number;\n\t/** Image MIME type. Default: \"image/png\". */\n\ttype?: string;\n\t/** Quality 0–1 for lossy types. Default: 0.92. */\n\tquality?: number;\n}\n\n/** Render a seed's gradient and return it as a data URL. Browser only. */\nexport function gradientToDataURL(\n\tseed: number | string,\n\toptions: ExportOptions = {},\n): string {\n\tconst { size = 512, type = \"image/png\", quality = 0.92 } = options;\n\tconst canvas = document.createElement(\"canvas\");\n\tcanvas.width = size;\n\tcanvas.height = size;\n\trenderGradient(canvas, seed, options);\n\treturn canvas.toDataURL(type, quality);\n}\n\n/** Render a seed's gradient and resolve a Blob (or null). Browser only. */\nexport function gradientToBlob(\n\tseed: number | string,\n\toptions: ExportOptions = {},\n): Promise<Blob | null> {\n\tconst { size = 512, type = \"image/png\", quality = 0.92 } = options;\n\tconst canvas = document.createElement(\"canvas\");\n\tcanvas.width = size;\n\tcanvas.height = size;\n\trenderGradient(canvas, seed, options);\n\treturn new Promise((resolve) => canvas.toBlob(resolve, type, quality));\n}\n","import type { CSSProperties } from \"react\";\nimport { useEffect, useRef } from \"react\";\nimport { drawMeshGradient } from \"./engine\";\n\nexport interface GradientAvatarProps {\n\t/** Any string or number — each unique seed produces a unique gradient. */\n\tseed: number | string;\n\t/** Rendered size in pixels. Default: 32. */\n\tsize?: number;\n\t/**\n\t * Corner radius. Number = pixels, string = any CSS length.\n\t * Defaults to a full circle; pass `0` for a square or e.g. `12` for a\n\t * rounded square. Default: \"9999px\".\n\t */\n\tradius?: number | string;\n\t/**\n\t * Slowly drift the gradient with a GPU-only CSS animation. The canvas is\n\t * still painted just once — the motion is a compositor transform on the\n\t * cached layer, so there is no per-frame JavaScript, no repaint, and no\n\t * extra memory. Automatically disabled for users who request reduced\n\t * motion. Default: `false`.\n\t */\n\tanimate?: boolean;\n\t/**\n\t * Duration of one full drift cycle in seconds. Slower is subtler and\n\t * cheaper. Only applies when {@link animate} is `true`. Default: `16`.\n\t */\n\tanimationDuration?: number;\n\t/** Additional CSS classes on the wrapper. */\n\tclassName?: string;\n\t/** Extra inline styles merged onto the wrapper. */\n\tstyle?: CSSProperties;\n}\n\n/** Internal render resolution. Higher than display size so the CSS blur is smooth. */\nconst RENDER_SIZE = 256;\n/** Blur radius as a fraction of display size. */\nconst BLUR_FRACTION = 0.06;\n\n/** Class applied to the canvas when animating. Styles injected once, lazily. */\nconst ANIMATED_CLASS = \"oa-avatar-canvas--animated\";\nconst STYLE_ELEMENT_ID = \"oa-avatar-keyframes\";\n\n/**\n * Keyframes for the drift. The canvas is scaled up so that the rotation and\n * translation never expose the clipped container edges, and the whole thing is\n * a single `transform` track so the browser can run it entirely on the\n * compositor thread. `prefers-reduced-motion` opts out with no JS listener.\n */\nconst KEYFRAMES_CSS = `\n@keyframes oa-avatar-drift {\n\t0% { transform: scale(1.25) translate(0%, 0%) rotate(0deg); }\n\t20% { transform: scale(1.30) translate(-3%, 2%) rotate(8deg); }\n\t40% { transform: scale(1.22) translate(3%, 3%) rotate(-6deg); }\n\t60% { transform: scale(1.28) translate(2%, -3%) rotate(5deg); }\n\t80% { transform: scale(1.24) translate(-2%, -2%) rotate(-4deg); }\n\t100% { transform: scale(1.25) translate(0%, 0%) rotate(0deg); }\n}\n.${ANIMATED_CLASS} {\n\ttransform-origin: center;\n\twill-change: transform;\n\tanimation-name: oa-avatar-drift;\n\tanimation-timing-function: ease-in-out;\n\tanimation-iteration-count: infinite;\n\tanimation-direction: alternate;\n}\n@media (prefers-reduced-motion: reduce) {\n\t.${ANIMATED_CLASS} {\n\t\tanimation: none;\n\t\ttransform: none;\n\t\twill-change: auto;\n\t}\n}\n`;\n\nlet stylesInjected = false;\n\n/** Inject the drift keyframes into the document head exactly once (client-only). */\nfunction ensureAnimationStyles(): void {\n\tif (stylesInjected || typeof document === \"undefined\") return;\n\tif (document.getElementById(STYLE_ELEMENT_ID)) {\n\t\tstylesInjected = true;\n\t\treturn;\n\t}\n\tconst el = document.createElement(\"style\");\n\tel.id = STYLE_ELEMENT_ID;\n\tel.textContent = KEYFRAMES_CSS;\n\tdocument.head.appendChild(el);\n\tstylesInjected = true;\n}\n\n/**\n * Renders a deterministic mesh-gradient avatar on a `<canvas>`.\n * The same seed always produces the same gradient.\n */\nexport function GradientAvatar({\n\tseed,\n\tsize = 32,\n\tradius = \"9999px\",\n\tanimate = false,\n\tanimationDuration = 16,\n\tclassName,\n\tstyle,\n}: GradientAvatarProps) {\n\tconst canvasRef = useRef<HTMLCanvasElement>(null);\n\n\tuseEffect(() => {\n\t\tconst canvas = canvasRef.current;\n\t\tif (!canvas) return;\n\t\tconst ctx = canvas.getContext(\"2d\");\n\t\tif (!ctx) return;\n\t\tctx.clearRect(0, 0, RENDER_SIZE, RENDER_SIZE);\n\t\tdrawMeshGradient(ctx, seed, RENDER_SIZE);\n\t}, [seed]);\n\n\tuseEffect(() => {\n\t\tif (animate) ensureAnimationStyles();\n\t}, [animate]);\n\n\tconst blurPx = Math.max(1, Math.round(size * BLUR_FRACTION));\n\n\tconst canvasStyle: CSSProperties = {\n\t\twidth: \"100%\",\n\t\theight: \"100%\",\n\t\tdisplay: \"block\",\n\t\tfilter: `blur(${blurPx}px)`,\n\t};\n\tif (animate) {\n\t\tcanvasStyle.animationDuration = `${animationDuration}s`;\n\t}\n\n\treturn (\n\t\t<span\n\t\t\tclassName={className}\n\t\t\tstyle={{\n\t\t\t\tdisplay: \"inline-block\",\n\t\t\t\toverflow: \"hidden\",\n\t\t\t\tborderRadius: radius,\n\t\t\t\twidth: size,\n\t\t\t\theight: size,\n\t\t\t\t...style,\n\t\t\t}}\n\t\t>\n\t\t\t<canvas\n\t\t\t\tref={canvasRef}\n\t\t\t\twidth={RENDER_SIZE}\n\t\t\t\theight={RENDER_SIZE}\n\t\t\t\tclassName={animate ? ANIMATED_CLASS : undefined}\n\t\t\t\tstyle={canvasStyle}\n\t\t\t/>\n\t\t</span>\n\t);\n}\n\nexport type {\n\tExportOptions,\n\tGradientPalette,\n\tHarmony,\n\tRenderOptions,\n} from \"./engine\";\nexport {\n\tdrawMeshGradient,\n\tgeneratePalette,\n\tgradientToBlob,\n\tgradientToDataURL,\n\trenderGradient,\n\tseedFromString,\n\ttoSeed,\n} from \"./engine\";\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { CSSProperties } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Gradient engine for @apollo-deploy/avatars.
|
|
6
|
+
*
|
|
7
|
+
* Framework-agnostic mesh-gradient avatar generator. Every seed (string or
|
|
8
|
+
* number) deterministically produces a unique gradient — no stored images,
|
|
9
|
+
* no network. Pure palette/RNG core plus optional Canvas2D render helpers.
|
|
10
|
+
*/
|
|
11
|
+
type Harmony = "analogous" | "triadic" | "splitComplementary" | "tetradic" | "complementary";
|
|
12
|
+
interface GradientPalette {
|
|
13
|
+
/** The numeric seed the palette was derived from. */
|
|
14
|
+
seed: number;
|
|
15
|
+
/** Hex color stops used to paint the mesh. */
|
|
16
|
+
colors: string[];
|
|
17
|
+
/** Which color-harmony rule produced the hues. */
|
|
18
|
+
harmony: Harmony;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Stable string → 32-bit unsigned hash (FNV-1a + bit-mixing avalanche).
|
|
22
|
+
* Uses the full uint32 range as a seed so similar strings diverge fully.
|
|
23
|
+
*/
|
|
24
|
+
declare function seedFromString(input: string): number;
|
|
25
|
+
/** Normalize a string or number seed to the numeric seed used internally. */
|
|
26
|
+
declare function toSeed(seed: number | string): number;
|
|
27
|
+
/** Derive the deterministic color palette for a seed. */
|
|
28
|
+
declare function generatePalette(seed: number | string): GradientPalette;
|
|
29
|
+
/**
|
|
30
|
+
* Minimal Canvas2D context surface the renderer needs. Both
|
|
31
|
+
* `HTMLCanvasElement` and `OffscreenCanvas` 2D contexts satisfy it.
|
|
32
|
+
*/
|
|
33
|
+
type GradientContext = {
|
|
34
|
+
fillStyle: string | CanvasGradient | CanvasPattern;
|
|
35
|
+
globalCompositeOperation: GlobalCompositeOperation;
|
|
36
|
+
fillRect(x: number, y: number, w: number, h: number): void;
|
|
37
|
+
createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): CanvasGradient;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Draw the mesh gradient for `seed` into `ctx` at `size` x `size`.
|
|
41
|
+
* The caller is responsible for any blur — apply `filter: blur(…)` on the
|
|
42
|
+
* displayed canvas (≈6% of the rendered dimension) for the signature look,
|
|
43
|
+
* or use {@link renderGradient} / {@link gradientToDataURL} which bake it in.
|
|
44
|
+
*/
|
|
45
|
+
declare function drawMeshGradient(ctx: GradientContext, seed: number | string, size: number): void;
|
|
46
|
+
interface RenderOptions {
|
|
47
|
+
/**
|
|
48
|
+
* Blur radius in pixels. Defaults to ~6% of the canvas size for the
|
|
49
|
+
* signature soft look. Pass `0` to disable.
|
|
50
|
+
*/
|
|
51
|
+
blur?: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Render a seed's gradient into an existing canvas, baking in the soft blur.
|
|
55
|
+
* Draws at the canvas's current `width`/`height`. Browser/OffscreenCanvas only.
|
|
56
|
+
*/
|
|
57
|
+
declare function renderGradient(canvas: HTMLCanvasElement | OffscreenCanvas, seed: number | string, options?: RenderOptions): void;
|
|
58
|
+
interface ExportOptions extends RenderOptions {
|
|
59
|
+
/** Output pixel dimensions (square). Default: 512. */
|
|
60
|
+
size?: number;
|
|
61
|
+
/** Image MIME type. Default: "image/png". */
|
|
62
|
+
type?: string;
|
|
63
|
+
/** Quality 0–1 for lossy types. Default: 0.92. */
|
|
64
|
+
quality?: number;
|
|
65
|
+
}
|
|
66
|
+
/** Render a seed's gradient and return it as a data URL. Browser only. */
|
|
67
|
+
declare function gradientToDataURL(seed: number | string, options?: ExportOptions): string;
|
|
68
|
+
/** Render a seed's gradient and resolve a Blob (or null). Browser only. */
|
|
69
|
+
declare function gradientToBlob(seed: number | string, options?: ExportOptions): Promise<Blob | null>;
|
|
70
|
+
|
|
71
|
+
interface GradientAvatarProps {
|
|
72
|
+
/** Any string or number — each unique seed produces a unique gradient. */
|
|
73
|
+
seed: number | string;
|
|
74
|
+
/** Rendered size in pixels. Default: 32. */
|
|
75
|
+
size?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Corner radius. Number = pixels, string = any CSS length.
|
|
78
|
+
* Defaults to a full circle; pass `0` for a square or e.g. `12` for a
|
|
79
|
+
* rounded square. Default: "9999px".
|
|
80
|
+
*/
|
|
81
|
+
radius?: number | string;
|
|
82
|
+
/**
|
|
83
|
+
* Slowly drift the gradient with a GPU-only CSS animation. The canvas is
|
|
84
|
+
* still painted just once — the motion is a compositor transform on the
|
|
85
|
+
* cached layer, so there is no per-frame JavaScript, no repaint, and no
|
|
86
|
+
* extra memory. Automatically disabled for users who request reduced
|
|
87
|
+
* motion. Default: `false`.
|
|
88
|
+
*/
|
|
89
|
+
animate?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Duration of one full drift cycle in seconds. Slower is subtler and
|
|
92
|
+
* cheaper. Only applies when {@link animate} is `true`. Default: `16`.
|
|
93
|
+
*/
|
|
94
|
+
animationDuration?: number;
|
|
95
|
+
/** Additional CSS classes on the wrapper. */
|
|
96
|
+
className?: string;
|
|
97
|
+
/** Extra inline styles merged onto the wrapper. */
|
|
98
|
+
style?: CSSProperties;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Renders a deterministic mesh-gradient avatar on a `<canvas>`.
|
|
102
|
+
* The same seed always produces the same gradient.
|
|
103
|
+
*/
|
|
104
|
+
declare function GradientAvatar({ seed, size, radius, animate, animationDuration, className, style, }: GradientAvatarProps): react_jsx_runtime.JSX.Element;
|
|
105
|
+
|
|
106
|
+
export { type ExportOptions, GradientAvatar, type GradientAvatarProps, type GradientPalette, type Harmony, type RenderOptions, drawMeshGradient, generatePalette, gradientToBlob, gradientToDataURL, renderGradient, seedFromString, toSeed };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { CSSProperties } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Gradient engine for @apollo-deploy/avatars.
|
|
6
|
+
*
|
|
7
|
+
* Framework-agnostic mesh-gradient avatar generator. Every seed (string or
|
|
8
|
+
* number) deterministically produces a unique gradient — no stored images,
|
|
9
|
+
* no network. Pure palette/RNG core plus optional Canvas2D render helpers.
|
|
10
|
+
*/
|
|
11
|
+
type Harmony = "analogous" | "triadic" | "splitComplementary" | "tetradic" | "complementary";
|
|
12
|
+
interface GradientPalette {
|
|
13
|
+
/** The numeric seed the palette was derived from. */
|
|
14
|
+
seed: number;
|
|
15
|
+
/** Hex color stops used to paint the mesh. */
|
|
16
|
+
colors: string[];
|
|
17
|
+
/** Which color-harmony rule produced the hues. */
|
|
18
|
+
harmony: Harmony;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Stable string → 32-bit unsigned hash (FNV-1a + bit-mixing avalanche).
|
|
22
|
+
* Uses the full uint32 range as a seed so similar strings diverge fully.
|
|
23
|
+
*/
|
|
24
|
+
declare function seedFromString(input: string): number;
|
|
25
|
+
/** Normalize a string or number seed to the numeric seed used internally. */
|
|
26
|
+
declare function toSeed(seed: number | string): number;
|
|
27
|
+
/** Derive the deterministic color palette for a seed. */
|
|
28
|
+
declare function generatePalette(seed: number | string): GradientPalette;
|
|
29
|
+
/**
|
|
30
|
+
* Minimal Canvas2D context surface the renderer needs. Both
|
|
31
|
+
* `HTMLCanvasElement` and `OffscreenCanvas` 2D contexts satisfy it.
|
|
32
|
+
*/
|
|
33
|
+
type GradientContext = {
|
|
34
|
+
fillStyle: string | CanvasGradient | CanvasPattern;
|
|
35
|
+
globalCompositeOperation: GlobalCompositeOperation;
|
|
36
|
+
fillRect(x: number, y: number, w: number, h: number): void;
|
|
37
|
+
createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): CanvasGradient;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Draw the mesh gradient for `seed` into `ctx` at `size` x `size`.
|
|
41
|
+
* The caller is responsible for any blur — apply `filter: blur(…)` on the
|
|
42
|
+
* displayed canvas (≈6% of the rendered dimension) for the signature look,
|
|
43
|
+
* or use {@link renderGradient} / {@link gradientToDataURL} which bake it in.
|
|
44
|
+
*/
|
|
45
|
+
declare function drawMeshGradient(ctx: GradientContext, seed: number | string, size: number): void;
|
|
46
|
+
interface RenderOptions {
|
|
47
|
+
/**
|
|
48
|
+
* Blur radius in pixels. Defaults to ~6% of the canvas size for the
|
|
49
|
+
* signature soft look. Pass `0` to disable.
|
|
50
|
+
*/
|
|
51
|
+
blur?: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Render a seed's gradient into an existing canvas, baking in the soft blur.
|
|
55
|
+
* Draws at the canvas's current `width`/`height`. Browser/OffscreenCanvas only.
|
|
56
|
+
*/
|
|
57
|
+
declare function renderGradient(canvas: HTMLCanvasElement | OffscreenCanvas, seed: number | string, options?: RenderOptions): void;
|
|
58
|
+
interface ExportOptions extends RenderOptions {
|
|
59
|
+
/** Output pixel dimensions (square). Default: 512. */
|
|
60
|
+
size?: number;
|
|
61
|
+
/** Image MIME type. Default: "image/png". */
|
|
62
|
+
type?: string;
|
|
63
|
+
/** Quality 0–1 for lossy types. Default: 0.92. */
|
|
64
|
+
quality?: number;
|
|
65
|
+
}
|
|
66
|
+
/** Render a seed's gradient and return it as a data URL. Browser only. */
|
|
67
|
+
declare function gradientToDataURL(seed: number | string, options?: ExportOptions): string;
|
|
68
|
+
/** Render a seed's gradient and resolve a Blob (or null). Browser only. */
|
|
69
|
+
declare function gradientToBlob(seed: number | string, options?: ExportOptions): Promise<Blob | null>;
|
|
70
|
+
|
|
71
|
+
interface GradientAvatarProps {
|
|
72
|
+
/** Any string or number — each unique seed produces a unique gradient. */
|
|
73
|
+
seed: number | string;
|
|
74
|
+
/** Rendered size in pixels. Default: 32. */
|
|
75
|
+
size?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Corner radius. Number = pixels, string = any CSS length.
|
|
78
|
+
* Defaults to a full circle; pass `0` for a square or e.g. `12` for a
|
|
79
|
+
* rounded square. Default: "9999px".
|
|
80
|
+
*/
|
|
81
|
+
radius?: number | string;
|
|
82
|
+
/**
|
|
83
|
+
* Slowly drift the gradient with a GPU-only CSS animation. The canvas is
|
|
84
|
+
* still painted just once — the motion is a compositor transform on the
|
|
85
|
+
* cached layer, so there is no per-frame JavaScript, no repaint, and no
|
|
86
|
+
* extra memory. Automatically disabled for users who request reduced
|
|
87
|
+
* motion. Default: `false`.
|
|
88
|
+
*/
|
|
89
|
+
animate?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Duration of one full drift cycle in seconds. Slower is subtler and
|
|
92
|
+
* cheaper. Only applies when {@link animate} is `true`. Default: `16`.
|
|
93
|
+
*/
|
|
94
|
+
animationDuration?: number;
|
|
95
|
+
/** Additional CSS classes on the wrapper. */
|
|
96
|
+
className?: string;
|
|
97
|
+
/** Extra inline styles merged onto the wrapper. */
|
|
98
|
+
style?: CSSProperties;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Renders a deterministic mesh-gradient avatar on a `<canvas>`.
|
|
102
|
+
* The same seed always produces the same gradient.
|
|
103
|
+
*/
|
|
104
|
+
declare function GradientAvatar({ seed, size, radius, animate, animationDuration, className, style, }: GradientAvatarProps): react_jsx_runtime.JSX.Element;
|
|
105
|
+
|
|
106
|
+
export { type ExportOptions, GradientAvatar, type GradientAvatarProps, type GradientPalette, type Harmony, type RenderOptions, drawMeshGradient, generatePalette, gradientToBlob, gradientToDataURL, renderGradient, seedFromString, toSeed };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { useRef, useEffect } from 'react';
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/index.tsx
|
|
5
|
+
|
|
6
|
+
// src/engine.ts
|
|
7
|
+
var HARMONY_TYPES = [
|
|
8
|
+
"analogous",
|
|
9
|
+
"triadic",
|
|
10
|
+
"splitComplementary",
|
|
11
|
+
"tetradic",
|
|
12
|
+
"complementary"
|
|
13
|
+
];
|
|
14
|
+
var GOLDEN_ANGLE = 137.5;
|
|
15
|
+
var DEFAULT_BLUR_FRACTION = 0.06;
|
|
16
|
+
function seededRandom(seed) {
|
|
17
|
+
let s = seed;
|
|
18
|
+
return () => {
|
|
19
|
+
s += 1831565813;
|
|
20
|
+
let t = s;
|
|
21
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
22
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
23
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function hslToHex(h, s, l) {
|
|
27
|
+
h = (h % 360 + 360) % 360;
|
|
28
|
+
s = Math.max(0, Math.min(100, s)) / 100;
|
|
29
|
+
l = Math.max(0, Math.min(100, l)) / 100;
|
|
30
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
31
|
+
const x = c * (1 - Math.abs(h / 60 % 2 - 1));
|
|
32
|
+
const m = l - c / 2;
|
|
33
|
+
let r = 0;
|
|
34
|
+
let g = 0;
|
|
35
|
+
let b = 0;
|
|
36
|
+
if (h < 60) {
|
|
37
|
+
r = c;
|
|
38
|
+
g = x;
|
|
39
|
+
} else if (h < 120) {
|
|
40
|
+
r = x;
|
|
41
|
+
g = c;
|
|
42
|
+
} else if (h < 180) {
|
|
43
|
+
g = c;
|
|
44
|
+
b = x;
|
|
45
|
+
} else if (h < 240) {
|
|
46
|
+
g = x;
|
|
47
|
+
b = c;
|
|
48
|
+
} else if (h < 300) {
|
|
49
|
+
r = x;
|
|
50
|
+
b = c;
|
|
51
|
+
} else {
|
|
52
|
+
r = c;
|
|
53
|
+
b = x;
|
|
54
|
+
}
|
|
55
|
+
const toHex = (n) => {
|
|
56
|
+
const hex = Math.round((n + m) * 255).toString(16);
|
|
57
|
+
return hex.length === 1 ? `0${hex}` : hex;
|
|
58
|
+
};
|
|
59
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
|
|
60
|
+
}
|
|
61
|
+
function harmonyHues(baseHue, harmony) {
|
|
62
|
+
switch (harmony) {
|
|
63
|
+
case "analogous":
|
|
64
|
+
return [baseHue, baseHue + 30, baseHue + 60, baseHue - 30];
|
|
65
|
+
case "triadic":
|
|
66
|
+
return [baseHue, baseHue + 120, baseHue + 240];
|
|
67
|
+
case "splitComplementary":
|
|
68
|
+
return [baseHue, baseHue + 150, baseHue + 210];
|
|
69
|
+
case "tetradic":
|
|
70
|
+
return [baseHue, baseHue + 90, baseHue + 180, baseHue + 270];
|
|
71
|
+
case "complementary":
|
|
72
|
+
return [baseHue, baseHue + 180, baseHue + 20, baseHue + 200];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function seedFromString(input) {
|
|
76
|
+
let h = 2166136261 >>> 0;
|
|
77
|
+
for (let i = 0; i < input.length; i++) {
|
|
78
|
+
h ^= input.charCodeAt(i);
|
|
79
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
80
|
+
}
|
|
81
|
+
h ^= h >>> 16;
|
|
82
|
+
h = Math.imul(h, 2146121005) >>> 0;
|
|
83
|
+
h ^= h >>> 15;
|
|
84
|
+
h = Math.imul(h, 2221713035) >>> 0;
|
|
85
|
+
h ^= h >>> 16;
|
|
86
|
+
return h >>> 0;
|
|
87
|
+
}
|
|
88
|
+
function toSeed(seed) {
|
|
89
|
+
if (typeof seed === "number") return seed;
|
|
90
|
+
return seedFromString(seed);
|
|
91
|
+
}
|
|
92
|
+
function generatePalette(seed) {
|
|
93
|
+
const s = toSeed(seed);
|
|
94
|
+
const random = seededRandom(s);
|
|
95
|
+
const baseHue = s * GOLDEN_ANGLE % 360;
|
|
96
|
+
const harmonyIndex = Math.floor(random() * HARMONY_TYPES.length);
|
|
97
|
+
const harmony = HARMONY_TYPES[harmonyIndex];
|
|
98
|
+
const hues = harmonyHues(baseHue, harmony);
|
|
99
|
+
const colors = hues.map((hue) => {
|
|
100
|
+
const saturation = 75 + random() * 25;
|
|
101
|
+
const lightness = 50 + random() * 20;
|
|
102
|
+
return hslToHex(hue, saturation, lightness);
|
|
103
|
+
});
|
|
104
|
+
return { seed: s, colors, harmony };
|
|
105
|
+
}
|
|
106
|
+
function drawMeshGradient(ctx, seed, size) {
|
|
107
|
+
const s = toSeed(seed);
|
|
108
|
+
const { colors } = generatePalette(s);
|
|
109
|
+
const random = seededRandom(s * 12345);
|
|
110
|
+
ctx.fillStyle = colors[0];
|
|
111
|
+
ctx.fillRect(0, 0, size, size);
|
|
112
|
+
const numSpots = 8 + Math.floor(random() * 5);
|
|
113
|
+
const spots = [];
|
|
114
|
+
for (let i = 0; i < numSpots; i++) {
|
|
115
|
+
const angle = random() * Math.PI * 2;
|
|
116
|
+
const distance = random() * size * 0.4;
|
|
117
|
+
const centerX = size / 2 + Math.cos(angle) * distance;
|
|
118
|
+
const centerY = size / 2 + Math.sin(angle) * distance;
|
|
119
|
+
spots.push({
|
|
120
|
+
x: centerX + (random() - 0.5) * size * 0.3,
|
|
121
|
+
y: centerY + (random() - 0.5) * size * 0.3,
|
|
122
|
+
radius: size * (0.3 + random() * 0.4),
|
|
123
|
+
color: colors[i % colors.length]
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
spots.sort((a, b) => b.radius - a.radius);
|
|
127
|
+
ctx.globalCompositeOperation = "source-over";
|
|
128
|
+
for (const spot of spots) {
|
|
129
|
+
const g = ctx.createRadialGradient(
|
|
130
|
+
spot.x,
|
|
131
|
+
spot.y,
|
|
132
|
+
0,
|
|
133
|
+
spot.x,
|
|
134
|
+
spot.y,
|
|
135
|
+
spot.radius
|
|
136
|
+
);
|
|
137
|
+
g.addColorStop(0, `${spot.color}FF`);
|
|
138
|
+
g.addColorStop(0.3, `${spot.color}DD`);
|
|
139
|
+
g.addColorStop(0.6, `${spot.color}88`);
|
|
140
|
+
g.addColorStop(1, `${spot.color}00`);
|
|
141
|
+
ctx.fillStyle = g;
|
|
142
|
+
ctx.fillRect(0, 0, size, size);
|
|
143
|
+
}
|
|
144
|
+
const hx = size * 0.3 + random() * size * 0.2;
|
|
145
|
+
const hy = size * 0.3 + random() * size * 0.2;
|
|
146
|
+
const hg = ctx.createRadialGradient(hx, hy, 0, hx, hy, size * 0.3);
|
|
147
|
+
hg.addColorStop(0, "rgba(255,255,255,0.15)");
|
|
148
|
+
hg.addColorStop(1, "rgba(255,255,255,0)");
|
|
149
|
+
ctx.fillStyle = hg;
|
|
150
|
+
ctx.fillRect(0, 0, size, size);
|
|
151
|
+
}
|
|
152
|
+
function blurFor(size, blur) {
|
|
153
|
+
if (blur === 0) return 0;
|
|
154
|
+
return blur ?? Math.round(size * DEFAULT_BLUR_FRACTION);
|
|
155
|
+
}
|
|
156
|
+
function renderGradient(canvas, seed, options = {}) {
|
|
157
|
+
const size = canvas.width;
|
|
158
|
+
const blur = blurFor(size, options.blur);
|
|
159
|
+
const ctx = canvas.getContext("2d");
|
|
160
|
+
if (!ctx) return;
|
|
161
|
+
if (blur <= 0) {
|
|
162
|
+
ctx.clearRect(0, 0, size, size);
|
|
163
|
+
drawMeshGradient(ctx, seed, size);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const scratch = createCanvas(size, size);
|
|
167
|
+
const sctx = scratch.getContext("2d");
|
|
168
|
+
if (!sctx) return;
|
|
169
|
+
drawMeshGradient(sctx, seed, size);
|
|
170
|
+
const scaleUp = 1 + blur / size * 4;
|
|
171
|
+
const dw = size * scaleUp;
|
|
172
|
+
const offset = (dw - size) / 2;
|
|
173
|
+
ctx.clearRect(0, 0, size, size);
|
|
174
|
+
if (supportsCanvasFilter()) {
|
|
175
|
+
ctx.filter = `blur(${blur}px)`;
|
|
176
|
+
ctx.drawImage(scratch, -offset, -offset, dw, dw);
|
|
177
|
+
ctx.filter = "none";
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const factor = Math.max(2, Math.min(16, blur / 2));
|
|
181
|
+
const sw = Math.max(1, Math.round(size / factor));
|
|
182
|
+
const small = createCanvas(sw, sw);
|
|
183
|
+
const smallCtx = small.getContext("2d");
|
|
184
|
+
if (!smallCtx) {
|
|
185
|
+
ctx.drawImage(scratch, -offset, -offset, dw, dw);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
smallCtx.imageSmoothingEnabled = true;
|
|
189
|
+
smallCtx.imageSmoothingQuality = "high";
|
|
190
|
+
smallCtx.drawImage(scratch, 0, 0, sw, sw);
|
|
191
|
+
ctx.imageSmoothingEnabled = true;
|
|
192
|
+
ctx.imageSmoothingQuality = "high";
|
|
193
|
+
ctx.drawImage(small, -offset, -offset, dw, dw);
|
|
194
|
+
}
|
|
195
|
+
var canvasFilterSupport = null;
|
|
196
|
+
function supportsCanvasFilter() {
|
|
197
|
+
if (canvasFilterSupport !== null) return canvasFilterSupport;
|
|
198
|
+
const probe = createCanvas(1, 1).getContext(
|
|
199
|
+
"2d"
|
|
200
|
+
);
|
|
201
|
+
if (!probe) {
|
|
202
|
+
canvasFilterSupport = false;
|
|
203
|
+
return canvasFilterSupport;
|
|
204
|
+
}
|
|
205
|
+
probe.filter = "blur(1px)";
|
|
206
|
+
canvasFilterSupport = probe.filter === "blur(1px)";
|
|
207
|
+
return canvasFilterSupport;
|
|
208
|
+
}
|
|
209
|
+
function createCanvas(w, h) {
|
|
210
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
211
|
+
return new OffscreenCanvas(w, h);
|
|
212
|
+
}
|
|
213
|
+
const c = document.createElement("canvas");
|
|
214
|
+
c.width = w;
|
|
215
|
+
c.height = h;
|
|
216
|
+
return c;
|
|
217
|
+
}
|
|
218
|
+
function gradientToDataURL(seed, options = {}) {
|
|
219
|
+
const { size = 512, type = "image/png", quality = 0.92 } = options;
|
|
220
|
+
const canvas = document.createElement("canvas");
|
|
221
|
+
canvas.width = size;
|
|
222
|
+
canvas.height = size;
|
|
223
|
+
renderGradient(canvas, seed, options);
|
|
224
|
+
return canvas.toDataURL(type, quality);
|
|
225
|
+
}
|
|
226
|
+
function gradientToBlob(seed, options = {}) {
|
|
227
|
+
const { size = 512, type = "image/png", quality = 0.92 } = options;
|
|
228
|
+
const canvas = document.createElement("canvas");
|
|
229
|
+
canvas.width = size;
|
|
230
|
+
canvas.height = size;
|
|
231
|
+
renderGradient(canvas, seed, options);
|
|
232
|
+
return new Promise((resolve) => canvas.toBlob(resolve, type, quality));
|
|
233
|
+
}
|
|
234
|
+
var RENDER_SIZE = 256;
|
|
235
|
+
var BLUR_FRACTION = 0.06;
|
|
236
|
+
var ANIMATED_CLASS = "oa-avatar-canvas--animated";
|
|
237
|
+
var STYLE_ELEMENT_ID = "oa-avatar-keyframes";
|
|
238
|
+
var KEYFRAMES_CSS = `
|
|
239
|
+
@keyframes oa-avatar-drift {
|
|
240
|
+
0% { transform: scale(1.25) translate(0%, 0%) rotate(0deg); }
|
|
241
|
+
20% { transform: scale(1.30) translate(-3%, 2%) rotate(8deg); }
|
|
242
|
+
40% { transform: scale(1.22) translate(3%, 3%) rotate(-6deg); }
|
|
243
|
+
60% { transform: scale(1.28) translate(2%, -3%) rotate(5deg); }
|
|
244
|
+
80% { transform: scale(1.24) translate(-2%, -2%) rotate(-4deg); }
|
|
245
|
+
100% { transform: scale(1.25) translate(0%, 0%) rotate(0deg); }
|
|
246
|
+
}
|
|
247
|
+
.${ANIMATED_CLASS} {
|
|
248
|
+
transform-origin: center;
|
|
249
|
+
will-change: transform;
|
|
250
|
+
animation-name: oa-avatar-drift;
|
|
251
|
+
animation-timing-function: ease-in-out;
|
|
252
|
+
animation-iteration-count: infinite;
|
|
253
|
+
animation-direction: alternate;
|
|
254
|
+
}
|
|
255
|
+
@media (prefers-reduced-motion: reduce) {
|
|
256
|
+
.${ANIMATED_CLASS} {
|
|
257
|
+
animation: none;
|
|
258
|
+
transform: none;
|
|
259
|
+
will-change: auto;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
`;
|
|
263
|
+
var stylesInjected = false;
|
|
264
|
+
function ensureAnimationStyles() {
|
|
265
|
+
if (stylesInjected || typeof document === "undefined") return;
|
|
266
|
+
if (document.getElementById(STYLE_ELEMENT_ID)) {
|
|
267
|
+
stylesInjected = true;
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const el = document.createElement("style");
|
|
271
|
+
el.id = STYLE_ELEMENT_ID;
|
|
272
|
+
el.textContent = KEYFRAMES_CSS;
|
|
273
|
+
document.head.appendChild(el);
|
|
274
|
+
stylesInjected = true;
|
|
275
|
+
}
|
|
276
|
+
function GradientAvatar({
|
|
277
|
+
seed,
|
|
278
|
+
size = 32,
|
|
279
|
+
radius = "9999px",
|
|
280
|
+
animate = false,
|
|
281
|
+
animationDuration = 16,
|
|
282
|
+
className,
|
|
283
|
+
style
|
|
284
|
+
}) {
|
|
285
|
+
const canvasRef = useRef(null);
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
const canvas = canvasRef.current;
|
|
288
|
+
if (!canvas) return;
|
|
289
|
+
const ctx = canvas.getContext("2d");
|
|
290
|
+
if (!ctx) return;
|
|
291
|
+
ctx.clearRect(0, 0, RENDER_SIZE, RENDER_SIZE);
|
|
292
|
+
drawMeshGradient(ctx, seed, RENDER_SIZE);
|
|
293
|
+
}, [seed]);
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
if (animate) ensureAnimationStyles();
|
|
296
|
+
}, [animate]);
|
|
297
|
+
const blurPx = Math.max(1, Math.round(size * BLUR_FRACTION));
|
|
298
|
+
const canvasStyle = {
|
|
299
|
+
width: "100%",
|
|
300
|
+
height: "100%",
|
|
301
|
+
display: "block",
|
|
302
|
+
filter: `blur(${blurPx}px)`
|
|
303
|
+
};
|
|
304
|
+
if (animate) {
|
|
305
|
+
canvasStyle.animationDuration = `${animationDuration}s`;
|
|
306
|
+
}
|
|
307
|
+
return /* @__PURE__ */ jsx(
|
|
308
|
+
"span",
|
|
309
|
+
{
|
|
310
|
+
className,
|
|
311
|
+
style: {
|
|
312
|
+
display: "inline-block",
|
|
313
|
+
overflow: "hidden",
|
|
314
|
+
borderRadius: radius,
|
|
315
|
+
width: size,
|
|
316
|
+
height: size,
|
|
317
|
+
...style
|
|
318
|
+
},
|
|
319
|
+
children: /* @__PURE__ */ jsx(
|
|
320
|
+
"canvas",
|
|
321
|
+
{
|
|
322
|
+
ref: canvasRef,
|
|
323
|
+
width: RENDER_SIZE,
|
|
324
|
+
height: RENDER_SIZE,
|
|
325
|
+
className: animate ? ANIMATED_CLASS : void 0,
|
|
326
|
+
style: canvasStyle
|
|
327
|
+
}
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export { GradientAvatar, drawMeshGradient, generatePalette, gradientToBlob, gradientToDataURL, renderGradient, seedFromString, toSeed };
|
|
334
|
+
//# sourceMappingURL=index.js.map
|
|
335
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/engine.ts","../src/index.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Gradient engine for @apollo-deploy/avatars.\n *\n * Framework-agnostic mesh-gradient avatar generator. Every seed (string or\n * number) deterministically produces a unique gradient — no stored images,\n * no network. Pure palette/RNG core plus optional Canvas2D render helpers.\n */\n\nexport type Harmony =\n\t| \"analogous\"\n\t| \"triadic\"\n\t| \"splitComplementary\"\n\t| \"tetradic\"\n\t| \"complementary\";\n\nexport interface GradientPalette {\n\t/** The numeric seed the palette was derived from. */\n\tseed: number;\n\t/** Hex color stops used to paint the mesh. */\n\tcolors: string[];\n\t/** Which color-harmony rule produced the hues. */\n\tharmony: Harmony;\n}\n\nconst HARMONY_TYPES: Harmony[] = [\n\t\"analogous\",\n\t\"triadic\",\n\t\"splitComplementary\",\n\t\"tetradic\",\n\t\"complementary\",\n];\n\nconst GOLDEN_ANGLE = 137.5;\n\n/** Default blur radius as a fraction of the rendered dimension. */\nexport const DEFAULT_BLUR_FRACTION = 0.06;\n\nfunction seededRandom(seed: number): () => number {\n\tlet s = seed;\n\treturn () => {\n\t\ts += 0x6d2b79f5;\n\t\tlet t = s;\n\t\tt = Math.imul(t ^ (t >>> 15), t | 1);\n\t\tt ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n\t\treturn ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n\t};\n}\n\nfunction hslToHex(h: number, s: number, l: number): string {\n\th = ((h % 360) + 360) % 360;\n\ts = Math.max(0, Math.min(100, s)) / 100;\n\tl = Math.max(0, Math.min(100, l)) / 100;\n\n\tconst c = (1 - Math.abs(2 * l - 1)) * s;\n\tconst x = c * (1 - Math.abs(((h / 60) % 2) - 1));\n\tconst m = l - c / 2;\n\n\tlet r = 0;\n\tlet g = 0;\n\tlet b = 0;\n\tif (h < 60) {\n\t\tr = c;\n\t\tg = x;\n\t} else if (h < 120) {\n\t\tr = x;\n\t\tg = c;\n\t} else if (h < 180) {\n\t\tg = c;\n\t\tb = x;\n\t} else if (h < 240) {\n\t\tg = x;\n\t\tb = c;\n\t} else if (h < 300) {\n\t\tr = x;\n\t\tb = c;\n\t} else {\n\t\tr = c;\n\t\tb = x;\n\t}\n\n\tconst toHex = (n: number) => {\n\t\tconst hex = Math.round((n + m) * 255).toString(16);\n\t\treturn hex.length === 1 ? `0${hex}` : hex;\n\t};\n\treturn `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();\n}\n\nfunction harmonyHues(baseHue: number, harmony: Harmony): number[] {\n\tswitch (harmony) {\n\t\tcase \"analogous\":\n\t\t\treturn [baseHue, baseHue + 30, baseHue + 60, baseHue - 30];\n\t\tcase \"triadic\":\n\t\t\treturn [baseHue, baseHue + 120, baseHue + 240];\n\t\tcase \"splitComplementary\":\n\t\t\treturn [baseHue, baseHue + 150, baseHue + 210];\n\t\tcase \"tetradic\":\n\t\t\treturn [baseHue, baseHue + 90, baseHue + 180, baseHue + 270];\n\t\tcase \"complementary\":\n\t\t\treturn [baseHue, baseHue + 180, baseHue + 20, baseHue + 200];\n\t}\n}\n\n/**\n * Stable string → 32-bit unsigned hash (FNV-1a + bit-mixing avalanche).\n * Uses the full uint32 range as a seed so similar strings diverge fully.\n */\nexport function seedFromString(input: string): number {\n\tlet h = 2166136261 >>> 0;\n\tfor (let i = 0; i < input.length; i++) {\n\t\th ^= input.charCodeAt(i);\n\t\th = Math.imul(h, 16777619) >>> 0;\n\t}\n\th ^= h >>> 16;\n\th = Math.imul(h, 0x7feb352d) >>> 0;\n\th ^= h >>> 15;\n\th = Math.imul(h, 0x846ca68b) >>> 0;\n\th ^= h >>> 16;\n\treturn h >>> 0;\n}\n\n/** Normalize a string or number seed to the numeric seed used internally. */\nexport function toSeed(seed: number | string): number {\n\tif (typeof seed === \"number\") return seed;\n\treturn seedFromString(seed);\n}\n\n/** Derive the deterministic color palette for a seed. */\nexport function generatePalette(seed: number | string): GradientPalette {\n\tconst s = toSeed(seed);\n\tconst random = seededRandom(s);\n\tconst baseHue = (s * GOLDEN_ANGLE) % 360;\n\tconst harmonyIndex = Math.floor(random() * HARMONY_TYPES.length);\n\tconst harmony = HARMONY_TYPES[harmonyIndex];\n\tconst hues = harmonyHues(baseHue, harmony);\n\tconst colors = hues.map((hue) => {\n\t\tconst saturation = 75 + random() * 25;\n\t\tconst lightness = 50 + random() * 20;\n\t\treturn hslToHex(hue, saturation, lightness);\n\t});\n\treturn { seed: s, colors, harmony };\n}\n\n/**\n * Minimal Canvas2D context surface the renderer needs. Both\n * `HTMLCanvasElement` and `OffscreenCanvas` 2D contexts satisfy it.\n */\nexport type GradientContext = {\n\tfillStyle: string | CanvasGradient | CanvasPattern;\n\tglobalCompositeOperation: GlobalCompositeOperation;\n\tfillRect(x: number, y: number, w: number, h: number): void;\n\tcreateRadialGradient(\n\t\tx0: number,\n\t\ty0: number,\n\t\tr0: number,\n\t\tx1: number,\n\t\ty1: number,\n\t\tr1: number,\n\t): CanvasGradient;\n};\n\n/**\n * Draw the mesh gradient for `seed` into `ctx` at `size` x `size`.\n * The caller is responsible for any blur — apply `filter: blur(…)` on the\n * displayed canvas (≈6% of the rendered dimension) for the signature look,\n * or use {@link renderGradient} / {@link gradientToDataURL} which bake it in.\n */\nexport function drawMeshGradient(\n\tctx: GradientContext,\n\tseed: number | string,\n\tsize: number,\n): void {\n\tconst s = toSeed(seed);\n\tconst { colors } = generatePalette(s);\n\tconst random = seededRandom(s * 12345);\n\n\tctx.fillStyle = colors[0];\n\tctx.fillRect(0, 0, size, size);\n\n\tconst numSpots = 8 + Math.floor(random() * 5);\n\tconst spots: Array<{ x: number; y: number; radius: number; color: string }> =\n\t\t[];\n\n\tfor (let i = 0; i < numSpots; i++) {\n\t\tconst angle = random() * Math.PI * 2;\n\t\tconst distance = random() * size * 0.4;\n\t\tconst centerX = size / 2 + Math.cos(angle) * distance;\n\t\tconst centerY = size / 2 + Math.sin(angle) * distance;\n\t\tspots.push({\n\t\t\tx: centerX + (random() - 0.5) * size * 0.3,\n\t\t\ty: centerY + (random() - 0.5) * size * 0.3,\n\t\t\tradius: size * (0.3 + random() * 0.4),\n\t\t\tcolor: colors[i % colors.length],\n\t\t});\n\t}\n\n\tspots.sort((a, b) => b.radius - a.radius);\n\n\tctx.globalCompositeOperation = \"source-over\";\n\tfor (const spot of spots) {\n\t\tconst g = ctx.createRadialGradient(\n\t\t\tspot.x,\n\t\t\tspot.y,\n\t\t\t0,\n\t\t\tspot.x,\n\t\t\tspot.y,\n\t\t\tspot.radius,\n\t\t);\n\t\tg.addColorStop(0, `${spot.color}FF`);\n\t\tg.addColorStop(0.3, `${spot.color}DD`);\n\t\tg.addColorStop(0.6, `${spot.color}88`);\n\t\tg.addColorStop(1, `${spot.color}00`);\n\t\tctx.fillStyle = g;\n\t\tctx.fillRect(0, 0, size, size);\n\t}\n\n\tconst hx = size * 0.3 + random() * size * 0.2;\n\tconst hy = size * 0.3 + random() * size * 0.2;\n\tconst hg = ctx.createRadialGradient(hx, hy, 0, hx, hy, size * 0.3);\n\thg.addColorStop(0, \"rgba(255,255,255,0.15)\");\n\thg.addColorStop(1, \"rgba(255,255,255,0)\");\n\tctx.fillStyle = hg;\n\tctx.fillRect(0, 0, size, size);\n}\n\nexport interface RenderOptions {\n\t/**\n\t * Blur radius in pixels. Defaults to ~6% of the canvas size for the\n\t * signature soft look. Pass `0` to disable.\n\t */\n\tblur?: number;\n}\n\nfunction blurFor(size: number, blur?: number): number {\n\tif (blur === 0) return 0;\n\treturn blur ?? Math.round(size * DEFAULT_BLUR_FRACTION);\n}\n\n/**\n * Render a seed's gradient into an existing canvas, baking in the soft blur.\n * Draws at the canvas's current `width`/`height`. Browser/OffscreenCanvas only.\n */\nexport function renderGradient(\n\tcanvas: HTMLCanvasElement | OffscreenCanvas,\n\tseed: number | string,\n\toptions: RenderOptions = {},\n): void {\n\tconst size = canvas.width;\n\tconst blur = blurFor(size, options.blur);\n\n\tconst ctx = canvas.getContext(\"2d\") as CanvasRenderingContext2D | null;\n\tif (!ctx) return;\n\n\tif (blur <= 0) {\n\t\tctx.clearRect(0, 0, size, size);\n\t\tdrawMeshGradient(ctx, seed, size);\n\t\treturn;\n\t}\n\n\t// Draw the raw mesh on a scratch canvas, then composite it back with blur\n\t// scaled up slightly so the soft edges fall outside the frame (no ring).\n\tconst scratch = createCanvas(size, size);\n\tconst sctx = scratch.getContext(\"2d\") as CanvasRenderingContext2D | null;\n\tif (!sctx) return;\n\tdrawMeshGradient(sctx, seed, size);\n\n\tconst scaleUp = 1 + (blur / size) * 4;\n\tconst dw = size * scaleUp;\n\tconst offset = (dw - size) / 2;\n\tctx.clearRect(0, 0, size, size);\n\tif (supportsCanvasFilter()) {\n\t\tctx.filter = `blur(${blur}px)`;\n\t\tctx.drawImage(scratch as CanvasImageSource, -offset, -offset, dw, dw);\n\t\tctx.filter = \"none\";\n\t\treturn;\n\t}\n\t// 2D-canvas `filter` is a silent no-op on Safari < 17: approximate the\n\t// gaussian by bouncing through a small canvas. Bilinear resampling on the\n\t// way down and back up smears by roughly the downscale factor, which is\n\t// plenty for a mesh that is already smooth gradients.\n\tconst factor = Math.max(2, Math.min(16, blur / 2));\n\tconst sw = Math.max(1, Math.round(size / factor));\n\tconst small = createCanvas(sw, sw);\n\tconst smallCtx = small.getContext(\"2d\") as CanvasRenderingContext2D | null;\n\tif (!smallCtx) {\n\t\tctx.drawImage(scratch as CanvasImageSource, -offset, -offset, dw, dw);\n\t\treturn;\n\t}\n\tsmallCtx.imageSmoothingEnabled = true;\n\tsmallCtx.imageSmoothingQuality = \"high\";\n\tsmallCtx.drawImage(scratch as CanvasImageSource, 0, 0, sw, sw);\n\tctx.imageSmoothingEnabled = true;\n\tctx.imageSmoothingQuality = \"high\";\n\tctx.drawImage(small as CanvasImageSource, -offset, -offset, dw, dw);\n}\n\n/* Engines that honor 2D-canvas `filter` echo an assigned value back from the\n * property; Safari < 17 ignores the assignment. Probed once, then cached. */\nlet canvasFilterSupport: boolean | null = null;\nfunction supportsCanvasFilter(): boolean {\n\tif (canvasFilterSupport !== null) return canvasFilterSupport;\n\tconst probe = createCanvas(1, 1).getContext(\n\t\t\"2d\",\n\t) as CanvasRenderingContext2D | null;\n\tif (!probe) {\n\t\tcanvasFilterSupport = false;\n\t\treturn canvasFilterSupport;\n\t}\n\tprobe.filter = \"blur(1px)\";\n\tcanvasFilterSupport = probe.filter === \"blur(1px)\";\n\treturn canvasFilterSupport;\n}\n\nfunction createCanvas(\n\tw: number,\n\th: number,\n): HTMLCanvasElement | OffscreenCanvas {\n\tif (typeof OffscreenCanvas !== \"undefined\") {\n\t\treturn new OffscreenCanvas(w, h);\n\t}\n\tconst c = document.createElement(\"canvas\");\n\tc.width = w;\n\tc.height = h;\n\treturn c;\n}\n\nexport interface ExportOptions extends RenderOptions {\n\t/** Output pixel dimensions (square). Default: 512. */\n\tsize?: number;\n\t/** Image MIME type. Default: \"image/png\". */\n\ttype?: string;\n\t/** Quality 0–1 for lossy types. Default: 0.92. */\n\tquality?: number;\n}\n\n/** Render a seed's gradient and return it as a data URL. Browser only. */\nexport function gradientToDataURL(\n\tseed: number | string,\n\toptions: ExportOptions = {},\n): string {\n\tconst { size = 512, type = \"image/png\", quality = 0.92 } = options;\n\tconst canvas = document.createElement(\"canvas\");\n\tcanvas.width = size;\n\tcanvas.height = size;\n\trenderGradient(canvas, seed, options);\n\treturn canvas.toDataURL(type, quality);\n}\n\n/** Render a seed's gradient and resolve a Blob (or null). Browser only. */\nexport function gradientToBlob(\n\tseed: number | string,\n\toptions: ExportOptions = {},\n): Promise<Blob | null> {\n\tconst { size = 512, type = \"image/png\", quality = 0.92 } = options;\n\tconst canvas = document.createElement(\"canvas\");\n\tcanvas.width = size;\n\tcanvas.height = size;\n\trenderGradient(canvas, seed, options);\n\treturn new Promise((resolve) => canvas.toBlob(resolve, type, quality));\n}\n","import type { CSSProperties } from \"react\";\nimport { useEffect, useRef } from \"react\";\nimport { drawMeshGradient } from \"./engine\";\n\nexport interface GradientAvatarProps {\n\t/** Any string or number — each unique seed produces a unique gradient. */\n\tseed: number | string;\n\t/** Rendered size in pixels. Default: 32. */\n\tsize?: number;\n\t/**\n\t * Corner radius. Number = pixels, string = any CSS length.\n\t * Defaults to a full circle; pass `0` for a square or e.g. `12` for a\n\t * rounded square. Default: \"9999px\".\n\t */\n\tradius?: number | string;\n\t/**\n\t * Slowly drift the gradient with a GPU-only CSS animation. The canvas is\n\t * still painted just once — the motion is a compositor transform on the\n\t * cached layer, so there is no per-frame JavaScript, no repaint, and no\n\t * extra memory. Automatically disabled for users who request reduced\n\t * motion. Default: `false`.\n\t */\n\tanimate?: boolean;\n\t/**\n\t * Duration of one full drift cycle in seconds. Slower is subtler and\n\t * cheaper. Only applies when {@link animate} is `true`. Default: `16`.\n\t */\n\tanimationDuration?: number;\n\t/** Additional CSS classes on the wrapper. */\n\tclassName?: string;\n\t/** Extra inline styles merged onto the wrapper. */\n\tstyle?: CSSProperties;\n}\n\n/** Internal render resolution. Higher than display size so the CSS blur is smooth. */\nconst RENDER_SIZE = 256;\n/** Blur radius as a fraction of display size. */\nconst BLUR_FRACTION = 0.06;\n\n/** Class applied to the canvas when animating. Styles injected once, lazily. */\nconst ANIMATED_CLASS = \"oa-avatar-canvas--animated\";\nconst STYLE_ELEMENT_ID = \"oa-avatar-keyframes\";\n\n/**\n * Keyframes for the drift. The canvas is scaled up so that the rotation and\n * translation never expose the clipped container edges, and the whole thing is\n * a single `transform` track so the browser can run it entirely on the\n * compositor thread. `prefers-reduced-motion` opts out with no JS listener.\n */\nconst KEYFRAMES_CSS = `\n@keyframes oa-avatar-drift {\n\t0% { transform: scale(1.25) translate(0%, 0%) rotate(0deg); }\n\t20% { transform: scale(1.30) translate(-3%, 2%) rotate(8deg); }\n\t40% { transform: scale(1.22) translate(3%, 3%) rotate(-6deg); }\n\t60% { transform: scale(1.28) translate(2%, -3%) rotate(5deg); }\n\t80% { transform: scale(1.24) translate(-2%, -2%) rotate(-4deg); }\n\t100% { transform: scale(1.25) translate(0%, 0%) rotate(0deg); }\n}\n.${ANIMATED_CLASS} {\n\ttransform-origin: center;\n\twill-change: transform;\n\tanimation-name: oa-avatar-drift;\n\tanimation-timing-function: ease-in-out;\n\tanimation-iteration-count: infinite;\n\tanimation-direction: alternate;\n}\n@media (prefers-reduced-motion: reduce) {\n\t.${ANIMATED_CLASS} {\n\t\tanimation: none;\n\t\ttransform: none;\n\t\twill-change: auto;\n\t}\n}\n`;\n\nlet stylesInjected = false;\n\n/** Inject the drift keyframes into the document head exactly once (client-only). */\nfunction ensureAnimationStyles(): void {\n\tif (stylesInjected || typeof document === \"undefined\") return;\n\tif (document.getElementById(STYLE_ELEMENT_ID)) {\n\t\tstylesInjected = true;\n\t\treturn;\n\t}\n\tconst el = document.createElement(\"style\");\n\tel.id = STYLE_ELEMENT_ID;\n\tel.textContent = KEYFRAMES_CSS;\n\tdocument.head.appendChild(el);\n\tstylesInjected = true;\n}\n\n/**\n * Renders a deterministic mesh-gradient avatar on a `<canvas>`.\n * The same seed always produces the same gradient.\n */\nexport function GradientAvatar({\n\tseed,\n\tsize = 32,\n\tradius = \"9999px\",\n\tanimate = false,\n\tanimationDuration = 16,\n\tclassName,\n\tstyle,\n}: GradientAvatarProps) {\n\tconst canvasRef = useRef<HTMLCanvasElement>(null);\n\n\tuseEffect(() => {\n\t\tconst canvas = canvasRef.current;\n\t\tif (!canvas) return;\n\t\tconst ctx = canvas.getContext(\"2d\");\n\t\tif (!ctx) return;\n\t\tctx.clearRect(0, 0, RENDER_SIZE, RENDER_SIZE);\n\t\tdrawMeshGradient(ctx, seed, RENDER_SIZE);\n\t}, [seed]);\n\n\tuseEffect(() => {\n\t\tif (animate) ensureAnimationStyles();\n\t}, [animate]);\n\n\tconst blurPx = Math.max(1, Math.round(size * BLUR_FRACTION));\n\n\tconst canvasStyle: CSSProperties = {\n\t\twidth: \"100%\",\n\t\theight: \"100%\",\n\t\tdisplay: \"block\",\n\t\tfilter: `blur(${blurPx}px)`,\n\t};\n\tif (animate) {\n\t\tcanvasStyle.animationDuration = `${animationDuration}s`;\n\t}\n\n\treturn (\n\t\t<span\n\t\t\tclassName={className}\n\t\t\tstyle={{\n\t\t\t\tdisplay: \"inline-block\",\n\t\t\t\toverflow: \"hidden\",\n\t\t\t\tborderRadius: radius,\n\t\t\t\twidth: size,\n\t\t\t\theight: size,\n\t\t\t\t...style,\n\t\t\t}}\n\t\t>\n\t\t\t<canvas\n\t\t\t\tref={canvasRef}\n\t\t\t\twidth={RENDER_SIZE}\n\t\t\t\theight={RENDER_SIZE}\n\t\t\t\tclassName={animate ? ANIMATED_CLASS : undefined}\n\t\t\t\tstyle={canvasStyle}\n\t\t\t/>\n\t\t</span>\n\t);\n}\n\nexport type {\n\tExportOptions,\n\tGradientPalette,\n\tHarmony,\n\tRenderOptions,\n} from \"./engine\";\nexport {\n\tdrawMeshGradient,\n\tgeneratePalette,\n\tgradientToBlob,\n\tgradientToDataURL,\n\trenderGradient,\n\tseedFromString,\n\ttoSeed,\n} from \"./engine\";\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@apollo-deploy/avatars",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "React component for generative gradient avatars. Every seed renders a unique, deterministic mesh gradient, with no stored images.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"avatars",
|
|
7
|
+
"generative",
|
|
8
|
+
"generative-avatars",
|
|
9
|
+
"gradient",
|
|
10
|
+
"mesh-gradient",
|
|
11
|
+
"avatar-generator",
|
|
12
|
+
"react",
|
|
13
|
+
"profile-pictures"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://avatars.outpacestudios.com",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/outpacelabs/avatars.git",
|
|
19
|
+
"directory": "packages/avatars"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/outpacelabs/avatars/issues"
|
|
23
|
+
},
|
|
24
|
+
"author": "Outpace Studios (https://outpacestudios.com)",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"main": "./dist/index.cjs",
|
|
29
|
+
"module": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"import": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"default": "./dist/index.js"
|
|
36
|
+
},
|
|
37
|
+
"require": {
|
|
38
|
+
"types": "./dist/index.d.cts",
|
|
39
|
+
"default": "./dist/index.cjs"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"./package.json": "./package.json"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist",
|
|
46
|
+
"README.md",
|
|
47
|
+
"LICENSE"
|
|
48
|
+
],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsup",
|
|
51
|
+
"test": "node test/properties.mjs",
|
|
52
|
+
"dev": "tsup --watch",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"prepublishOnly": "pnpm build"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"react": ">=18"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/react": "^19.2.10",
|
|
61
|
+
"react": "^19.2.4",
|
|
62
|
+
"tsup": "^8.3.5",
|
|
63
|
+
"typescript": "^5.9.3"
|
|
64
|
+
},
|
|
65
|
+
"publishConfig": {
|
|
66
|
+
"access": "public",
|
|
67
|
+
"registry": "https://registry.npmjs.org/"
|
|
68
|
+
}
|
|
69
|
+
}
|