@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.mjs CHANGED
@@ -1890,14 +1890,155 @@ function createDefaultWarpDeformer(model) {
1890
1890
  };
1891
1891
  }
1892
1892
 
1893
+ // src/auto-rig/profile.ts
1894
+ var PROFILE_VALUES = {
1895
+ slide: 0.12,
1896
+ lead: 0.1,
1897
+ noseDepth: 2,
1898
+ hairFollow: 1,
1899
+ fore: 0.2,
1900
+ backShare: 0.1,
1901
+ earFar: 0.5,
1902
+ nodDown: 0.18,
1903
+ nodUp: 0.1,
1904
+ nodLead: 0.06,
1905
+ chinShare: 0.75,
1906
+ rollDeg: 14,
1907
+ gaze: 0.2,
1908
+ blink: 0.6,
1909
+ mouthOpen: 0.8,
1910
+ mouthWiden: 1.2,
1911
+ brow: 0.15,
1912
+ breath: 0.03,
1913
+ sway: 0.08
1914
+ };
1915
+ function deriveProfile(v) {
1916
+ const farLead = v.lead * (1 - v.fore / 2);
1917
+ const nearLead = v.lead * (1 + v.fore / 2);
1918
+ const turn = {
1919
+ /** The face plate's translation. */
1920
+ face: v.slide,
1921
+ /** The chin tip's lead over the plate. */
1922
+ chinLead: v.lead,
1923
+ /** The plate's width at full turn: eye and cheek rows, then the jaw. The
1924
+ * plate translates; it does not narrow. */
1925
+ widthUpper: 1,
1926
+ widthJaw: 1,
1927
+ eyeFar: v.slide + farLead,
1928
+ eyeNear: v.slide + nearLead,
1929
+ /** Each eye's width at full turn, about its own centre. */
1930
+ eyeFarScale: 1 - v.fore,
1931
+ eyeNearScale: 1 + v.fore / 2,
1932
+ browFar: v.slide + farLead,
1933
+ browNear: v.slide + nearLead,
1934
+ nose: v.slide + v.noseDepth * v.lead,
1935
+ mouth: v.slide + v.lead,
1936
+ mouthWidth: 1,
1937
+ /** The mouth's far corner rises (its near end drops) by this much: it
1938
+ * stays level. */
1939
+ mouthTiltDeg: 0,
1940
+ /** The front hair (bangs and side locks) rides the face at this share of
1941
+ * its translation. */
1942
+ hairFollow: v.hairFollow,
1943
+ /** The head's outline at the eye row holds: where the front hair draws
1944
+ * it, its outer edge follows the face this much on a row where no back
1945
+ * hair paints behind that edge. Where back hair does, the edge rides
1946
+ * further, as far as that back hair stays behind it through the turn and
1947
+ * the nod, up to the face's follow there (`hairFollow`; over the far
1948
+ * eye's rows, as far as that eye's outer corner goes). On the cap (the
1949
+ * rows above the eyes) it follows less where the turn would otherwise
1950
+ * carry it further outside its back hair than the nod alone does. */
1951
+ outlineFollow: 0,
1952
+ /** The back hair's slight counter-motion. */
1953
+ hairBack: -v.backShare * v.slide,
1954
+ /** The ears lag the plate: the far one's outer edge moves this share of
1955
+ * its slide; the near one rides the head. */
1956
+ earFar: v.earFar,
1957
+ earNear: 1,
1958
+ /** The far ear's width at full turn, about its outer edge, where the head
1959
+ * leaves it room; less where it does not. */
1960
+ earFarScale: 1 - v.fore
1961
+ };
1962
+ const mid = v.nodDown * (1 + v.chinShare) / 2;
1963
+ const feature = (depth) => ({
1964
+ up: -(v.nodUp + depth * v.nodLead),
1965
+ down: mid + depth * v.nodLead
1966
+ });
1967
+ const nod = {
1968
+ faceTop: { up: -v.nodUp, down: v.nodDown },
1969
+ chin: { up: -v.nodUp, down: v.chinShare * v.nodDown },
1970
+ eye: feature(1),
1971
+ /** Eye height at the extreme, about the eye's centre: it holds. */
1972
+ eyeHeight: { up: 1, down: 1 },
1973
+ brow: feature(1),
1974
+ nose: feature(v.noseDepth),
1975
+ mouth: feature(1),
1976
+ hairFront: { up: -v.hairFollow * v.nodUp, down: v.hairFollow * v.nodDown },
1977
+ /** The front hair's cap top: half the plate's rise looking up; looking
1978
+ * down the plate's top, which the rig slides only as far as the back
1979
+ * hair covers the crown behind it. */
1980
+ hairFrontTop: { up: -v.nodUp / 2, down: v.nodDown },
1981
+ hairBack: { up: -v.backShare * v.nodUp, down: v.backShare * v.nodDown }
1982
+ };
1983
+ const amplitude = {
1984
+ /** Iris travel, in iris widths. */
1985
+ gazeX: v.gaze,
1986
+ gazeY: v.gaze / 2,
1987
+ /** How far the upper lid comes down, over the eye's height. */
1988
+ blink: v.blink,
1989
+ /** A single mouth drawing opens to this height over its rest width. */
1990
+ mouthOpenHeight: v.mouthOpen,
1991
+ /** The mouth widens this much open. */
1992
+ mouthOpenWidth: v.mouthWiden,
1993
+ /** Brow raise / lower, hh. */
1994
+ brow: v.brow,
1995
+ /** Breath lifts the shoulders and the head, hh. */
1996
+ breathBody: v.breath,
1997
+ breathHead: v.breath * 2 / 3,
1998
+ /** Hair tip travel at full sway (±20), hh. */
1999
+ sway: v.sway,
2000
+ /** The neck under the face reaches this far above the jaw, hh — what the
2001
+ * chin uncovers as it slides off it: half again the chin's full-turn
2002
+ * slide, to the next 0.05. */
2003
+ hiddenNeck: Math.ceil(30 * (v.slide + v.lead) - 1e-9) / 20
2004
+ };
2005
+ return { TURN: turn, NOD: nod, ROLL_DEG: v.rollDeg, AMPLITUDE: amplitude };
2006
+ }
2007
+ var { TURN, NOD, ROLL_DEG, AMPLITUDE } = deriveProfile(PROFILE_VALUES);
2008
+ var STYLE_KNOBS = [
2009
+ "turn",
2010
+ "featureLead",
2011
+ "hairFollow",
2012
+ "outlineFollow",
2013
+ "blink",
2014
+ "sway"
2015
+ ];
2016
+ function resolveStyle(style, check) {
2017
+ const s = style ?? {};
2018
+ const get = (key, def, lo, hi) => s[key] === void 0 ? def : check(`style.${key}`, s[key], lo, hi);
2019
+ return {
2020
+ turn: get("turn", 1, 0, 3),
2021
+ featureLead: get("featureLead", 1, 0, 3),
2022
+ hairFollow: get("hairFollow", TURN.hairFollow, 0, 2),
2023
+ outlineFollow: get("outlineFollow", TURN.outlineFollow, 0, 2),
2024
+ blink: get("blink", AMPLITUDE.blink, 0.1, 1),
2025
+ sway: get("sway", 1, 0, 5)
2026
+ };
2027
+ }
2028
+
1893
2029
  // src/auto-rig/types.ts
