agent-avatars 1.0.0-rc.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.
@@ -0,0 +1,40 @@
1
+ const AVATAR_STYLE_VERSION = "1";
2
+ const GRID_HEIGHT = 4;
3
+ const MAX_ROW_VALUE = 31;
4
+
5
+ function normalizeHexColor(value, label) {
6
+ if (typeof value !== "string" || !/^#[0-9a-fA-F]{6}$/.test(value)) {
7
+ throw new TypeError(label + " must be a six-digit hexadecimal color such as #EAF3F8.");
8
+ }
9
+ return value.toUpperCase();
10
+ }
11
+
12
+ function snapshotRenderableDescriptor(descriptor) {
13
+ if (!descriptor || descriptor.styleVersion !== AVATAR_STYLE_VERSION) {
14
+ throw new TypeError("descriptor must be a " + AVATAR_STYLE_VERSION + " avatar descriptor.");
15
+ }
16
+
17
+ const rows = descriptor.rows;
18
+ if (
19
+ !Array.isArray(rows)
20
+ || rows.length !== GRID_HEIGHT
21
+ || rows.some((row) => !Number.isInteger(row) || row < 0 || row > MAX_ROW_VALUE)
22
+ ) {
23
+ throw new TypeError(
24
+ "descriptor.rows must contain " + GRID_HEIGHT + " integers in [0, " + MAX_ROW_VALUE + "]."
25
+ );
26
+ }
27
+
28
+ // Snapshot rows before reading colors so accessors cannot mutate the rendered bitmap after validation.
29
+ const safeRows = rows.slice();
30
+ const colors = descriptor.colors;
31
+ const background = normalizeHexColor(colors?.background, "descriptor.colors.background");
32
+ const foreground = normalizeHexColor(colors?.foreground, "descriptor.colors.foreground");
33
+
34
+ return {
35
+ rows: safeRows,
36
+ colors: { background, foreground },
37
+ };
38
+ }
39
+
40
+ export { normalizeHexColor, snapshotRenderableDescriptor };
@@ -0,0 +1,168 @@
1
+ const POP5 = Object.freeze(Array.from({ length: 32 }, (_, value) => {
2
+ let count = 0;
3
+ let bits = value;
4
+ while (bits !== 0) {
5
+ bits &= bits - 1;
6
+ count++;
7
+ }
8
+ return count;
9
+ }));
10
+
11
+ const DEGREES = 180 / Math.PI;
12
+ const RADIANS = Math.PI / 180;
13
+ const POW_25_7 = 25 ** 7;
14
+
15
+ function shapeHammingDistance(leftRows, rightRows) {
16
+ let distance = 0;
17
+ for (let row = 0; row < 4; row++) {
18
+ distance += POP5[(leftRows[row] ^ rightRows[row]) & 31];
19
+ }
20
+ return distance;
21
+ }
22
+
23
+ function deltaE2000(left, right) {
24
+ const [l1, a1, b1] = left;
25
+ const [l2, a2, b2] = right;
26
+ const c1 = Math.hypot(a1, b1);
27
+ const c2 = Math.hypot(a2, b2);
28
+ const meanC = (c1 + c2) / 2;
29
+ const meanC7 = meanC ** 7;
30
+ const g = 0.5 * (1 - Math.sqrt(meanC7 / (meanC7 + POW_25_7)));
31
+ const a1Prime = (1 + g) * a1;
32
+ const a2Prime = (1 + g) * a2;
33
+ const c1Prime = Math.hypot(a1Prime, b1);
34
+ const c2Prime = Math.hypot(a2Prime, b2);
35
+ const h1Prime = hueDegrees(b1, a1Prime);
36
+ const h2Prime = hueDegrees(b2, a2Prime);
37
+
38
+ const deltaLPrime = l2 - l1;
39
+ const deltaCPrime = c2Prime - c1Prime;
40
+ let deltaHDegrees = h2Prime - h1Prime;
41
+ if (c1Prime * c2Prime === 0) {
42
+ deltaHDegrees = 0;
43
+ } else if (deltaHDegrees > 180) {
44
+ deltaHDegrees -= 360;
45
+ } else if (deltaHDegrees < -180) {
46
+ deltaHDegrees += 360;
47
+ }
48
+ const deltaHPrime = 2 * Math.sqrt(c1Prime * c2Prime) * Math.sin(deltaHDegrees * RADIANS / 2);
49
+
50
+ const meanLPrime = (l1 + l2) / 2;
51
+ const meanCPrime = (c1Prime + c2Prime) / 2;
52
+ let meanHPrime;
53
+ if (c1Prime * c2Prime === 0) {
54
+ meanHPrime = h1Prime + h2Prime;
55
+ } else if (Math.abs(h1Prime - h2Prime) <= 180) {
56
+ meanHPrime = (h1Prime + h2Prime) / 2;
57
+ } else if (h1Prime + h2Prime < 360) {
58
+ meanHPrime = (h1Prime + h2Prime + 360) / 2;
59
+ } else {
60
+ meanHPrime = (h1Prime + h2Prime - 360) / 2;
61
+ }
62
+
63
+ const t = 1
64
+ - 0.17 * Math.cos((meanHPrime - 30) * RADIANS)
65
+ + 0.24 * Math.cos(2 * meanHPrime * RADIANS)
66
+ + 0.32 * Math.cos((3 * meanHPrime + 6) * RADIANS)
67
+ - 0.20 * Math.cos((4 * meanHPrime - 63) * RADIANS);
68
+ const lightnessOffset = meanLPrime - 50;
69
+ const sl = 1 + (0.015 * lightnessOffset ** 2) / Math.sqrt(20 + lightnessOffset ** 2);
70
+ const sc = 1 + 0.045 * meanCPrime;
71
+ const sh = 1 + 0.015 * meanCPrime * t;
72
+ const deltaTheta = 30 * Math.exp(-(((meanHPrime - 275) / 25) ** 2));
73
+ const meanCPrime7 = meanCPrime ** 7;
74
+ const rc = 2 * Math.sqrt(meanCPrime7 / (meanCPrime7 + POW_25_7));
75
+ const rt = -rc * Math.sin(2 * deltaTheta * RADIANS);
76
+ const lightnessTerm = deltaLPrime / sl;
77
+ const chromaTerm = deltaCPrime / sc;
78
+ const hueTerm = deltaHPrime / sh;
79
+ return Math.sqrt(
80
+ lightnessTerm ** 2
81
+ + chromaTerm ** 2
82
+ + hueTerm ** 2
83
+ + rt * chromaTerm * hueTerm
84
+ );
85
+ }
86
+
87
+ function hueDegrees(b, aPrime) {
88
+ const degrees = Math.atan2(b, aPrime) * DEGREES;
89
+ return degrees >= 0 ? degrees : degrees + 360;
90
+ }
91
+
92
+ function normalizeHex(value, label) {
93
+ if (typeof value !== "string" || !/^#[0-9a-fA-F]{6}$/.test(value)) {
94
+ throw new TypeError(`${label} must be a six-digit hexadecimal color.`);
95
+ }
96
+ return value;
97
+ }
98
+
99
+ function hexToLab(value, label) {
100
+ const hex = normalizeHex(value, label);
101
+ const red = srgbChannel(Number.parseInt(hex.slice(1, 3), 16) / 255);
102
+ const green = srgbChannel(Number.parseInt(hex.slice(3, 5), 16) / 255);
103
+ const blue = srgbChannel(Number.parseInt(hex.slice(5, 7), 16) / 255);
104
+ const x = (0.4124564 * red + 0.3575761 * green + 0.1804375 * blue) / 0.95047;
105
+ const y = 0.2126729 * red + 0.7151522 * green + 0.0721750 * blue;
106
+ const z = (0.0193339 * red + 0.1191920 * green + 0.9503041 * blue) / 1.08883;
107
+ const fx = labCurve(x);
108
+ const fy = labCurve(y);
109
+ const fz = labCurve(z);
110
+ return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];
111
+ }
112
+
113
+ function srgbChannel(value) {
114
+ return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
115
+ }
116
+
117
+ function labCurve(value) {
118
+ const delta = 6 / 29;
119
+ return value > delta ** 3 ? Math.cbrt(value) : value / (3 * delta ** 2) + 4 / 29;
120
+ }
121
+
122
+ function paletteToLab(item, index = "palette") {
123
+ return {
124
+ light: {
125
+ background: hexToLab(item?.light?.background, `${index}.light.background`),
126
+ foreground: hexToLab(item?.light?.foreground, `${index}.light.foreground`),
127
+ },
128
+ dark: {
129
+ background: hexToLab(item?.dark?.background, `${index}.dark.background`),
130
+ foreground: hexToLab(item?.dark?.foreground, `${index}.dark.foreground`),
131
+ },
132
+ };
133
+ }
134
+
135
+ function themeDistance(left, right) {
136
+ const backgroundDeltaE = deltaE2000(left.background, right.background);
137
+ const foregroundDeltaE = deltaE2000(left.foreground, right.foreground);
138
+ return Math.sqrt(0.85 * backgroundDeltaE ** 2 + 0.15 * foregroundDeltaE ** 2);
139
+ }
140
+
141
+ function paletteLabDistance(left, right) {
142
+ return Math.min(themeDistance(left.light, right.light), themeDistance(left.dark, right.dark));
143
+ }
144
+
145
+ function paletteDistance(left, right) {
146
+ return paletteLabDistance(paletteToLab(left, "leftPalette"), paletteToLab(right, "rightPalette"));
147
+ }
148
+
149
+ function buildPaletteDistanceMatrix(palettes) {
150
+ if (!Array.isArray(palettes)) throw new TypeError("palettes must be an array.");
151
+ const converted = palettes.map((item, index) => paletteToLab(item, `palettes[${index}]`));
152
+ const matrix = new Float64Array(palettes.length * palettes.length);
153
+ for (let left = 0; left < palettes.length; left++) {
154
+ for (let right = left + 1; right < palettes.length; right++) {
155
+ const distance = paletteLabDistance(converted[left], converted[right]);
156
+ matrix[left * palettes.length + right] = distance;
157
+ matrix[right * palettes.length + left] = distance;
158
+ }
159
+ }
160
+ return matrix;
161
+ }
162
+
163
+ module.exports = {
164
+ shapeHammingDistance,
165
+ deltaE2000,
166
+ paletteDistance,
167
+ buildPaletteDistanceMatrix,
168
+ };
@@ -0,0 +1,168 @@
1
+ const POP5 = Object.freeze(Array.from({ length: 32 }, (_, value) => {
2
+ let count = 0;
3
+ let bits = value;
4
+ while (bits !== 0) {
5
+ bits &= bits - 1;
6
+ count++;
7
+ }
8
+ return count;
9
+ }));
10
+
11
+ const DEGREES = 180 / Math.PI;
12
+ const RADIANS = Math.PI / 180;
13
+ const POW_25_7 = 25 ** 7;
14
+
15
+ function shapeHammingDistance(leftRows, rightRows) {
16
+ let distance = 0;
17
+ for (let row = 0; row < 4; row++) {
18
+ distance += POP5[(leftRows[row] ^ rightRows[row]) & 31];
19
+ }
20
+ return distance;
21
+ }
22
+
23
+ function deltaE2000(left, right) {
24
+ const [l1, a1, b1] = left;
25
+ const [l2, a2, b2] = right;
26
+ const c1 = Math.hypot(a1, b1);
27
+ const c2 = Math.hypot(a2, b2);
28
+ const meanC = (c1 + c2) / 2;
29
+ const meanC7 = meanC ** 7;
30
+ const g = 0.5 * (1 - Math.sqrt(meanC7 / (meanC7 + POW_25_7)));
31
+ const a1Prime = (1 + g) * a1;
32
+ const a2Prime = (1 + g) * a2;
33
+ const c1Prime = Math.hypot(a1Prime, b1);
34
+ const c2Prime = Math.hypot(a2Prime, b2);
35
+ const h1Prime = hueDegrees(b1, a1Prime);
36
+ const h2Prime = hueDegrees(b2, a2Prime);
37
+
38
+ const deltaLPrime = l2 - l1;
39
+ const deltaCPrime = c2Prime - c1Prime;
40
+ let deltaHDegrees = h2Prime - h1Prime;
41
+ if (c1Prime * c2Prime === 0) {
42
+ deltaHDegrees = 0;
43
+ } else if (deltaHDegrees > 180) {
44
+ deltaHDegrees -= 360;
45
+ } else if (deltaHDegrees < -180) {
46
+ deltaHDegrees += 360;
47
+ }
48
+ const deltaHPrime = 2 * Math.sqrt(c1Prime * c2Prime) * Math.sin(deltaHDegrees * RADIANS / 2);
49
+
50
+ const meanLPrime = (l1 + l2) / 2;
51
+ const meanCPrime = (c1Prime + c2Prime) / 2;
52
+ let meanHPrime;
53
+ if (c1Prime * c2Prime === 0) {
54
+ meanHPrime = h1Prime + h2Prime;
55
+ } else if (Math.abs(h1Prime - h2Prime) <= 180) {
56
+ meanHPrime = (h1Prime + h2Prime) / 2;
57
+ } else if (h1Prime + h2Prime < 360) {
58
+ meanHPrime = (h1Prime + h2Prime + 360) / 2;
59
+ } else {
60
+ meanHPrime = (h1Prime + h2Prime - 360) / 2;
61
+ }
62
+
63
+ const t = 1
64
+ - 0.17 * Math.cos((meanHPrime - 30) * RADIANS)
65
+ + 0.24 * Math.cos(2 * meanHPrime * RADIANS)
66
+ + 0.32 * Math.cos((3 * meanHPrime + 6) * RADIANS)
67
+ - 0.20 * Math.cos((4 * meanHPrime - 63) * RADIANS);
68
+ const lightnessOffset = meanLPrime - 50;
69
+ const sl = 1 + (0.015 * lightnessOffset ** 2) / Math.sqrt(20 + lightnessOffset ** 2);
70
+ const sc = 1 + 0.045 * meanCPrime;
71
+ const sh = 1 + 0.015 * meanCPrime * t;
72
+ const deltaTheta = 30 * Math.exp(-(((meanHPrime - 275) / 25) ** 2));
73
+ const meanCPrime7 = meanCPrime ** 7;
74
+ const rc = 2 * Math.sqrt(meanCPrime7 / (meanCPrime7 + POW_25_7));
75
+ const rt = -rc * Math.sin(2 * deltaTheta * RADIANS);
76
+ const lightnessTerm = deltaLPrime / sl;
77
+ const chromaTerm = deltaCPrime / sc;
78
+ const hueTerm = deltaHPrime / sh;
79
+ return Math.sqrt(
80
+ lightnessTerm ** 2
81
+ + chromaTerm ** 2
82
+ + hueTerm ** 2
83
+ + rt * chromaTerm * hueTerm
84
+ );
85
+ }
86
+
87
+ function hueDegrees(b, aPrime) {
88
+ const degrees = Math.atan2(b, aPrime) * DEGREES;
89
+ return degrees >= 0 ? degrees : degrees + 360;
90
+ }
91
+
92
+ function normalizeHex(value, label) {
93
+ if (typeof value !== "string" || !/^#[0-9a-fA-F]{6}$/.test(value)) {
94
+ throw new TypeError(`${label} must be a six-digit hexadecimal color.`);
95
+ }
96
+ return value;
97
+ }
98
+
99
+ function hexToLab(value, label) {
100
+ const hex = normalizeHex(value, label);
101
+ const red = srgbChannel(Number.parseInt(hex.slice(1, 3), 16) / 255);
102
+ const green = srgbChannel(Number.parseInt(hex.slice(3, 5), 16) / 255);
103
+ const blue = srgbChannel(Number.parseInt(hex.slice(5, 7), 16) / 255);
104
+ const x = (0.4124564 * red + 0.3575761 * green + 0.1804375 * blue) / 0.95047;
105
+ const y = 0.2126729 * red + 0.7151522 * green + 0.0721750 * blue;
106
+ const z = (0.0193339 * red + 0.1191920 * green + 0.9503041 * blue) / 1.08883;
107
+ const fx = labCurve(x);
108
+ const fy = labCurve(y);
109
+ const fz = labCurve(z);
110
+ return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];
111
+ }
112
+
113
+ function srgbChannel(value) {
114
+ return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
115
+ }
116
+
117
+ function labCurve(value) {
118
+ const delta = 6 / 29;
119
+ return value > delta ** 3 ? Math.cbrt(value) : value / (3 * delta ** 2) + 4 / 29;
120
+ }
121
+
122
+ function paletteToLab(item, index = "palette") {
123
+ return {
124
+ light: {
125
+ background: hexToLab(item?.light?.background, `${index}.light.background`),
126
+ foreground: hexToLab(item?.light?.foreground, `${index}.light.foreground`),
127
+ },
128
+ dark: {
129
+ background: hexToLab(item?.dark?.background, `${index}.dark.background`),
130
+ foreground: hexToLab(item?.dark?.foreground, `${index}.dark.foreground`),
131
+ },
132
+ };
133
+ }
134
+
135
+ function themeDistance(left, right) {
136
+ const backgroundDeltaE = deltaE2000(left.background, right.background);
137
+ const foregroundDeltaE = deltaE2000(left.foreground, right.foreground);
138
+ return Math.sqrt(0.85 * backgroundDeltaE ** 2 + 0.15 * foregroundDeltaE ** 2);
139
+ }
140
+
141
+ function paletteLabDistance(left, right) {
142
+ return Math.min(themeDistance(left.light, right.light), themeDistance(left.dark, right.dark));
143
+ }
144
+
145
+ function paletteDistance(left, right) {
146
+ return paletteLabDistance(paletteToLab(left, "leftPalette"), paletteToLab(right, "rightPalette"));
147
+ }
148
+
149
+ function buildPaletteDistanceMatrix(palettes) {
150
+ if (!Array.isArray(palettes)) throw new TypeError("palettes must be an array.");
151
+ const converted = palettes.map((item, index) => paletteToLab(item, `palettes[${index}]`));
152
+ const matrix = new Float64Array(palettes.length * palettes.length);
153
+ for (let left = 0; left < palettes.length; left++) {
154
+ for (let right = left + 1; right < palettes.length; right++) {
155
+ const distance = paletteLabDistance(converted[left], converted[right]);
156
+ matrix[left * palettes.length + right] = distance;
157
+ matrix[right * palettes.length + left] = distance;
158
+ }
159
+ }
160
+ return matrix;
161
+ }
162
+
163
+ export {
164
+ shapeHammingDistance,
165
+ deltaE2000,
166
+ paletteDistance,
167
+ buildPaletteDistanceMatrix,
168
+ };
Binary file
package/package.json ADDED
@@ -0,0 +1,148 @@
1
+ {
2
+ "name": "agent-avatars",
3
+ "version": "1.0.0-rc.2",
4
+ "description": "Zero-dependency deterministic SVG and PNG avatars for AI agents, bots, services, and users.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/NotXf1le/agent-avatars.git"
8
+ },
9
+ "homepage": "https://notxf1le.github.io/agent-avatars/",
10
+ "bugs": {
11
+ "url": "https://github.com/NotXf1le/agent-avatars/issues"
12
+ },
13
+ "type": "module",
14
+ "main": "./dist/index.cjs",
15
+ "module": "./dist/index.mjs",
16
+ "types": "./dist/index.d.mts",
17
+ "exports": {
18
+ ".": {
19
+ "import": {
20
+ "types": "./dist/index.d.mts",
21
+ "default": "./dist/index.mjs"
22
+ },
23
+ "require": {
24
+ "types": "./dist/index.d.cts",
25
+ "default": "./dist/index.cjs"
26
+ },
27
+ "default": "./dist/index.mjs"
28
+ },
29
+ "./png": {
30
+ "import": {
31
+ "types": "./dist/png.d.mts",
32
+ "default": "./dist/png.mjs"
33
+ },
34
+ "require": {
35
+ "types": "./dist/png.d.cts",
36
+ "default": "./dist/png.cjs"
37
+ }
38
+ },
39
+ "./react": {
40
+ "import": {
41
+ "types": "./dist/react.d.mts",
42
+ "default": "./dist/react.mjs"
43
+ },
44
+ "require": {
45
+ "types": "./dist/react.d.cts",
46
+ "default": "./dist/react.cjs"
47
+ }
48
+ },
49
+ "./private": {
50
+ "import": {
51
+ "types": "./dist/private.d.mts",
52
+ "default": "./dist/private.mjs"
53
+ },
54
+ "require": {
55
+ "types": "./dist/private.d.cts",
56
+ "default": "./dist/private.cjs"
57
+ }
58
+ },
59
+ "./package.json": "./package.json"
60
+ },
61
+ "files": [
62
+ "dist",
63
+ "examples/preview.png",
64
+ "README.md",
65
+ "CHANGELOG.md",
66
+ "SECURITY.md",
67
+ "LICENSE"
68
+ ],
69
+ "sideEffects": false,
70
+ "scripts": {
71
+ "build": "node scripts/build.mjs",
72
+ "build:pages": "node scripts/build-pages.mjs",
73
+ "benchmark": "node scripts/benchmark.mjs",
74
+ "demo": "node scripts/serve-demo.mjs",
75
+ "test": "npm run build && npm run test:runtime && tsc -p tests/tsconfig.json && npm run test:html && node tests/package.test.mjs && node tests/release-policy.test.mjs && node tests/consumer.test.mjs",
76
+ "test:runtime": "node tests/run-all.mjs",
77
+ "test:types": "npm run build && tsc -p tests/tsconfig.json",
78
+ "test:html": "html-validate index.html",
79
+ "test:browser": "npm run build && node tests/browser.test.mjs",
80
+ "test:package": "npm run build && node tests/package.test.mjs",
81
+ "test:packed-runtime": "npm run build && node tests/packed-runtime.test.mjs",
82
+ "test:consumer": "npm run build && node tests/consumer.test.mjs",
83
+ "test:react": "node tests/react.test.mjs",
84
+ "test:release": "npm run test:browser && npm run test:stress && npm run release:dry-run && npm audit",
85
+ "test:stress": "node tests/png-stress.test.mjs",
86
+ "test:update-golden": "node scripts/update-golden.mjs",
87
+ "release:dry-run": "node scripts/release-dry-run.mjs",
88
+ "prepublishOnly": "node scripts/check-release-tag.mjs",
89
+ "prepack": "npm test"
90
+ },
91
+ "engines": {
92
+ "node": ">=18"
93
+ },
94
+ "devEngines": {
95
+ "runtime": {
96
+ "name": "node",
97
+ "version": ">=24.8.0",
98
+ "onFail": "error"
99
+ },
100
+ "packageManager": {
101
+ "name": "npm",
102
+ "version": "11.11.0",
103
+ "onFail": "error"
104
+ }
105
+ },
106
+ "peerDependencies": {
107
+ "react": ">=18 <20"
108
+ },
109
+ "peerDependenciesMeta": {
110
+ "react": {
111
+ "optional": true
112
+ }
113
+ },
114
+ "devDependencies": {
115
+ "@arethetypeswrong/cli": "0.18.5",
116
+ "@types/node": "20.19.1",
117
+ "@types/react": "^18.3.31",
118
+ "esbuild": "0.25.6",
119
+ "html-validate": "11.5.6",
120
+ "playwright": "1.61.1",
121
+ "publint": "0.3.21",
122
+ "react": "^18.3.1",
123
+ "react-dom": "18.3.1",
124
+ "typescript": "^5.9.3",
125
+ "vite": "7.3.6",
126
+ "webpack": "5.108.4",
127
+ "webpack-cli": "6.0.1"
128
+ },
129
+ "keywords": [
130
+ "deterministic-avatar",
131
+ "agent-avatar",
132
+ "ai-agent",
133
+ "avatar-generator",
134
+ "identicon",
135
+ "svg",
136
+ "png",
137
+ "typescript",
138
+ "react",
139
+ "zero-dependency"
140
+ ],
141
+ "author": "Felix Koba",
142
+ "license": "MIT",
143
+ "funding": "https://ko-fi.com/felixkoba",
144
+ "packageManager": "npm@11.11.0",
145
+ "publishConfig": {
146
+ "access": "public"
147
+ }
148
+ }