@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.
- package/dist/chunk-5QZCUXJW.mjs +187 -0
- package/dist/components/index.d.mts +18 -0
- package/dist/components/index.d.ts +18 -0
- package/dist/components/index.js +476 -0
- package/dist/components/index.mjs +277 -0
- package/dist/index-DwPxw0AI.d.mts +77 -0
- package/dist/index-DwPxw0AI.d.ts +77 -0
- package/dist/index.d.mts +52 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +274 -0
- package/dist/index.mjs +72 -0
- package/package.json +49 -0
- package/src/components/AvatarEditor.tsx +371 -0
- package/src/components/index.ts +7 -0
- package/src/defaults.ts +89 -0
- package/src/hooks/useAvatarEditor.ts +41 -0
- package/src/index.ts +36 -0
- package/src/presets.ts +44 -0
- package/src/skeleton.ts +82 -0
- package/src/types/index.ts +96 -0
|
@@ -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 };
|
package/dist/index.d.mts
ADDED
|
@@ -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 };
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|