@ikijs/editor 0.32.1 → 0.33.1

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/README.md CHANGED
@@ -97,7 +97,7 @@ the turn's amount, the eyes' foreshortening, the hair's width, a feature's own
97
97
  shift. With none given the profile itself renders (`DEFAULT_TURN_TARGETS`
98
98
  expresses it in the cues' units). `options.rigPlan` tunes a character from the
99
99
  profile in three groups (`turn`, `hair`, `eyes`: the whole turn, the
100
- features' lead, the front hair's follow and its outline's, the blink and the
100
+ features' lead, the far ear's follow, the front hair's follow and its outline's, the blink and the
101
101
  hair sway), validated against `RIG_PLAN_FIELDS`. `options.patches` adds displacements to a generated rig's keyform
102
102
  stops (`RigPatch`), replayed on every rig and reported through
103
103
  `options.onPatchesApplied`; a refused op throws `PatchError`. `options.customParts` adds parts the
package/dist/index.d.mts CHANGED
@@ -709,6 +709,9 @@ interface RigPlan {
709
709
  /** How far the nose leads the plate (turn only — the nod keeps the
710
710
  * profile's nose depth). */
711
711
  noseLead?: number;
712
+ /** How far the far ear's outer edge follows the face (0: it holds and
713
+ * the head slides over it; 1: it rides with the face and stays whole). */
714
+ farEar?: number;
712
715
  };
713
716
  hair?: {
714
717
  /** The front hair's share of the face's turn. */
package/dist/index.d.ts CHANGED
@@ -709,6 +709,9 @@ interface RigPlan {
709
709
  /** How far the nose leads the plate (turn only — the nod keeps the
710
710
  * profile's nose depth). */
711
711
  noseLead?: number;
712
+ /** How far the far ear's outer edge follows the face (0: it holds and
713
+ * the head slides over it; 1: it rides with the face and stays whole). */
714
+ farEar?: number;
712
715
  };
713
716
  hair?: {
714
717
  /** The front hair's share of the face's turn. */
package/dist/index.js CHANGED
@@ -3025,6 +3025,7 @@ function mouthAnchor(byRole) {
3025
3025
  }
3026
3026
 
3027
3027
  // src/auto-rig/head.ts
3028
+ var EAR_EDGE = 3;
3028
3029
  function outerEdge(runs, axisX, side) {
3029
3030
  if (runs.length === 0) return void 0;
3030
3031
  const x = side > 0 ? runs[runs.length - 1] : runs[0];
@@ -3058,7 +3059,11 @@ function findEars(raw, wMax, firstRow, lastRow, minRows, maxRows) {
3058
3059
  if (raw[t] - raw[t - EAR_ROWS] < step) continue;
3059
3060
  for (let b = t + minRows; b <= Math.min(lastRow, t + maxRows); b++) {
3060
3061
  if (raw[b - EAR_ROWS] - raw[b] >= step) {
3061
- return { top: t - EAR_ROWS, bottom: b };
3062
+ let end = b;
3063
+ for (let k = b + 1; k <= Math.min(lastRow, b + minRows); k++) {
3064
+ if (raw[k - EAR_ROWS] - raw[k] >= step) end = k;
3065
+ }
3066
+ return { top: t - EAR_ROWS, bottom: end };
3062
3067
  }
3063
3068
  }
3064
3069
  return void 0;
@@ -3136,6 +3141,7 @@ function buildHeadFrame(layers, opts) {
3136
3141
  return rows[i] + (rows[j] - rows[i]) * clamp(r - i, 0, 1);
3137
3142
  };
3138
3143
  const rawRows = raw;
3144
+ let earRows;
3139
3145
  const earless = (chin) => {
3140
3146
  const rowOf = (y) => Math.round(canvasH / 2 - y - faceLayer.bbox.y - 0.5);
3141
3147
  const span3 = Math.max(1, eyeY - chin);
@@ -3158,8 +3164,10 @@ function buildHeadFrame(layers, opts) {
3158
3164
  top: rowY(top) + 0.5,
3159
3165
  bottom: rowY(bottom) - 0.5,
3160
3166
  attachAt: (y) => attach(clamp(canvasH / 2 - y - faceLayer.bbox.y - 0.5, top, bottom)),
3161
- outer
3167
+ outer,
3168
+ edgeAt: (y) => attach(clamp(canvasH / 2 - y - faceLayer.bbox.y - 0.5, top, bottom)) + EAR_EDGE
3162
3169
  };
3170
+ earRows = { top, bottom };
3163
3171
  return smoothProfile(
3164
3172
  rawRows.map((v, r) => r > top && r < bottom ? attach(r) : v)
3165
3173
  );
@@ -3281,9 +3289,13 @@ function buildHeadFrame(layers, opts) {
3281
3289
  headHalfAt = readRows(head);
3282
3290
  headMax = Math.max(...head);
3283
3291
  if (ears !== void 0) {
3292
+ const band = earRows;
3293
+ const edge = head.map(
3294
+ (w, r) => r >= band.top && r <= band.bottom ? w + EAR_EDGE : w
3295
+ );
3284
3296
  const bottomOf = (d) => {
3285
- for (let r = head.length - 1; r >= 0; r--) {
3286
- if (head[r] >= d) return rowY(r) - 0.5;
3297
+ for (let r = edge.length - 1; r >= 0; r--) {
3298
+ if (edge[r] >= d) return rowY(r) - 0.5;
3287
3299
  }
3288
3300
  return face.y1;
3289
3301
  };
@@ -3451,6 +3463,19 @@ function spin(x, y, ox, oy, a) {
3451
3463
  ];
3452
3464
  }
3453
3465
  function faceField(m) {
3466
+ const plate = plateField(m);
3467
+ const floor = foreheadFloor(m);
3468
+ let hold;
3469
+ return (x, y, ax, ay) => {
3470
+ const d = plate(x, y, ax, ay);
3471
+ if (ax === 0 || y <= floor) return d;
3472
+ if (hold === void 0) hold = foreheadHold(m, floor) ?? null;
3473
+ if (hold === null) return d;
3474
+ const s = Math.sign(ax);
3475
+ return [d[0] - s * Math.abs(ax) * hold(x, y, s) / 30, d[1]];
3476
+ };
3477
+ }
3478
+ function plateField(m) {
3454
3479
  const f = m.frame;
3455
3480
  return (x, y, ax, ay) => {
3456
3481
  const s = Math.sign(ax);
@@ -3470,6 +3495,77 @@ function faceField(m) {
3470
3495
  return [dx, nodDy(v, ay, f.hh)];
3471
3496
  };
3472
3497
  }
3498
+ function faceCellPx(frame) {
3499
+ return Math.min(FACE_CELL_PX, 2 * frame.wMax / 14);
3500
+ }
3501
+ var FACE_CELL_PX = 28;
3502
+ var FOREHEAD_FADE = 0.04;
3503
+ function foreheadFloor(m) {
3504
+ const f = m.frame;
3505
+ const eyes = [m.boxes.eye_L, m.boxes.eye_R].filter(
3506
+ (e) => e !== void 0
3507
+ );
3508
+ const eyeTop = eyes.length > 0 ? Math.max(...eyes.map((e) => e.y1)) : f.eyeY;
3509
+ return Math.max(eyeTop, f.ears === void 0 ? -Infinity : f.ears.top + 2);
3510
+ }
3511
+ function foreheadHold(m, floor) {
3512
+ const f = m.frame;
3513
+ const top = f.face.y1;
3514
+ const count = Math.floor(top - floor);
3515
+ if (count <= 0 || f.faceRuns(top - 0.5) === void 0 || f.hairRuns("hair_front", top - 0.5) === void 0) {
3516
+ return void 0;
3517
+ }
3518
+ const plate = plateField(m);
3519
+ const front = hairFrontField(m);
3520
+ const { inner } = hairFrontBends(f, m.boxes);
3521
+ const cell = faceCellPx(f);
3522
+ const fade = FOREHEAD_FADE * f.hh;
3523
+ const sides = [-1, 1];
3524
+ const hold = sides.map(() => new Float64Array(count));
3525
+ const whole = sides.map(() => new Float64Array(count));
3526
+ const span3 = sides.map(() => new Float64Array(count));
3527
+ let any = false;
3528
+ for (let i = 0; i < count; i++) {
3529
+ const y = top - i - 0.5;
3530
+ const faceRuns = f.faceRuns(y);
3531
+ const hairRuns = f.hairRuns("hair_front", y);
3532
+ sides.forEach((side, k) => {
3533
+ const edge = outerEdge(faceRuns, f.axisX, side);
3534
+ const hair = outerEdge(hairRuns, f.axisX, side);
3535
+ if (edge === void 0 || hair === void 0) return;
3536
+ const end = side * (edge - f.axisX) - cell;
3537
+ if (end <= 0) return;
3538
+ const cover = side * (hair - edge);
3539
+ const room = Math.max(0, cover);
3540
+ const goes = side * plate(edge - side * 0.5, y, 30 * side, 0)[0];
3541
+ const held = side * front(hair - side * 0.5, y, 30 * side, 0)[0];
3542
+ const need = clamp(1 + cover / fade, 0, 1) * Math.max(0, goes - held - room + Math.min(1, room));
3543
+ if (need <= 0) return;
3544
+ const over = Math.min(end, Math.max(2 * cell, end - inner, 2 * need));
3545
+ hold[k][i] = Math.min(need, over / 2);
3546
+ whole[k][i] = end;
3547
+ span3[k][i] = over;
3548
+ any = true;
3549
+ });
3550
+ }
3551
+ if (!any) return void 0;
3552
+ const reach = Math.ceil(cell);
3553
+ return (x, y, s) => {
3554
+ const u = s * (x - f.axisX);
3555
+ if (s === 0 || u <= 0 || y <= floor) return 0;
3556
+ const k = s < 0 ? 0 : 1;
3557
+ const i = clamp(Math.floor(top - y), 0, count - 1);
3558
+ let h = 0;
3559
+ const j1 = Math.min(count - 1, i + 3 * reach);
3560
+ for (let j = Math.max(0, i - 3 * reach); j <= j1; j++) {
3561
+ if (hold[k][j] === 0) continue;
3562
+ const near = clamp((3 * reach - Math.abs(i - j)) / (2 * reach), 0, 1);
3563
+ const across = clamp((u - (whole[k][j] - span3[k][j])) / span3[k][j], 0, 1);
3564
+ h = Math.max(h, hold[k][j] * near * across);
3565
+ }
3566
+ return h * clamp((y - floor) / (2 * cell), 0, 1);
3567
+ };
3568
+ }
3473
3569
  function earField(m) {
3474
3570
  const f = m.frame;
3475
3571
  const ears = f.ears;
@@ -3483,7 +3579,7 @@ function earField(m) {
3483
3579
  const attach = ears.attachAt(y);
3484
3580
  const span3 = Math.max(1, ears.outer - attach);
3485
3581
  if (sideOf(m, x, s) === "far") {
3486
- const rim2 = TURN.earFar * m.face;
3582
+ const rim2 = m.earFar * m.face;
3487
3583
  const root2 = earRoot(m, face, y, s);
3488
3584
  const lead = Math.max(
3489
3585
  0,
@@ -3508,7 +3604,7 @@ function farEarCovered(m) {
3508
3604
  const ears = m.frame.ears;
3509
3605
  if (ears === void 0) return true;
3510
3606
  const face = faceField(m);
3511
- const rim = TURN.earFar * m.face;
3607
+ const rim = m.earFar * m.face;
3512
3608
  const y0 = ears.bottom - 2;
3513
3609
  const y1 = ears.top + 2;
3514
3610
  const n = Math.max(1, Math.ceil(y1 - y0));
@@ -4349,6 +4445,7 @@ function fitTurn(ctx, targets, plan, build) {
4349
4445
  lead: plan.turn.featureLead,
4350
4446
  follow: plan.hair.follow,
4351
4447
  outline: plan.hair.outlineFollow,
4448
+ earFar: plan.turn.farEar,
4352
4449
  fore: targets.farEyeRatio === void 0 ? plan.turn.eyeForeshorten : 1,
4353
4450
  shell: 1,
4354
4451
  noseLead: targets.noseShift !== void 0 || plan.turn.noseLead === 1 ? void 0 : plan.turn.noseLead
@@ -4585,6 +4682,7 @@ function buildTurn(frame, landmarks, q, turn) {
4585
4682
  hairOuter: q.outline * face,
4586
4683
  hairBack: A * hh * turn.hairBack,
4587
4684
  shellScale: q.shell,
4685
+ earFar: q.earFar,
4588
4686
  earFarScale: 1 + A * (turn.earFarScale - 1),
4589
4687
  ...landmarks
4590
4688
  };
@@ -4672,6 +4770,23 @@ var RIG_PLAN_FIELDS = [
4672
4770
  migratedFrom: null,
4673
4771
  what: "how far the nose leads the face plate on the turn \u2014 turn only, the nod keeps the profile's nose depth"
4674
4772
  },
4773
+ {
4774
+ path: "turn.farEar",
4775
+ group: "turn",
4776
+ key: "farEar",
4777
+ kind: "absolute",
4778
+ unit: "share of the face plate's slide",
4779
+ default: PROFILE.TURN.earFar,
4780
+ range: [0, 1],
4781
+ recommended: [0, 0.5],
4782
+ step: 0.1,
4783
+ poses: TURN_POSES,
4784
+ cue: null,
4785
+ fitBy: null,
4786
+ provisional: false,
4787
+ migratedFrom: null,
4788
+ what: "how far the far ear's outer edge follows the face on the turn (0: it holds and the head slides over it, about half of a default-sized ear going under the face; 1: it rides with the face and stays whole)"
4789
+ },
4675
4790
  {
4676
4791
  path: "hair.follow",
4677
4792
  group: "hair",
@@ -6758,6 +6873,11 @@ function buildFaceMesh(frame, b, head, neckCells) {
6758
6873
  frame.axisX - frame.headMax + 0.5,
6759
6874
  frame.axisX + frame.headMax - 0.5
6760
6875
  ];
6876
+ const ears2 = frame.ears;
6877
+ if (ears2 !== void 0 && neck2 === void 0) {
6878
+ const d = ears2.edgeAt(ears2.bottom);
6879
+ at.push(frame.axisX - d, frame.axisX + d);
6880
+ }
6761
6881
  if (neck2 !== void 0) {
6762
6882
  at.push(
6763
6883
  frame.axisX,
@@ -7376,7 +7496,7 @@ function writeOcclusion(customParts, built, parts) {
7376
7496
 
7377
7497
  // src/auto-rig/generate.ts
7378
7498
  var MESH_CELLS = {
7379
- face: { px: 28, min: 4, max: 24 },
7499
+ face: { px: FACE_CELL_PX, min: 4, max: 24 },
7380
7500
  // The front hair eases from riding the face to holding the outline.
7381
7501
  hair_front: { px: 32, min: 3, max: 24 },
7382
7502
  // A side lock's rows come from `sideLockGrid`: even ones, plus the root
@@ -7684,7 +7804,7 @@ function buildPart(spec, b, order, frame, byRole, plan, frontGrid, lips) {
7684
7804
  let region;
7685
7805
  let headStart = 0;
7686
7806
  if (spec.role === "face") {
7687
- const px = Math.min(MESH_CELLS.face.px, 2 * frame.wMax / 14);
7807
+ const px = faceCellPx(frame);
7688
7808
  const fm = buildFaceMesh(frame, b, { ...MESH_CELLS.face, px }, NECK_CELLS);
7689
7809
  mesh = fm.mesh;
7690
7810
  region = fm.region;