@ikijs/editor 0.12.1 → 0.13.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.
package/dist/index.js CHANGED
@@ -1963,14 +1963,155 @@ function createDefaultWarpDeformer(model) {
1963
1963
  };
1964
1964
  }
1965
1965
 
1966
+ // src/auto-rig/profile.ts
1967
+ var PROFILE_VALUES = {
1968
+ slide: 0.12,
1969
+ lead: 0.1,
1970
+ noseDepth: 2,
1971
+ hairFollow: 1,
1972
+ fore: 0.2,
1973
+ backShare: 0.1,
1974
+ earFar: 0.5,
1975
+ nodDown: 0.18,
1976
+ nodUp: 0.1,
1977
+ nodLead: 0.06,
1978
+ chinShare: 0.75,
1979
+ rollDeg: 14,
1980
+ gaze: 0.2,
1981
+ blink: 0.6,
1982
+ mouthOpen: 0.8,
1983
+ mouthWiden: 1.2,
1984
+ brow: 0.15,
1985
+ breath: 0.03,
1986
+ sway: 0.08
1987
+ };
1988
+ function deriveProfile(v) {
1989
+ const farLead = v.lead * (1 - v.fore / 2);
1990
+ const nearLead = v.lead * (1 + v.fore / 2);
1991
+ const turn = {
1992
+ /** The face plate's translation. */
1993
+ face: v.slide,
1994
+ /** The chin tip's lead over the plate. */
1995
+ chinLead: v.lead,
1996
+ /** The plate's width at full turn: eye and cheek rows, then the jaw. The
1997
+ * plate translates; it does not narrow. */
1998
+ widthUpper: 1,
1999
+ widthJaw: 1,
2000
+ eyeFar: v.slide + farLead,
2001
+ eyeNear: v.slide + nearLead,
2002
+ /** Each eye's width at full turn, about its own centre. */
2003
+ eyeFarScale: 1 - v.fore,
2004
+ eyeNearScale: 1 + v.fore / 2,
2005
+ browFar: v.slide + farLead,
2006
+ browNear: v.slide + nearLead,
2007
+ nose: v.slide + v.noseDepth * v.lead,
2008
+ mouth: v.slide + v.lead,
2009
+ mouthWidth: 1,
2010
+ /** The mouth's far corner rises (its near end drops) by this much: it
2011
+ * stays level. */
2012
+ mouthTiltDeg: 0,
2013
+ /** The front hair (bangs and side locks) rides the face at this share of
2014
+ * its translation. */
2015
+ hairFollow: v.hairFollow,
2016
+ /** The head's outline at the eye row holds: where the front hair draws
2017
+ * it, its outer edge follows the face this much on a row where no back
2018
+ * hair paints behind that edge. Where back hair does, the edge rides
2019
+ * further, as far as that back hair stays behind it through the turn and
2020
+ * the nod, up to the face's follow there (`hairFollow`; over the far
2021
+ * eye's rows, as far as that eye's outer corner goes). On the cap (the
2022
+ * rows above the eyes) it follows less where the turn would otherwise
2023
+ * carry it further outside its back hair than the nod alone does. */
2024
+ outlineFollow: 0,
2025
+ /** The back hair's slight counter-motion. */
2026
+ hairBack: -v.backShare * v.slide,
2027
+ /** The ears lag the plate: the far one's outer edge moves this share of
2028
+ * its slide; the near one rides the head. */
2029
+ earFar: v.earFar,
2030
+ earNear: 1,
2031
+ /** The far ear's width at full turn, about its outer edge, where the head
2032
+ * leaves it room; less where it does not. */
2033
+ earFarScale: 1 - v.fore
2034
+ };
2035
+ const mid = v.nodDown * (1 + v.chinShare) / 2;
2036
+ const feature = (depth) => ({
2037
+ up: -(v.nodUp + depth * v.nodLead),
2038
+ down: mid + depth * v.nodLead
2039
+ });
2040
+ const nod = {
2041
+ faceTop: { up: -v.nodUp, down: v.nodDown },
2042
+ chin: { up: -v.nodUp, down: v.chinShare * v.nodDown },
2043
+ eye: feature(1),
2044
+ /** Eye height at the extreme, about the eye's centre: it holds. */
2045
+ eyeHeight: { up: 1, down: 1 },
2046
+ brow: feature(1),
2047
+ nose: feature(v.noseDepth),
2048
+ mouth: feature(1),
2049
+ hairFront: { up: -v.hairFollow * v.nodUp, down: v.hairFollow * v.nodDown },
2050
+ /** The front hair's cap top: half the plate's rise looking up; looking
2051
+ * down the plate's top, which the rig slides only as far as the back
2052
+ * hair covers the crown behind it. */
2053
+ hairFrontTop: { up: -v.nodUp / 2, down: v.nodDown },
2054
+ hairBack: { up: -v.backShare * v.nodUp, down: v.backShare * v.nodDown }
2055
+ };
2056
+ const amplitude = {
2057
+ /** Iris travel, in iris widths. */
2058
+ gazeX: v.gaze,
2059
+ gazeY: v.gaze / 2,
2060
+ /** How far the upper lid comes down, over the eye's height. */
2061
+ blink: v.blink,
2062
+ /** A single mouth drawing opens to this height over its rest width. */
2063
+ mouthOpenHeight: v.mouthOpen,
2064
+ /** The mouth widens this much open. */
2065
+ mouthOpenWidth: v.mouthWiden,
2066
+ /** Brow raise / lower, hh. */
2067
+ brow: v.brow,
2068
+ /** Breath lifts the shoulders and the head, hh. */
2069
+ breathBody: v.breath,
2070
+ breathHead: v.breath * 2 / 3,
2071
+ /** Hair tip travel at full sway (±20), hh. */
2072
+ sway: v.sway,
2073
+ /** The neck under the face reaches this far above the jaw, hh — what the
2074
+ * chin uncovers as it slides off it: half again the chin's full-turn
2075
+ * slide, to the next 0.05. */
2076
+ hiddenNeck: Math.ceil(30 * (v.slide + v.lead) - 1e-9) / 20
2077
+ };
2078
+ return { TURN: turn, NOD: nod, ROLL_DEG: v.rollDeg, AMPLITUDE: amplitude };
2079
+ }
2080
+ var { TURN, NOD, ROLL_DEG, AMPLITUDE } = deriveProfile(PROFILE_VALUES);
2081
+ var STYLE_KNOBS = [
2082
+ "turn",
2083
+ "featureLead",
2084
+ "hairFollow",
2085
+ "outlineFollow",
2086
+ "blink",
2087
+ "sway"
2088
+ ];
2089
+ function resolveStyle(style, check) {
2090
+ const s = style ?? {};
2091
+ const get = (key, def, lo, hi) => s[key] === void 0 ? def : check(`style.${key}`, s[key], lo, hi);
2092
+ return {
2093
+ turn: get("turn", 1, 0, 3),
2094
+ featureLead: get("featureLead", 1, 0, 3),
2095
+ hairFollow: get("hairFollow", TURN.hairFollow, 0, 2),
2096
+ outlineFollow: get("outlineFollow", TURN.outlineFollow, 0, 2),
2097
+ blink: get("blink", AMPLITUDE.blink, 0.1, 1),
2098
+ sway: get("sway", 1, 0, 5)
2099
+ };
2100
+ }
2101
+
1966
2102
  // src/auto-rig/types.ts