2030
+ var round2 = (v) => Math.round(v * 100) / 100;
1894
2031
  var TurnTargetError = class extends Error {
1895
2032
  constructor(message) {
1896
2033
  super(message);
1897
2034
  this.name = "TurnTargetError";
1898
2035
  }
1899
2036
  };
1900
- var DEFAULT_TURN_TARGETS = Object.freeze({ eyeShift: 0.22, farEyeRatio: 0.78, silhouetteRatio: 1 });
2037
+ var DEFAULT_TURN_TARGETS = Object.freeze({
2038
+ eyeShift: round2((TURN.eyeFar + TURN.eyeNear) / 2 - TURN.hairBack),
2039
+ farEyeRatio: round2(TURN.eyeFarScale / TURN.eyeNearScale),
2040
+ silhouetteRatio: 1
2041
+ });
1901
2042
 
1902
2043
  // src/auto-rig/roles.ts
1903
2044
  var ROLE_TABLE = [
@@ -2358,110 +2499,6 @@ function land(l, offsets, x, y) {
2358
2499
  return [topX + (botX - topX) * t, topY + (botY - topY) * t];
2359
2500
  }
2360
2501
 
2361
- // src/auto-rig/profile.ts
2362
- var HH_PER_EYE_TO_CHIN = 1.017;
2363
- var TURN = {
2364
- /** The face plate's translation (its cheek edges, 0.118 / 0.126). */
2365
- face: 0.118,
2366
- /** The chin tip's lead over the plate (it moves 0.194). */
2367
- chinLead: 0.076,
2368
- /** The plate's width at full turn: eye and cheek rows, then the jaw. */
2369
- widthUpper: 0.985,
2370
- widthJaw: 0.96,
2371
- eyeFar: 0.192,
2372
- eyeNear: 0.23,
2373
- /** Each eye's width at full turn, about its own centre. */
2374
- eyeFarScale: 0.85,
2375
- eyeNearScale: 1.085,
2376
- browFar: 0.213,
2377
- browNear: 0.243,
2378
- nose: 0.301,
2379
- mouth: 0.213,
2380
- mouthWidth: 0.975,
2381
- /** The mouth's far corner rises (its near end drops) by this much. */
2382
- mouthTiltDeg: 4.4,
2383
- /** The front hair (bangs and side locks) rides the face at this share of
2384
- * its translation (bangs 1.18×, side locks at the eye row ≈ 1.0×). */
2385
- hairFollow: 1.1,
2386
- /** The head's outline at the eye row holds (−0.014 / −0.004 hh): where the
2387
- * front hair draws it, its outer edge follows the face this much on a row
2388
- * where no back hair paints behind that edge. Where back hair does, the
2389
- * edge rides further, as far as that back hair stays behind it through
2390
- * the turn and the nod, up to the face's follow there (`hairFollow`;
2391
- * over the far eye's rows, as far as that eye's outer corner goes). */
2392
- outlineFollow: 0,
2393
- /** The back hair's slight counter-motion. */
2394
- hairBack: -0.027,
2395
- /** The ears lag the plate: the far one moves this share of its slide, the
2396
- * near one this at its widest reach, its root riding the head (the
2397
- * samples' parallax ratios, 0.44 and 0.87). */
2398
- earFar: 0.44,
2399
- earNear: 0.87
2400
- };
2401
- var NOD = {
2402
- faceTop: { up: -0.081, down: 0.178 },
2403
- chin: { up: -0.0875, down: 0.109 },
2404
- eye: { up: -0.167, down: 0.202 },
2405
- /** Eye height at the extreme, about the eye's centre. */
2406
- eyeHeight: { up: 0.994, down: 0.96 },
2407
- brow: { up: -0.167, down: 0.223 },
2408
- nose: { up: -0.207, down: 0.193 },
2409
- mouth: { up: -0.172, down: 0.177 },
2410
- hairFront: { up: -0.11, down: 0.189 },
2411
- /** The front hair's cap top. The samples' cap top moves 1.215× their face's
2412
- * centroid looking down (Mao 1.236, Haru 1.626, Hiyori 1.194, Natori
2413
- * 0.730) and 0.415× looking up (0.385, 0.430, 0.400, 0.487); each ratio
2414
- * times the plate's own nod halfway between its top and the chin (0.1435
2415
- * down, 0.084 up). Looking down, the rig slides it only as far as the
2416
- * back hair covers the crown behind it. */
2417
- hairFrontTop: { up: -0.035, down: 0.174 },
2418
- hairBack: { up: -0.011, down: 0.025 }
2419
- };
2420
- var ROLL_DEG = 10;
2421
- var AMPLITUDE = {
2422
- /** Iris travel, in iris widths (X 0.045 hh, Y 0.031 hh on the samples). */
2423
- gazeX: 0.17,
2424
- gazeY: 0.11,
2425
- /** How far the upper lid comes down, over the eye's height (Haru 0.30,
2426
- * Mao 0.58, Hiyori 0.67). */
2427
- blink: 0.58,
2428
- /** A single mouth drawing opens to this height over its rest width. */
2429
- mouthOpenHeight: 0.77,
2430
- /** The mouth widens this much open. */
2431
- mouthOpenWidth: 1.15,
2432
- /** Brow raise / lower, hh. */
2433
- brow: 0.089,
2434
- /** Breath lifts the shoulders and the head, hh. */
2435
- breathBody: 0.025,
2436
- breathHead: 0.016,
2437
- /** Hair tip travel at full sway (±20), hh (the keyform ranges at ±1 have a
2438
- * median of 0.05–0.06 hh). */
2439
- sway: 0.06,
2440
- /** The neck under the face reaches this far above the jaw, hh — what the
2441
- * chin uncovers as it slides off it. */
2442
- hiddenNeck: 0.3
2443
- };
2444
- var STYLE_KNOBS = [
2445
- "turn",
2446
- "featureLead",
2447
- "hairFollow",
2448
- "outlineFollow",
2449
- "blink",
2450
- "sway"
2451
- ];
2452
- function resolveStyle(style, check) {
2453
- const s = style ?? {};
2454
- const get = (key, def, lo, hi) => s[key] === void 0 ? def : check(`style.${key}`, s[key], lo, hi);
2455
- return {
2456
- turn: get("turn", 1, 0, 3),
2457
- featureLead: get("featureLead", 1, 0, 3),
2458
- hairFollow: get("hairFollow", TURN.hairFollow, 0, 2),
2459
- outlineFollow: get("outlineFollow", TURN.outlineFollow, 0, 2),
2460
- blink: get("blink", AMPLITUDE.blink, 0.1, 1),
2461
- sway: get("sway", 1, 0, 5)
2462
- };
2463
- }
2464
-
2465
2502
  // src/auto-rig/head.ts
2466
2503
  function outerEdge(runs, axisX, side) {
2467
2504
  if (runs.length === 0) return void 0;
@@ -2674,7 +2711,7 @@ function buildHeadFrame(layers, opts) {
2674
2711
  return face.y1;
2675
2712
  };
2676
2713
  const chin = chinY;
2677
- const shade = JAW_SHADE * HH_PER_EYE_TO_CHIN * Math.max(1, eyeY - chin);
2714
+ const shade = JAW_SHADE * Math.max(1, eyeY - chin);
2678
2715
  const reach = opts.chinSlide === void 0 ? nk.waist : clamp(
2679
2716
  nk.waist - opts.chinSlide - CUT_MARGIN - (CUT_LOW + 1),
2680
2717
  0,
@@ -2723,7 +2760,7 @@ function buildHeadFrame(layers, opts) {
2723
2760
  }
2724
2761
  }
2725
2762
  }
2726
- const hh = HH_PER_EYE_TO_CHIN * Math.max(1, eyeY - chinY);
2763
+ const hh = Math.max(1, eyeY - chinY);
2727
2764
  const headHalf = opts.headHalfWidth;
2728
2765
  const hairShell = headHalf !== void 0;
2729
2766
  let shellLeft = axisX - plateHalf;
@@ -2817,16 +2854,45 @@ function earField(m) {
2817
2854
  const s = Math.sign(ax);
2818
2855
  const a = Math.abs(ax) / 30;
2819
2856
  const dy = face(x, y, 0, ay)[1];
2820
- if (sideOf(m, x, s) === "far") return [s * a * TURN.earFar * m.face, dy];
2821
2857
  const d = Math.abs(x - f.axisX);
2822
2858
  const attach = ears.attachAt(y);
2859
+ const span = Math.max(1, ears.outer - attach);
2860
+ if (sideOf(m, x, s) === "far") {
2861
+ const rim2 = TURN.earFar * m.face;
2862
+ const root2 = earRoot(m, face, y, s);
2863
+ const lead = Math.max(
2864
+ 0,
2865
+ Math.min((1 - m.earFarScale) * span, root2 - rim2)
2866
+ );
2867
+ let along = rim2 + lead * (ears.outer - d) / span;
2868
+ if (d < attach) along = Math.min(along, s * face(x, y, 30 * s, 0)[0]);
2869
+ return [s * a * along, dy];
2870
+ }
2823
2871
  if (d <= attach) return [face(x, y, ax, 0)[0], dy];
2824
2872
  const root = face(f.axisX + Math.sign(x - f.axisX) * attach, y, ax, 0)[0];
2825
2873
  const rim = s * a * TURN.earNear * m.face;
2826
- const t = Math.min(1, (d - attach) / Math.max(1, ears.outer - attach));
2874
+ const t = Math.min(1, (d - attach) / span);
2827
2875
  return [root + (rim - root) * t, dy];
2828
2876
  };
2829
2877
  }
