@ikijs/editor 0.10.0 → 0.10.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/dist/index.mjs CHANGED
@@ -2364,7 +2364,8 @@ var TURN = {
2364
2364
  /** The back hair's slight counter-motion. */
2365
2365
  hairBack: -0.027,
2366
2366
  /** The ears lag the plate: the far one moves this share of its slide, the
2367
- * near one this (the samples' parallax ratios, 0.44 and 0.87). */
2367
+ * near one this at its widest reach, its root riding the head (the
2368
+ * samples' parallax ratios, 0.44 and 0.87). */
2368
2369
  earFar: 0.44,
2369
2370
  earNear: 0.87
2370
2371
  };
@@ -2462,14 +2463,28 @@ function faceField(m) {
2462
2463
  };
2463
2464
  }
2464
2465
  function earField(m) {
2466
+ const f = m.frame;
2467
+ const ears = f.ears;
2468
+ if (ears === void 0) return () => [0, 0];
2465
2469
  const face = faceField(m);
2466
2470
  return (x, y, ax, ay) => {
2467
2471
  const s = Math.sign(ax);
2468
2472
  const a = Math.abs(ax) / 30;
2469
- const share = sideOf(m, x, s) === "far" ? TURN.earFar : TURN.earNear;
2470
- return [s * a * share * m.face, face(x, y, 0, ay)[1]];
2473
+ const dy = face(x, y, 0, ay)[1];
2474
+ if (sideOf(m, x, s) === "far") return [s * a * TURN.earFar * m.face, dy];
2475
+ const d = Math.abs(x - f.axisX);
2476
+ const attach = ears.attachAt(y);
2477
+ if (d <= attach) return [face(x, y, ax, 0)[0], dy];
2478
+ const root = face(f.axisX + Math.sign(x - f.axisX) * attach, y, ax, 0)[0];
2479
+ const rim = s * a * TURN.earNear * m.face;
2480
+ const t = Math.min(1, (d - attach) / Math.max(1, ears.outer - attach));
2481
+ return [root + (rim - root) * t, dy];
2471
2482
  };
2472
2483
  }
2484
+ function chinSlide(m) {
2485
+ const f = m.frame;
2486
+ return Math.abs(faceField(m)(f.axisX, f.chinY, 30, 0)[0]);
2487
+ }
2473
2488
  var NECK_SHADE_MAX = 0.45;
2474
2489
  var NECK_SHADE_FULL = 0.2;
2475
2490
  function neckField(m) {
@@ -2477,7 +2492,7 @@ function neckField(m) {
2477
2492
  const nk = f.neck;
2478
2493
  if (nk === void 0) return () => [0, 0];
2479
2494
  const face = faceField(m);
2480
- const full = Math.abs(face(f.axisX, f.chinY, 30, 0)[0]);
2495
+ const full = chinSlide(m);
2481
2496
  const share = Math.min(1, NECK_SHADE_MAX * nk.waist / Math.max(1e-6, full));
2482
2497
  const bottom = f.face.y0;
2483
2498
  const tip = f.cutAt(f.axisX);
@@ -2816,7 +2831,8 @@ function boxSmooth(v, k) {
2816
2831
  });
2817
2832
  }
2818
2833
  var CUT_MARGIN = 2;