2103
+ var round2 = (v) => Math.round(v * 100) / 100;
1967
2104
  var TurnTargetError = class extends Error {
1968
2105
  constructor(message) {
1969
2106
  super(message);
1970
2107
  this.name = "TurnTargetError";
1971
2108
  }
1972
2109
  };
1973
- var DEFAULT_TURN_TARGETS = Object.freeze({ eyeShift: 0.22, farEyeRatio: 0.78, silhouetteRatio: 1 });
2110
+ var DEFAULT_TURN_TARGETS = Object.freeze({
2111
+ eyeShift: round2((TURN.eyeFar + TURN.eyeNear) / 2 - TURN.hairBack),
2112
+ farEyeRatio: round2(TURN.eyeFarScale / TURN.eyeNearScale),
2113
+ silhouetteRatio: 1
2114
+ });
1974
2115
 
1975
2116
  // src/auto-rig/roles.ts
1976
2117
  var ROLE_TABLE = [
@@ -2427,110 +2568,6 @@ function land(l, offsets, x, y) {
2427
2568
  return [topX + (botX - topX) * t, topY + (botY - topY) * t];
2428
2569
  }
2429
2570
 
2430
- // src/auto-rig/profile.ts
2431
- var HH_PER_EYE_TO_CHIN = 1.017;
2432
- var TURN = {
2433
- /** The face plate's translation (its cheek edges, 0.118 / 0.126). */
2434
- face: 0.118,
2435
- /** The chin tip's lead over the plate (it moves 0.194). */
2436
- chinLead: 0.076,
2437
- /** The plate's width at full turn: eye and cheek rows, then the jaw. */
2438
- widthUpper: 0.985,
2439
- widthJaw: 0.96,
2440
- eyeFar: 0.192,
2441
- eyeNear: 0.23,
2442
- /** Each eye's width at full turn, about its own centre. */
2443
- eyeFarScale: 0.85,
2444
- eyeNearScale: 1.085,
2445
- browFar: 0.213,
2446
- browNear: 0.243,
2447
- nose: 0.301,
2448
- mouth: 0.213,
2449
- mouthWidth: 0.975,
2450
- /** The mouth's far corner rises (its near end drops) by this much. */
2451
- mouthTiltDeg: 4.4,
2452
- /** The front hair (bangs and side locks) rides the face at this share of
2453
- * its translation (bangs 1.18×, side locks at the eye row ≈ 1.0×). */
2454
- hairFollow: 1.1,
2455
- /** The head's outline at the eye row holds (−0.014 / −0.004 hh): where the
2456
- * front hair draws it, its outer edge follows the face this much on a row
2457
- * where no back hair paints behind that edge. Where back hair does, the
2458
- * edge rides further, as far as that back hair stays behind it through
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). */
2461
- outlineFollow: 0,
2462
- /** The back hair's slight counter-motion. */
2463
- 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
- earFar: 0.44,
2468
- earNear: 0.87
2469
- };
2470
- var NOD = {
2471
- faceTop: { up: -0.081, down: 0.178 },
2472
- chin: { up: -0.0875, down: 0.109 },
2473
- eye: { up: -0.167, down: 0.202 },
2474
- /** Eye height at the extreme, about the eye's centre. */
2475
- eyeHeight: { up: 0.994, down: 0.96 },
2476
- brow: { up: -0.167, down: 0.223 },
2477
- nose: { up: -0.207, down: 0.193 },
2478
- mouth: { up: -0.172, down: 0.177 },
2479
- hairFront: { up: -0.11, down: 0.189 },
2480
- /** The front hair's cap top. The samples' cap top moves 1.215× their face's
2481
- * centroid looking down (Mao 1.236, Haru 1.626, Hiyori 1.194, Natori
2482
- * 0.730) and 0.415× looking up (0.385, 0.430, 0.400, 0.487); each ratio
2483
- * times the plate's own nod halfway between its top and the chin (0.1435
2484
- * down, 0.084 up). Looking down, the rig slides it only as far as the
2485
- * back hair covers the crown behind it. */
2486
- hairFrontTop: { up: -0.035, down: 0.174 },
2487
- hairBack: { up: -0.011, down: 0.025 }
2488
- };
2489
- var ROLL_DEG = 10;
2490
- var AMPLITUDE = {
2491
- /** Iris travel, in iris widths (X 0.045 hh, Y 0.031 hh on the samples). */
2492
- gazeX: 0.17,
2493
- gazeY: 0.11,
2494
- /** How far the upper lid comes down, over the eye's height (Haru 0.30,
2495
- * Mao 0.58, Hiyori 0.67). */
2496
- blink: 0.58,
2497
- /** A single mouth drawing opens to this height over its rest width. */
2498
- mouthOpenHeight: 0.77,
2499
- /** The mouth widens this much open. */
2500
- mouthOpenWidth: 1.15,
2501
- /** Brow raise / lower, hh. */
2502
- brow: 0.089,
2503
- /** Breath lifts the shoulders and the head, hh. */
2504
- breathBody: 0.025,
2505
- breathHead: 0.016,
2506
- /** Hair tip travel at full sway (±20), hh (the keyform ranges at ±1 have a
2507
- * median of 0.05–0.06 hh). */
2508
- sway: 0.06,
2509
- /** The neck under the face reaches this far above the jaw, hh — what the
2510
- * chin uncovers as it slides off it. */
2511
- hiddenNeck: 0.3
2512
- };
2513
- var STYLE_KNOBS = [
2514
- "turn",
2515
- "featureLead",
2516
- "hairFollow",
2517
- "outlineFollow",
2518
- "blink",
2519
- "sway"
2520
- ];
2521
- function resolveStyle(style, check) {
2522
- const s = style ?? {};
2523
- const get = (key, def, lo, hi) => s[key] === void 0 ? def : check(`style.${key}`, s[key], lo, hi);
2524
- return {
2525
- turn: get("turn", 1, 0, 3),
2526
- featureLead: get("featureLead", 1, 0, 3),
2527
- hairFollow: get("hairFollow", TURN.hairFollow, 0, 2),
2528
- outlineFollow: get("outlineFollow", TURN.outlineFollow, 0, 2),
2529
- blink: get("blink", AMPLITUDE.blink, 0.1, 1),
2530
- sway: get("sway", 1, 0, 5)
2531
- };
2532
- }
2533
-
2534
2571
  // src/auto-rig/head.ts
2535
2572
  function outerEdge(runs, axisX, side) {
2536
2573
  if (runs.length === 0) return void 0;
@@ -2743,7 +2780,7 @@ function buildHeadFrame(layers, opts) {
2743
2780
  return face.y1;
2744
2781
  };
2745
2782
  const chin = chinY;
2746
- const shade = JAW_SHADE * HH_PER_EYE_TO_CHIN * Math.max(1, eyeY - chin);
2783
+ const shade = JAW_SHADE * Math.max(1, eyeY - chin);
2747
2784
  const reach = opts.chinSlide === void 0 ? nk.waist : clamp(
2748
2785
  nk.waist - opts.chinSlide - CUT_MARGIN - (CUT_LOW + 1),
2749
2786
  0,
@@ -2792,7 +2829,7 @@ function buildHeadFrame(layers, opts) {
2792
2829
  }
2793
2830
  }
2794
2831
  }
2795
- const hh = HH_PER_EYE_TO_CHIN * Math.max(1, eyeY - chinY);
2832
+ const hh = Math.max(1, eyeY - chinY);
2796
2833
  const headHalf = opts.headHalfWidth;
2797
2834
  const hairShell = headHalf !== void 0;
2798
2835
  let shellLeft = axisX - plateHalf;
@@ -2886,16 +2923,45 @@ function earField(m) {
2886
2923
  const s = Math.sign(ax);
2887
2924
  const a = Math.abs(ax) / 30;
2888
2925
  const dy = face(x, y, 0, ay)[1];
2889
- if (sideOf(m, x, s) === "far") return [s * a * TURN.earFar * m.face, dy];
2890
2926
  const d = Math.abs(x - f.axisX);
2891
2927
  const attach = ears.attachAt(y);
2928
+ const span = Math.max(1, ears.outer - attach);
2929
+ if (sideOf(m, x, s) === "far") {
2930
+ const rim2 = TURN.earFar * m.face;
2931
+ const root2 = earRoot(m, face, y, s);
2932
+ const lead = Math.max(
2933
+ 0,
2934
+ Math.min((1 - m.earFarScale) * span, root2 - rim2)
2935
+ );
2936
+ let along = rim2 + lead * (ears.outer - d) / span;
2937
+ if (d < attach) along = Math.min(along, s * face(x, y, 30 * s, 0)[0]);
2938
+ return [s * a * along, dy];
2939
+ }
2892
2940
  if (d <= attach) return [face(x, y, ax, 0)[0], dy];
2893
2941
  const root = face(f.axisX + Math.sign(x - f.axisX) * attach, y, ax, 0)[0];
2894
2942
  const rim = s * a * TURN.earNear * m.face;
2895
- const t = Math.min(1, (d - attach) / Math.max(1, ears.outer - attach));
2943
+ const t = Math.min(1, (d - attach) / span);
2896
2944
  return [root + (rim - root) * t, dy];
2897
2945
  };
2898
2946
  }
