@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.
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,12 +17,23 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/components/index.ts
21
31
  var components_exports = {};
22
32
  __export(components_exports, {
23
- AvatarEditor: () => AvatarEditor
33
+ AvatarEditor: () => AvatarEditor,
34
+ AvatarPreview: () => AvatarPreview,
35
+ AvatarRenderer: () => AvatarRenderer,
36
+ AvatarThumbnail: () => AvatarThumbnail
24
37
  });
25
38
  module.exports = __toCommonJS(components_exports);
26
39
 
@@ -204,8 +217,271 @@ function useAvatarEditor(initial) {
204
217
  return { appearance, tab, setTab, update, randomize, reset };
205
218
  }
206
219
 
207
- // src/components/AvatarEditor.tsx
220
+ // src/components/AvatarRenderer.tsx
221
+ var import_react2 = require("react");
222
+ var import_fiber = require("@react-three/fiber");
223
+ var import_drei = require("@react-three/drei");
224
+ var THREE2 = __toESM(require("three"));
225
+
226
+ // src/skeleton.ts
227
+ function mirrorX(p) {
228
+ return [-p[0], p[1], p[2]];
229
+ }
230
+ function buildSkeleton(body) {
231
+ const h = 0.85 + body.height * 0.35;
232
+ const b = 0.75 + body.build * 0.5;
233
+ const footY = 0.02 * h;
234
+ const ankleY = 0.08 * h;
235
+ const kneeY = 0.45 * h;
236
+ const hipY = 0.88 * h;
237
+ const waistY = 0.95 * h;
238
+ const chestY = 1.18 * h;
239
+ const shoulderY = 1.3 * h;
240
+ const neckY = 1.38 * h;
241
+ const headBaseY = 1.42 * h;
242
+ const headCenterY = 1.55 * h;
243
+ const hipSpread = 0.09 * b;
244
+ const shoulderSpread = 0.16 * b;
245
+ const bones = [];
246
+ bones.push({ start: [0, hipY, 0], end: [0, waistY, 0], radius: 0.1 * b });
247
+ bones.push({ start: [0, waistY, 0], end: [0, chestY, 0], radius: 0.11 * b });
248
+ bones.push({ start: [0, chestY, 0], end: [0, shoulderY, 0], radius: 0.12 * b });
249
+ bones.push({ start: [0, neckY, 0], end: [0, headBaseY, 0], radius: 0.04 * b });
250
+ const lShoulder = [shoulderSpread, shoulderY, 0];
251
+ const lElbow = [shoulderSpread + 0.22 * h, shoulderY - 0.08 * h, 0];
252
+ const lWrist = [shoulderSpread + 0.42 * h, shoulderY - 0.18 * h, 0];
253
+ const lHand = [shoulderSpread + 0.5 * h, shoulderY - 0.22 * h, 0];
254
+ bones.push({ start: lShoulder, end: lElbow, radius: 0.04 * b });
255
+ bones.push({ start: lElbow, end: lWrist, radius: 0.035 * b });
256
+ bones.push({ start: lWrist, end: lHand, radius: 0.03 * b });
257
+ bones.push({ start: mirrorX(lShoulder), end: mirrorX(lElbow), radius: 0.04 * b });
258
+ bones.push({ start: mirrorX(lElbow), end: mirrorX(lWrist), radius: 0.035 * b });
259
+ bones.push({ start: mirrorX(lWrist), end: mirrorX(lHand), radius: 0.03 * b });
260
+ const lHip = [hipSpread, hipY, 0];
261
+ const lKnee = [hipSpread + 0.01, kneeY, 0];
262
+ const lAnkle = [hipSpread, ankleY, 0];
263
+ const lToe = [hipSpread, footY, 0.06];
264
+ bones.push({ start: lHip, end: lKnee, radius: 0.055 * b });
265
+ bones.push({ start: lKnee, end: lAnkle, radius: 0.045 * b });
266
+ bones.push({ start: lAnkle, end: lToe, radius: 0.035 * b });
267
+ bones.push({ start: mirrorX(lHip), end: mirrorX(lKnee), radius: 0.055 * b });
268
+ bones.push({ start: mirrorX(lKnee), end: mirrorX(lAnkle), radius: 0.045 * b });
269
+ bones.push({ start: mirrorX(lAnkle), end: mirrorX(lToe), radius: 0.035 * b });
270
+ const head = {
271
+ center: [0, headCenterY, 0],
272
+ radius_x: 0.11,
273
+ radius_y: 0.12,
274
+ radius_z: 0.1
275
+ };
276
+ return { bones, head };
277
+ }
278
+
279
+ // src/utils/geometry.ts
280
+ var THREE = __toESM(require("three"));
281
+ var RING_SEGMENTS = 10;
282
+ var CAP_RINGS = 4;
283
+ function capsuleBetweenGeometry(start, end, radius) {
284
+ const s = new THREE.Vector3(...start);
285
+ const e = new THREE.Vector3(...end);
286
+ const dir = new THREE.Vector3().subVectors(e, s);
287
+ const length = dir.length();
288
+ let up, right, forward;
289
+ if (length > 1e-4) {
290
+ up = dir.clone().divideScalar(length);
291
+ const ref = Math.abs(up.y) > 0.99 ? new THREE.Vector3(0, 0, 1) : new THREE.Vector3(0, 1, 0);
292
+ right = new THREE.Vector3().crossVectors(up, ref).normalize();
293
+ forward = new THREE.Vector3().crossVectors(right, up);
294
+ } else {
295
+ up = new THREE.Vector3(0, 1, 0);
296
+ right = new THREE.Vector3(1, 0, 0);
297
+ forward = new THREE.Vector3(0, 0, 1);
298
+ }
299
+ const positions = [];
300
+ const normals = [];
301
+ const indices = [];
302
+ const seg = RING_SEGMENTS;
303
+ const transformPos = (lx, ly, lz) => [
304
+ s.x + right.x * lx + up.x * ly + forward.x * lz,
305
+ s.y + right.y * lx + up.y * ly + forward.y * lz,
306
+ s.z + right.z * lx + up.z * ly + forward.z * lz
307
+ ];
308
+ const transformNorm = (nx, ny, nz) => {
309
+ const v = new THREE.Vector3(
310
+ right.x * nx + up.x * ny + forward.x * nz,
311
+ right.y * nx + up.y * ny + forward.y * nz,
312
+ right.z * nx + up.z * ny + forward.z * nz
313
+ ).normalize();
314
+ return [v.x, v.y, v.z];
315
+ };
316
+ for (let ring = 0; ring <= CAP_RINGS; ring++) {
317
+ const phi = Math.PI * 0.5 * (1 - ring / CAP_RINGS);
318
+ const ly = -Math.sin(phi) * radius;
319
+ const rr = Math.cos(phi) * radius;
320
+ for (let j = 0; j <= seg; j++) {
321
+ const theta = 2 * Math.PI * j / seg;
322
+ const lx = rr * Math.cos(theta);
323
+ const lz = rr * Math.sin(theta);
324
+ positions.push(...transformPos(lx, ly, lz));
325
+ normals.push(...transformNorm(lx / radius, -Math.sin(phi), lz / radius));
326
+ }
327
+ }
328
+ for (let ring = 0; ring <= 1; ring++) {
329
+ const ly = ring * length;
330
+ for (let j = 0; j <= seg; j++) {
331
+ const theta = 2 * Math.PI * j / seg;
332
+ const lx = radius * Math.cos(theta);
333
+ const lz = radius * Math.sin(theta);
334
+ positions.push(...transformPos(lx, ly, lz));
335
+ normals.push(...transformNorm(lx / radius, 0, lz / radius));
336
+ }
337
+ }
338
+ for (let ring = 0; ring <= CAP_RINGS; ring++) {
339
+ const phi = Math.PI * 0.5 * ring / CAP_RINGS;
340
+ const ly = length + Math.sin(phi) * radius;
341
+ const rr = Math.cos(phi) * radius;
342
+ for (let j = 0; j <= seg; j++) {
343
+ const theta = 2 * Math.PI * j / seg;
344
+ const lx = rr * Math.cos(theta);
345
+ const lz = rr * Math.sin(theta);
346
+ positions.push(...transformPos(lx, ly, lz));
347
+ normals.push(...transformNorm(lx / radius, Math.sin(phi), lz / radius));
348
+ }
349
+ }
350
+ const totalRings = CAP_RINGS + 2 + CAP_RINGS;
351
+ for (let ring = 0; ring < totalRings; ring++) {
352
+ for (let j = 0; j < seg; j++) {
353
+ const cur = ring * (seg + 1) + j;
354
+ const next = cur + seg + 1;
355
+ indices.push(cur, next, cur + 1);
356
+ indices.push(cur + 1, next, next + 1);
357
+ }
358
+ }
359
+ const geo = new THREE.BufferGeometry();
360
+ geo.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
361
+ geo.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3));
362
+ geo.setIndex(indices);
363
+ return geo;
364
+ }
365
+ function ellipsoidGeometry(rx, ry, rz) {
366
+ const segs = 14;
367
+ const rings = 12;
368
+ const positions = [];
369
+ const normals = [];
370
+ const indices = [];
371
+ for (let ring = 0; ring <= rings; ring++) {
372
+ const phi = Math.PI * ring / rings;
373
+ const y = ry * Math.cos(phi);
374
+ const rr = Math.sin(phi);
375
+ for (let seg = 0; seg <= segs; seg++) {
376
+ const theta = 2 * Math.PI * seg / segs;
377
+ const x = rx * rr * Math.cos(theta);
378
+ const z = rz * rr * Math.sin(theta);
379
+ positions.push(x, y, z);
380
+ const nx = x / (rx * rx);
381
+ const ny = y / (ry * ry);
382
+ const nz = z / (rz * rz);
383
+ const len = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
384
+ normals.push(nx / len, ny / len, nz / len);
385
+ }
386
+ }
387
+ for (let ring = 0; ring < rings; ring++) {
388
+ for (let seg = 0; seg < segs; seg++) {
389
+ const cur = ring * (segs + 1) + seg;
390
+ const next = cur + segs + 1;
391
+ indices.push(cur, next, cur + 1);
392
+ indices.push(cur + 1, next, next + 1);
393
+ }
394
+ }
395
+ const geo = new THREE.BufferGeometry();
396
+ geo.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
397
+ geo.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3));
398
+ geo.setIndex(indices);
399
+ return geo;
400
+ }
401
+
402
+ // src/components/AvatarRenderer.tsx
208
403
  var import_jsx_runtime = require("react/jsx-runtime");