2878
+ function earRoot(m, face, y, s) {
2879
+ const f = m.frame;
2880
+ return s * face(f.axisX + s * f.ears.attachAt(y), y, 30 * s, 0)[0];
2881
+ }
2882
+ function farEarCovered(m) {
2883
+ const ears = m.frame.ears;
2884
+ if (ears === void 0) return true;
2885
+ const face = faceField(m);
2886
+ const rim = TURN.earFar * m.face;
2887
+ const y0 = ears.bottom - 2;
2888
+ const y1 = ears.top + 2;
2889
+ const n = Math.max(1, Math.ceil(y1 - y0));
2890
+ for (let i = 0; i <= n; i++) {
2891
+ const y = y0 + (y1 - y0) * i / n;
2892
+ for (const s of [-1, 1]) if (earRoot(m, face, y, s) < rim) return false;
2893
+ }
2894
+ return true;
2895
+ }
2830
2896
  function chinSlide(m) {
2831
2897
  const f = m.frame;
2832
2898
  return Math.abs(faceField(m)(f.axisX, f.chinY, 30, 0)[0]);
@@ -2969,14 +3035,19 @@ function hairFrontField(m) {
2969
3035
  );
2970
3036
  return core + (m.hairOuter - core) * out;
2971
3037
  };
2972
- const ride = outerRide(m, inner, coreAt, followAt);
2973
- const followOf = (x, y, s) => followAt(x, y, s) + (ride === void 0 ? 0 : ride(x, y, s));
3038
+ const ride = outerRide(m, inner, coreAt, followAt, eyeY + 1.5 * eyeH);
3039
+ const followOf = (x, y, s) => followAt(x, y, s) + (ride === void 0 ? 0 : ride.lift(x, y, s));
2974
3040
  const drift = Math.abs(m.hairBack);