2947
+ function earRoot(m, face, y, s) {
2948
+ const f = m.frame;
2949
+ return s * face(f.axisX + s * f.ears.attachAt(y), y, 30 * s, 0)[0];
2950
+ }
2951
+ function farEarCovered(m) {
2952
+ const ears = m.frame.ears;
2953
+ if (ears === void 0) return true;
2954
+ const face = faceField(m);
2955
+ const rim = TURN.earFar * m.face;
2956
+ const y0 = ears.bottom - 2;
2957
+ const y1 = ears.top + 2;
2958
+ const n = Math.max(1, Math.ceil(y1 - y0));
2959
+ for (let i = 0; i <= n; i++) {
2960
+ const y = y0 + (y1 - y0) * i / n;
2961
+ for (const s of [-1, 1]) if (earRoot(m, face, y, s) < rim) return false;
2962
+ }
2963
+ return true;
2964
+ }
2899
2965
  function chinSlide(m) {
2900
2966
  const f = m.frame;
2901
2967
  return Math.abs(faceField(m)(f.axisX, f.chinY, 30, 0)[0]);
@@ -3038,14 +3104,19 @@ function hairFrontField(m) {
3038
3104
  );
3039
3105
  return core + (m.hairOuter - core) * out;
3040
3106
  };
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));
3107
+ const ride = outerRide(m, inner, coreAt, followAt, eyeY + 1.5 * eyeH);
3108
+ const followOf = (x, y, s) => followAt(x, y, s) + (ride === void 0 ? 0 : ride.lift(x, y, s));
3043
3109
  const drift = Math.abs(m.hairBack);