404
+ function AvatarModel({ appearance }) {
405
+ const groupRef = (0, import_react2.useRef)(null);
406
+ const { boneGeos, headGeo, headPos, material } = (0, import_react2.useMemo)(() => {
407
+ const { bones, head } = buildSkeleton(appearance.body);
408
+ const skinColor = new THREE2.Color(...appearance.skin_color);
409
+ const mat = new THREE2.MeshStandardMaterial({
410
+ color: skinColor,
411
+ roughness: 0.65,
412
+ metalness: 0.05
413
+ });
414
+ const geos = bones.map((b) => capsuleBetweenGeometry(b.start, b.end, b.radius));
415
+ const hGeo = ellipsoidGeometry(
416
+ head.radius_x * appearance.head.width,
417
+ head.radius_y * appearance.head.height,
418
+ head.radius_z * appearance.head.width
419
+ );
420
+ return {
421
+ boneGeos: geos,
422
+ headGeo: hGeo,
423
+ headPos: head.center,
424
+ material: mat
425
+ };
426
+ }, [appearance]);
427
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("group", { ref: groupRef, children: [
428
+ boneGeos.map((geo, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("mesh", { geometry: geo, material }, i)),
429
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("mesh", { geometry: headGeo, material, position: headPos })
430
+ ] });
431
+ }
432
+ function Scene({ appearance, autoRotate }) {
433
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
434
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ambientLight", { intensity: 0.5 }),
435
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("directionalLight", { position: [2, 4, 3], intensity: 1.2 }),
436
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("directionalLight", { position: [-1, 2, -2], intensity: 0.3 }),
437
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AvatarModel, { appearance }),
438
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
439
+ import_drei.OrbitControls,
440
+ {
441
+ autoRotate,
442
+ autoRotateSpeed: 2,
443
+ enablePan: false,
444
+ minDistance: 0.8,
445
+ maxDistance: 4,
446
+ target: [0, 0.75, 0]
447
+ }
448
+ )
449
+ ] });
450
+ }
451
+ function AvatarRenderer({
452
+ appearance,
453
+ width = "100%",
454
+ height = 300,
455
+ autoRotate = false,
456
+ background = "#15151f",
457
+ className,
458
+ style
459
+ }) {
460
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
461
+ "div",
462
+ {
463
+ className,
464
+ style: {
465
+ width,
466
+ height,
467
+ borderRadius: 8,
468
+ overflow: "hidden",
469
+ ...style
470
+ },
471
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
472
+ import_fiber.Canvas,
473
+ {
474
+ camera: { position: [0, 1, 1.8], fov: 50 },
475
+ style: { background },
476
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Scene, { appearance, autoRotate })
477
+ }
478
+ )
479
+ }
480
+ );
481
+ }
482
+
483
+ // src/components/AvatarEditor.tsx
484
+ var import_jsx_runtime2 = require("react/jsx-runtime");
209
485
  var TABS = [
210
486
  { key: "body", label: "Body" },
211
487
  { key: "head", label: "Head" },
@@ -228,113 +504,127 @@ function AvatarEditor({
228
504
  update(path, value);
229
505
  setTimeout(() => onChange?.(appearance), 0);
230
506
  };
231
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className, style: rootStyle, children: [
232
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: headerStyle, children: [
233
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontWeight: 700, fontSize: 18 }, children: "Avatar Editor" }),
234
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 8 }, children: [
235
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: btnStyle, onClick: randomize, children: "Randomize" }),
236
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: btnStyle, onClick: reset, children: "Reset" }),
237
- onCancel && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: btnStyle, onClick: onCancel, children: "Cancel" }),
238
- onDone && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: { ...btnStyle, background: "#3b6" }, onClick: () => onDone(appearance), children: "Done" })
507
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className, style: rootStyle, children: [
508
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: headerStyle, children: [
509
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontWeight: 700, fontSize: 18 }, children: "Avatar Editor" }),
510
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: 8 }, children: [
511
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { style: btnStyle, onClick: randomize, children: "Randomize" }),
512
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { style: btnStyle, onClick: reset, children: "Reset" }),
513
+ onCancel && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { style: btnStyle, onClick: onCancel, children: "Cancel" }),
514
+ onDone && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { style: { ...btnStyle, background: "#3b6" }, onClick: () => onDone(appearance), children: "Done" })
239
515
  ] })