2975
3041
  const crown = crownRide(m, cap, (x, y, s) => followOf(x, y, s) + drift);
3042
+ const boundOf = (x, y, s) => {
3043
+ if (ride === void 0) return followOf(x, y, s);
3044
+ const b = ride.blend(y, s);
3045
+ return (1 - b) * (followOf(x, y, s) + ride.cut(x, y, s)) - b * drift;
3046
+ };
2976
3047
  return (x, y, ax, ay) => {
2977
3048
  const s = Math.sign(ax);
2978
3049
  const a = Math.abs(ax) / 30;
2979
- const rx = s * a * followOf(x, y, s) + a * (m.shellScale - 1) * (x - f.axisX);
3050
+ const rx = s * a * boundOf(x, y, s) + a * (m.shellScale - 1) * (x - f.axisX);
2980
3051
  const ry = nodDy(NOD.hairFront, ay, f.hh);
2981
3052
  const bx = back(x, y, ax, ay)[0];
2982
3053
  const by = nodDy(cap, ay, f.hh);
@@ -2996,6 +3067,18 @@ function gridRow(ys, y, snap = false) {
2996
3067
  const r = Math.min(n - 2, Math.floor(v));
2997
3068
  return { r, t: v - r };
2998
3069
  }
3070
+ function gridCol(xs, x) {
3071
+ let c = 0;
3072
+ while (c < xs.length - 2 && xs[c + 1] < x) c++;
3073
+ return { c, t: clamp((x - xs[c]) / (xs[c + 1] - xs[c]), 0, 1) };
3074
+ }
3075
+ function rowWindow(ys, r, limit) {
3076
+ const n = ys.length;
3077
+ return [
3078
+ Math.max(0, Math.floor(ys[0] - ys[Math.max(0, r - 1)])),
3079
+ Math.min(limit, Math.ceil(ys[0] - ys[Math.min(n - 1, r + 1)]))
3080
+ ];
3081
+ }
2999
3082
  function renderedCrownBlend(m, ys) {
3000
3083
  const wr = ys.map((y) => crownBlend(m, y));
3001
3084
  return {
@@ -3071,10 +3154,11 @@ function paints(runs, px) {
3071
3154
  }
3072
3155
  return false;
3073
3156
  }
3074
- function outerRide(m, inner, coreAt, followAt) {
3157
+ function outerRide(m, inner, coreAt, followAt, capLimit) {
3075
3158
  const f = m.frame;
3076
- const ys = m.hairFrontGrid?.ys;
3077
- if (ys === void 0) return void 0;
3159
+ const grid = m.hairFrontGrid;
3160
+ if (grid === void 0) return void 0;
3161
+ const { xs, ys } = grid;
3078
3162
  const top = ys[0];
3079
3163
  if (f.hairRuns("hair_front", top) === void 0 || f.hairRuns("hair_back", top) === void 0) {
3080
3164
  return void 0;
@@ -3083,6 +3167,9 @@ function outerRide(m, inner, coreAt, followAt) {
3083
3167
  const sides = [-1, 1];
3084
3168
  const n = ys.length;
3085
3169
  const count = Math.round(top - ys[n - 1]);
3170
+ let last = 0;
3171
+ while (last + 1 < n && ys[last + 1] >= capLimit) last++;
3172
+ const capRows = last === 0 ? 0 : Math.min(count, Math.floor(top - ys[last] - 0.5) + 1);
3086
3173
  const below = (NOD.hairFront.down - NOD.hairBack.down) * f.hh;
3087
3174
  const above = (NOD.hairBack.up - NOD.hairFront.up) * f.hh;
3088
3175
  const j0 = Math.floor(-above);
@@ -3091,8 +3178,9 @@ function outerRide(m, inner, coreAt, followAt) {
3091
3178
  (_, j) => f.hairRuns("hair_back", top - (j + j0) - 0.5)
3092
3179
  );
3093
3180
  const offset = sides.map(() => new Float64Array(count).fill(NaN));
3094
- const far = sides.map(() => new Float64Array(count));
3095
- const near = sides.map(() => new Float64Array(count));
3181
+ const reachOut = sides.map(() => new Float64Array(count));
3182
+ const reachIn = sides.map(() => new Float64Array(count));
3183
+ const bound = sides.map(() => new Uint8Array(count));
3096
3184
  for (let i = 0; i < count; i++) {
3097
3185
  const y = top - i - 0.5;
3098
3186
  const front = f.hairRuns("hair_front", y);
@@ -3107,24 +3195,25 @@ function outerRide(m, inner, coreAt, followAt) {
3107
3195
  inward = Math.min(inward, c.inward);
3108
3196
  }
3109
3197
  offset[k][i] = side * (edge - f.axisX);
3110
- far[k][i] = m.shellScale * outward - drift;
3111
- near[k][i] = m.shellScale * inward - drift;
3198
+ reachOut[k][i] = outward;
3199
+ reachIn[k][i] = inward;
3200
+ if (i < capRows && runReach(backRows[i - j0], edge, side).inward > 0)
3201
+ bound[k][i] = 1;
3112
3202
  });
3113
3203
  }
3114
3204
  const end = new Float64Array(n * 2);
3115
3205
  const lift = new Float64Array(n * 4);
3116
3206
  for (let r = 0; r < n; r++) {
3117
- const i0 = Math.max(0, Math.floor(top - ys[Math.max(0, r - 1)]));
3118
- const i1 = Math.min(count, Math.ceil(top - ys[Math.min(n - 1, r + 1)]));
3207
+ const [i0, i1] = rowWindow(ys, r, count);
3119
3208
  sides.forEach((side, k) => {
3120
3209
  let d = Infinity;
3121
- let allowFar = Infinity;
3122
- let allowNear = Infinity;
3210
+ let outward = Infinity;
3211
+ let inward = Infinity;
3123
3212
  for (let i = i0; i < i1; i++) {
3124
3213
  if (Number.isNaN(offset[k][i])) continue;
3125
3214
  d = Math.min(d, offset[k][i]);
3126
- allowFar = Math.min(allowFar, far[k][i]);
3127
- allowNear = Math.min(allowNear, near[k][i]);
3215
+ outward = Math.min(outward, reachOut[k][i]);
3216
+ inward = Math.min(inward, reachIn[k][i]);
3128
3217
  }
3129
3218
  end[r * 2 + k] = d;
3130
3219
  if (d === Infinity || d <= inner) return;
@@ -3132,7 +3221,7 @@ function outerRide(m, inner, coreAt, followAt) {
3132
3221
  sides.forEach((s, j) => {
3133
3222
  const allow = Math.min(
3134
3223
  coreAt(x, ys[r], s),
3135
- s === side ? allowFar : allowNear
3224
+ m.shellScale * (s === side ? outward : inward) - drift
3136
3225
  );
3137
3226
  lift[(r * 2 + k) * 2 + j] = Math.max(0, allow - followAt(x, ys[r], s));
3138
3227
  });
@@ -3142,7 +3231,7 @@ function outerRide(m, inner, coreAt, followAt) {
3142
3231
  const v = lift[(r * 2 + k) * 2 + j];
3143
3232
  return v === 0 ? 0 : v * Math.min(1, (d - inner) / (end[r * 2 + k] - inner));
3144
3233
  };
3145
- return (x, y, s) => {
3234
+ const liftAt = (x, y, s) => {
3146
3235
  const d = Math.abs(x - f.axisX);
3147
3236
  if (s === 0 || d <= inner) return 0;
3148
3237
  const k = x < f.axisX ? 0 : 1;
@@ -3150,6 +3239,130 @@ function outerRide(m, inner, coreAt, followAt) {
3150
3239
  const { r, t } = gridRow(ys, y, true);
3151
3240
  return rowRide(r, k, j, d) * (1 - t) + rowRide(r + 1, k, j, d) * t;
3152
3241
  };
3242
+ const nc = xs.length;
3243
+ const along = (v, x) => {
3244
+ const { c, t } = gridCol(xs, x);
3245
+ return v[c] + (v[c + 1] - v[c]) * t;
3246
+ };
3247
+ const readAt = (r, x, y) => {
3248
+ const { c, t: fx } = gridCol(xs, x);
3249
+ const dx = xs[c + 1] - xs[c];
3250
+ if (r + 1 < n && y <= ys[r] && y >= ys[r + 1]) {
3251
+ const fy = (ys[r] - y) / (ys[r] - ys[r + 1]);
3252
+ if (fy >= 1) return void 0;
3253
+ return fx <= fy ? xs[c] : xs[c] + (fx - fy) / (1 - fy) * dx;
3254
+ }
3255
+ if (r > 0 && y > ys[r] && y < ys[r - 1]) {
3256
+ const fy = (ys[r - 1] - y) / (ys[r - 1] - ys[r]);
3257
+ return fx <= fy ? xs[c] + fx / fy * dx : xs[c + 1];
3258
+ }
3259
+ return void 0;
3260
+ };
3261
+ const ahead = (e, s) => s === e.side ? e.outward : e.inward;
3262
+ const behind = (e, s) => s === e.side ? e.inward : e.outward;
3263
+ const capCut = (r, s, side, today, mine) => {
3264
+ const reach = Math.max(...mine.map((e) => side * (e.x - f.axisX)));
3265
+ const quarter = new Float64Array(nc);
3266
+ const cols = xs.map((_, c) => c).filter((c) => (xs[c] < f.axisX ? -1 : 1) === side);
3267
+ if (side < 0) cols.reverse();
3268
+ let total = 0;
3269
+ let x0 = f.axisX;
3270
+ let d0 = s * followAt(f.axisX, ys[r], s);
3271
+ let open = true;
3272
+ for (const c of cols) {
3273
+ if (open) {
3274
+ const d = (m.shellScale - 1) * (xs[c] - f.axisX) + s * today[c];
3275
+ const rest = side * (xs[c] - x0);
3276
+ const full = rest + side * (d - d0);
3277
+ total += Math.max(0, Math.min(full, rest)) / 4;
3278
+ x0 = xs[c];
3279
+ d0 = d;
3280
+ open = side * (xs[c] - f.axisX) < reach;
3281
+ }
3282
+ quarter[c] = total;
3283
+ }
3284
+ let scale = 0;
3285
+ for (const e of mine) {
3286
+ const need = along(today, e.at) - (m.shellScale * ahead(e, s) - drift);
3287
+ if (need <= 0) continue;
3288
+ const q = along(quarter, e.at);
3289
+ scale = Math.max(scale, q > 0 ? need / q : 1);
3290
+ }
3291
+ scale = Math.min(1, scale);
3292
+ return quarter.map((q) => scale * q);
3293
+ };
3294
+ const cuts = new Float64Array(n * 2 * nc);
3295
+ const blend = new Float64Array(n * 2);
3296
+ const wr = ys.map((y) => crownBlend(m, y));
3297
+ for (let r = 0; r <= last && capRows > 0; r++) {
3298
+ const w = wr[r];
3299
+ if (w <= 0) continue;
3300
+ const edges = [];
3301
+ const [i0, i1] = rowWindow(ys, r, capRows);
3302
+ for (let i = i0; i < i1; i++) {
3303
+ sides.forEach((side, k) => {
3304
+ if (bound[k][i] === 0) return;
3305
+ const x = f.axisX + side * offset[k][i];
3306
+ const at = readAt(r, x, top - i - 0.5);
3307
+ if (at === void 0) return;
3308
+ edges.push({
3309
+ side,
3310
+ x,
3311
+ at,
3312
+ outward: reachOut[k][i],
3313
+ inward: reachIn[k][i]
3314
+ });
3315
+ });
3316
+ }
3317
+ if (edges.length === 0) continue;
3318
+ sides.forEach((s, j) => {
3319
+ const today = xs.map((x) => followAt(x, ys[r], s) + liftAt(x, ys[r], s));
3320
+ const after = today.slice();
3321
+ if (w >= 1) {
3322
+ for (const side of sides) {
3323
+ const mine = edges.filter((e) => e.side === side);
3324
+ if (mine.length === 0) continue;
3325
+ const cut = capCut(r, s, side, today, mine);
3326
+ for (let c = 0; c < nc; c++) after[c] -= cut[c];
3327
+ }
3328
+ }
3329
+ let b = 0;
3330
+ for (const e of edges) {
3331
+ const rel = w * (along(after, e.at) + drift);
3332
+ const hi = m.shellScale * ahead(e, s);
3333
+ const lo = -m.shellScale * behind(e, s);
3334
+ if (rel > hi) b = Math.max(b, 1 - hi / rel);
3335
+ else if (rel < lo) b = Math.max(b, 1 - lo / rel);
3336
+ }
3337
+ blend[r * 2 + j] = b;
3338
+ for (let c = 0; c < nc; c++)
3339
+ cuts[(r * 2 + j) * nc + c] = after[c] - today[c];
3340
+ });
3341
+ }
3342
+ for (let j = 0; j < 2; j++) {
3343
+ for (let r = n - 2; r >= 0; r--) {
3344
+ if (wr[r] < 1)
3345
+ blend[r * 2 + j] = Math.max(blend[r * 2 + j], blend[(r + 1) * 2 + j]);
3346
+ }
3347
+ }
3348
+ const cutRow = (r, j, x) => r > last ? 0 : along(cuts.subarray((r * 2 + j) * nc, (r * 2 + j + 1) * nc), x);
3349
+ return {
3350
+ lift: liftAt,
3351
+ // Between vertex rows, as the render interpolates; a vertex reads its own
3352
+ // row's alone.
3353
+ cut: (x, y, s) => {
3354
+ if (s === 0) return 0;
3355
+ const j = s < 0 ? 0 : 1;
3356
+ const { r, t } = gridRow(ys, y, true);
3357
+ return cutRow(r, j, x) * (1 - t) + cutRow(r + 1, j, x) * t;
3358
+ },
3359
+ blend: (y, s) => {
3360
+ if (s === 0) return 0;
3361
+ const j = s < 0 ? 0 : 1;
3362
+ const { r, t } = gridRow(ys, y, true);
3363
+ return blend[r * 2 + j] * (1 - t) + blend[(r + 1) * 2 + j] * t;
3364
+ }
3365
+ };
3153
3366
  }
3154
3367
  function crownRide(m, cap, relAt) {
3155
3368
  const f = m.frame;
@@ -3189,8 +3402,7 @@ function crownRide(m, cap, relAt) {
3189
3402
  return rel[at];
3190
3403
  };
3191
3404
  const cellR = (x, y, k) => {
3192
- let c = 0;
3193
- while (c < xs.length - 2 && xs[c + 1] < x) c++;
3405
+ const { c } = gridCol(xs, x);
3194
3406
  const { r } = gridRow(ys, y);
3195
3407
  return Math.max(
3196
3408
  relOf(r, c, k),
@@ -3280,8 +3492,7 @@ function crownRide(m, cap, relAt) {
3280
3492
  const out = wr.slice();
3281
3493
  for (let r = 0; r < n; r++) {
3282
3494
  if (wr[r] >= 1) continue;
3283
- const i0 = Math.max(0, Math.floor(top - ys[Math.max(0, r - 1)]));
3284
- const i1 = Math.min(count, Math.ceil(top - ys[Math.min(n - 1, r + 1)]));
3495
+ const [i0, i1] = rowWindow(ys, r, count);
3285
3496
  let least = Infinity;
3286
3497
  for (let i = i0; i < i1; i++) least = Math.min(least, slackAt(i, k));
3287
3498
  out[r] = wr[r] + clamp(least, 0, 1 - wr[r]);
@@ -3950,6 +4161,18 @@ function fitTurn(ctx, targets, style, build) {
3950
4161
  }
3951
4162
  p.shell = bisect((k) => at(k) - s, SHELL_RANGE[0], SHELL_RANGE[1]);
3952
4163
  }
4164
+ const covered = (shell) => farEarCovered(build({ ...p, shell }));
4165
+ if (!covered(p.shell) && covered(Math.max(1, p.shell))) {
4166
+ let lo = p.shell;
4167
+ let hi = Math.max(1, p.shell);
4168
+ for (let i = 0; i < 50; i++) {
4169
+ const mid = (lo + hi) / 2;
4170
+ if (covered(mid)) hi = mid;
4171
+ else lo = mid;
4172
+ }
4173
+ p.shell = hi;
4174
+ clamped.push("silhouetteRatio");
4175
+ }
3953
4176
  const hb = ctx.restHalf;
3954
4177
  const feature = (key, family, at, box) => {
3955
4178
  const base = build(p);
@@ -4108,6 +4331,7 @@ function buildTurn(frame, landmarks, q) {
4108
4331
  hairOuter: q.outline * face,
4109
4332
  hairBack: A * hh * TURN.hairBack,
4110
4333
  shellScale: q.shell,
4334
+ earFarScale: 1 + A * (TURN.earFarScale - 1),
4111
4335
  ...landmarks
4112
4336
  };
4113
4337
  }