@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/dist/index.mjs CHANGED
@@ -2388,15 +2388,22 @@ var TURN = {
2388
2388
  * where no back hair paints behind that edge. Where back hair does, the
2389
2389
  * edge rides further, as far as that back hair stays behind it through
2390
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). */
2391
+ * over the far eye's rows, as far as that eye's outer corner goes). On
2392
+ * the cap (the rows above the eyes) it follows less where the turn would
2393
+ * otherwise carry it further outside its back hair than the nod alone
2394
+ * does. */
2392
2395
  outlineFollow: 0,
2393
2396
  /** The back hair's slight counter-motion. */
2394
2397
  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
+ /** The ears lag the plate: the far one's outer edge moves this share of
2399
+ * its slide, the near one this at its widest reach, its root riding the
2400
+ * head (the samples' parallax ratios, 0.44 and 0.87). */
2398
2401
  earFar: 0.44,
2399
- earNear: 0.87
2402
+ earNear: 0.87,
2403
+ /** The far ear's width at full turn, about its outer edge, where the head
2404
+ * leaves it room (the samples' median: Haru 0.81 / 0.91, Hiyori 0.90 /
2405
+ * 0.85, Mao 0.77 / 0.79; none fades); less where it does not. */
2406
+ earFarScale: 0.83
2400
2407
  };
2401
2408
  var NOD = {
2402
2409
  faceTop: { up: -0.081, down: 0.178 },
@@ -2817,16 +2824,45 @@ function earField(m) {
2817
2824
  const s = Math.sign(ax);
2818
2825
  const a = Math.abs(ax) / 30;
2819
2826
  const dy = face(x, y, 0, ay)[1];
2820
- if (sideOf(m, x, s) === "far") return [s * a * TURN.earFar * m.face, dy];
2821
2827
  const d = Math.abs(x - f.axisX);
2822
2828
  const attach = ears.attachAt(y);
2829
+ const span = Math.max(1, ears.outer - attach);
2830
+ if (sideOf(m, x, s) === "far") {
2831
+ const rim2 = TURN.earFar * m.face;
2832
+ const root2 = earRoot(m, face, y, s);
2833
+ const lead = Math.max(
2834
+ 0,
2835
+ Math.min((1 - m.earFarScale) * span, root2 - rim2)
2836
+ );
2837
+ let along = rim2 + lead * (ears.outer - d) / span;
2838
+ if (d < attach) along = Math.min(along, s * face(x, y, 30 * s, 0)[0]);
2839
+ return [s * a * along, dy];
2840
+ }
2823
2841
  if (d <= attach) return [face(x, y, ax, 0)[0], dy];
2824
2842
  const root = face(f.axisX + Math.sign(x - f.axisX) * attach, y, ax, 0)[0];
2825
2843
  const rim = s * a * TURN.earNear * m.face;
2826
- const t = Math.min(1, (d - attach) / Math.max(1, ears.outer - attach));
2844
+ const t = Math.min(1, (d - attach) / span);
2827
2845
  return [root + (rim - root) * t, dy];
2828
2846
  };
2829
2847
  }
2848
+ function earRoot(m, face, y, s) {
2849
+ const f = m.frame;
2850
+ return s * face(f.axisX + s * f.ears.attachAt(y), y, 30 * s, 0)[0];
2851
+ }
2852
+ function farEarCovered(m) {
2853
+ const ears = m.frame.ears;
2854
+ if (ears === void 0) return true;
2855
+ const face = faceField(m);
2856
+ const rim = TURN.earFar * m.face;
2857
+ const y0 = ears.bottom - 2;
2858
+ const y1 = ears.top + 2;
2859
+ const n = Math.max(1, Math.ceil(y1 - y0));
2860
+ for (let i = 0; i <= n; i++) {
2861
+ const y = y0 + (y1 - y0) * i / n;
2862
+ for (const s of [-1, 1]) if (earRoot(m, face, y, s) < rim) return false;
2863
+ }
2864
+ return true;
2865
+ }
2830
2866
  function chinSlide(m) {
2831
2867
  const f = m.frame;
2832
2868
  return Math.abs(faceField(m)(f.axisX, f.chinY, 30, 0)[0]);
@@ -2969,14 +3005,19 @@ function hairFrontField(m) {
2969
3005
  );
2970
3006
  return core + (m.hairOuter - core) * out;
2971
3007
  };
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));
3008
+ const ride = outerRide(m, inner, coreAt, followAt, eyeY + 1.5 * eyeH);
3009
+ const followOf = (x, y, s) => followAt(x, y, s) + (ride === void 0 ? 0 : ride.lift(x, y, s));
2974
3010
  const drift = Math.abs(m.hairBack);
