@ikijs/editor 0.11.0 → 0.12.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/README.md CHANGED
@@ -135,10 +135,10 @@ rather than its soft feather, and its top edge at its centre x is the bridge top
135
135
  the nose tilts 6° about on the turn, its tip toward the far side; absent, the
136
136
  crop stands in for both. And it takes the optional `rowRuns` (per crop row, the
137
137
  row's alpha ≥ 128 runs as a flat list of canvas columns `[start, end, …]`, each
138
- end exclusive), measured for `hair_front` and `hair_back` so the turn and the
139
- nod can tell where back hair is painted behind the front hair; absent, the front
140
- hair keeps its outline hold on the turn and its crown's ease to the back hair's
141
- nod looking down.
138
+ end exclusive), measured for `face`, `hair_front` and `hair_back` so the turn
139
+ and the nod can tell where back hair is painted behind the front hair; absent,
140
+ the front hair keeps its outline hold and its crown's ease to the back hair on
141
+ the turn, and that ease looking down on the nod.
142
142
 
143
143
  `createLayerSetMeasurer(canvas)` measures all of it — `rowHalfWidths`,
144
144
  `rowRuns`, `denseCore`, `headHalfWidth`, `headEdges` and `strandEdges` — off
package/dist/index.d.mts CHANGED
@@ -710,11 +710,12 @@ interface LayerInput {
710
710
  cropH: number;
711
711
  /** Face only: half the opaque span of each crop row, canvas px (0 = none). */
712
712
  rowHalfWidths?: number[];
713
- /** `hair_front` / `hair_back` only: per crop row, its opaque (alpha ≥ 128)
713
+ /** `face` / `hair_front` / `hair_back` only: per crop row, its opaque (alpha ≥ 128)
714
714
  * runs as a flat list of canvas columns `[start0, end0, start1, end1, …]`,
715
715
  * each end exclusive (`[]` for a row with none). The actual pixels, so a
716
716
  * gap in the hair is a gap here: the turn and the nod read off it where
717
- * back hair is painted behind the front hair. */
717
+ * back hair is painted behind the front hair, and the crown's turn reads the
718
+ * face's so it never bares skin. */
718
719
  rowRuns?: number[][];
719
720
  /** Nose only: the tight box of its alpha ≥ 128 pixels, canvas px. */
720
721
  denseCore?: {
@@ -1000,7 +1001,7 @@ interface LayerSetMeasurer {
1000
1001
  * it: only the set's opaque union, this layer's per-row opaque extent and
1001
1002
  * the `LayerInput` it returns outlive the call, so the host may drop `rgba`
1002
1003
  * as soon as it returns. Returns the layer's `LayerInput` — its crop
1003
- * box, the face's `rowHalfWidths`, the hair layers' `rowRuns`, the nose's
1004
+ * box, the face's `rowHalfWidths`, the face's and hair layers' `rowRuns`, the nose's
1004
1005
  * `denseCore` unless that is a speck of its crop (`isSpeckCore`) — or
1005
1006
  * `null` for an empty layer, one with no pixel at or above
1006
1007
  * `ALPHA_BBOX_THRESHOLD`, which it records nothing for: the host reports
package/dist/index.d.ts CHANGED
@@ -710,11 +710,12 @@ interface LayerInput {
710
710
  cropH: number;
711
711
  /** Face only: half the opaque span of each crop row, canvas px (0 = none). */
712
712
  rowHalfWidths?: number[];
713
- /** `hair_front` / `hair_back` only: per crop row, its opaque (alpha ≥ 128)
713
+ /** `face` / `hair_front` / `hair_back` only: per crop row, its opaque (alpha ≥ 128)
714
714
  * runs as a flat list of canvas columns `[start0, end0, start1, end1, …]`,
715
715
  * each end exclusive (`[]` for a row with none). The actual pixels, so a
716
716
  * gap in the hair is a gap here: the turn and the nod read off it where
717
- * back hair is painted behind the front hair. */
717
+ * back hair is painted behind the front hair, and the crown's turn reads the
718
+ * face's so it never bares skin. */
718
719
  rowRuns?: number[][];
719
720
  /** Nose only: the tight box of its alpha ≥ 128 pixels, canvas px. */
720
721
  denseCore?: {
@@ -1000,7 +1001,7 @@ interface LayerSetMeasurer {
1000
1001
  * it: only the set's opaque union, this layer's per-row opaque extent and
1001
1002
  * the `LayerInput` it returns outlive the call, so the host may drop `rgba`
1002
1003
  * as soon as it returns. Returns the layer's `LayerInput` — its crop
1003
- * box, the face's `rowHalfWidths`, the hair layers' `rowRuns`, the nose's
1004
+ * box, the face's `rowHalfWidths`, the face's and hair layers' `rowRuns`, the nose's
1004
1005
  * `denseCore` unless that is a speck of its crop (`isSpeckCore`) — or
1005
1006
  * `null` for an empty layer, one with no pixel at or above
1006
1007
  * `ALPHA_BBOX_THRESHOLD`, which it records nothing for: the host reports
package/dist/index.js CHANGED
@@ -1642,7 +1642,7 @@ function createLayerSetMeasurer(canvas) {
1642
1642
  if (bbox === null) return null;
1643
1643
  const rowLeft = new Int32Array(canvasH).fill(canvasW);
1644
1644
  const rowRight = new Int32Array(canvasH).fill(-1);
1645
- const rowRuns = role === "hair_front" || role === "hair_back" ? [] : void 0;
1645
+ const rowRuns = role === "face" || role === "hair_front" || role === "hair_back" ? [] : void 0;
1646
1646
  for (let y = 0; y < canvasH; y++) {
1647
1647
  let runs;
1648
1648
  if (rowRuns !== void 0 && y >= bbox.y && y < bbox.y + bbox.h) {
@@ -2806,12 +2806,14 @@ function buildHeadFrame(layers, opts) {
2806
2806
  shellRight = axisX + headHalf;
2807
2807
  }
2808
2808
  }
2809
- const hairRuns = (role, y) => {
2809
+ const runsOf = (role, y) => {
2810
2810
  const l = byRole.get(role);
2811
2811
  if (l?.rowRuns === void 0) return void 0;
2812
2812
  const runs = l.rowRuns[Math.floor(canvasH / 2 - y) - l.bbox.y];
2813
2813
  return runs === void 0 ? [] : runs.map((c) => c - canvasW / 2);
2814
2814
  };
2815
+ const hairRuns = (role, y) => runsOf(role, y);
2816
+ const faceRuns = (y) => runsOf("face", y);
2815
2817
  return {
2816
2818
  canvasW,
2817
2819
  canvasH,
@@ -2834,7 +2836,8 @@ function buildHeadFrame(layers, opts) {
2834
2836
  shellRight,
2835
2837
  hairShell,
2836
2838
  holdBase: headHalf ?? plateHalf,
2837
- hairRuns
2839
+ hairRuns,
2840
+ faceRuns
2838
2841
  };
2839
2842
  }
2840
2843
 
@@ -3036,22 +3039,42 @@ function hairFrontField(m) {
3036
3039
  return core + (m.hairOuter - core) * out;
3037
3040
  };
3038
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));
3043
+ const drift = Math.abs(m.hairBack);
3044
+ const crown = crownRide(m, cap, (x, y, s) => followOf(x, y, s) + drift);
3039
3045
  return (x, y, ax, ay) => {
3040
3046
  const s = Math.sign(ax);
3041
3047
  const a = Math.abs(ax) / 30;
3042
- const follow = followAt(x, y, s) + (ride === void 0 ? 0 : ride(x, y, s));
3043
- const rx = s * a * follow + a * (m.shellScale - 1) * (x - f.axisX);
3048
+ const rx = s * a * followOf(x, y, s) + a * (m.shellScale - 1) * (x - f.axisX);
3044
3049
  const ry = nodDy(NOD.hairFront, ay, f.hh);
3045
3050
  const bx = back(x, y, ax, ay)[0];
3046
3051
  const by = nodDy(cap, ay, f.hh);
3047
3052
  const w = crownBlend(m, y);
3048
- return [bx + w * (rx - bx), by + w * (ry - by)];
3053
+ const wx = crown === void 0 ? w : w + crown(y, s);
3054
+ return [bx + wx * (rx - bx), by + w * (ry - by)];
3049
3055
  };
3050
3056
  }
3051
3057
  function crownBlend(m, y) {
3052
3058
  const top = m.frame.face.y1;
3053
3059
  return 1 - smoothstep(top, Math.max(top + 1, m.hairTop), y);
3054
3060
  }
3061
+ function gridRow(ys, y, snap = false) {
3062
+ const n = ys.length;
3063
+ let v = clamp((ys[0] - y) / (ys[0] - ys[n - 1]) * (n - 1), 0, n - 1);
3064
+ if (snap && Math.abs(v - Math.round(v)) < 1e-3) v = Math.round(v);
3065
+ const r = Math.min(n - 2, Math.floor(v));
3066
+ return { r, t: v - r };
3067
+ }
3068
+ function renderedCrownBlend(m, ys) {
3069
+ const wr = ys.map((y) => crownBlend(m, y));
3070
+ return {
3071
+ wr,
3072
+ blendAt: (y) => {
3073
+ const { r, t } = gridRow(ys, y);
3074
+ return wr[r] + (wr[r + 1] - wr[r]) * t;
3075
+ }
3076
+ };
3077
+ }
3055
3078
  var CAP_COVER_LOSS = 0.01;
3056
3079
  function capSlide(m) {
3057
3080
  const f = m.frame;
@@ -3064,12 +3087,7 @@ function capSlide(m) {
3064
3087
  const top = ys[0];
3065
3088
  const n = ys.length;
3066
3089
  const bangs = NOD.hairFront.down;
3067
- const wr = ys.map((y) => crownBlend(m, y));
3068
- const blendAt = (y) => {
3069
- const v = clamp((top - y) / (top - ys[n - 1]) * (n - 1), 0, n - 1);
3070
- const r = Math.min(n - 2, Math.floor(v));
3071
- return wr[r] + (wr[r + 1] - wr[r]) * (v - r);
3072
- };
3090
+ const { wr, blendAt } = renderedCrownBlend(m, ys);
3073
3091
  const flat = ys[wr.findIndex((w) => w >= 1)] ?? ys[n - 1];
3074
3092
  const x0 = xs[0];
3075
3093
  const tops = new Float64Array(Math.round(xs[xs.length - 1] - x0)).fill(NaN);
@@ -3198,13 +3216,157 @@ function outerRide(m, inner, coreAt, followAt) {
3198
3216
  if (s === 0 || d <= inner) return 0;
3199
3217
  const k = x < f.axisX ? 0 : 1;
3200
3218
  const j = s < 0 ? 0 : 1;
3201
- let v = clamp((top - y) / (top - ys[n - 1]) * (n - 1), 0, n - 1);
3202
- if (Math.abs(v - Math.round(v)) < 1e-3) v = Math.round(v);
3203
- const r = Math.min(n - 2, Math.floor(v));
3204
- const t = v - r;
3219
+ const { r, t } = gridRow(ys, y, true);
3205
3220
  return rowRide(r, k, j, d) * (1 - t) + rowRide(r + 1, k, j, d) * t;
3206
3221
  };
3207
3222
  }
3223
+ function crownRide(m, cap, relAt) {
3224
+ const f = m.frame;
3225
+ const grid = m.hairFrontGrid;
3226
+ if (grid === void 0) return void 0;
3227
+ const { xs, ys } = grid;
3228
+ const top = ys[0];
3229
+ if (f.hairRuns("hair_front", top) === void 0 || f.hairRuns("hair_back", top) === void 0) {
3230
+ return void 0;
3231
+ }
3232
+ const n = ys.length;
3233
+ const count = Math.round(top - ys[n - 1]);
3234
+ const hh = f.hh;
3235
+ const face = faceField(m);
3236
+ const { wr, blendAt } = renderedCrownBlend(m, ys);
3237
+ const backRows = /* @__PURE__ */ new Map();
3238
+ const backAt = (j) => {
3239
+ let runs = backRows.get(j);
3240
+ if (runs === void 0) {
3241
+ runs = f.hairRuns("hair_back", top - j - 0.5);
3242
+ backRows.set(j, runs);
3243
+ }
3244
+ return runs;
3245
+ };
3246
+ const faceAt = (j) => {
3247
+ const y = top - j - 0.5;
3248
+ return f.faceRuns(y) ?? (y > f.face.y0 && y < f.face.y1 ? [f.face.x0, f.face.x1] : []);
3249
+ };
3250
+ const passed = (i, dys) => [
3251
+ Math.floor(i - Math.max(0, ...dys)),
3252
+ Math.ceil(i + 1 - Math.min(0, ...dys))
3253
+ ];
3254
+ const rel = new Float64Array(n * xs.length * 2).fill(NaN);
3255
+ const relOf = (r, c, k) => {
3256
+ const at = (r * xs.length + c) * 2 + k;
3257
+ if (Number.isNaN(rel[at])) rel[at] = relAt(xs[c], ys[r], k === 0 ? -1 : 1);
3258
+ return rel[at];
3259
+ };
3260
+ const cellR = (x, y, k) => {
3261
+ let c = 0;
3262
+ while (c < xs.length - 2 && xs[c + 1] < x) c++;
3263
+ const { r } = gridRow(ys, y);
3264
+ return Math.max(
3265
+ relOf(r, c, k),
3266
+ relOf(r, c + 1, k),
3267
+ relOf(r + 1, c, k),
3268
+ relOf(r + 1, c + 1, k)
3269
+ );
3270
+ };
3271
+ const someAngle = (pairs) => {
3272
+ let lo = 0;
3273
+ let hi = 1;
3274
+ for (const [c0, c1] of pairs) {
3275
+ if (c1 === 0) {
3276
+ if (c0 <= 0) return false;
3277
+ } else if (c1 > 0) lo = Math.max(lo, -c0 / c1);
3278
+ else hi = Math.min(hi, -c0 / c1);
3279
+ }
3280
+ return lo < hi;
3281
+ };
3282
+ const slack = [-1, 1].map(() => new Float64Array(count).fill(NaN));
3283
+ const slackAt = (i, k) => {
3284
+ if (!Number.isNaN(slack[k][i])) return slack[k][i];
3285
+ const y = top - i - 0.5;
3286
+ const w = blendAt(y);
3287
+ const nod = {
3288
+ up: w * NOD.hairFront.up + (1 - w) * cap.up,
3289
+ down: w * NOD.hairFront.down + (1 - w) * cap.down
3290
+ };
3291
+ const ays = [-30, 30];
3292
+ const [b0, b1] = passed(
3293
+ i,
3294
+ ays.map((ay) => nodDy(nod, ay, hh) - nodDy(NOD.hairBack, ay, hh))
3295
+ );
3296
+ const band = [];
3297
+ for (let j = b0; j < b1; j++) band.push(backAt(j));
3298
+ const faces = [];
3299
+ const [f0, f1] = passed(
3300
+ i,
3301
+ ays.map((ay) => nodDy(nod, ay, hh) - face(f.axisX, y, 0, ay)[1])
3302
+ );
3303
+ for (let j = f0; j < f1; j++)
3304
+ faces.push({ y: top - j - 0.5, runs: faceAt(j) });
3305
+ const front = f.hairRuns("hair_front", y);
3306
+ [-1, 1].forEach((s, side) => {
3307
+ let least = Infinity;
3308
+ const far = outerEdge(front, f.axisX, s);
3309
+ if (far !== void 0) {
3310
+ const R = cellR(far, y, side);
3311
+ const c = Math.min(...band.map((b) => runReach(b, far, s).outward));
3312
+ if (R > 0) least = Math.min(least, m.shellScale * c / R);
3313
+ }
3314
+ for (let t = s > 0 ? 0 : 1; t < front.length; t += 2) {
3315
+ const x = front[t];
3316
+ const R = cellR(x, y, side);
3317
+ if (R <= 0) continue;
3318
+ let c = Math.min(
3319
+ ...band.map((b) => runReach(b, x, s > 0 ? -1 : 1).inward)
3320
+ );
3321
+ const sh = (m.shellScale - 1) * (x - f.axisX);
3322
+ const d0 = sh + s * m.hairBack;
3323
+ const d1 = d0 + s * R;
3324
+ const lo = Math.min(d0, d1);
3325
+ const hi = Math.max(d0, d1);
3326
+ const bares = faces.some(({ y: fy, runs }) => {
3327
+ for (let r = 0; r < runs.length; r += 2) {
3328
+ const e0 = runs[r];
3329
+ const e1 = runs[r + 1];
3330
+ const de0 = face(e0, fy, 30 * s, 0)[0];
3331
+ const de1 = face(e1, fy, 30 * s, 0)[0];
3332
+ if (someAngle([
3333
+ [e1 - x, de1 - lo],
3334
+ [x - e0, hi - de0]
3335
+ ])) {
3336
+ return true;
3337
+ }
3338
+ }
3339
+ return false;
3340
+ });
3341
+ if (bares) c = 0;
3342
+ least = Math.min(least, m.shellScale * c / R);
3343
+ }
3344
+ slack[side][i] = least - w;
3345
+ });
3346
+ return slack[k][i];
3347
+ };
3348
+ const extra = [0, 1].map((k) => {
3349
+ const out = wr.slice();
3350
+ for (let r = 0; r < n; r++) {
3351
+ 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)]));
3354
+ let least = Infinity;
3355
+ for (let i = i0; i < i1; i++) least = Math.min(least, slackAt(i, k));
3356
+ out[r] = wr[r] + clamp(least, 0, 1 - wr[r]);
3357
+ }
3358
+ for (let r = n - 2; r >= 0; r--) {
3359
+ if (wr[r] < 1) out[r] = Math.max(wr[r], Math.min(out[r], out[r + 1]));
3360
+ }
3361
+ return out.map((v, r) => v - wr[r]);
3362
+ });
3363
+ return (y, s) => {
3364
+ if (s === 0) return 0;
3365
+ const e = extra[s < 0 ? 0 : 1];
3366
+ const { r, t } = gridRow(ys, y, true);
3367
+ return e[r] * (1 - t) + e[r + 1] * t;
3368
+ };
3369
+ }
3208
3370
  function familyField(m, family) {
3209
3371
  switch (family) {
3210
3372
  case "face":