@ikijs/editor 0.12.1 → 0.12.2

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
@@ -68,10 +68,12 @@ not a reshaped face. The face plate translates, changing its width by a few
68
68
  percent, the chin leading it; the features lead the plate by their own amounts
69
69
  (the nose most, then the brows, eyes and mouth), the eyes foreshortening mildly
70
70
  about their own centres; the front hair rides the face where it lies over it and holds the head's
71
- outline where it draws it, and the back hair stays behind; the neck and the
71
+ outline where it draws it — though on its cap the turn never carries its outer
72
+ edge further outside the back hair painted behind it than the nod alone does —
73
+ and the back hair stays behind; the neck and the
72
74
  ears the face layer paints are cut off into islands of the plate's own mesh,
73
75
  drawn behind the head — the neck stays (only the chin's shade slides across
74
- it), the ears lag the face. AngleZ rolls the head 10° about the chin; the torso only
76
+ it), the ears lag the face, the far one narrowing. AngleZ rolls the head 10° about the chin; the torso only
75
77
  breathes.
76
78
 
77
79
  On a layer set with a `nose`, `options.turnTargets` — the cues a 30° reference
@@ -95,9 +97,11 @@ how much it covers at rest, and whether the bound held.
95
97
  An `eyeShift` you passed that runs past the room the art leaves the far eye —
96
98
  the face plate's edge, or the bangs' strand — is **clamped** and the rig is
97
99
  built, since that room is a fact about these layers, not about the reference;
98
- so is the profile's own turn where the art has less room than it needs. Every
99
- other target you passed that this layer set cannot reach **throws**, naming the
100
- field and the range it could have had: a `farEyeRatio` or `silhouetteRatio`
100
+ so is the profile's own turn where the art has less room than it needs, and
101
+ a `silhouetteRatio` that would narrow a plate drawn without hair too far for
102
+ its head to cover the far ear's slide (see AUTO-RIG.md). Every other target
103
+ you passed that this layer set cannot reach **throws**, naming the field and
104
+ the range it could have had: a `farEyeRatio` or `silhouetteRatio`
101
105
  out of the rig's reach, or a `noseShift` / `mouthShift` outside what that
102
106
  feature can reach. Pass `options.onTurnSolved` to see what the turn settled on
103
107
  and which targets were clamped.
package/dist/index.d.mts CHANGED
@@ -678,7 +678,10 @@ interface RigStyle {
678
678
  * `hairFollow` rides it whole). Where back hair does, the edge rides
679
679
  * further, as far as that back hair stays behind it through the turn and
680
680
  * the nod, up to the front hair's follow there (`hairFollow`; over the
681
- * far eye's rows, as far as that eye's outer corner goes). */
681
+ * far eye's rows, as far as that eye's outer corner goes). On the cap
682
+ * (the rows above the eyes) the edge follows less where the turn would
683
+ * otherwise carry it further outside its back hair than the nod alone
684
+ * does. */
682
685
  outlineFollow?: number;
683
686
  /** How far the upper lid comes down, over the eye's height (0.30–0.67). */
684
687
  blink?: number;
package/dist/index.d.ts CHANGED
@@ -678,7 +678,10 @@ interface RigStyle {
678
678
  * `hairFollow` rides it whole). Where back hair does, the edge rides
679
679
  * further, as far as that back hair stays behind it through the turn and
680
680
  * the nod, up to the front hair's follow there (`hairFollow`; over the
681
- * far eye's rows, as far as that eye's outer corner goes). */
681
+ * far eye's rows, as far as that eye's outer corner goes). On the cap
682
+ * (the rows above the eyes) the edge follows less where the turn would
683
+ * otherwise carry it further outside its back hair than the nod alone
684
+ * does. */
682
685
  outlineFollow?: number;
683
686
  /** How far the upper lid comes down, over the eye's height (0.30–0.67). */
684
687
  blink?: number;
package/dist/index.js CHANGED
@@ -2457,15 +2457,22 @@ var TURN = {
2457
2457
  * where no back hair paints behind that edge. Where back hair does, the
2458
2458
  * edge rides further, as far as that back hair stays behind it through
2459
2459
  * the turn and the nod, up to the face's follow there (`hairFollow`;
2460
- * over the far eye's rows, as far as that eye's outer corner goes). */
2460
+ * over the far eye's rows, as far as that eye's outer corner goes). On
2461
+ * the cap (the rows above the eyes) it follows less where the turn would
2462
+ * otherwise carry it further outside its back hair than the nod alone
2463
+ * does. */
2461
2464
  outlineFollow: 0,