2975
3011
  const crown = crownRide(m, cap, (x, y, s) => followOf(x, y, s) + drift);
3012
+ const boundOf = (x, y, s) => {
3013
+ if (ride === void 0) return followOf(x, y, s);
3014
+ const b = ride.blend(y, s);
3015
+ return (1 - b) * (followOf(x, y, s) + ride.cut(x, y, s)) - b * drift;
3016
+ };
2976
3017
  return (x, y, ax, ay) => {
2977
3018
  const s = Math.sign(ax);
2978
3019
  const a = Math.abs(ax) / 30;
2979
- const rx = s * a * followOf(x, y, s) + a * (m.shellScale - 1) * (x - f.axisX);
3020
+ const rx = s * a * boundOf(x, y, s) + a * (m.shellScale - 1) * (x - f.axisX);
2980
3021
  const ry = nodDy(NOD.hairFront, ay, f.hh);
2981
3022
  const bx = back(x, y, ax, ay)[0];
2982
3023
  const by = nodDy(cap, ay, f.hh);
@@ -2996,6 +3037,18 @@ function gridRow(ys, y, snap = false) {
2996
3037
  const r = Math.min(n - 2, Math.floor(v));
2997
3038
  return { r, t: v - r };
2998
3039
  }
3040
+ function gridCol(xs, x) {
3041
+ let c = 0;
3042
+ while (c < xs.length - 2 && xs[c + 1] < x) c++;
3043
+ return { c, t: clamp((x - xs[c]) / (xs[c + 1] - xs[c]), 0, 1) };
3044
+ }
3045
+ function rowWindow(ys, r, limit) {
3046
+ const n = ys.length;
3047
+ return [
3048
+ Math.max(0, Math.floor(ys[0] - ys[Math.max(0, r - 1)])),
3049
+ Math.min(limit, Math.ceil(ys[0] - ys[Math.min(n - 1, r + 1)]))
3050
+ ];
3051
+ }
2999
3052
  function renderedCrownBlend(m, ys) {
3000
3053
  const wr = ys.map((y) => crownBlend(m, y));
3001
3054
  return {
@@ -3071,10 +3124,11 @@ function paints(runs, px) {
3071
3124
  }
3072
3125
  return false;
3073
3126
  }
3074
- function outerRide(m, inner, coreAt, followAt) {
3127
+ function outerRide(m, inner, coreAt, followAt, capLimit) {
3075
3128
  const f = m.frame;
3076
- const ys = m.hairFrontGrid?.ys;
3077
- if (ys === void 0) return void 0;
3129
+ const grid = m.hairFrontGrid;
3130
+ if (grid === void 0) return void 0;
3131
+ const { xs, ys } = grid;
3078
3132
  const top = ys[0];
3079
3133
  if (f.hairRuns("hair_front", top) === void 0 || f.hairRuns("hair_back", top) === void 0) {
3080
3134
  return void 0;
@@ -3083,6 +3137,9 @@ function outerRide(m, inner, coreAt, followAt) {
3083
3137
  const sides = [-1, 1];
3084
3138
  const n = ys.length;
3085
3139
  const count = Math.round(top - ys[n - 1]);
3140
+ let last = 0;
3141
+ while (last + 1 < n && ys[last + 1] >= capLimit) last++;
3142
+ const capRows = last === 0 ? 0 : Math.min(count, Math.floor(top - ys[last] - 0.5) + 1);
3086
3143
  const below = (NOD.hairFront.down - NOD.hairBack.down) * f.hh;
3087
3144
  const above = (NOD.hairBack.up - NOD.hairFront.up) * f.hh;
3088
3145
  const j0 = Math.floor(-above);
@@ -3091,8 +3148,9 @@ function outerRide(m, inner, coreAt, followAt) {
3091
3148
  (_, j) => f.hairRuns("hair_back", top - (j + j0) - 0.5)
3092
3149
  );
3093
3150
  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));
3151
+ const reachOut = sides.map(() => new Float64Array(count));
3152
+ const reachIn = sides.map(() => new Float64Array(count));
3153
+ const bound = sides.map(() => new Uint8Array(count));
3096
3154
  for (let i = 0; i < count; i++) {
3097
3155
  const y = top - i - 0.5;
3098
3156
  const front = f.hairRuns("hair_front", y);
@@ -3107,24 +3165,25 @@ function outerRide(m, inner, coreAt, followAt) {
3107
3165
  inward = Math.min(inward, c.inward);
3108
3166
  }
3109
3167
  offset[k][i] = side * (edge - f.axisX);
3110
- far[k][i] = m.shellScale * outward - drift;
3111
- near[k][i] = m.shellScale * inward - drift;
3168
+ reachOut[k][i] = outward;
3169
+ reachIn[k][i] = inward;
3170
+ if (i < capRows && runReach(backRows[i - j0], edge, side).inward > 0)
3171
+ bound[k][i] = 1;
3112
3172
  });
3113
3173
  }
3114
3174
  const end = new Float64Array(n * 2);
3115
3175
  const lift = new Float64Array(n * 4);
3116
3176
  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)]));
