@pixygon/avatar 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,390 @@
1
+ // src/types/index.ts
2
+ var STYLE_COUNTS = {
3
+ eye: 12,
4
+ brow: 8,
5
+ nose: 8,
6
+ mouth: 8,
7
+ hair: 16,
8
+ facial_hair: 8,
9
+ glasses: 6
10
+ };
11
+
12
+ // src/presets.ts
13
+ var SKIN_PRESETS = [
14
+ [0.98, 0.89, 0.78],
15
+ // Porcelain
16
+ [0.96, 0.84, 0.71],
17
+ // Fair
18
+ [0.92, 0.78, 0.63],
19
+ // Light
20
+ [0.87, 0.72, 0.55],
21
+ // Medium-light
22
+ [0.78, 0.62, 0.46],
23
+ // Medium
24
+ [0.68, 0.51, 0.36],
25
+ // Olive
26
+ [0.58, 0.42, 0.3],
27
+ // Tan
28
+ [0.47, 0.33, 0.22],
29
+ // Brown
30
+ [0.36, 0.24, 0.16],
31
+ // Dark
32
+ [0.26, 0.17, 0.11]
33
+ // Deep
34
+ ];
35
+ var EYE_COLOR_PRESETS = [
36
+ [0.1, 0.1, 0.1],
37
+ // Black
38
+ [0.35, 0.22, 0.1],
39
+ // Brown
40
+ [0.55, 0.35, 0.15],
41
+ // Hazel
42
+ [0.25, 0.5, 0.25],
43
+ // Green
44
+ [0.2, 0.4, 0.7],
45
+ // Blue
46
+ [0.45, 0.45, 0.5],
47
+ // Gray
48
+ [0.55, 0.25, 0.25],
49
+ // Red
50
+ [0.5, 0.3, 0.6]
51
+ // Violet
52
+ ];
53
+ var HAIR_COLOR_PRESETS = [
54
+ [0.08, 0.06, 0.05],
55
+ // Black
56
+ [0.22, 0.14, 0.08],
57
+ // Dark brown
58
+ [0.4, 0.26, 0.14],
59
+ // Brown
60
+ [0.58, 0.42, 0.24],
61
+ // Light brown
62
+ [0.78, 0.62, 0.34],
63
+ // Dirty blonde
64
+ [0.9, 0.78, 0.48],
65
+ // Blonde
66
+ [0.72, 0.26, 0.12],
67
+ // Red
68
+ [0.88, 0.52, 0.22],
69
+ // Ginger
70
+ [0.6, 0.6, 0.62],
71
+ // Gray
72
+ [0.92, 0.92, 0.94]
73
+ // White
74
+ ];
75
+
76
+ // src/defaults.ts
77
+ function defaultAppearance() {
78
+ return {
79
+ body: { height: 0.5, build: 0.5 },
80
+ head: {
81
+ width: 1,
82
+ height: 1,
83
+ eye_style: 0,
84
+ eye_color: [...EYE_COLOR_PRESETS[1]],
85
+ eye_y: 0.5,
86
+ eye_spacing: 0.5,
87
+ eye_size: 0.5,
88
+ eye_rotation: 0.5,
89
+ brow_style: 0,
90
+ brow_color: [...HAIR_COLOR_PRESETS[0]],
91
+ brow_y: 0.5,
92
+ brow_spacing: 0.5,
93
+ brow_size: 0.5,
94
+ brow_rotation: 0.5,
95
+ nose_style: 0,
96
+ nose_y: 0.5,
97
+ nose_size: 0.5,
98
+ mouth_style: 0,
99
+ mouth_y: 0.5,
100
+ mouth_size: 0.5,
101
+ mouth_color: [0.75, 0.35, 0.35],
102
+ hair_style: 0,
103
+ hair_color: [...HAIR_COLOR_PRESETS[2]],
104
+ facial_hair_style: 0,
105
+ facial_hair_color: [...HAIR_COLOR_PRESETS[0]],
106
+ glasses_style: 0,
107
+ glasses_color: [0.1, 0.1, 0.1]
108
+ },
109
+ skin_color: [...SKIN_PRESETS[2]]
110
+ };
111
+ }
112
+ function randomAppearance() {
113
+ const rng = (min, max) => min + Math.random() * (max - min);
114
+ const rngInt = (max) => Math.floor(Math.random() * max);
115
+ const pick = (arr) => arr[rngInt(arr.length)];
116
+ const hairColor = pick(HAIR_COLOR_PRESETS);
117
+ return {
118
+ body: {
119
+ height: rng(0.2, 0.8),
120
+ build: rng(0.2, 0.8)
121
+ },
122
+ head: {
123
+ width: rng(0.7, 1.3),
124
+ height: rng(0.7, 1.3),
125
+ eye_style: rngInt(STYLE_COUNTS.eye),
126
+ eye_color: [...pick(EYE_COLOR_PRESETS)],
127
+ eye_y: rng(0.3, 0.7),
128
+ eye_spacing: rng(0.3, 0.7),
129
+ eye_size: rng(0.3, 0.7),
130
+ eye_rotation: 0.5,
131
+ brow_style: rngInt(STYLE_COUNTS.brow),
132
+ brow_color: [...hairColor],
133
+ brow_y: rng(0.3, 0.7),
134
+ brow_spacing: rng(0.3, 0.7),
135
+ brow_size: rng(0.3, 0.7),
136
+ brow_rotation: 0.5,
137
+ nose_style: rngInt(STYLE_COUNTS.nose),
138
+ nose_y: rng(0.3, 0.7),
139
+ nose_size: rng(0.3, 0.7),
140
+ mouth_style: rngInt(STYLE_COUNTS.mouth),
141
+ mouth_y: rng(0.3, 0.7),
142
+ mouth_size: rng(0.3, 0.7),
143
+ mouth_color: [0.75, 0.35, 0.35],
144
+ hair_style: rngInt(STYLE_COUNTS.hair),
145
+ hair_color: [...hairColor],
146
+ facial_hair_style: Math.random() < 0.3 ? 1 + rngInt(STYLE_COUNTS.facial_hair - 1) : 0,
147
+ facial_hair_color: [...hairColor],
148
+ glasses_style: Math.random() < 0.2 ? 1 + rngInt(STYLE_COUNTS.glasses - 1) : 0,
149
+ glasses_color: [0.1, 0.1, 0.1]
150
+ },
151
+ skin_color: [...pick(SKIN_PRESETS)]
152
+ };
153
+ }
154
+
155
+ // src/skeleton.ts
156
+ function mirrorX(p) {
157
+ return [-p[0], p[1], p[2]];
158
+ }
159
+ function buildSkeleton(body) {
160
+ const h = 0.85 + body.height * 0.35;
161
+ const b = 0.75 + body.build * 0.5;
162
+ const footY = 0.02 * h;
163
+ const ankleY = 0.08 * h;
164
+ const kneeY = 0.45 * h;
165
+ const hipY = 0.88 * h;
166
+ const waistY = 0.95 * h;
167
+ const chestY = 1.18 * h;
168
+ const shoulderY = 1.3 * h;
169
+ const neckY = 1.38 * h;
170
+ const headBaseY = 1.42 * h;
171
+ const headCenterY = 1.55 * h;
172
+ const hipSpread = 0.09 * b;
173
+ const shoulderSpread = 0.16 * b;
174
+ const bones = [];
175
+ bones.push({ start: [0, hipY, 0], end: [0, waistY, 0], radius: 0.1 * b });
176
+ bones.push({ start: [0, waistY, 0], end: [0, chestY, 0], radius: 0.11 * b });
177
+ bones.push({ start: [0, chestY, 0], end: [0, shoulderY, 0], radius: 0.12 * b });
178
+ bones.push({ start: [0, neckY, 0], end: [0, headBaseY, 0], radius: 0.04 * b });
179
+ const lShoulder = [shoulderSpread, shoulderY, 0];
180
+ const lElbow = [shoulderSpread + 0.22 * h, shoulderY - 0.08 * h, 0];
181
+ const lWrist = [shoulderSpread + 0.42 * h, shoulderY - 0.18 * h, 0];
182
+ const lHand = [shoulderSpread + 0.5 * h, shoulderY - 0.22 * h, 0];
183
+ bones.push({ start: lShoulder, end: lElbow, radius: 0.04 * b });
184
+ bones.push({ start: lElbow, end: lWrist, radius: 0.035 * b });
185
+ bones.push({ start: lWrist, end: lHand, radius: 0.03 * b });
186
+ bones.push({ start: mirrorX(lShoulder), end: mirrorX(lElbow), radius: 0.04 * b });
187
+ bones.push({ start: mirrorX(lElbow), end: mirrorX(lWrist), radius: 0.035 * b });
188
+ bones.push({ start: mirrorX(lWrist), end: mirrorX(lHand), radius: 0.03 * b });
189
+ const lHip = [hipSpread, hipY, 0];
190
+ const lKnee = [hipSpread + 0.01, kneeY, 0];
191
+ const lAnkle = [hipSpread, ankleY, 0];
192
+ const lToe = [hipSpread, footY, 0.06];
193
+ bones.push({ start: lHip, end: lKnee, radius: 0.055 * b });
194
+ bones.push({ start: lKnee, end: lAnkle, radius: 0.045 * b });
195
+ bones.push({ start: lAnkle, end: lToe, radius: 0.035 * b });
196
+ bones.push({ start: mirrorX(lHip), end: mirrorX(lKnee), radius: 0.055 * b });
197
+ bones.push({ start: mirrorX(lKnee), end: mirrorX(lAnkle), radius: 0.045 * b });
198
+ bones.push({ start: mirrorX(lAnkle), end: mirrorX(lToe), radius: 0.035 * b });
199
+ const head = {
200
+ center: [0, headCenterY, 0],
201
+ radius_x: 0.11,
202
+ radius_y: 0.12,
203
+ radius_z: 0.1
204
+ };
205
+ return { bones, head };
206
+ }
207
+
208
+ // src/hooks/useAvatarEditor.ts
209
+ import { useCallback, useState } from "react";
210
+ function useAvatarEditor(initial) {
211
+ const [appearance, setAppearance] = useState(
212
+ () => initial ?? defaultAppearance()
213
+ );
214
+ const [tab, setTab] = useState("body");
215
+ const update = useCallback((path, value) => {
216
+ setAppearance((prev) => {
217
+ const next = structuredClone(prev);
218
+ const parts = path.split(".");
219
+ let obj = next;
220
+ for (let i = 0; i < parts.length - 1; i++) {
221
+ obj = obj[parts[i]];
222
+ }
223
+ obj[parts[parts.length - 1]] = value;
224
+ return next;
225
+ });
226
+ }, []);
227
+ const randomize = useCallback(() => setAppearance(randomAppearance()), []);
228
+ const reset = useCallback(() => setAppearance(initial ?? defaultAppearance()), [initial]);
229
+ return { appearance, tab, setTab, update, randomize, reset };
230
+ }
231
+
232
+ // src/utils/geometry.ts
233
+ import * as THREE from "three";
234
+ var RING_SEGMENTS = 10;
235
+ var CAP_RINGS = 4;
236
+ function capsuleBetweenGeometry(start, end, radius) {
237
+ const s = new THREE.Vector3(...start);
238
+ const e = new THREE.Vector3(...end);
239
+ const dir = new THREE.Vector3().subVectors(e, s);
240
+ const length = dir.length();
241
+ let up, right, forward;
242
+ if (length > 1e-4) {
243
+ up = dir.clone().divideScalar(length);
244
+ const ref = Math.abs(up.y) > 0.99 ? new THREE.Vector3(0, 0, 1) : new THREE.Vector3(0, 1, 0);
245
+ right = new THREE.Vector3().crossVectors(up, ref).normalize();
246
+ forward = new THREE.Vector3().crossVectors(right, up);
247
+ } else {
248
+ up = new THREE.Vector3(0, 1, 0);
249
+ right = new THREE.Vector3(1, 0, 0);
250
+ forward = new THREE.Vector3(0, 0, 1);
251
+ }
252
+ const positions = [];
253
+ const normals = [];
254
+ const indices = [];
255
+ const seg = RING_SEGMENTS;
256
+ const transformPos = (lx, ly, lz) => [
257
+ s.x + right.x * lx + up.x * ly + forward.x * lz,
258
+ s.y + right.y * lx + up.y * ly + forward.y * lz,
259
+ s.z + right.z * lx + up.z * ly + forward.z * lz
260
+ ];
261
+ const transformNorm = (nx, ny, nz) => {
262
+ const v = new THREE.Vector3(
263
+ right.x * nx + up.x * ny + forward.x * nz,
264
+ right.y * nx + up.y * ny + forward.y * nz,
265
+ right.z * nx + up.z * ny + forward.z * nz
266
+ ).normalize();
267
+ return [v.x, v.y, v.z];
268
+ };
269
+ for (let ring = 0; ring <= CAP_RINGS; ring++) {
270
+ const phi = Math.PI * 0.5 * (1 - ring / CAP_RINGS);
271
+ const ly = -Math.sin(phi) * radius;
272
+ const rr = Math.cos(phi) * radius;
273
+ for (let j = 0; j <= seg; j++) {
274
+ const theta = 2 * Math.PI * j / seg;
275
+ const lx = rr * Math.cos(theta);
276
+ const lz = rr * Math.sin(theta);
277
+ positions.push(...transformPos(lx, ly, lz));
278
+ normals.push(...transformNorm(lx / radius, -Math.sin(phi), lz / radius));
279
+ }
280
+ }
281
+ for (let ring = 0; ring <= 1; ring++) {
282
+ const ly = ring * length;
283
+ for (let j = 0; j <= seg; j++) {
284
+ const theta = 2 * Math.PI * j / seg;
285
+ const lx = radius * Math.cos(theta);
286
+ const lz = radius * Math.sin(theta);
287
+ positions.push(...transformPos(lx, ly, lz));
288
+ normals.push(...transformNorm(lx / radius, 0, lz / radius));
289
+ }
290
+ }
291
+ for (let ring = 0; ring <= CAP_RINGS; ring++) {
292
+ const phi = Math.PI * 0.5 * ring / CAP_RINGS;
293
+ const ly = length + Math.sin(phi) * radius;
294
+ const rr = Math.cos(phi) * radius;
295
+ for (let j = 0; j <= seg; j++) {
296
+ const theta = 2 * Math.PI * j / seg;
297
+ const lx = rr * Math.cos(theta);
298
+ const lz = rr * Math.sin(theta);
299
+ positions.push(...transformPos(lx, ly, lz));
300
+ normals.push(...transformNorm(lx / radius, Math.sin(phi), lz / radius));
301
+ }
302
+ }
303
+ const totalRings = CAP_RINGS + 2 + CAP_RINGS;
304
+ for (let ring = 0; ring < totalRings; ring++) {
305
+ for (let j = 0; j < seg; j++) {
306
+ const cur = ring * (seg + 1) + j;
307
+ const next = cur + seg + 1;
308
+ indices.push(cur, next, cur + 1);
309
+ indices.push(cur + 1, next, next + 1);
310
+ }
311
+ }
312
+ const geo = new THREE.BufferGeometry();
313
+ geo.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
314
+ geo.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3));
315
+ geo.setIndex(indices);
316
+ return geo;
317
+ }
318
+ function ellipsoidGeometry(rx, ry, rz) {
319
+ const segs = 14;
320
+ const rings = 12;
321
+ const positions = [];
322
+ const normals = [];
323
+ const indices = [];
324
+ for (let ring = 0; ring <= rings; ring++) {
325
+ const phi = Math.PI * ring / rings;
326
+ const y = ry * Math.cos(phi);
327
+ const rr = Math.sin(phi);
328
+ for (let seg = 0; seg <= segs; seg++) {
329
+ const theta = 2 * Math.PI * seg / segs;
330
+ const x = rx * rr * Math.cos(theta);
331
+ const z = rz * rr * Math.sin(theta);
332
+ positions.push(x, y, z);
333
+ const nx = x / (rx * rx);
334
+ const ny = y / (ry * ry);
335
+ const nz = z / (rz * rz);
336
+ const len = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
337
+ normals.push(nx / len, ny / len, nz / len);
338
+ }
339
+ }
340
+ for (let ring = 0; ring < rings; ring++) {
341
+ for (let seg = 0; seg < segs; seg++) {
342
+ const cur = ring * (segs + 1) + seg;
343
+ const next = cur + segs + 1;
344
+ indices.push(cur, next, cur + 1);
345
+ indices.push(cur + 1, next, next + 1);
346
+ }
347
+ }
348
+ const geo = new THREE.BufferGeometry();
349
+ geo.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
350
+ geo.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3));
351
+ geo.setIndex(indices);
352
+ return geo;
353
+ }
354
+ function buildAvatarGroup(appearance) {
355
+ const { bones, head } = buildSkeleton(appearance.body);
356
+ const skinColor = new THREE.Color(...appearance.skin_color);
357
+ const material = new THREE.MeshStandardMaterial({
358
+ color: skinColor,
359
+ roughness: 0.7,
360
+ metalness: 0.05
361
+ });
362
+ const group = new THREE.Group();
363
+ for (const bone of bones) {
364
+ const geo = capsuleBetweenGeometry(bone.start, bone.end, bone.radius);
365
+ group.add(new THREE.Mesh(geo, material));
366
+ }
367
+ const headGeo = ellipsoidGeometry(
368
+ head.radius_x * appearance.head.width,
369
+ head.radius_y * appearance.head.height,
370
+ head.radius_z * appearance.head.width
371
+ );
372
+ const headMesh = new THREE.Mesh(headGeo, material);
373
+ headMesh.position.set(...head.center);
374
+ group.add(headMesh);
375
+ return group;
376
+ }
377
+
378
+ export {
379
+ STYLE_COUNTS,
380
+ SKIN_PRESETS,
381
+ EYE_COLOR_PRESETS,
382
+ HAIR_COLOR_PRESETS,
383
+ defaultAppearance,
384
+ randomAppearance,
385
+ buildSkeleton,
386
+ useAvatarEditor,
387
+ capsuleBetweenGeometry,
388
+ ellipsoidGeometry,
389
+ buildAvatarGroup
390
+ };
@@ -1,5 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { A as AvatarAppearance } from '../index-DwPxw0AI.mjs';
3
+ import React from 'react';
3
4
 