240
516
  ] }),
241
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: tabBarStyle, children: TABS.map((t) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
242
- "button",
243
- {
244
- onClick: () => setTab(t.key),
245
- style: {
246
- ...tabStyle,
247
- ...tab === t.key ? tabActiveStyle : {}
248
- },
249
- children: t.label
250
- },
251
- t.key
252
- )) }),
253
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: contentStyle, children: [
254
- tab === "body" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BodyTab, { appearance, onUpdate: handleUpdate }),
255
- tab === "head" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HeadTab, { appearance, onUpdate: handleUpdate }),
256
- tab === "eyes" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EyesTab, { appearance, onUpdate: handleUpdate }),
257
- tab === "brows" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BrowsTab, { appearance, onUpdate: handleUpdate }),
258
- tab === "nose" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NoseTab, { appearance, onUpdate: handleUpdate }),
259
- tab === "mouth" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MouthTab, { appearance, onUpdate: handleUpdate }),
260
- tab === "hair" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HairTab, { appearance, onUpdate: handleUpdate }),
261
- tab === "extras" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ExtrasTab, { appearance, onUpdate: handleUpdate })
517
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: mainStyle, children: [
518
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: previewPaneStyle, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
519
+ AvatarRenderer,
520
+ {
521
+ appearance,
522
+ width: "100%",
523
+ height: "100%",
524
+ autoRotate: false,
525
+ background: "#12121a"
526
+ }
527
+ ) }),
528
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: controlsPaneStyle, children: [
529
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: tabBarStyle, children: TABS.map((t) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
530
+ "button",
531
+ {
532
+ onClick: () => setTab(t.key),
533
+ style: {
534
+ ...tabStyle,
535
+ ...tab === t.key ? tabActiveStyle : {}
536
+ },
537
+ children: t.label
538
+ },
539
+ t.key
540
+ )) }),
541
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: contentStyle, children: [
542
+ tab === "body" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BodyTab, { appearance, onUpdate: handleUpdate }),
543
+ tab === "head" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(HeadTab, { appearance, onUpdate: handleUpdate }),
544
+ tab === "eyes" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(EyesTab, { appearance, onUpdate: handleUpdate }),
545
+ tab === "brows" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BrowsTab, { appearance, onUpdate: handleUpdate }),
546
+ tab === "nose" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(NoseTab, { appearance, onUpdate: handleUpdate }),
547
+ tab === "mouth" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MouthTab, { appearance, onUpdate: handleUpdate }),
548
+ tab === "hair" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(HairTab, { appearance, onUpdate: handleUpdate }),
549
+ tab === "extras" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ExtrasTab, { appearance, onUpdate: handleUpdate })
550
+ ] })
551
+ ] })
262
552
  ] })