3177
+ const [i0, i1] = rowWindow(ys, r, count);
3119
3178
  sides.forEach((side, k) => {
3120
3179
  let d = Infinity;
3121
- let allowFar = Infinity;
3122
- let allowNear = Infinity;
3180
+ let outward = Infinity;
3181
+ let inward = Infinity;
3123
3182
  for (let i = i0; i < i1; i++) {
3124
3183
  if (Number.isNaN(offset[k][i])) continue;
3125
3184
  d = Math.min(d, offset[k][i]);
3126
- allowFar = Math.min(allowFar, far[k][i]);
3127
- allowNear = Math.min(allowNear, near[k][i]);
3185
+ outward = Math.min(outward, reachOut[k][i]);
3186
+ inward = Math.min(inward, reachIn[k][i]);
3128
3187
  }
3129
3188
  end[r * 2 + k] = d;
3130
3189
  if (d === Infinity || d <= inner) return;
@@ -3132,7 +3191,7 @@ function outerRide(m, inner, coreAt, followAt) {
3132
3191
  sides.forEach((s, j) => {
3133
3192
  const allow = Math.min(
3134
3193
  coreAt(x, ys[r], s),
3135
- s === side ? allowFar : allowNear
3194
+ m.shellScale * (s === side ? outward : inward) - drift
3136
3195
  );
3137
3196
  lift[(r * 2 + k) * 2 + j] = Math.max(0, allow - followAt(x, ys[r], s));
3138
3197
  });
@@ -3142,7 +3201,7 @@ function outerRide(m, inner, coreAt, followAt) {
3142
3201
  const v = lift[(r * 2 + k) * 2 + j];
3143
3202
  return v === 0 ? 0 : v * Math.min(1, (d - inner) / (end[r * 2 + k] - inner));
3144
3203
  };
3145
- return (x, y, s) => {
3204
+ const liftAt = (x, y, s) => {
3146
3205
  const d = Math.abs(x - f.axisX);
3147
3206
  if (s === 0 || d <= inner) return 0;
3148
3207
  const k = x < f.axisX ? 0 : 1;
@@ -3150,6 +3209,130 @@ function outerRide(m, inner, coreAt, followAt) {
3150
3209
  const { r, t } = gridRow(ys, y, true);
3151
3210
  return rowRide(r, k, j, d) * (1 - t) + rowRide(r + 1, k, j, d) * t;
3152
3211
  };
3212
+ const nc = xs.length;
3213
+ const along = (v, x) => {
3214
+ const { c, t } = gridCol(xs, x);
3215
+ return v[c] + (v[c + 1] - v[c]) * t;
3216
+ };
3217
+ const readAt = (r, x, y) => {
3218
+ const { c, t: fx } = gridCol(xs, x);
3219
+ const dx = xs[c + 1] - xs[c];
3220
+ if (r + 1 < n && y <= ys[r] && y >= ys[r + 1]) {
3221
+ const fy = (ys[r] - y) / (ys[r] - ys[r + 1]);
3222
+ if (fy >= 1) return void 0;
3223
+ return fx <= fy ? xs[c] : xs[c] + (fx - fy) / (1 - fy) * dx;
3224
+ }
3225
+ if (r > 0 && y > ys[r] && y < ys[r - 1]) {
3226
+ const fy = (ys[r - 1] - y) / (ys[r - 1] - ys[r]);
3227
+ return fx <= fy ? xs[c] + fx / fy * dx : xs[c + 1];
3228
+ }
3229
+ return void 0;
3230
+ };
3231
+ const ahead = (e, s) => s === e.side ? e.outward : e.inward;
3232
+ const behind = (e, s) => s === e.side ? e.inward : e.outward;
3233
+ const capCut = (r, s, side, today, mine) => {
3234
+ const reach = Math.max(...mine.map((e) => side * (e.x - f.axisX)));
3235
+ const quarter = new Float64Array(nc);
3236
+ const cols = xs.map((_, c) => c).filter((c) => (xs[c] < f.axisX ? -1 : 1) === side);
3237
+ if (side < 0) cols.reverse();
3238
+ let total = 0;
3239
+ let x0 = f.axisX;
3240
+ let d0 = s * followAt(f.axisX, ys[r], s);
3241
+ let open = true;
3242
+ for (const c of cols) {
3243
+ if (open) {
3244
+ const d = (m.shellScale - 1) * (xs[c] - f.axisX) + s * today[c];
3245
+ const rest = side * (xs[c] - x0);
3246
+ const full = rest + side * (d - d0);
3247
+ total += Math.max(0, Math.min(full, rest)) / 4;
3248
+ x0 = xs[c];
3249
+ d0 = d;
3250
+ open = side * (xs[c] - f.axisX) < reach;
3251
+ }
3252
+ quarter[c] = total;
3253
+ }
3254
+ let scale = 0;
3255
+ for (const e of mine) {
3256
+ const need = along(today, e.at) - (m.shellScale * ahead(e, s) - drift);
3257
+ if (need <= 0) continue;
3258
+ const q = along(quarter, e.at);
3259
+ scale = Math.max(scale, q > 0 ? need / q : 1);
3260
+ }
3261
+ scale = Math.min(1, scale);
3262
+ return quarter.map((q) => scale * q);
3263
+ };
3264
+ const cuts = new Float64Array(n * 2 * nc);
3265
+ const blend = new Float64Array(n * 2);
3266
+ const wr = ys.map((y) => crownBlend(m, y));
3267
+ for (let r = 0; r <= last && capRows > 0; r++) {
3268
+ const w = wr[r];
3269
+ if (w <= 0) continue;
3270
+ const edges = [];
3271
+ const [i0, i1] = rowWindow(ys, r, capRows);
3272
+ for (let i = i0; i < i1; i++) {
3273
+ sides.forEach((side, k) => {
3274
+ if (bound[k][i] === 0) return;
3275
+ const x = f.axisX + side * offset[k][i];
3276
+ const at = readAt(r, x, top - i - 0.5);
3277
+ if (at === void 0) return;
3278
+ edges.push({
3279
+ side,
3280
+ x,
3281
+ at,
3282
+ outward: reachOut[k][i],
3283
+ inward: reachIn[k][i]
3284
+ });
3285
+ });
3286
+ }
3287
+ if (edges.length === 0) continue;
3288
+ sides.forEach((s, j) => {
3289
+ const today = xs.map((x) => followAt(x, ys[r], s) + liftAt(x, ys[r], s));
3290
+ const after = today.slice();
3291
+ if (w >= 1) {
3292
+ for (const side of sides) {
3293
+ const mine = edges.filter((e) => e.side === side);
3294
+ if (mine.length === 0) continue;
3295
+ const cut = capCut(r, s, side, today, mine);
3296
+ for (let c = 0; c < nc; c++) after[c] -= cut[c];
3297
+ }
3298
+ }
3299
+ let b = 0;
3300
+ for (const e of edges) {
3301
+ const rel = w * (along(after, e.at) + drift);
3302
+ const hi = m.shellScale * ahead(e, s);
3303
+ const lo = -m.shellScale * behind(e, s);
3304
+ if (rel > hi) b = Math.max(b, 1 - hi / rel);
3305
+ else if (rel < lo) b = Math.max(b, 1 - lo / rel);
3306
+ }
3307
+ blend[r * 2 + j] = b;
3308
+ for (let c = 0; c < nc; c++)
3309
+ cuts[(r * 2 + j) * nc + c] = after[c] - today[c];
3310
+ });
3311
+ }
3312
+ for (let j = 0; j < 2; j++) {
3313
+ for (let r = n - 2; r >= 0; r--) {
3314
+ if (wr[r] < 1)
3315
+ blend[r * 2 + j] = Math.max(blend[r * 2 + j], blend[(r + 1) * 2 + j]);
3316
+ }
3317
+ }
3318
+ const cutRow = (r, j, x) => r > last ? 0 : along(cuts.subarray((r * 2 + j) * nc, (r * 2 + j + 1) * nc), x);
3319
+ return {
3320
+ lift: liftAt,
3321
+ // Between vertex rows, as the render interpolates; a vertex reads its own
3322
+ // row's alone.
3323
+ cut: (x, y, s) => {
3324
+ if (s === 0) return 0;
3325
+ const j = s < 0 ? 0 : 1;
3326
+ const { r, t } = gridRow(ys, y, true);
3327
+ return cutRow(r, j, x) * (1 - t) + cutRow(r + 1, j, x) * t;
3328
+ },
3329
+ blend: (y, s) => {
3330
+ if (s === 0) return 0;
3331
+ const j = s < 0 ? 0 : 1;
3332
+ const { r, t } = gridRow(ys, y, true);
3333
+ return blend[r * 2 + j] * (1 - t) + blend[(r + 1) * 2 + j] * t;
3334
+ }
3335
+ };
3153
3336
  }
3154
3337
  function crownRide(m, cap, relAt) {
3155
3338
  const f = m.frame;
@@ -3189,8 +3372,7 @@ function crownRide(m, cap, relAt) {
3189
3372
  return rel[at];
3190
3373
  };
3191
3374
  const cellR = (x, y, k) => {
3192
- let c = 0;
3193
- while (c < xs.length - 2 && xs[c + 1] < x) c++;
3375
+ const { c } = gridCol(xs, x);
3194
3376
  const { r } = gridRow(ys, y);
3195
3377
  return Math.max(
3196
3378
  relOf(r, c, k),
@@ -3280,8 +3462,7 @@ function crownRide(m, cap, relAt) {
3280
3462
  const out = wr.slice();
3281
3463
  for (let r = 0; r < n; r++) {
3282
3464
  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)]));
3465
+ const [i0, i1] = rowWindow(ys, r, count);
3285
3466
  let least = Infinity;
3286
3467
  for (let i = i0; i < i1; i++) least = Math.min(least, slackAt(i, k));
3287
3468
  out[r] = wr[r] + clamp(least, 0, 1 - wr[r]);
@@ -3950,6 +4131,18 @@ function fitTurn(ctx, targets, style, build) {
3950
4131
  }
3951
4132
  p.shell = bisect((k) => at(k) - s, SHELL_RANGE[0], SHELL_RANGE[1]);
3952
4133
  }
4134
+ const covered = (shell) => farEarCovered(build({ ...p, shell }));
4135
+ if (!covered(p.shell) && covered(Math.max(1, p.shell))) {
4136
+ let lo = p.shell;
4137
+ let hi = Math.max(1, p.shell);
4138
+ for (let i = 0; i < 50; i++) {
4139
+ const mid = (lo + hi) / 2;
4140
+ if (covered(mid)) hi = mid;
4141
+ else lo = mid;
4142
+ }
4143
+ p.shell = hi;
4144
+ clamped.push("silhouetteRatio");
4145
+ }
3953
4146
  const hb = ctx.restHalf;
3954
4147
  const feature = (key, family, at, box) => {
3955
4148
  const base = build(p);
@@ -4108,6 +4301,7 @@ function buildTurn(frame, landmarks, q) {
4108
4301
  hairOuter: q.outline * face,
4109
4302
  hairBack: A * hh * TURN.hairBack,
4110
4303
  shellScale: q.shell,
4304
+ earFarScale: 1 + A * (TURN.earFarScale - 1),
4111
4305
  ...landmarks
4112
4306
  };
4113
4307
  }