2819
- function shadeAcross(d, nk) {
2834
+ var CUT_LOW = 3;
2835
+ function fadeToOutline(d, nk) {
2820
2836
  return Math.max(0, 1 - d / nk.waist);
2821
2837
  }
2822
2838
  var STROKE_RUN = 4;
@@ -2974,20 +2990,30 @@ function buildHeadFrame(layers, opts) {
2974
2990
  };
2975
2991
  const chin = chinY;
2976
2992
  const shade = JAW_SHADE * HH_PER_EYE_TO_CHIN * Math.max(1, eyeY - chin);
2993
+ const reach = opts.chinSlide === void 0 ? nk.waist : clamp(
2994
+ nk.waist - opts.chinSlide - CUT_MARGIN - (CUT_LOW + 1),
2995
+ 0,
2996
+ nk.waist
2997
+ );
2998
+ let banded = -1;
2977
2999
  const cut = new Float64Array(faceLayer.cropW);
2978
3000
  for (let i = 0; i < faceLayer.cropW; i++) {
2979
3001
  const d = Math.abs(colX(i) - axisX);
2980
3002
  const stroke = d <= nk.half ? strokeAt(i) : void 0;
2981
- const y = stroke !== void 0 ? stroke - (shade + CUT_MARGIN) * shadeAcross(d, nk) : (d <= nk.half ? chin + (nk.topY - chin) * (d / Math.max(1, nk.half)) : outlineAt(d)) - CUT_MARGIN;
3003
+ const band = reach > 0 ? Math.max(0, 1 - d / reach) : 0;
3004
+ if (stroke !== void 0 && band > 0) banded = Math.max(banded, d);
3005
+ const y = stroke !== void 0 ? stroke - shade * band - CUT_MARGIN * fadeToOutline(d, nk) : (d <= nk.half ? chin + (nk.topY - chin) * (d / Math.max(1, nk.half)) : outlineAt(d)) - CUT_MARGIN;
2982
3006
  cut[i] = y >= face.y1 ? face.y1 : y;
2983
3007
  }
2984
3008
  const low = cut.map((_, i) => {
2985
3009
  let m = Infinity;
2986
- for (let j = i - 3; j <= i + 3; j++) {
3010
+ for (let j = i - CUT_LOW; j <= i + CUT_LOW; j++) {
2987
3011
  if (j >= 0 && j < cut.length) m = Math.min(m, cut[j]);
2988
3012
  }
2989
3013
  return m;
2990
3014
  });
3015
+ if (opts.chinSlide !== void 0 && banded >= 0)
3016
+ nk.bandEnd = banded + CUT_LOW + 1;
2991
3017
  cutAt = (x) => {
2992
3018
  const f = x - (faceLayer.bbox.x - canvasW / 2) - 0.5;
2993
3019
  const i = clamp(Math.floor(f), 0, low.length - 1);
@@ -3124,12 +3150,20 @@ function buildFaceMesh(frame, b, head, neckCells) {
3124
3150
  frame.axisX - neck2.waist,
3125
3151
  frame.axisX + neck2.waist
3126
3152
  );
3153
+ if (neck2.bandEnd !== void 0)
3154
+ at.push(frame.axisX - neck2.bandEnd, frame.axisX + neck2.bandEnd);
3127
3155
  }
3156
+ const placed = /* @__PURE__ */ new Set();
3128
3157
  for (const k of at) {
3129
3158
  if (k <= x0 || k >= x1) continue;
3130
- const near = xs2.findIndex((x) => Math.abs(x - k) < 0.3 * step);
3131
- if (near >= 0 && near !== 0 && near !== xs2.length - 1) xs2[near] = k;
3132
- else if (near < 0) xs2.push(k);
3159
+ if ([...placed].some((i) => Math.abs(xs2[i] - k) < 0.5)) continue;
3160
+ const near = xs2.findIndex(
3161
+ (x, i) => !placed.has(i) && Math.abs(x - k) < 0.3 * step
3162
+ );
3163
+ if (near > 0 && near < n) {
3164
+ xs2[near] = k;
3165
+ placed.add(near);
3166
+ } else if (near < 0) placed.add(xs2.push(k) - 1);
3133
3167
  }
3134
3168
  return xs2.sort((a, b2) => a - b2);
3135
3169
  };
@@ -3145,7 +3179,7 @@ function buildFaceMesh(frame, b, head, neckCells) {
3145
3179
  );
3146
3180
  const sampleBelow = (x) => Math.max(
3147
3181
  1,
3148
- SAMPLE_BELOW * frame.hh * shadeAcross(Math.abs(x - frame.axisX), neck)
3182
+ SAMPLE_BELOW * frame.hh * fadeToOutline(Math.abs(x - frame.axisX), neck)
3149
3183
  );
3150
3184
  let highest = -Infinity;
3151
3185
  for (const x of xs2) {
@@ -3597,8 +3631,7 @@ function eyesInPlate(ctx, m) {
3597
3631
  function chinOverNeck(ctx, m) {
3598
3632
  const { frame } = ctx;
3599
3633
  if (frame.neck === void 0) return true;
3600
- const f = familyField(m, "face");
3601
- return Math.abs(f(frame.axisX, frame.chinY, 30, 0)[0]) <= frame.neck.half;
3634
+ return chinSlide(m) <= frame.neck.half;
3602
3635
  }
3603
3636
  function strandHeld(ctx, m) {
3604
3637
  const land2 = analyticLander(m);
@@ -3915,10 +3948,28 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
3915
3948
  checkedKeys("turnTargets", options.turnTargets, TURN_TARGET_KEYS);
3916
3949
  const targets = hasNose ? resolveTurnTargets(options.turnTargets, plateHalf) : { given: /* @__PURE__ */ new Set() };
3917
3950
  const turnOptions = hasNose ? options : {};
3918
- const frame = buildHeadFrame(layers, {
3951
+ const frameOptions = {
3919
3952
  headHalfWidth: targets.headHalfWidth,
3920
3953
  headEdges: targets.headHalfWidth !== void 0 ? options.headEdges : void 0
3954
+ };
3955
+ const solveFrame = buildHeadFrame(layers, frameOptions);
3956
+ const ctx = solveContext(solveFrame, byRole, turnOptions, hasNose);
3957
+ const landmarks = {
3958
+ boxes: Object.fromEntries(
3959
+ ["eye_L", "eye_R", "brow_L", "brow_R"].filter(has).map((r) => [r, box(r)])
3960
+ ),
3961
+ noseAt: ctx.noseAt,
3962
+ noseTopY: ctx.noseBox?.y1,
3963
+ mouthAt: ctx.mouthAt,
3964
+ hairTop: has("hair_front") ? box("hair_front").y1 : solveFrame.face.y1
3965
+ };
3966
+ const build = (q) => buildTurn(solveFrame, landmarks, q);
3967
+ const fit = fitTurn(ctx, targets, style, build);
3968
+ const frame = buildHeadFrame(layers, {
3969
+ ...frameOptions,
3970
+ chinSlide: chinSlide(fit.model)
3921
3971
  });
3972
+ const turn = { ...fit.model, frame };
3922
3973
  const parts = [];
3923
3974
  const reach = /* @__PURE__ */ new Map();
3924
3975
  let regionOf = [];
@@ -3941,19 +3992,6 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
3941
3992
  reach.set(spec.role, built.reach);
3942
3993
  parts.push(built.part);
3943
3994
  }
3944
- const ctx = solveContext(frame, byRole, turnOptions, hasNose);
3945
- const landmarks = {
3946
- boxes: Object.fromEntries(
3947
- ["eye_L", "eye_R", "brow_L", "brow_R"].filter(has).map((r) => [r, box(r)])
3948
- ),
3949
- noseAt: ctx.noseAt,
3950
- noseTopY: ctx.noseBox?.y1,
3951
- mouthAt: ctx.mouthAt,
3952
- hairTop: has("hair_front") ? box("hair_front").y1 : frame.face.y1
3953
- };
3954
- const build = (q) => buildTurn(frame, landmarks, q);
3955
- const fit = fitTurn(ctx, targets, style, build);
3956
- const turn = fit.model;
3957
3995
  const grids = /* @__PURE__ */ new Map();
3958
3996
  for (const family of Object.keys(WARP_ID)) {
3959
3997
  const members = parts.filter((p) => roleSpec(p.id).family === family);