263
553
  ] });
264
554
  }
265
555
  function BodyTab({ appearance, onUpdate }) {
266
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
267
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionLabel, { children: "Body" }),
268
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slider, { label: "Height", value: appearance.body.height, onChange: (v) => onUpdate("body.height", v) }),
269
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slider, { label: "Build", value: appearance.body.build, onChange: (v) => onUpdate("body.build", v) }),
270
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionLabel, { children: "Skin Colour" }),
271
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ColourPresets, { current: appearance.skin_color, presets: SKIN_PRESETS, onPick: (c) => onUpdate("skin_color", c) })
556
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
557
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SectionLabel, { children: "Body" }),
558
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Slider, { label: "Height", value: appearance.body.height, onChange: (v) => onUpdate("body.height", v) }),
559
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Slider, { label: "Build", value: appearance.body.build, onChange: (v) => onUpdate("body.build", v) }),
560
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SectionLabel, { children: "Skin Colour" }),
561
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ColourPresets, { current: appearance.skin_color, presets: SKIN_PRESETS, onPick: (c) => onUpdate("skin_color", c) })
272
562
  ] });
273
563
  }
274
564
  function HeadTab({ appearance, onUpdate }) {
275
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
276
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionLabel, { children: "Head Shape" }),
277
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slider, { label: "Width", value: appearance.head.width, min: 0.5, max: 1.5, onChange: (v) => onUpdate("head.width", v) }),
278
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slider, { label: "Height", value: appearance.head.height, min: 0.5, max: 1.5, onChange: (v) => onUpdate("head.height", v) })
565
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
566
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SectionLabel, { children: "Head Shape" }),
567
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Slider, { label: "Width", value: appearance.head.width, min: 0.5, max: 1.5, onChange: (v) => onUpdate("head.width", v) }),
568
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Slider, { label: "Height", value: appearance.head.height, min: 0.5, max: 1.5, onChange: (v) => onUpdate("head.height", v) })
279
569
  ] });