2462
2465
  /** The back hair's slight counter-motion. */
2463
2466
  hairBack: -0.027,
2464
- /** The ears lag the plate: the far one moves this share of its slide, the
2465
- * near one this at its widest reach, its root riding the head (the
2466
- * samples' parallax ratios, 0.44 and 0.87). */
2467
+ /** The ears lag the plate: the far one's outer edge moves this share of
2468
+ * its slide, the near one this at its widest reach, its root riding the
2469
+ * head (the samples' parallax ratios, 0.44 and 0.87). */
2467
2470
  earFar: 0.44,
2468
- earNear: 0.87
2471
+ earNear: 0.87,
2472
+ /** The far ear's width at full turn, about its outer edge, where the head
2473
+ * leaves it room (the samples' median: Haru 0.81 / 0.91, Hiyori 0.90 /
2474
+ * 0.85, Mao 0.77 / 0.79; none fades); less where it does not. */
2475
+ earFarScale: 0.83
2469
2476
  };
2470
2477
  var NOD = {
2471
2478
  faceTop: { up: -0.081, down: 0.178 },
@@ -2886,16 +2893,45 @@ function earField(m) {
2886
2893
  const s = Math.sign(ax);
2887
2894
  const a = Math.abs(ax) / 30;
2888
2895
  const dy = face(x, y, 0, ay)[1];
2889
- if (sideOf(m, x, s) === "far") return [s * a * TURN.earFar * m.face, dy];
2890
2896
  const d = Math.abs(x - f.axisX);
2891
2897
  const attach = ears.attachAt(y);
2898
+ const span = Math.max(1, ears.outer - attach);
2899
+ if (sideOf(m, x, s) === "far") {
2900
+ const rim2 = TURN.earFar * m.face;
2901
+ const root2 = earRoot(m, face, y, s);
2902
+ const lead = Math.max(
2903
+ 0,
2904
+ Math.min((1 - m.earFarScale) * span, root2 - rim2)
2905
+ );
2906
+ let along = rim2 + lead * (ears.outer - d) / span;
2907
+ if (d < attach) along = Math.min(along, s * face(x, y, 30 * s, 0)[0]);
2908
+ return [s * a * along, dy];
2909
+ }
2892
2910
  if (d <= attach) return [face(x, y, ax, 0)[0], dy];
2893
2911
  const root = face(f.axisX + Math.sign(x - f.axisX) * attach, y, ax, 0)[0];
2894
2912
  const rim = s * a * TURN.earNear * m.face;
2895
- const t = Math.min(1, (d - attach) / Math.max(1, ears.outer - attach));
2913
+ const t = Math.min(1, (d - attach) / span);
2896
2914
  return [root + (rim - root) * t, dy];
2897
2915
  };
2898
2916
  }
2917
+ function earRoot(m, face, y, s) {
2918
+ const f = m.frame;
2919
+ return s * face(f.axisX + s * f.ears.attachAt(y), y, 30 * s, 0)[0];
2920
+ }
2921
+ function farEarCovered(m) {
2922
+ const ears = m.frame.ears;
2923
+ if (ears === void 0) return true;
2924
+ const face = faceField(m);
2925
+ const rim = TURN.earFar * m.face;
2926
+ const y0 = ears.bottom - 2;
2927
+ const y1 = ears.top + 2;
2928
+ const n = Math.max(1, Math.ceil(y1 - y0));
2929
+ for (let i = 0; i <= n; i++) {
2930
+ const y = y0 + (y1 - y0) * i / n;
2931
+ for (const s of [-1, 1]) if (earRoot(m, face, y, s) < rim) return false;
2932
+ }
2933
+ return true;
2934
+ }
2899
2935
  function chinSlide(m) {
2900
2936
  const f = m.frame;
2901
2937
  return Math.abs(faceField(m)(f.axisX, f.chinY, 30, 0)[0]);
@@ -3038,14 +3074,19 @@ function hairFrontField(m) {
3038
3074
  );
3039
3075
  return core + (m.hairOuter - core) * out;
3040
3076
  };
