@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.js CHANGED
@@ -2433,7 +2433,8 @@ var TURN = {
2433
2433
  /** The back hair's slight counter-motion. */
2434
2434
  hairBack: -0.027,
2435
2435
  /** The ears lag the plate: the far one moves this share of its slide, the
2436
- * near one this (the samples' parallax ratios, 0.44 and 0.87). */
2436
+ * near one this at its widest reach, its root riding the head (the
2437
+ * samples' parallax ratios, 0.44 and 0.87). */
2437
2438
  earFar: 0.44,
2438
2439
  earNear: 0.87
2439
2440
  };
@@ -2531,14 +2532,28 @@ function faceField(m) {
2531
2532
  };
2532
2533
  }
2533
2534
  function earField(m) {
2535
+ const f = m.frame;
2536
+ const ears = f.ears;
2537
+ if (ears === void 0) return () => [0, 0];
2534
2538
  const face = faceField(m);
2535
2539
  return (x, y, ax, ay) => {
2536
2540
  const s = Math.sign(ax);
2537
2541
  const a = Math.abs(ax) / 30;
2538
- const share = sideOf(m, x, s) === "far" ? TURN.earFar : TURN.earNear;
2539
- return [s * a * share * m.face, face(x, y, 0, ay)[1]];
2542
+ const dy = face(x, y, 0, ay)[1];
2543
+ if (sideOf(m, x, s) === "far") return [s * a * TURN.earFar * m.face, dy];
2544
+ const d = Math.abs(x - f.axisX);
2545
+ const attach = ears.attachAt(y);
2546
+ if (d <= attach) return [face(x, y, ax, 0)[0], dy];
2547
+ const root = face(f.axisX + Math.sign(x - f.axisX) * attach, y, ax, 0)[0];
2548
+ const rim = s * a * TURN.earNear * m.face;
2549
+ const t = Math.min(1, (d - attach) / Math.max(1, ears.outer - attach));
2550
+ return [root + (rim - root) * t, dy];
2540
2551
  };
2541
2552
  }
2553
+ function chinSlide(m) {
2554
+ const f = m.frame;
2555
+ return Math.abs(faceField(m)(f.axisX, f.chinY, 30, 0)[0]);
2556
+ }
2542
2557
  var NECK_SHADE_MAX = 0.45;
2543
2558
  var NECK_SHADE_FULL = 0.2;
2544
2559
  function neckField(m) {
@@ -2546,7 +2561,7 @@ function neckField(m) {
2546
2561
  const nk = f.neck;
2547
2562
  if (nk === void 0) return () => [0, 0];
2548
2563
  const face = faceField(m);
2549
- const full = Math.abs(face(f.axisX, f.chinY, 30, 0)[0]);
2564
+ const full = chinSlide(m);
2550
2565
  const share = Math.min(1, NECK_SHADE_MAX * nk.waist / Math.max(1e-6, full));
2551
2566
  const bottom = f.face.y0;
2552
2567
  const tip = f.cutAt(f.axisX);
@@ -2885,7 +2900,8 @@ function boxSmooth(v, k) {
2885
2900
  });
2886
2901
  }
2887
2902
  var CUT_MARGIN = 2;