280
570
  }
281
571
  function EyesTab({ appearance, onUpdate }) {
282
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
283
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionLabel, { children: "Eyes" }),
284
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StyleSelector, { label: "Style", value: appearance.head.eye_style, count: STYLE_COUNTS.eye, onChange: (v) => onUpdate("head.eye_style", v) }),
285
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ColourPresets, { current: appearance.head.eye_color, presets: EYE_COLOR_PRESETS, onPick: (c) => onUpdate("head.eye_color", c) }),
286
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slider, { label: "Vertical Pos", value: appearance.head.eye_y, onChange: (v) => onUpdate("head.eye_y", v) }),
287
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slider, { label: "Spacing", value: appearance.head.eye_spacing, onChange: (v) => onUpdate("head.eye_spacing", v) }),
288
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slider, { label: "Size", value: appearance.head.eye_size, onChange: (v) => onUpdate("head.eye_size", v) }),
289
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slider, { label: "Rotation", value: appearance.head.eye_rotation, onChange: (v) => onUpdate("head.eye_rotation", v) })
572
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
573
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SectionLabel, { children: "Eyes" }),
574
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(StyleSelector, { label: "Style", value: appearance.head.eye_style, count: STYLE_COUNTS.eye, onChange: (v) => onUpdate("head.eye_style", v) }),
575
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ColourPresets, { current: appearance.head.eye_color, presets: EYE_COLOR_PRESETS, onPick: (c) => onUpdate("head.eye_color", c) }),
576
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Slider, { label: "Vertical Pos", value: appearance.head.eye_y, onChange: (v) => onUpdate("head.eye_y", v) }),
577
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Slider, { label: "Spacing", value: appearance.head.eye_spacing, onChange: (v) => onUpdate("head.eye_spacing", v) }),
578
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Slider, { label: "Size", value: appearance.head.eye_size, onChange: (v) => onUpdate("head.eye_size", v) }),
579
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Slider, { label: "Rotation", value: appearance.head.eye_rotation, onChange: (v) => onUpdate("head.eye_rotation", v) })
290
580
  ] });