3041
- const ride = outerRide(m, inner, coreAt, followAt);
3042
- const followOf = (x, y, s) => followAt(x, y, s) + (ride === void 0 ? 0 : ride(x, y, s));
3077
+ const ride = outerRide(m, inner, coreAt, followAt, eyeY + 1.5 * eyeH);
3078
+ const followOf = (x, y, s) => followAt(x, y, s) + (ride === void 0 ? 0 : ride.lift(x, y, s));
3043
3079
  const drift = Math.abs(m.hairBack);
3044
3080
  const crown = crownRide(m, cap, (x, y, s) => followOf(x, y, s) + drift);
3081
+ const boundOf = (x, y, s) => {
3082
+ if (ride === void 0) return followOf(x, y, s);
3083
+ const b = ride.blend(y, s);
3084
+ return (1 - b) * (followOf(x, y, s) + ride.cut(x, y, s)) - b * drift;
3085
+ };
3045
3086
  return (x, y, ax, ay) => {
3046
3087
  const s = Math.sign(ax);
3047
3088
  const a = Math.abs(ax) / 30;
3048
- const rx = s * a * followOf(x, y, s) + a * (m.shellScale - 1) * (x - f.axisX);
3089
+ const rx = s * a * boundOf(x, y, s) + a * (m.shellScale - 1) * (x - f.axisX);
3049
3090
  const ry = nodDy(NOD.hairFront, ay, f.hh);
3050
3091
  const bx = back(x, y, ax, ay)[0];
3051
3092
  const by = nodDy(cap, ay, f.hh);
@@ -3065,6 +3106,18 @@ function gridRow(ys, y, snap = false) {
3065
3106
  const r = Math.min(n - 2, Math.floor(v));
3066
3107
  return { r, t: v - r };
3067
3108
  }
3109
+ function gridCol(xs, x) {
3110
+ let c = 0;
3111
+ while (c < xs.length - 2 && xs[c + 1] < x) c++;
3112
+ return { c, t: clamp((x - xs[c]) / (xs[c + 1] - xs[c]), 0, 1) };
3113
+ }
3114
+ function rowWindow(ys, r, limit) {
3115
+ const n = ys.length;
3116
+ return [
3117
+ Math.max(0, Math.floor(ys[0] - ys[Math.max(0, r - 1)])),
3118
+ Math.min(limit, Math.ceil(ys[0] - ys[Math.min(n - 1, r + 1)]))
3119
+ ];
3120
+ }
3068
3121
  function renderedCrownBlend(m, ys) {
3069
3122
  const wr = ys.map((y) => crownBlend(m, y));
3070
3123
  return {
@@ -3140,10 +3193,11 @@ function paints(runs, px) {
3140
3193
  }
3141
3194
  return false;
3142
3195
  }
3143
- function outerRide(m, inner, coreAt, followAt) {
3196
+ function outerRide(m, inner, coreAt, followAt, capLimit) {
3144
3197
  const f = m.frame;
3145
- const ys = m.hairFrontGrid?.ys;
3146
- if (ys === void 0) return void 0;
3198
+ const grid = m.hairFrontGrid;
3199
+ if (grid === void 0) return void 0;
3200
+ const { xs, ys } = grid;
3147
3201
  const top = ys[0];
3148
3202
  if (f.hairRuns("hair_front", top) === void 0 || f.hairRuns("hair_back", top) === void 0) {
3149
3203
  return void 0;
@@ -3152,6 +3206,9 @@ function outerRide(m, inner, coreAt, followAt) {
3152
3206
  const sides = [-1, 1];
3153
3207
  const n = ys.length;
3154
3208
  const count = Math.round(top - ys[n - 1]);
3209
+ let last = 0;
3210
+ while (last + 1 < n && ys[last + 1] >= capLimit) last++;
3211
+ const capRows = last === 0 ? 0 : Math.min(count, Math.floor(top - ys[last] - 0.5) + 1);
3155
3212
  const below = (NOD.hairFront.down - NOD.hairBack.down) * f.hh;
3156
3213
  const above = (NOD.hairBack.up - NOD.hairFront.up) * f.hh;
3157
3214
  const j0 = Math.floor(-above);
@@ -3160,8 +3217,9 @@ function outerRide(m, inner, coreAt, followAt) {
3160
3217
  (_, j) => f.hairRuns("hair_back", top - (j + j0) - 0.5)
3161
3218
  );
3162
3219
  const offset = sides.map(() => new Float64Array(count).fill(NaN));
3163
- const far = sides.map(() => new Float64Array(count));
3164
- const near = sides.map(() => new Float64Array(count));
3220
+ const reachOut = sides.map(() => new Float64Array(count));
3221
+ const reachIn = sides.map(() => new Float64Array(count));
3222
+ const bound = sides.map(() => new Uint8Array(count));
3165
3223
  for (let i = 0; i < count; i++) {
3166
3224
  const y = top - i - 0.5;
3167
3225
  const front = f.hairRuns("hair_front", y);
@@ -3176,24 +3234,25 @@ function outerRide(m, inner, coreAt, followAt) {
3176
3234
  inward = Math.min(inward, c.inward);
3177
3235
  }
3178
3236
  offset[k][i] = side * (edge - f.axisX);
3179
- far[k][i] = m.shellScale * outward - drift;
3180
- near[k][i] = m.shellScale * inward - drift;
3237
+ reachOut[k][i] = outward;
3238
+ reachIn[k][i] = inward;
3239
+ if (i < capRows && runReach(backRows[i - j0], edge, side).inward > 0)
3240
+ bound[k][i] = 1;
3181
3241
  });
3182
3242
  }
3183
3243
  const end = new Float64Array(n * 2);
3184
3244
  const lift = new Float64Array(n * 4);
3185
3245
  for (let r = 0; r < n; r++) {
3186
- const i0 = Math.max(0, Math.floor(top - ys[Math.max(0, r - 1)]));
3187
- const i1 = Math.min(count, Math.ceil(top - ys[Math.min(n - 1, r + 1)]));
3246
+ const [i0, i1] = rowWindow(ys, r, count);
3188
3247
  sides.forEach((side, k) => {
3189
3248
  let d = Infinity;
3190
- let allowFar = Infinity;
3191
- let allowNear = Infinity;
3249
+ let outward = Infinity;
3250
+ let inward = Infinity;
3192
3251
  for (let i = i0; i < i1; i++) {
3193
3252
  if (Number.isNaN(offset[k][i])) continue;
3194
3253
  d = Math.min(d, offset[k][i]);
3195
- allowFar = Math.min(allowFar, far[k][i]);
3196
- allowNear = Math.min(allowNear, near[k][i]);
3254
+ outward = Math.min(outward, reachOut[k][i]);
3255
+ inward = Math.min(inward, reachIn[k][i]);
3197
3256
  }
3198
3257
  end[r * 2 + k] = d;
3199
3258
  if (d === Infinity || d <= inner) return;
@@ -3201,7 +3260,7 @@ function outerRide(m, inner, coreAt, followAt) {
3201
3260
  sides.forEach((s, j) => {
3202
3261
  const allow = Math.min(
3203
3262
  coreAt(x, ys[r], s),
3204
- s === side ? allowFar : allowNear
3263
+ m.shellScale * (s === side ? outward : inward) - drift
3205
3264
  );
3206
3265
  lift[(r * 2 + k) * 2 + j] = Math.max(0, allow - followAt(x, ys[r], s));
3207
3266
  });
@@ -3211,7 +3270,7 @@ function outerRide(m, inner, coreAt, followAt) {
3211
3270
  const v = lift[(r * 2 + k) * 2 + j];
3212
3271
  return v === 0 ? 0 : v * Math.min(1, (d - inner) / (end[r * 2 + k] - inner));
3213
3272
  };
3214
- return (x, y, s) => {
3273
+ const liftAt = (x, y, s) => {
3215
3274
  const d = Math.abs(x - f.axisX);
3216
3275
  if (s === 0 || d <= inner) return 0;
3217
3276
  const k = x < f.axisX ? 0 : 1;
@@ -3219,6 +3278,130 @@ function outerRide(m, inner, coreAt, followAt) {
3219
3278
  const { r, t } = gridRow(ys, y, true);
3220
3279
  return rowRide(r, k, j, d) * (1 - t) + rowRide(r + 1, k, j, d) * t;
3221
3280
  };
3281
+ const nc = xs.length;
3282
+ const along = (v, x) => {
3283
+ const { c, t } = gridCol(xs, x);
3284
+ return v[c] + (v[c + 1] - v[c]) * t;
3285
+ };
3286
+ const readAt = (r, x, y) => {
3287
+ const { c, t: fx } = gridCol(xs, x);
3288
+ const dx = xs[c + 1] - xs[c];
3289
+ if (r + 1 < n && y <= ys[r] && y >= ys[r + 1]) {
3290
+ const fy = (ys[r] - y) / (ys[r] - ys[r + 1]);
3291
+ if (fy >= 1) return void 0;
3292
+ return fx <= fy ? xs[c] : xs[c] + (fx - fy) / (1 - fy) * dx;
3293
+ }
3294
+ if (r > 0 && y > ys[r] && y < ys[r - 1]) {
3295
+ const fy = (ys[r - 1] - y) / (ys[r - 1] - ys[r]);
3296
+ return fx <= fy ? xs[c] + fx / fy * dx : xs[c + 1];
3297
+ }
3298
+ return void 0;
3299
+ };
3300
+ const ahead = (e, s) => s === e.side ? e.outward : e.inward;
3301
+ const behind = (e, s) => s === e.side ? e.inward : e.outward;
3302
+ const capCut = (r, s, side, today, mine) => {
3303
+ const reach = Math.max(...mine.map((e) => side * (e.x - f.axisX)));
3304
+ const quarter = new Float64Array(nc);
3305
+ const cols = xs.map((_, c) => c).filter((c) => (xs[c] < f.axisX ? -1 : 1) === side);
3306
+ if (side < 0) cols.reverse();
3307
+ let total = 0;
3308
+ let x0 = f.axisX;
3309
+ let d0 = s * followAt(f.axisX, ys[r], s);
3310
+ let open = true;
3311
+ for (const c of cols) {
3312
+ if (open) {
3313
+ const d = (m.shellScale - 1) * (xs[c] - f.axisX) + s * today[c];
3314
+ const rest = side * (xs[c] - x0);
3315
+ const full = rest + side * (d - d0);
3316
+ total += Math.max(0, Math.min(full, rest)) / 4;
3317
+ x0 = xs[c];
3318
+ d0 = d;
3319
+ open = side * (xs[c] - f.axisX) < reach;
3320
+ }
3321
+ quarter[c] = total;
3322
+ }
3323
+ let scale = 0;
3324
+ for (const e of mine) {
3325
+ const need = along(today, e.at) - (m.shellScale * ahead(e, s) - drift);
3326
+ if (need <= 0) continue;
3327
+ const q = along(quarter, e.at);
3328
+ scale = Math.max(scale, q > 0 ? need / q : 1);
3329
+ }
3330
+ scale = Math.min(1, scale);
3331
+ return quarter.map((q) => scale * q);
3332
+ };
3333
+ const cuts = new Float64Array(n * 2 * nc);
3334
+ const blend = new Float64Array(n * 2);
3335
+ const wr = ys.map((y) => crownBlend(m, y));
3336
+ for (let r = 0; r <= last && capRows > 0; r++) {
3337
+ const w = wr[r];
3338
+ if (w <= 0) continue;
3339
+ const edges = [];
3340
+ const [i0, i1] = rowWindow(ys, r, capRows);
3341
+ for (let i = i0; i < i1; i++) {
3342
+ sides.forEach((side, k) => {
3343
+ if (bound[k][i] === 0) return;
3344
+ const x = f.axisX + side * offset[k][i];
3345
+ const at = readAt(r, x, top - i - 0.5);
3346
+ if (at === void 0) return;
3347
+ edges.push({
3348
+ side,
3349
+ x,
3350
+ at,
3351
+ outward: reachOut[k][i],
3352
+ inward: reachIn[k][i]
3353
+ });
3354
+ });
3355
+ }
3356
+ if (edges.length === 0) continue;
3357
+ sides.forEach((s, j) => {
3358
+ const today = xs.map((x) => followAt(x, ys[r], s) + liftAt(x, ys[r], s));
3359
+ const after = today.slice();
3360
+ if (w >= 1) {
3361
+ for (const side of sides) {
3362
+ const mine = edges.filter((e) => e.side === side);
3363
+ if (mine.length === 0) continue;
3364
+ const cut = capCut(r, s, side, today, mine);
3365
+ for (let c = 0; c < nc; c++) after[c] -= cut[c];
3366
+ }
3367
+ }
3368
+ let b = 0;
3369
+ for (const e of edges) {
3370
+ const rel = w * (along(after, e.at) + drift);
3371
+ const hi = m.shellScale * ahead(e, s);
3372
+ const lo = -m.shellScale * behind(e, s);
3373
+ if (rel > hi) b = Math.max(b, 1 - hi / rel);
3374
+ else if (rel < lo) b = Math.max(b, 1 - lo / rel);
3375
+ }
3376
+ blend[r * 2 + j] = b;
3377
+ for (let c = 0; c < nc; c++)
3378
+ cuts[(r * 2 + j) * nc + c] = after[c] - today[c];
3379
+ });
3380
+ }
3381
+ for (let j = 0; j < 2; j++) {
3382
+ for (let r = n - 2; r >= 0; r--) {
3383
+ if (wr[r] < 1)
3384
+ blend[r * 2 + j] = Math.max(blend[r * 2 + j], blend[(r + 1) * 2 + j]);
3385
+ }
3386
+ }
3387
+ const cutRow = (r, j, x) => r > last ? 0 : along(cuts.subarray((r * 2 + j) * nc, (r * 2 + j + 1) * nc), x);
3388
+ return {
3389
+ lift: liftAt,
3390
+ // Between vertex rows, as the render interpolates; a vertex reads its own
3391
+ // row's alone.
3392
+ cut: (x, y, s) => {
3393
+ if (s === 0) return 0;
3394
+ const j = s < 0 ? 0 : 1;
3395
+ const { r, t } = gridRow(ys, y, true);
3396
+ return cutRow(r, j, x) * (1 - t) + cutRow(r + 1, j, x) * t;
3397
+ },
3398
+ blend: (y, s) => {
3399
+ if (s === 0) return 0;
3400
+ const j = s < 0 ? 0 : 1;
3401
+ const { r, t } = gridRow(ys, y, true);
3402
+ return blend[r * 2 + j] * (1 - t) + blend[(r + 1) * 2 + j] * t;
3403
+ }
3404
+ };
3222
3405
  }
3223
3406
  function crownRide(m, cap, relAt) {
3224
3407
  const f = m.frame;
@@ -3258,8 +3441,7 @@ function crownRide(m, cap, relAt) {
3258
3441
  return rel[at];
3259
3442
  };
3260
3443
  const cellR = (x, y, k) => {
3261
- let c = 0;
3262
- while (c < xs.length - 2 && xs[c + 1] < x) c++;
3444
+ const { c } = gridCol(xs, x);
3263
3445
  const { r } = gridRow(ys, y);
3264
3446
  return Math.max(
3265
3447
  relOf(r, c, k),
@@ -3349,8 +3531,7 @@ function crownRide(m, cap, relAt) {
3349
3531
  const out = wr.slice();
3350
3532
  for (let r = 0; r < n; r++) {
3351
3533
  if (wr[r] >= 1) continue;
3352
- const i0 = Math.max(0, Math.floor(top - ys[Math.max(0, r - 1)]));
3353
- const i1 = Math.min(count, Math.ceil(top - ys[Math.min(n - 1, r + 1)]));
3534
+ const [i0, i1] = rowWindow(ys, r, count);
3354
3535
  let least = Infinity;
3355
3536
  for (let i = i0; i < i1; i++) least = Math.min(least, slackAt(i, k));
3356
3537
  out[r] = wr[r] + clamp(least, 0, 1 - wr[r]);
@@ -4019,6 +4200,18 @@ function fitTurn(ctx, targets, style, build) {
4019
4200
  }
4020
4201
  p.shell = bisect((k) => at(k) - s, SHELL_RANGE[0], SHELL_RANGE[1]);
4021
4202
  }
4203
+ const covered = (shell) => farEarCovered(build({ ...p, shell }));
4204
+ if (!covered(p.shell) && covered(Math.max(1, p.shell))) {
4205
+ let lo = p.shell;
4206
+ let hi = Math.max(1, p.shell);
4207
+ for (let i = 0; i < 50; i++) {
4208
+ const mid = (lo + hi) / 2;
4209
+ if (covered(mid)) hi = mid;
4210
+ else lo = mid;
4211
+ }
4212
+ p.shell = hi;
4213
+ clamped.push("silhouetteRatio");
4214
+ }
4022
4215
  const hb = ctx.restHalf;
4023
4216
  const feature = (key, family, at, box) => {
4024
4217
  const base = build(p);
@@ -4177,6 +4370,7 @@ function buildTurn(frame, landmarks, q) {
4177
4370
  hairOuter: q.outline * face,
4178
4371
  hairBack: A * hh * TURN.hairBack,
4179
4372
  shellScale: q.shell,
4373
+ earFarScale: 1 + A * (TURN.earFarScale - 1),
4180
4374
  ...landmarks
4181
4375
  };
4182
4376
  }