4
5
  interface AvatarEditorProps {
5
6
  /** Initial appearance (uses default if omitted). */
@@ -15,4 +16,45 @@ interface AvatarEditorProps {
15
16
  }
16
17
  declare function AvatarEditor({ initial, onChange, onDone, onCancel, className, }: AvatarEditorProps): react_jsx_runtime.JSX.Element;
17
18
 
18
- export { AvatarEditor, type AvatarEditorProps };
19
+ interface AvatarRendererProps {
20
+ appearance: AvatarAppearance;
21
+ /** Width of the canvas (CSS). Default: '100%'. */
22
+ width?: string | number;
23
+ /** Height of the canvas (CSS). Default: 300. */
24
+ height?: string | number;
25
+ /** Auto-rotate the camera. Default: false. */
26
+ autoRotate?: boolean;
27
+ /** Background colour. Default: '#15151f'. */
28
+ background?: string;
29
+ /** Optional CSS class. */
30
+ className?: string;
31
+ /** Optional inline style for the container. */
32
+ style?: React.CSSProperties;
33
+ }
34
+ declare function AvatarRenderer({ appearance, width, height, autoRotate, background, className, style, }: AvatarRendererProps): react_jsx_runtime.JSX.Element;
35
+
36
+ interface AvatarPreviewProps {
37
+ appearance: AvatarAppearance;
38
+ /** Size in px. Default: 200. */
39
+ size?: number;
40
+ /** Auto-rotate. Default: true. */
41
+ autoRotate?: boolean;
42
+ /** Background colour. */
43
+ background?: string;
44
+ className?: string;
45
+ style?: React.CSSProperties;
46
+ }
47
+ declare function AvatarPreview({ appearance, size, autoRotate, background, className, style, }: AvatarPreviewProps): react_jsx_runtime.JSX.Element;
48
+
49
+ interface AvatarThumbnailProps {
50
+ appearance: AvatarAppearance;
51
+ /** Diameter in px. Default: 48. */
52
+ size?: number;
53
+ /** Background colour. Default: '#1a1a26'. */
54
+ background?: string;
55
+ className?: string;
56
+ style?: React.CSSProperties;
57
+ }
58
+ declare function AvatarThumbnail({ appearance, size, background, className, style, }: AvatarThumbnailProps): react_jsx_runtime.JSX.Element;
59
+
60
+ export { AvatarEditor, type AvatarEditorProps, AvatarPreview, type AvatarPreviewProps, AvatarRenderer, type AvatarRendererProps, AvatarThumbnail, type AvatarThumbnailProps };
@@ -1,5 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { A as AvatarAppearance } from '../index-DwPxw0AI.js';
3
+ import React from 'react';
3
4
 
4
5
  interface AvatarEditorProps {
5
6
  /** Initial appearance (uses default if omitted). */
@@ -15,4 +16,45 @@ interface AvatarEditorProps {
15
16
  }
16
17
  declare function AvatarEditor({ initial, onChange, onDone, onCancel, className, }: AvatarEditorProps): react_jsx_runtime.JSX.Element;
17
18
 
18
- export { AvatarEditor, type AvatarEditorProps };
19
+ interface AvatarRendererProps {
20
+ appearance: AvatarAppearance;
21
+ /** Width of the canvas (CSS). Default: '100%'. */
22
+ width?: string | number;
23
+ /** Height of the canvas (CSS). Default: 300. */
24
+ height?: string | number;
25
+ /** Auto-rotate the camera. Default: false. */
26
+ autoRotate?: boolean;
27
+ /** Background colour. Default: '#15151f'. */
28
+ background?: string;
29
+ /** Optional CSS class. */
30
+ className?: string;
31
+ /** Optional inline style for the container. */
32
+ style?: React.CSSProperties;
33
+ }
34
+ declare function AvatarRenderer({ appearance, width, height, autoRotate, background, className, style, }: AvatarRendererProps): react_jsx_runtime.JSX.Element;
35
+
36
+ interface AvatarPreviewProps {
37
+ appearance: AvatarAppearance;
38
+ /** Size in px. Default: 200. */
39
+ size?: number;
40
+ /** Auto-rotate. Default: true. */
41
+ autoRotate?: boolean;
42
+ /** Background colour. */
43
+ background?: string;
44
+ className?: string;
45
+ style?: React.CSSProperties;
46
+ }
47
+ declare function AvatarPreview({ appearance, size, autoRotate, background, className, style, }: AvatarPreviewProps): react_jsx_runtime.JSX.Element;
48
+
49
+ interface AvatarThumbnailProps {
50
+ appearance: AvatarAppearance;
51
+ /** Diameter in px. Default: 48. */
52
+ size?: number;
53
+ /** Background colour. Default: '#1a1a26'. */
54
+ background?: string;
55
+ className?: string;
56
+ style?: React.CSSProperties;
57
+ }
58
+ declare function AvatarThumbnail({ appearance, size, background, className, style, }: AvatarThumbnailProps): react_jsx_runtime.JSX.Element;
59
+
60
+ export { AvatarEditor, type AvatarEditorProps, AvatarPreview, type AvatarPreviewProps, AvatarRenderer, type AvatarRendererProps, AvatarThumbnail, type AvatarThumbnailProps };