291
581
  }
292
582
  function BrowsTab({ appearance, onUpdate }) {
293
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
294
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionLabel, { children: "Eyebrows" }),
295
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StyleSelector, { label: "Style", value: appearance.head.brow_style, count: STYLE_COUNTS.brow, onChange: (v) => onUpdate("head.brow_style", v) }),
296
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ColourPresets, { current: appearance.head.brow_color, presets: HAIR_COLOR_PRESETS, onPick: (c) => onUpdate("head.brow_color", c) }),
297
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slider, { label: "Vertical Pos", value: appearance.head.brow_y, onChange: (v) => onUpdate("head.brow_y", v) }),
298
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slider, { label: "Spacing", value: appearance.head.brow_spacing, onChange: (v) => onUpdate("head.brow_spacing", v) }),
299
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slider, { label: "Size", value: appearance.head.brow_size, onChange: (v) => onUpdate("head.brow_size", v) }),
300
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slider, { label: "Rotation", value: appearance.head.brow_rotation, onChange: (v) => onUpdate("head.brow_rotation", v) })
583
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
584
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SectionLabel, { children: "Eyebrows" }),
585
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(StyleSelector, { label: "Style", value: appearance.head.brow_style, count: STYLE_COUNTS.brow, onChange: (v) => onUpdate("head.brow_style", v) }),
586
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ColourPresets, { current: appearance.head.brow_color, presets: HAIR_COLOR_PRESETS, onPick: (c) => onUpdate("head.brow_color", c) }),
587
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Slider, { label: "Vertical Pos", value: appearance.head.brow_y, onChange: (v) => onUpdate("head.brow_y", v) }),
588
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Slider, { label: "Spacing", value: appearance.head.brow_spacing, onChange: (v) => onUpdate("head.brow_spacing", v) }),
589
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Slider, { label: "Size", value: appearance.head.brow_size, onChange: (v) => onUpdate("head.brow_size", v) }),
590
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Slider, { label: "Rotation", value: appearance.head.brow_rotation, onChange: (v) => onUpdate("head.brow_rotation", v) })
301
591
  ] });
302
592
  }
303
593
  function NoseTab({ appearance, onUpdate }) {
304
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
305
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionLabel, { children: "Nose" }),
306
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StyleSelector, { label: "Style", value: appearance.head.nose_style, count: STYLE_COUNTS.nose, onChange: (v) => onUpdate("head.nose_style", v) }),
307
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slider, { label: "Vertical Pos", value: appearance.head.nose_y, onChange: (v) => onUpdate("head.nose_y", v) }),
308
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slider, { label: "Size", value: appearance.head.nose_size, onChange: (v) => onUpdate("head.nose_size", v) })
594
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
595
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SectionLabel, { children: "Nose" }),
596
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(StyleSelector, { label: "Style", value: appearance.head.nose_style, count: STYLE_COUNTS.nose, onChange: (v) => onUpdate("head.nose_style", v) }),
597
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Slider, { label: "Vertical Pos", value: appearance.head.nose_y, onChange: (v) => onUpdate("head.nose_y", v) }),
598
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Slider, { label: "Size", value: appearance.head.nose_size, onChange: (v) => onUpdate("head.nose_size", v) })
309
599
  ] });
310
600
  }
311
601
  function MouthTab({ appearance, onUpdate }) {
312
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
313
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionLabel, { children: "Mouth" }),
314
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StyleSelector, { label: "Style", value: appearance.head.mouth_style, count: STYLE_COUNTS.mouth, onChange: (v) => onUpdate("head.mouth_style", v) }),
315
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slider, { label: "Vertical Pos", value: appearance.head.mouth_y, onChange: (v) => onUpdate("head.mouth_y", v) }),
316
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slider, { label: "Size", value: appearance.head.mouth_size, onChange: (v) => onUpdate("head.mouth_size", v) })
602
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
603
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SectionLabel, { children: "Mouth" }),
604
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(StyleSelector, { label: "Style", value: appearance.head.mouth_style, count: STYLE_COUNTS.mouth, onChange: (v) => onUpdate("head.mouth_style", v) }),
605
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Slider, { label: "Vertical Pos", value: appearance.head.mouth_y, onChange: (v) => onUpdate("head.mouth_y", v) }),
606
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Slider, { label: "Size", value: appearance.head.mouth_size, onChange: (v) => onUpdate("head.mouth_size", v) })
317
607
  ] });