3044
3110
  const crown = crownRide(m, cap, (x, y, s) => followOf(x, y, s) + drift);
3111
+ const boundOf = (x, y, s) => {
3112
+ if (ride === void 0) return followOf(x, y, s);
3113
+ const b = ride.blend(y, s);
3114
+ return (1 - b) * (followOf(x, y, s) + ride.cut(x, y, s)) - b * drift;
3115
+ };
3045
3116
  return (x, y, ax, ay) => {
3046
3117
  const s = Math.sign(ax);
3047
3118
  const a = Math.abs(ax) / 30;
3048
- const rx = s * a * followOf(x, y, s) + a * (m.shellScale - 1) * (x - f.axisX);
3119
+ const rx = s * a * boundOf(x, y, s) + a * (m.shellScale - 1) * (x - f.axisX);
3049
3120
  const ry = nodDy(NOD.hairFront, ay, f.hh);
3050
3121
  const bx = back(x, y, ax, ay)[0];
3051
3122
  const by = nodDy(cap, ay, f.hh);
@@ -3065,6 +3136,18 @@ function gridRow(ys, y, snap = false) {
3065
3136
  const r = Math.min(n - 2, Math.floor(v));
3066
3137
  return { r, t: v - r };
3067
3138
  }
3139
+ function gridCol(xs, x) {
3140
+ let c = 0;
3141
+ while (c < xs.length - 2 && xs[c + 1] < x) c++;
3142
+ return { c, t: clamp((x - xs[c]) / (xs[c + 1] - xs[c]), 0, 1) };
3143
+ }
3144
+ function rowWindow(ys, r, limit) {
3145
+ const n = ys.length;
3146
+ return [
3147
+ Math.max(0, Math.floor(ys[0] - ys[Math.max(0, r - 1)])),
3148
+ Math.min(limit, Math.ceil(ys[0] - ys[Math.min(n - 1, r + 1)]))
3149
+ ];
3150
+ }
3068
3151
  function renderedCrownBlend(m, ys) {
3069
3152
  const wr = ys.map((y) => crownBlend(m, y));
3070
3153
  return {
@@ -3140,10 +3223,11 @@ function paints(runs, px) {
3140
3223
  }
3141
3224
  return false;
3142
3225
  }
3143
- function outerRide(m, inner, coreAt, followAt) {
3226
+ function outerRide(m, inner, coreAt, followAt, capLimit) {
3144
3227
  const f = m.frame;
3145
- const ys = m.hairFrontGrid?.ys;
3146
- if (ys === void 0) return void 0;
3228
+ const grid = m.hairFrontGrid;
3229
+ if (grid === void 0) return void 0;
3230
+ const { xs, ys } = grid;
3147
3231
  const top = ys[0];
3148
3232
  if (f.hairRuns("hair_front", top) === void 0 || f.hairRuns("hair_back", top) === void 0) {
3149
3233
  return void 0;
@@ -3152,6 +3236,9 @@ function outerRide(m, inner, coreAt, followAt) {
3152
3236
  const sides = [-1, 1];
3153
3237
  const n = ys.length;
3154
3238
  const count = Math.round(top - ys[n - 1]);
3239
+ let last = 0;
3240
+ while (last + 1 < n && ys[last + 1] >= capLimit) last++;
3241
+ const capRows = last === 0 ? 0 : Math.min(count, Math.floor(top - ys[last] - 0.5) + 1);
3155
3242
  const below = (NOD.hairFront.down - NOD.hairBack.down) * f.hh;
3156
3243
  const above = (NOD.hairBack.up - NOD.hairFront.up) * f.hh;
3157
3244
  const j0 = Math.floor(-above);
@@ -3160,8 +3247,9 @@ function outerRide(m, inner, coreAt, followAt) {
3160
3247
  (_, j) => f.hairRuns("hair_back", top - (j + j0) - 0.5)
3161
3248
  );
3162
3249
  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));
3250
+ const reachOut = sides.map(() => new Float64Array(count));
3251
+ const reachIn = sides.map(() => new Float64Array(count));
3252
+ const bound = sides.map(() => new Uint8Array(count));
3165
3253
  for (let i = 0; i < count; i++) {
3166
3254
  const y = top - i - 0.5;
3167
3255
  const front = f.hairRuns("hair_front", y);
@@ -3176,24 +3264,25 @@ function outerRide(m, inner, coreAt, followAt) {
3176
3264
  inward = Math.min(inward, c.inward);
3177
3265
  }
3178
3266
  offset[k][i] = side * (edge - f.axisX);
3179
- far[k][i] = m.shellScale * outward - drift;
3180
- near[k][i] = m.shellScale * inward - drift;
3267
+ reachOut[k][i] = outward;
3268
+ reachIn[k][i] = inward;
3269
+ if (i < capRows && runReach(backRows[i - j0], edge, side).inward > 0)
3270
+ bound[k][i] = 1;
3181
3271
  });
3182
3272
  }
3183
3273
  const end = new Float64Array(n * 2);
3184
3274
  const lift = new Float64Array(n * 4);
3185
3275
  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)]));