2888
- function shadeAcross(d, nk) {
2903
+ var CUT_LOW = 3;
2904
+ function fadeToOutline(d, nk) {
2889
2905
  return Math.max(0, 1 - d / nk.waist);
2890
2906
  }
2891
2907
  var STROKE_RUN = 4;
@@ -3043,20 +3059,30 @@ function buildHeadFrame(layers, opts) {
3043
3059
  };
3044
3060
  const chin = chinY;
3045
3061
  const shade = JAW_SHADE * HH_PER_EYE_TO_CHIN * Math.max(1, eyeY - chin);
3062
+ const reach = opts.chinSlide === void 0 ? nk.waist : clamp(
3063
+ nk.waist - opts.chinSlide - CUT_MARGIN - (CUT_LOW + 1),
3064
+ 0,
3065
+ nk.waist
3066
+ );
3067
+ let banded = -1;
3046
3068
  const cut = new Float64Array(faceLayer.cropW);
3047
3069
  for (let i = 0; i < faceLayer.cropW; i++) {
3048
3070
  const d = Math.abs(colX(i) - axisX);
3049
3071
  const stroke = d <= nk.half ? strokeAt(i) : void 0;
3050
- 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;
3072
+ const band = reach > 0 ? Math.max(0, 1 - d / reach) : 0;
3073
+ if (stroke !== void 0 && band > 0) banded = Math.max(banded, d);
3074
+ 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;
3051
3075
  cut[i] = y >= face.y1 ? face.y1 : y;
3052
3076
  }
3053
3077
  const low = cut.map((_, i) => {
3054
3078
  let m = Infinity;
3055
- for (let j = i - 3; j <= i + 3; j++) {
3079
+ for (let j = i - CUT_LOW; j <= i + CUT_LOW; j++) {
3056
3080
  if (j >= 0 && j < cut.length) m = Math.min(m, cut[j]);
3057
3081
  }
3058
3082
  return m;
3059
3083
  });
3084
+ if (opts.chinSlide !== void 0 && banded >= 0)
3085
+ nk.bandEnd = banded + CUT_LOW + 1;
3060
3086
  cutAt = (x) => {
3061
3087
  const f = x - (faceLayer.bbox.x - canvasW / 2) - 0.5;
3062
3088
  const i = clamp(Math.floor(f), 0, low.length - 1);
@@ -3193,12 +3219,20 @@ function buildFaceMesh(frame, b, head, neckCells) {
3193
3219
  frame.axisX - neck2.waist,
3194
3220
  frame.axisX + neck2.waist
3195
3221
  );
3222
+ if (neck2.bandEnd !== void 0)
3223
+ at.push(frame.axisX - neck2.bandEnd, frame.axisX + neck2.bandEnd);
3196
3224
  }
3225
+ const placed = /* @__PURE__ */ new Set();
3197
3226
  for (const k of at) {
3198
3227
  if (k <= x0 || k >= x1) continue;
3199
- const near = xs2.findIndex((x) => Math.abs(x - k) < 0.3 * step);
3200
- if (near >= 0 && near !== 0 && near !== xs2.length - 1) xs2[near] = k;
3201
- else if (near < 0) xs2.push(k);
3228
+ if ([...placed].some((i) => Math.abs(xs2[i] - k) < 0.5)) continue;
3229
+ const near = xs2.findIndex(
3230
+ (x, i) => !placed.has(i) && Math.abs(x - k) < 0.3 * step
3231
+ );
3232
+ if (near > 0 && near < n) {
3233
+ xs2[near] = k;
3234
+ placed.add(near);
3235
+ } else if (near < 0) placed.add(xs2.push(k) - 1);
3202
3236
  }
3203
3237
  return xs2.sort((a, b2) => a - b2);
3204
3238
  };
@@ -3214,7 +3248,7 @@ function buildFaceMesh(frame, b, head, neckCells) {
3214
3248
  );
3215
3249
  const sampleBelow = (x) => Math.max(
3216
3250
  1,
3217
- SAMPLE_BELOW * frame.hh * shadeAcross(Math.abs(x - frame.axisX), neck)
3251
+ SAMPLE_BELOW * frame.hh * fadeToOutline(Math.abs(x - frame.axisX), neck)
3218
3252
  );
3219
3253
  let highest = -Infinity;
3220
3254
  for (const x of xs2) {
@@ -3666,8 +3700,7 @@ function eyesInPlate(ctx, m) {
3666
3700
  function chinOverNeck(ctx, m) {
3667
3701
  const { frame } = ctx;
3668
3702
  if (frame.neck === void 0) return true;
3669
- const f = familyField(m, "face");
3670
- return Math.abs(f(frame.axisX, frame.chinY, 30, 0)[0]) <= frame.neck.half;
3703
+ return chinSlide(m) <= frame.neck.half;
3671
3704
  }
3672
3705
  function strandHeld(ctx, m) {
3673
3706
  const land2 = analyticLander(m);
@@ -3982,10 +4015,28 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
3982
4015
  checkedKeys("turnTargets", options.turnTargets, TURN_TARGET_KEYS);
3983
4016
  const targets = hasNose ? resolveTurnTargets(options.turnTargets, plateHalf) : { given: /* @__PURE__ */ new Set() };
3984
4017
  const turnOptions = hasNose ? options : {};
3985
- const frame = buildHeadFrame(layers, {
4018
+ const frameOptions = {
3986
4019
  headHalfWidth: targets.headHalfWidth,
3987
4020
  headEdges: targets.headHalfWidth !== void 0 ? options.headEdges : void 0
4021
+ };
4022
+ const solveFrame = buildHeadFrame(layers, frameOptions);
4023
+ const ctx = solveContext(solveFrame, byRole, turnOptions, hasNose);
4024
+ const landmarks = {
4025
+ boxes: Object.fromEntries(
4026
+ ["eye_L", "eye_R", "brow_L", "brow_R"].filter(has).map((r) => [r, box(r)])
4027
+ ),
4028
+ noseAt: ctx.noseAt,
4029
+ noseTopY: ctx.noseBox?.y1,
4030
+ mouthAt: ctx.mouthAt,
4031
+ hairTop: has("hair_front") ? box("hair_front").y1 : solveFrame.face.y1
4032
+ };
4033
+ const build = (q) => buildTurn(solveFrame, landmarks, q);
4034
+ const fit = fitTurn(ctx, targets, style, build);
4035
+ const frame = buildHeadFrame(layers, {
4036
+ ...frameOptions,
4037
+ chinSlide: chinSlide(fit.model)
3988
4038
  });
4039
+ const turn = { ...fit.model, frame };
3989
4040
  const parts = [];
3990
4041
  const reach = /* @__PURE__ */ new Map();
3991
4042
  let regionOf = [];
@@ -4008,19 +4059,6 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
4008
4059
  reach.set(spec.role, built.reach);
4009
4060
  parts.push(built.part);
4010
4061
  }
4011
- const ctx = solveContext(frame, byRole, turnOptions, hasNose);
4012
- const landmarks = {
4013
- boxes: Object.fromEntries(
4014
- ["eye_L", "eye_R", "brow_L", "brow_R"].filter(has).map((r) => [r, box(r)])
4015
- ),
4016
- noseAt: ctx.noseAt,
4017
- noseTopY: ctx.noseBox?.y1,
4018
- mouthAt: ctx.mouthAt,
4019
- hairTop: has("hair_front") ? box("hair_front").y1 : frame.face.y1
4020
- };
4021
- const build = (q) => buildTurn(frame, landmarks, q);
4022
- const fit = fitTurn(ctx, targets, style, build);
4023
- const turn = fit.model;
4024
4062
  const grids = /* @__PURE__ */ new Map();
4025
4063
  for (const family of Object.keys(WARP_ID)) {
4026
4064
  const members = parts.filter((p) => roleSpec(p.id).family === family);