318
608
  }
319
609
  function HairTab({ appearance, onUpdate }) {
320
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
321
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionLabel, { children: "Hair" }),
322
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StyleSelector, { label: "Style", value: appearance.head.hair_style, count: STYLE_COUNTS.hair, onChange: (v) => onUpdate("head.hair_style", v) }),
323
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionLabel, { children: "Colour" }),
324
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ColourPresets, { current: appearance.head.hair_color, presets: HAIR_COLOR_PRESETS, onPick: (c) => onUpdate("head.hair_color", c) })
610
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
611
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SectionLabel, { children: "Hair" }),
612
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(StyleSelector, { label: "Style", value: appearance.head.hair_style, count: STYLE_COUNTS.hair, onChange: (v) => onUpdate("head.hair_style", v) }),
613
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SectionLabel, { children: "Colour" }),
614
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ColourPresets, { current: appearance.head.hair_color, presets: HAIR_COLOR_PRESETS, onPick: (c) => onUpdate("head.hair_color", c) })
325
615
  ] });
326
616
  }
327
617
  function ExtrasTab({ appearance, onUpdate }) {
328
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
329
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionLabel, { children: "Facial Hair" }),
330
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StyleSelector, { label: "Style", value: appearance.head.facial_hair_style, count: STYLE_COUNTS.facial_hair, onChange: (v) => onUpdate("head.facial_hair_style", v) }),
331
- appearance.head.facial_hair_style > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ColourPresets, { current: appearance.head.facial_hair_color, presets: HAIR_COLOR_PRESETS, onPick: (c) => onUpdate("head.facial_hair_color", c) }),
332
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionLabel, { children: "Glasses" }),
333
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StyleSelector, { label: "Style", value: appearance.head.glasses_style, count: STYLE_COUNTS.glasses, onChange: (v) => onUpdate("head.glasses_style", v) })
618
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
619
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SectionLabel, { children: "Facial Hair" }),
620
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(StyleSelector, { label: "Style", value: appearance.head.facial_hair_style, count: STYLE_COUNTS.facial_hair, onChange: (v) => onUpdate("head.facial_hair_style", v) }),
621
+ appearance.head.facial_hair_style > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ColourPresets, { current: appearance.head.facial_hair_color, presets: HAIR_COLOR_PRESETS, onPick: (c) => onUpdate("head.facial_hair_color", c) }),
622
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SectionLabel, { children: "Glasses" }),
623
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(StyleSelector, { label: "Style", value: appearance.head.glasses_style, count: STYLE_COUNTS.glasses, onChange: (v) => onUpdate("head.glasses_style", v) })
334
624
  ] });
335
625
  }
336
626
  function SectionLabel({ children }) {
337
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "#ccc", fontWeight: 600, fontSize: 13, margin: "8px 0 4px" }, children });
627
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { color: "#ccc", fontWeight: 600, fontSize: 13, margin: "8px 0 4px" }, children });
338
628
  }
339
629
  function Slider({
340
630
  label,
@@ -343,9 +633,9 @@ function Slider({
343
633
  max = 1,
344
634
  onChange
345
635
  }) {
346
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: sliderRowStyle, children: [
347
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { minWidth: 90, color: "#aab" }, children: label }),
348
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
636
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { style: sliderRowStyle, children: [
637
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { minWidth: 90, color: "#aab" }, children: label }),
638
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
349
639
  "input",
350
640
  {
351
641
  type: "range",
@@ -357,7 +647,7 @@ function Slider({
357
647
  style: { flex: 1 }
358
648
  }
359
649
  ),
360
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { minWidth: 40, textAlign: "right", color: "#889", fontSize: 12 }, children: value.toFixed(2) })
650
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { minWidth: 40, textAlign: "right", color: "#889", fontSize: 12 }, children: value.toFixed(2) })
361
651
  ] });
362
652
  }