3276
+ const [i0, i1] = rowWindow(ys, r, count);
3188
3277
  sides.forEach((side, k) => {
3189
3278
  let d = Infinity;
3190
- let allowFar = Infinity;
3191
- let allowNear = Infinity;
3279
+ let outward = Infinity;
3280
+ let inward = Infinity;
3192
3281
  for (let i = i0; i < i1; i++) {
3193
3282
  if (Number.isNaN(offset[k][i])) continue;
3194
3283
  d = Math.min(d, offset[k][i]);
3195
- allowFar = Math.min(allowFar, far[k][i]);
3196
- allowNear = Math.min(allowNear, near[k][i]);
3284
+ outward = Math.min(outward, reachOut[k][i]);
3285
+ inward = Math.min(inward, reachIn[k][i]);
3197
3286
  }
3198
3287
  end[r * 2 + k] = d;
3199
3288
  if (d === Infinity || d <= inner) return;
@@ -3201,7 +3290,7 @@ function outerRide(m, inner, coreAt, followAt) {
3201
3290
  sides.forEach((s, j) => {
3202
3291
  const allow = Math.min(
3203
3292
  coreAt(x, ys[r], s),
3204
- s === side ? allowFar : allowNear
3293
+ m.shellScale * (s === side ? outward : inward) - drift
3205
3294
  );
3206
3295
  lift[(r * 2 + k) * 2 + j] = Math.max(0, allow - followAt(x, ys[r], s));
3207
3296
  });
@@ -3211,7 +3300,7 @@ function outerRide(m, inner, coreAt, followAt) {
3211
3300
  const v = lift[(r * 2 + k) * 2 + j];
3212
3301
  return v === 0 ? 0 : v * Math.min(1, (d - inner) / (end[r * 2 + k] - inner));
3213
3302
  };
3214
- return (x, y, s) => {
3303
+ const liftAt = (x, y, s) => {
3215
3304
  const d = Math.abs(x - f.axisX);
3216
3305
  if (s === 0 || d <= inner) return 0;
3217
3306
  const k = x < f.axisX ? 0 : 1;
@@ -3219,6 +3308,130 @@ function outerRide(m, inner, coreAt, followAt) {
3219
3308
  const { r, t } = gridRow(ys, y, true);
3220
3309
  return rowRide(r, k, j, d) * (1 - t) + rowRide(r + 1, k, j, d) * t;
3221
3310
  };
3311
+ const nc = xs.length;
3312
+ const along = (v, x) => {
3313
+ const { c, t } = gridCol(xs, x);
3314
+ return v[c] + (v[c + 1] - v[c]) * t;
3315
+ };
3316
+ const readAt = (r, x, y) => {
3317
+ const { c, t: fx } = gridCol(xs, x);
3318
+ const dx = xs[c + 1] - xs[c];
3319
+ if (r + 1 < n && y <= ys[r] && y >= ys[r + 1]) {
3320
+ const fy = (ys[r] - y) / (ys[r] - ys[r + 1]);
3321
+ if (fy >= 1) return void 0;
3322
+ return fx <= fy ? xs[c] : xs[c] + (fx - fy) / (1 - fy) * dx;
3323
+ }
3324
+ if (r > 0 && y > ys[r] && y < ys[r - 1]) {
3325
+ const fy = (ys[r - 1] - y) / (ys[r - 1] - ys[r]);
3326
+ return fx <= fy ? xs[c] + fx / fy * dx : xs[c + 1];
3327
+ }
3328
+ return void 0;
3329
+ };
3330
+ const ahead = (e, s) => s === e.side ? e.outward : e.inward;
3331
+ const behind = (e, s) => s === e.side ? e.inward : e.outward;
3332
+ const capCut = (r, s, side, today, mine) => {
3333
+ const reach = Math.max(...mine.map((e) => side * (e.x - f.axisX)));
3334
+ const quarter = new Float64Array(nc);
3335
+ const cols = xs.map((_, c) => c).filter((c) => (xs[c] < f.axisX ? -1 : 1) === side);
3336
+ if (side < 0) cols.reverse();
3337
+ let total = 0;
3338
+ let x0 = f.axisX;
3339
+ let d0 = s * followAt(f.axisX, ys[r], s);
3340
+ let open = true;
3341
+ for (const c of cols) {
3342
+ if (open) {
3343
+ const d = (m.shellScale - 1) * (xs[c] - f.axisX) + s * today[c];
3344
+ const rest = side * (xs[c] - x0);
3345
+ const full = rest + side * (d - d0);
3346
+ total += Math.max(0, Math.min(full, rest)) / 4;
3347
+ x0 = xs[c];
3348
+ d0 = d;
3349
+ open = side * (xs[c] - f.axisX) < reach;
3350
+ }
3351
+ quarter[c] = total;
3352
+ }
3353
+ let scale = 0;
3354
+ for (const e of mine) {
3355
+ const need = along(today, e.at) - (m.shellScale * ahead(e, s) - drift);
3356
+ if (need <= 0) continue;
3357
+ const q = along(quarter, e.at);
3358
+ scale = Math.max(scale, q > 0 ? need / q : 1);
3359
+ }
3360
+ scale = Math.min(1, scale);
3361
+ return quarter.map((q) => scale * q);
3362
+ };
3363
+ const cuts = new Float64Array(n * 2 * nc);
3364
+ const blend = new Float64Array(n * 2);
3365
+ const wr = ys.map((y) => crownBlend(m, y));
3366
+ for (let r = 0; r <= last && capRows > 0; r++) {
3367
+ const w = wr[r];
3368
+ if (w <= 0) continue;
3369
+ const edges = [];
3370
+ const [i0, i1] = rowWindow(ys, r, capRows);
3371
+ for (let i = i0; i < i1; i++) {
3372
+ sides.forEach((side, k) => {
3373
+ if (bound[k][i] === 0) return;
3374
+ const x = f.axisX + side * offset[k][i];
3375
+ const at = readAt(r, x, top - i - 0.5);
3376
+ if (at === void 0) return;
3377
+ edges.push({
3378
+ side,
3379
+ x,
3380
+ at,
3381
+ outward: reachOut[k][i],
3382
+ inward: reachIn[k][i]
3383
+ });
3384
+ });
3385
+ }
3386
+ if (edges.length === 0) continue;
3387
+ sides.forEach((s, j) => {
3388
+ const today = xs.map((x) => followAt(x, ys[r], s) + liftAt(x, ys[r], s));
3389
+ const after = today.slice();
3390
+ if (w >= 1) {
3391
+ for (const side of sides) {
3392
+ const mine = edges.filter((e) => e.side === side);
3393
+ if (mine.length === 0) continue;
3394
+ const cut = capCut(r, s, side, today, mine);
3395
+ for (let c = 0; c < nc; c++) after[c] -= cut[c];
3396
+ }
3397
+ }
3398
+ let b = 0;
3399
+ for (const e of edges) {
3400
+ const rel = w * (along(after, e.at) + drift);
3401
+ const hi = m.shellScale * ahead(e, s);
3402
+ const lo = -m.shellScale * behind(e, s);
3403
+ if (rel > hi) b = Math.max(b, 1 - hi / rel);
3404
+ else if (rel < lo) b = Math.max(b, 1 - lo / rel);
3405
+ }
3406
+ blend[r * 2 + j] = b;
3407
+ for (let c = 0; c < nc; c++)
3408
+ cuts[(r * 2 + j) * nc + c] = after[c] - today[c];
3409
+ });
3410
+ }
3411
+ for (let j = 0; j < 2; j++) {
3412
+ for (let r = n - 2; r >= 0; r--) {
3413
+ if (wr[r] < 1)
3414
+ blend[r * 2 + j] = Math.max(blend[r * 2 + j], blend[(r + 1) * 2 + j]);
3415
+ }
3416
+ }
3417
+ const cutRow = (r, j, x) => r > last ? 0 : along(cuts.subarray((r * 2 + j) * nc, (r * 2 + j + 1) * nc), x);
3418
+ return {
3419
+ lift: liftAt,
3420
+ // Between vertex rows, as the render interpolates; a vertex reads its own
3421
+ // row's alone.
3422
+ cut: (x, y, s) => {
3423
+ if (s === 0) return 0;
3424
+ const j = s < 0 ? 0 : 1;
3425
+ const { r, t } = gridRow(ys, y, true);
3426
+ return cutRow(r, j, x) * (1 - t) + cutRow(r + 1, j, x) * t;
3427
+ },
3428
+ blend: (y, s) => {
3429
+ if (s === 0) return 0;
3430
+ const j = s < 0 ? 0 : 1;
3431
+ const { r, t } = gridRow(ys, y, true);
3432
+ return blend[r * 2 + j] * (1 - t) + blend[(r + 1) * 2 + j] * t;
3433
+ }
3434
+ };
3222
3435
  }
3223
3436
  function crownRide(m, cap, relAt) {
3224
3437
  const f = m.frame;
@@ -3258,8 +3471,7 @@ function crownRide(m, cap, relAt) {
3258
3471
  return rel[at];
3259
3472
  };
3260
3473
  const cellR = (x, y, k) => {
3261
- let c = 0;
3262
- while (c < xs.length - 2 && xs[c + 1] < x) c++;
3474
+ const { c } = gridCol(xs, x);
3263
3475
  const { r } = gridRow(ys, y);
3264
3476
  return Math.max(
3265
3477
  relOf(r, c, k),
@@ -3349,8 +3561,7 @@ function crownRide(m, cap, relAt) {
3349
3561
  const out = wr.slice();
3350
3562
  for (let r = 0; r < n; r++) {
3351
3563
  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)]));
3564
+ const [i0, i1] = rowWindow(ys, r, count);
3354
3565
  let least = Infinity;
3355
3566
  for (let i = i0; i < i1; i++) least = Math.min(least, slackAt(i, k));
3356
3567
  out[r] = wr[r] + clamp(least, 0, 1 - wr[r]);
@@ -4019,6 +4230,18 @@ function fitTurn(ctx, targets, style, build) {
4019
4230
  }
4020
4231
  p.shell = bisect((k) => at(k) - s, SHELL_RANGE[0], SHELL_RANGE[1]);
4021
4232
  }
4233
+ const covered = (shell) => farEarCovered(build({ ...p, shell }));
4234
+ if (!covered(p.shell) && covered(Math.max(1, p.shell))) {
4235
+ let lo = p.shell;
4236
+ let hi = Math.max(1, p.shell);
4237
+ for (let i = 0; i < 50; i++) {
4238
+ const mid = (lo + hi) / 2;
4239
+ if (covered(mid)) hi = mid;
4240
+ else lo = mid;
4241
+ }
4242
+ p.shell = hi;
4243
+ clamped.push("silhouetteRatio");
4244
+ }
4022
4245
  const hb = ctx.restHalf;
4023
4246
  const feature = (key, family, at, box) => {
4024
4247
  const base = build(p);
@@ -4177,6 +4400,7 @@ function buildTurn(frame, landmarks, q) {
4177
4400
  hairOuter: q.outline * face,
4178
4401
  hairBack: A * hh * TURN.hairBack,
4179
4402
  shellScale: q.shell,
4403
+ earFarScale: 1 + A * (TURN.earFarScale - 1),
4180
4404
  ...landmarks
4181
4405
  };
4182
4406
  }