@pixygon/avatar 1.0.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,277 @@
1
+ import {
2
+ EYE_COLOR_PRESETS,
3
+ HAIR_COLOR_PRESETS,
4
+ SKIN_PRESETS,
5
+ STYLE_COUNTS,
6
+ useAvatarEditor
7
+ } from "../chunk-5QZCUXJW.mjs";
8
+
9
+ // src/components/AvatarEditor.tsx
10
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
+ var TABS = [
12
+ { key: "body", label: "Body" },
13
+ { key: "head", label: "Head" },
14
+ { key: "eyes", label: "Eyes" },
15
+ { key: "brows", label: "Brows" },
16
+ { key: "nose", label: "Nose" },
17
+ { key: "mouth", label: "Mouth" },
18
+ { key: "hair", label: "Hair" },
19
+ { key: "extras", label: "Extras" }
20
+ ];
21
+ function AvatarEditor({
22
+ initial,
23
+ onChange,
24
+ onDone,
25
+ onCancel,
26
+ className
27
+ }) {
28
+ const { appearance, tab, setTab, update, randomize, reset } = useAvatarEditor(initial);
29
+ const handleUpdate = (path, value) => {
30
+ update(path, value);
31
+ setTimeout(() => onChange?.(appearance), 0);
32
+ };
33
+ return /* @__PURE__ */ jsxs("div", { className, style: rootStyle, children: [
34
+ /* @__PURE__ */ jsxs("div", { style: headerStyle, children: [
35
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: 700, fontSize: 18 }, children: "Avatar Editor" }),
36
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8 }, children: [
37
+ /* @__PURE__ */ jsx("button", { style: btnStyle, onClick: randomize, children: "Randomize" }),
38
+ /* @__PURE__ */ jsx("button", { style: btnStyle, onClick: reset, children: "Reset" }),
39
+ onCancel && /* @__PURE__ */ jsx("button", { style: btnStyle, onClick: onCancel, children: "Cancel" }),
40
+ onDone && /* @__PURE__ */ jsx("button", { style: { ...btnStyle, background: "#3b6" }, onClick: () => onDone(appearance), children: "Done" })
41
+ ] })
42
+ ] }),
43
+ /* @__PURE__ */ jsx("div", { style: tabBarStyle, children: TABS.map((t) => /* @__PURE__ */ jsx(
44
+ "button",
45
+ {
46
+ onClick: () => setTab(t.key),
47
+ style: {
48
+ ...tabStyle,
49
+ ...tab === t.key ? tabActiveStyle : {}
50
+ },
51
+ children: t.label
52
+ },
53
+ t.key
54
+ )) }),
55
+ /* @__PURE__ */ jsxs("div", { style: contentStyle, children: [
56
+ tab === "body" && /* @__PURE__ */ jsx(BodyTab, { appearance, onUpdate: handleUpdate }),
57
+ tab === "head" && /* @__PURE__ */ jsx(HeadTab, { appearance, onUpdate: handleUpdate }),
58
+ tab === "eyes" && /* @__PURE__ */ jsx(EyesTab, { appearance, onUpdate: handleUpdate }),
59
+ tab === "brows" && /* @__PURE__ */ jsx(BrowsTab, { appearance, onUpdate: handleUpdate }),
60
+ tab === "nose" && /* @__PURE__ */ jsx(NoseTab, { appearance, onUpdate: handleUpdate }),
61
+ tab === "mouth" && /* @__PURE__ */ jsx(MouthTab, { appearance, onUpdate: handleUpdate }),
62
+ tab === "hair" && /* @__PURE__ */ jsx(HairTab, { appearance, onUpdate: handleUpdate }),
63
+ tab === "extras" && /* @__PURE__ */ jsx(ExtrasTab, { appearance, onUpdate: handleUpdate })
64
+ ] })
65
+ ] });
66
+ }
67
+ function BodyTab({ appearance, onUpdate }) {
68
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
69
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Body" }),
70
+ /* @__PURE__ */ jsx(Slider, { label: "Height", value: appearance.body.height, onChange: (v) => onUpdate("body.height", v) }),
71
+ /* @__PURE__ */ jsx(Slider, { label: "Build", value: appearance.body.build, onChange: (v) => onUpdate("body.build", v) }),
72
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Skin Colour" }),
73
+ /* @__PURE__ */ jsx(ColourPresets, { current: appearance.skin_color, presets: SKIN_PRESETS, onPick: (c) => onUpdate("skin_color", c) })
74
+ ] });
75
+ }
76
+ function HeadTab({ appearance, onUpdate }) {
77
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
78
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Head Shape" }),
79
+ /* @__PURE__ */ jsx(Slider, { label: "Width", value: appearance.head.width, min: 0.5, max: 1.5, onChange: (v) => onUpdate("head.width", v) }),
80
+ /* @__PURE__ */ jsx(Slider, { label: "Height", value: appearance.head.height, min: 0.5, max: 1.5, onChange: (v) => onUpdate("head.height", v) })
81
+ ] });
82
+ }
83
+ function EyesTab({ appearance, onUpdate }) {
84
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
85
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Eyes" }),
86
+ /* @__PURE__ */ jsx(StyleSelector, { label: "Style", value: appearance.head.eye_style, count: STYLE_COUNTS.eye, onChange: (v) => onUpdate("head.eye_style", v) }),
87
+ /* @__PURE__ */ jsx(ColourPresets, { current: appearance.head.eye_color, presets: EYE_COLOR_PRESETS, onPick: (c) => onUpdate("head.eye_color", c) }),
88
+ /* @__PURE__ */ jsx(Slider, { label: "Vertical Pos", value: appearance.head.eye_y, onChange: (v) => onUpdate("head.eye_y", v) }),
89
+ /* @__PURE__ */ jsx(Slider, { label: "Spacing", value: appearance.head.eye_spacing, onChange: (v) => onUpdate("head.eye_spacing", v) }),
90
+ /* @__PURE__ */ jsx(Slider, { label: "Size", value: appearance.head.eye_size, onChange: (v) => onUpdate("head.eye_size", v) }),
91
+ /* @__PURE__ */ jsx(Slider, { label: "Rotation", value: appearance.head.eye_rotation, onChange: (v) => onUpdate("head.eye_rotation", v) })
92
+ ] });
93
+ }
94
+ function BrowsTab({ appearance, onUpdate }) {
95
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
96
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Eyebrows" }),
97
+ /* @__PURE__ */ jsx(StyleSelector, { label: "Style", value: appearance.head.brow_style, count: STYLE_COUNTS.brow, onChange: (v) => onUpdate("head.brow_style", v) }),
98
+ /* @__PURE__ */ jsx(ColourPresets, { current: appearance.head.brow_color, presets: HAIR_COLOR_PRESETS, onPick: (c) => onUpdate("head.brow_color", c) }),
99
+ /* @__PURE__ */ jsx(Slider, { label: "Vertical Pos", value: appearance.head.brow_y, onChange: (v) => onUpdate("head.brow_y", v) }),
100
+ /* @__PURE__ */ jsx(Slider, { label: "Spacing", value: appearance.head.brow_spacing, onChange: (v) => onUpdate("head.brow_spacing", v) }),
101
+ /* @__PURE__ */ jsx(Slider, { label: "Size", value: appearance.head.brow_size, onChange: (v) => onUpdate("head.brow_size", v) }),
102
+ /* @__PURE__ */ jsx(Slider, { label: "Rotation", value: appearance.head.brow_rotation, onChange: (v) => onUpdate("head.brow_rotation", v) })
103
+ ] });
104
+ }
105
+ function NoseTab({ appearance, onUpdate }) {
106
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
107
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Nose" }),
108
+ /* @__PURE__ */ jsx(StyleSelector, { label: "Style", value: appearance.head.nose_style, count: STYLE_COUNTS.nose, onChange: (v) => onUpdate("head.nose_style", v) }),
109
+ /* @__PURE__ */ jsx(Slider, { label: "Vertical Pos", value: appearance.head.nose_y, onChange: (v) => onUpdate("head.nose_y", v) }),
110
+ /* @__PURE__ */ jsx(Slider, { label: "Size", value: appearance.head.nose_size, onChange: (v) => onUpdate("head.nose_size", v) })
111
+ ] });
112
+ }
113
+ function MouthTab({ appearance, onUpdate }) {
114
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
115
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Mouth" }),
116
+ /* @__PURE__ */ jsx(StyleSelector, { label: "Style", value: appearance.head.mouth_style, count: STYLE_COUNTS.mouth, onChange: (v) => onUpdate("head.mouth_style", v) }),
117
+ /* @__PURE__ */ jsx(Slider, { label: "Vertical Pos", value: appearance.head.mouth_y, onChange: (v) => onUpdate("head.mouth_y", v) }),
118
+ /* @__PURE__ */ jsx(Slider, { label: "Size", value: appearance.head.mouth_size, onChange: (v) => onUpdate("head.mouth_size", v) })
119
+ ] });
120
+ }
121
+ function HairTab({ appearance, onUpdate }) {
122
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
123
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Hair" }),
124
+ /* @__PURE__ */ jsx(StyleSelector, { label: "Style", value: appearance.head.hair_style, count: STYLE_COUNTS.hair, onChange: (v) => onUpdate("head.hair_style", v) }),
125
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Colour" }),
126
+ /* @__PURE__ */ jsx(ColourPresets, { current: appearance.head.hair_color, presets: HAIR_COLOR_PRESETS, onPick: (c) => onUpdate("head.hair_color", c) })
127
+ ] });
128
+ }
129
+ function ExtrasTab({ appearance, onUpdate }) {
130
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
131
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Facial Hair" }),
132
+ /* @__PURE__ */ jsx(StyleSelector, { label: "Style", value: appearance.head.facial_hair_style, count: STYLE_COUNTS.facial_hair, onChange: (v) => onUpdate("head.facial_hair_style", v) }),
133
+ appearance.head.facial_hair_style > 0 && /* @__PURE__ */ jsx(ColourPresets, { current: appearance.head.facial_hair_color, presets: HAIR_COLOR_PRESETS, onPick: (c) => onUpdate("head.facial_hair_color", c) }),
134
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Glasses" }),
135
+ /* @__PURE__ */ jsx(StyleSelector, { label: "Style", value: appearance.head.glasses_style, count: STYLE_COUNTS.glasses, onChange: (v) => onUpdate("head.glasses_style", v) })
136
+ ] });
137
+ }
138
+ function SectionLabel({ children }) {
139
+ return /* @__PURE__ */ jsx("div", { style: { color: "#ccc", fontWeight: 600, fontSize: 13, margin: "8px 0 4px" }, children });
140
+ }
141
+ function Slider({
142
+ label,
143
+ value,
144
+ min = 0,
145
+ max = 1,
146
+ onChange
147
+ }) {
148
+ return /* @__PURE__ */ jsxs("label", { style: sliderRowStyle, children: [
149
+ /* @__PURE__ */ jsx("span", { style: { minWidth: 90, color: "#aab" }, children: label }),
150
+ /* @__PURE__ */ jsx(
151
+ "input",
152
+ {
153
+ type: "range",
154
+ min,
155
+ max,
156
+ step: 0.01,
157
+ value,
158
+ onChange: (e) => onChange(parseFloat(e.target.value)),
159
+ style: { flex: 1 }
160
+ }
161
+ ),
162
+ /* @__PURE__ */ jsx("span", { style: { minWidth: 40, textAlign: "right", color: "#889", fontSize: 12 }, children: value.toFixed(2) })
163
+ ] });
164
+ }
165
+ function StyleSelector({
166
+ label,
167
+ value,
168
+ count,
169
+ onChange
170
+ }) {
171
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8, margin: "4px 0" }, children: [
172
+ /* @__PURE__ */ jsx("span", { style: { color: "#aab", minWidth: 50 }, children: label }),
173
+ /* @__PURE__ */ jsx("button", { style: smallBtnStyle, onClick: () => onChange(Math.max(0, value - 1)), disabled: value <= 0, children: "<" }),
174
+ /* @__PURE__ */ jsxs("span", { style: { color: "#fff", minWidth: 40, textAlign: "center" }, children: [
175
+ value + 1,
176
+ "/",
177
+ count
178
+ ] }),
179
+ /* @__PURE__ */ jsx("button", { style: smallBtnStyle, onClick: () => onChange(Math.min(count - 1, value + 1)), disabled: value >= count - 1, children: ">" })
180
+ ] });
181
+ }
182
+ function ColourPresets({
183
+ current,
184
+ presets,
185
+ onPick
186
+ }) {
187
+ return /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4, margin: "4px 0" }, children: presets.map((p, i) => {
188
+ const selected = Math.abs(current[0] - p[0]) < 0.01 && Math.abs(current[1] - p[1]) < 0.01 && Math.abs(current[2] - p[2]) < 0.01;
189
+ return /* @__PURE__ */ jsx(
190
+ "button",
191
+ {
192
+ onClick: () => onPick([...p]),
193
+ style: {
194
+ width: selected ? 26 : 22,
195
+ height: selected ? 26 : 22,
196
+ borderRadius: 4,
197
+ border: selected ? "2px solid #fff" : "1px solid #555",
198
+ background: `rgb(${p[0] * 255}, ${p[1] * 255}, ${p[2] * 255})`,
199
+ cursor: "pointer",
200
+ padding: 0
201
+ }
202
+ },
203
+ i
204
+ );
205
+ }) });
206
+ }
207
+ var rootStyle = {
208
+ display: "flex",
209
+ flexDirection: "column",
210
+ background: "#1e1e2a",
211
+ color: "#ddd",
212
+ borderRadius: 8,
213
+ overflow: "hidden",
214
+ fontFamily: "system-ui, sans-serif",
215
+ fontSize: 14
216
+ };
217
+ var headerStyle = {
218
+ display: "flex",
219
+ justifyContent: "space-between",
220
+ alignItems: "center",
221
+ padding: "10px 14px",
222
+ background: "#16161f"
223
+ };
224
+ var tabBarStyle = {
225
+ display: "flex",
226
+ gap: 2,
227
+ padding: "0 8px",
228
+ background: "#1a1a26",
229
+ overflowX: "auto"
230
+ };
231
+ var tabStyle = {
232
+ padding: "8px 12px",
233
+ background: "transparent",
234
+ border: "none",
235
+ color: "#99a",
236
+ cursor: "pointer",
237
+ fontSize: 13,
238
+ whiteSpace: "nowrap"
239
+ };
240
+ var tabActiveStyle = {
241
+ color: "#fff",
242
+ fontWeight: 700,
243
+ borderBottom: "2px solid #6af"
244
+ };
245
+ var contentStyle = {
246
+ padding: "8px 14px 14px",
247
+ overflowY: "auto",
248
+ flex: 1
249
+ };
250
+ var btnStyle = {
251
+ padding: "6px 14px",
252
+ border: "none",
253
+ borderRadius: 4,
254
+ background: "#333",
255
+ color: "#ddd",
256
+ cursor: "pointer",
257
+ fontSize: 13
258
+ };
259
+ var smallBtnStyle = {
260
+ width: 28,
261
+ height: 28,
262
+ border: "1px solid #444",
263
+ borderRadius: 4,
264
+ background: "#2a2a36",
265
+ color: "#ccc",
266
+ cursor: "pointer",
267
+ fontSize: 14
268
+ };
269
+ var sliderRowStyle = {
270
+ display: "flex",
271
+ alignItems: "center",
272
+ gap: 8,
273
+ margin: "4px 0"
274
+ };
275
+ export {
276
+ AvatarEditor
277
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Avatar type definitions — mirrors the Rust AvatarAppearance from infinite-game.
3
+ *
4
+ * Keep in sync with: crates/infinite-game/src/avatar/mod.rs
5
+ */
6
+ /** RGB colour triplet (0.0 – 1.0 per channel). */
7
+ type Colour3 = [number, number, number];
8
+ /** Complete avatar appearance. */
9
+ interface AvatarAppearance {
10
+ body: AvatarBody;
11
+ head: AvatarHead;
12
+ skin_color: Colour3;
13
+ }
14
+ /** Body shape parameters. */
15
+ interface AvatarBody {
16
+ /** 0.0 = short, 1.0 = tall */
17
+ height: number;
18
+ /** 0.0 = thin, 1.0 = heavy */
19
+ build: number;
20
+ }
21
+ /** Head & facial feature parameters. */
22
+ interface AvatarHead {
23
+ /** Head width scale (0.5 – 1.5, default 1.0) */
24
+ width: number;
25
+ /** Head height scale (0.5 – 1.5, default 1.0) */
26
+ height: number;
27
+ eye_style: number;
28
+ eye_color: Colour3;
29
+ eye_y: number;
30
+ eye_spacing: number;
31
+ eye_size: number;
32
+ eye_rotation: number;
33
+ brow_style: number;
34
+ brow_color: Colour3;
35
+ brow_y: number;
36
+ brow_spacing: number;
37
+ brow_size: number;
38
+ brow_rotation: number;
39
+ nose_style: number;
40
+ nose_y: number;
41
+ nose_size: number;
42
+ mouth_style: number;
43
+ mouth_y: number;
44
+ mouth_size: number;
45
+ mouth_color: Colour3;
46
+ hair_style: number;
47
+ hair_color: Colour3;
48
+ facial_hair_style: number;
49
+ facial_hair_color: Colour3;
50
+ glasses_style: number;
51
+ glasses_color: Colour3;
52
+ }
53
+ /** One bone segment for the capsule-body renderer. */
54
+ interface BoneSegment {
55
+ start: [number, number, number];
56
+ end: [number, number, number];
57
+ radius: number;
58
+ }
59
+ /** Head transform — position + ellipsoid radii. */
60
+ interface HeadInfo {
61
+ center: [number, number, number];
62
+ radius_x: number;
63
+ radius_y: number;
64
+ radius_z: number;
65
+ }
66
+ /** Style count constants — keep in sync with Rust. */
67
+ declare const STYLE_COUNTS: {
68
+ readonly eye: 12;
69
+ readonly brow: 8;
70
+ readonly nose: 8;
71
+ readonly mouth: 8;
72
+ readonly hair: 16;
73
+ readonly facial_hair: 8;
74
+ readonly glasses: 6;
75
+ };
76
+
77
+ export { type AvatarAppearance as A, type BoneSegment as B, type Colour3 as C, type HeadInfo as H, STYLE_COUNTS as S, type AvatarBody as a, type AvatarHead as b };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Avatar type definitions — mirrors the Rust AvatarAppearance from infinite-game.
3
+ *
4
+ * Keep in sync with: crates/infinite-game/src/avatar/mod.rs
5
+ */
6
+ /** RGB colour triplet (0.0 – 1.0 per channel). */
7
+ type Colour3 = [number, number, number];
8
+ /** Complete avatar appearance. */
9
+ interface AvatarAppearance {
10
+ body: AvatarBody;
11
+ head: AvatarHead;
12
+ skin_color: Colour3;
13
+ }
14
+ /** Body shape parameters. */
15
+ interface AvatarBody {
16
+ /** 0.0 = short, 1.0 = tall */
17
+ height: number;
18
+ /** 0.0 = thin, 1.0 = heavy */
19
+ build: number;
20
+ }
21
+ /** Head & facial feature parameters. */
22
+ interface AvatarHead {
23
+ /** Head width scale (0.5 – 1.5, default 1.0) */
24
+ width: number;
25
+ /** Head height scale (0.5 – 1.5, default 1.0) */
26
+ height: number;
27
+ eye_style: number;
28
+ eye_color: Colour3;
29
+ eye_y: number;
30
+ eye_spacing: number;
31
+ eye_size: number;
32
+ eye_rotation: number;
33
+ brow_style: number;
34
+ brow_color: Colour3;
35
+ brow_y: number;
36
+ brow_spacing: number;
37
+ brow_size: number;
38
+ brow_rotation: number;
39
+ nose_style: number;
40
+ nose_y: number;
41
+ nose_size: number;
42
+ mouth_style: number;
43
+ mouth_y: number;
44
+ mouth_size: number;
45
+ mouth_color: Colour3;
46
+ hair_style: number;
47
+ hair_color: Colour3;
48
+ facial_hair_style: number;
49
+ facial_hair_color: Colour3;
50
+ glasses_style: number;
51
+ glasses_color: Colour3;
52
+ }
53
+ /** One bone segment for the capsule-body renderer. */
54
+ interface BoneSegment {
55
+ start: [number, number, number];
56
+ end: [number, number, number];
57
+ radius: number;
58
+ }
59
+ /** Head transform — position + ellipsoid radii. */
60
+ interface HeadInfo {
61
+ center: [number, number, number];
62
+ radius_x: number;
63
+ radius_y: number;
64
+ radius_z: number;
65
+ }
66
+ /** Style count constants — keep in sync with Rust. */
67
+ declare const STYLE_COUNTS: {
68
+ readonly eye: 12;
69
+ readonly brow: 8;
70
+ readonly nose: 8;
71
+ readonly mouth: 8;
72
+ readonly hair: 16;
73
+ readonly facial_hair: 8;
74
+ readonly glasses: 6;
75
+ };
76
+
77
+ export { type AvatarAppearance as A, type BoneSegment as B, type Colour3 as C, type HeadInfo as H, STYLE_COUNTS as S, type AvatarBody as a, type AvatarHead as b };
@@ -0,0 +1,52 @@
1
+ import { C as Colour3, A as AvatarAppearance, a as AvatarBody, B as BoneSegment, H as HeadInfo } from './index-DwPxw0AI.mjs';
2
+ export { b as AvatarHead, S as STYLE_COUNTS } from './index-DwPxw0AI.mjs';
3
+
4
+ /**
5
+ * Colour presets — mirrors the Rust constants from infinite-game/src/avatar/mod.rs.
6
+ */
7
+
8
+ /** 10 skin colour presets (light to dark). */
9
+ declare const SKIN_PRESETS: readonly Colour3[];
10
+ /** 8 eye colour presets. */
11
+ declare const EYE_COLOR_PRESETS: readonly Colour3[];
12
+ /** 10 hair colour presets. */
13
+ declare const HAIR_COLOR_PRESETS: readonly Colour3[];
14
+
15
+ /**
16
+ * Default avatar appearance + randomiser.
17
+ */
18
+
19
+ /** Default avatar (matches Rust Default impl). */
20
+ declare function defaultAppearance(): AvatarAppearance;
21
+ /** Generate a random avatar appearance. */
22
+ declare function randomAppearance(): AvatarAppearance;
23
+
24
+ /**
25
+ * Humanoid skeleton builder — TypeScript port of
26
+ * infinite-game/src/avatar/mod.rs `build_skeleton()`.
27
+ */
28
+
29
+ /**
30
+ * Build a humanoid capsule skeleton from body parameters.
31
+ *
32
+ * Returns 16 bone segments (4 torso + 6 arm + 6 leg) and a head ellipsoid.
33
+ * The character stands at the origin with feet on y = 0.
34
+ */
35
+ declare function buildSkeleton(body: AvatarBody): {
36
+ bones: BoneSegment[];
37
+ head: HeadInfo;
38
+ };
39
+
40
+ type AvatarEditorTab = 'body' | 'head' | 'eyes' | 'brows' | 'nose' | 'mouth' | 'hair' | 'extras';
41
+ interface UseAvatarEditorReturn {
42
+ appearance: AvatarAppearance;
43
+ tab: AvatarEditorTab;
44
+ setTab: (tab: AvatarEditorTab) => void;
45
+ /** Update a nested field. path examples: 'body.height', 'head.eye_y', 'skin_color' */
46
+ update: (path: string, value: number | Colour3) => void;
47
+ randomize: () => void;
48
+ reset: () => void;
49
+ }
50
+ declare function useAvatarEditor(initial?: AvatarAppearance): UseAvatarEditorReturn;
51
+
52
+ export { AvatarAppearance, AvatarBody, type AvatarEditorTab, BoneSegment, Colour3, EYE_COLOR_PRESETS, HAIR_COLOR_PRESETS, HeadInfo, SKIN_PRESETS, type UseAvatarEditorReturn, buildSkeleton, defaultAppearance, randomAppearance, useAvatarEditor };
@@ -0,0 +1,52 @@
1
+ import { C as Colour3, A as AvatarAppearance, a as AvatarBody, B as BoneSegment, H as HeadInfo } from './index-DwPxw0AI.js';
2
+ export { b as AvatarHead, S as STYLE_COUNTS } from './index-DwPxw0AI.js';
3
+
4
+ /**
5
+ * Colour presets — mirrors the Rust constants from infinite-game/src/avatar/mod.rs.
6
+ */
7
+
8
+ /** 10 skin colour presets (light to dark). */
9
+ declare const SKIN_PRESETS: readonly Colour3[];
10
+ /** 8 eye colour presets. */
11
+ declare const EYE_COLOR_PRESETS: readonly Colour3[];
12
+ /** 10 hair colour presets. */
13
+ declare const HAIR_COLOR_PRESETS: readonly Colour3[];
14
+
15
+ /**
16
+ * Default avatar appearance + randomiser.
17
+ */
18
+
19
+ /** Default avatar (matches Rust Default impl). */
20
+ declare function defaultAppearance(): AvatarAppearance;
21
+ /** Generate a random avatar appearance. */
22
+ declare function randomAppearance(): AvatarAppearance;
23
+
24
+ /**
25
+ * Humanoid skeleton builder — TypeScript port of
26
+ * infinite-game/src/avatar/mod.rs `build_skeleton()`.
27
+ */
28
+
29
+ /**
30
+ * Build a humanoid capsule skeleton from body parameters.
31
+ *
32
+ * Returns 16 bone segments (4 torso + 6 arm + 6 leg) and a head ellipsoid.
33
+ * The character stands at the origin with feet on y = 0.
34
+ */
35
+ declare function buildSkeleton(body: AvatarBody): {
36
+ bones: BoneSegment[];
37
+ head: HeadInfo;
38
+ };
39
+
40
+ type AvatarEditorTab = 'body' | 'head' | 'eyes' | 'brows' | 'nose' | 'mouth' | 'hair' | 'extras';
41
+ interface UseAvatarEditorReturn {
42
+ appearance: AvatarAppearance;
43
+ tab: AvatarEditorTab;
44
+ setTab: (tab: AvatarEditorTab) => void;
45
+ /** Update a nested field. path examples: 'body.height', 'head.eye_y', 'skin_color' */
46
+ update: (path: string, value: number | Colour3) => void;
47
+ randomize: () => void;
48
+ reset: () => void;
49
+ }
50
+ declare function useAvatarEditor(initial?: AvatarAppearance): UseAvatarEditorReturn;
51
+
52
+ export { AvatarAppearance, AvatarBody, type AvatarEditorTab, BoneSegment, Colour3, EYE_COLOR_PRESETS, HAIR_COLOR_PRESETS, HeadInfo, SKIN_PRESETS, type UseAvatarEditorReturn, buildSkeleton, defaultAppearance, randomAppearance, useAvatarEditor };