363
653
  function StyleSelector({
@@ -366,15 +656,15 @@ function StyleSelector({
366
656
  count,
367
657
  onChange
368
658
  }) {
369
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 8, margin: "4px 0" }, children: [
370
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: "#aab", minWidth: 50 }, children: label }),
371
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: smallBtnStyle, onClick: () => onChange(Math.max(0, value - 1)), disabled: value <= 0, children: "<" }),
372
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { color: "#fff", minWidth: 40, textAlign: "center" }, children: [
659
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 8, margin: "4px 0" }, children: [
660
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: "#aab", minWidth: 50 }, children: label }),
661
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { style: smallBtnStyle, onClick: () => onChange(Math.max(0, value - 1)), disabled: value <= 0, children: "<" }),
662
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { color: "#fff", minWidth: 40, textAlign: "center" }, children: [
373
663
  value + 1,
374
664
  "/",
375
665
  count
376
666
  ] }),
377
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: smallBtnStyle, onClick: () => onChange(Math.min(count - 1, value + 1)), disabled: value >= count - 1, children: ">" })
667
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { style: smallBtnStyle, onClick: () => onChange(Math.min(count - 1, value + 1)), disabled: value >= count - 1, children: ">" })
378
668
  ] });
379
669
  }
380
670
  function ColourPresets({
@@ -382,9 +672,9 @@ function ColourPresets({
382
672
  presets,
383
673
  onPick
384
674
  }) {
385
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", flexWrap: "wrap", gap: 4, margin: "4px 0" }, children: presets.map((p, i) => {
675
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { display: "flex", flexWrap: "wrap", gap: 4, margin: "4px 0" }, children: presets.map((p, i) => {
386
676
  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;
387
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
677
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
388
678
  "button",
389
679
  {
390
680
  onClick: () => onPick([...p]),
@@ -410,7 +700,24 @@ var rootStyle = {
410
700
  borderRadius: 8,
411
701
  overflow: "hidden",
412
702
  fontFamily: "system-ui, sans-serif",
413
- fontSize: 14
703
+ fontSize: 14,
704
+ minHeight: 400
705
+ };
706
+ var mainStyle = {
707
+ display: "flex",
708
+ flex: 1,
709
+ minHeight: 0
710
+ };
711
+ var previewPaneStyle = {
712
+ flex: "0 0 45%",
713
+ minHeight: 280,
714
+ borderRight: "1px solid #2a2a38"
715
+ };
716
+ var controlsPaneStyle = {
717
+ flex: 1,
718
+ display: "flex",
719
+ flexDirection: "column",
720
+ minWidth: 0
414
721
  };
415
722
  var headerStyle = {
416
723
  display: "flex",
@@ -470,7 +777,60 @@ var sliderRowStyle = {
470
777
  gap: 8,
471
778
  margin: "4px 0"
472
779
  };
780
+
781
+ // src/components/AvatarPreview.tsx
782
+ var import_jsx_runtime3 = require("react/jsx-runtime");
783
+ function AvatarPreview({
784
+ appearance,
785
+ size = 200,
786
+ autoRotate = true,
787
+ background,
788
+ className,
789
+ style
790
+ }) {
791
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
792
+ AvatarRenderer,
793
+ {
794
+ appearance,
795
+ width: size,
796
+ height: size,
797
+ autoRotate,
798
+ background,
799
+ className,
800
+ style: { borderRadius: 8, ...style }
801
+ }
802
+ );
803
+ }
804
+
805
+ // src/components/AvatarThumbnail.tsx
806
+ var import_jsx_runtime4 = require("react/jsx-runtime");
807
+ function AvatarThumbnail({
808
+ appearance,
809
+ size = 48,
810
+ background = "#1a1a26",
811
+ className,
812
+ style
813
+ }) {
814
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
815
+ AvatarRenderer,
816
+ {
817
+ appearance,
818
+ width: size,
819
+ height: size,
820
+ autoRotate: false,
821
+ background,
822
+ className,
823
+ style: {
824
+ borderRadius: "50%",
825
+ ...style
826
+ }
827
+ }
828
+ );
829
+ }
473
830
  // Annotate the CommonJS export names for ESM import in node:
474
831
  0 && (module.exports = {
475
- AvatarEditor
832
+ AvatarEditor,
833
+ AvatarPreview,
834
+ AvatarRenderer,
835
+ AvatarThumbnail
476
836
  });