@ikijs/editor 0.22.0 → 0.23.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
@@ -3026,6 +3026,8 @@ import {
3026
3026
  // src/auto-rig/roles.ts
3027
3027
  var LIP_ROLES = ["mouth_inner", "lip_lower", "lip_upper"];
3028
3028
  var isLipRole = (role) => LIP_ROLES.includes(role);
3029
+ var LIP_INSIDE_ROLES = ["mouth_tongue", "mouth_teeth"];
3030
+ var isLipInsideRole = (role) => LIP_INSIDE_ROLES.includes(role);
3029
3031
  var ROLE_TABLE = [
3030
3032
  { role: "hair_back", family: "hair_back" },
3031
3033
  { role: "body", family: "body" },
@@ -3037,10 +3039,13 @@ var ROLE_TABLE = [
3037
3039
  { role: "nose", family: "nose" },
3038
3040
  { role: "mouth", family: "mouth" },
3039
3041
  { role: "mouth_open", family: "mouth" },
3040
- // The lip set, back to front: the interior, the lower lip's skin, the upper
3041
- // lip and its line over both. It folds open instead of crossfading
3042
- // (`mouth.ts`), so it stands in for `mouth`, never beside it.
3042
+ // The lip set, back to front: the interior, the tongue and the teeth clipped
3043
+ // to it, the lower lip's skin, the upper lip and its line over both. It
3044
+ // folds open instead of crossfading (`mouth.ts`), so it stands in for
3045
+ // `mouth`, never beside it.
3043
3046
  { role: "mouth_inner", family: "mouth" },
3047
+ { role: "mouth_tongue", family: "mouth" },
3048
+ { role: "mouth_teeth", family: "mouth" },
3044
3049
  { role: "lip_lower", family: "mouth" },
3045
3050
  { role: "lip_upper", family: "mouth" },
3046
3051
  { role: "eye_L", family: "eye_L", required: true },
@@ -3103,6 +3108,12 @@ function checkArmsHaveBody(roles) {
3103
3108
  function checkMouth(roles) {
3104
3109
  const has = new Set(roles);
3105
3110
  const present = LIP_ROLES.filter((r) => has.has(r));
3111
+ const inside = LIP_INSIDE_ROLES.filter((r) => has.has(r));
3112
+ if (inside.length > 0 && present.length === 0) {
3113
+ throw new Error(
3114
+ `auto-rig: ${inside[0]} needs the lip set ${LIP_ROLES.join(", ")} (it is cut from the interior and clipped to it)`
3115
+ );
3116
+ }
3106
3117
  if (present.length === 0) {
3107
3118
  if (!has.has("mouth")) {
3108
3119
  throw new Error(
@@ -3166,6 +3177,11 @@ function parseLayerRoles(fileNames) {
3166
3177
 
3167
3178
  // src/auto-rig/mouth.ts
3168
3179
  var UPPER_SHARE = 0.4;
3180
+ var UPPER_THIN = 0.7;
3181
+ var END_TAPER = 0.35;
3182
+ var END_TAPER_FROM = 0.7;
3183
+ var FLICK_PULL = 0.5;
3184
+ var FLICK_SQUASH = 0.25;
3169
3185
  var MOUTH_KNOT_PX = 4;
3170
3186
  var isLipSet = (byRole) => LIP_ROLES.every((r) => byRole.has(r));
3171
3187
  function columnRuns(layer) {
@@ -3255,9 +3271,17 @@ function mouthOpening(byRole) {
3255
3271
  const x = x0 + i;
3256
3272
  return { ...r, seam: r.Tu + (x - x0) * (x - x1) * (p + q * (x - m)) };
3257
3273
  });
3258
- const base = (col) => columns[Math.min(x1, Math.max(x0, col)) - x0];
3274
+ const clamp2 = (col) => Math.min(x1, Math.max(x0, col));
3275
+ const base = (col) => columns[clamp2(col) - x0];
3259
3276
  const centre = Math.round((x0 + x1) / 2);
3260
3277
  const w = base(centre).lineH;
3278
+ const from = END_TAPER_FROM * ((x1 - x0 + 1) / 2);
3279
+ const squash = (col) => {
3280
+ const d = Math.abs(col - centre);
3281
+ const edge = col < centre ? centre - upper.bbox.x : upper.bbox.x + upper.bbox.w - 1 - centre;
3282
+ const t = d <= from ? 0 : d >= edge ? 1 : (d - from) / (edge - from);
3283
+ return 1 - (1 - END_TAPER) * t;
3284
+ };
3261
3285
  return {
3262
3286
  x0,
3263
3287
  x1,
@@ -3266,39 +3290,80 @@ function mouthOpening(byRole) {
3266
3290
  w,
3267
3291
  at(col) {
3268
3292
  const b = base(col);
3269
- return { ...b, overlap: Math.max(0, Math.min(w, b.lineH)) };
3293
+ const s = squash(clamp2(col));
3294
+ return {
3295
+ ...b,
3296
+ overlap: UPPER_THIN * (b.lineH * (1 - s) / 2 + Math.min(w, b.lineH) * s)
3297
+ };
3298
+ },
3299
+ line(col) {
3300
+ if (col >= x0 && col <= x1) {
3301
+ const b = base(col);
3302
+ return { bottom: b.Tu, top: b.lineTop, squash: squash(col) };
3303
+ }
3304
+ const runs = upperCols.get(col);
3305
+ if (runs === void 0) return void 0;
3306
+ return {
3307
+ bottom: edgesOf(upper, runs[runs.length - 1]).bottom,
3308
+ top: edgesOf(upper, runs[0]).top,
3309
+ squash: FLICK_SQUASH
3310
+ };
3270
3311
  }
3271
3312
  };
3272
3313
  }
3273
3314
  function mouthFold(role, opening) {
3315
+ const { x0, x1 } = opening;
3316
+ const stackThin = (c, y) => y > c.Tu ? c.Tu + UPPER_THIN * (y - c.Tu) : y;
3274
3317
  return (col, y, v) => {
3275
- if (v === 1) return 0;
3276
- if (role !== "mouth_inner" && (col < opening.x0 || col > opening.x1))
3277
- return 0;
3278
- const c = opening.at(col);
3279
3318
  const k = 1 - v;
3319
+ const inside = col >= x0 && col <= x1;
3280
3320
  switch (role) {
3281
- case "lip_upper":
3282
- return (c.seam - c.Tu) * k;
3283
- case "lip_lower":
3284
- return (c.seam + c.overlap - c.Bl) * k;
3285
- case "mouth_inner":
3286
- return (c.seam + c.overlap - y) * k;
3321
+ case "lip_lower": {
3322
+ if (!inside) return [0, 0];
3323
+ const c = opening.at(col);
3324
+ return [0, (c.seam + c.overlap - c.Bl) * k];
3325
+ }
3326
+ case "mouth_inner": {
3327
+ const c = opening.at(col);
3328
+ const S = c.seam + c.overlap;
3329
+ return [0, S + v * (stackThin(c, y) - S) - y];
3330
+ }
3331
+ case "mouth_teeth": {
3332
+ const c = opening.at(col);
3333
+ return [0, stackThin(c, y) - y + (c.seam - c.Tu) * k];
3334
+ }
3335
+ case "mouth_tongue": {
3336
+ const c = opening.at(col);
3337
+ return [0, (c.seam + c.overlap - c.Bb) * k];
3338
+ }
3339
+ case "lip_upper": {
3340
+ let c = col;
3341
+ let line = opening.line(c);
3342
+ while (line === void 0)
3343
+ line = opening.line(c += col < x0 ? 1 : -1);
3344
+ const thin = line.bottom + UPPER_THIN * (y - line.bottom);
3345
+ const mid = line.bottom + UPPER_THIN * (line.top - line.bottom) / 2;
3346
+ const travel = inside ? opening.at(col).seam - line.bottom : 0;
3347
+ const shut = mid + line.squash * (thin - mid) + travel;
3348
+ const pull = inside ? 0 : (1 - FLICK_PULL) * ((col < x0 ? x0 : x1 + 1) - col);
3349
+ return [pull * k, thin - y + (shut - thin) * k];
3350
+ }
3287
3351
  }
3288
3352
  };
3289
3353
  }
3290
- function columnOfKnot(knot, opening, role) {
3354
+ function columnOfKnot(knot, opening) {
3291
3355
  const boundary = Math.round(knot + opening.canvasW / 2);
3292
- const col = boundary === opening.x1 + 1 ? opening.x1 : boundary;
3293
- if (col >= opening.x0 && col <= opening.x1) return col;
3294
- return role === "mouth_inner" ? Math.min(opening.x1, Math.max(opening.x0, col)) : void 0;
3356
+ return boundary === opening.x1 + 1 ? opening.x1 : boundary;
3295
3357
  }
3296
3358
  function mouthFoldWarp(role, mesh, xs, box, opening) {
3297
3359
  const fold = mouthFold(role, opening);
3298
- return localWarp(P5.MouthOpen, mesh, box, [0, 1], (p, v, i) => {
3299
- const col = columnOfKnot(xs[i % xs.length], opening, role);
3300
- return [0, col === void 0 ? 0 : fold(col, p[1], v)];
3301
- });
3360
+ return localWarp(
3361
+ P5.MouthOpen,
3362
+ mesh,
3363
+ box,
3364
+ [0, 1],
3365
+ (p, v, i) => fold(columnOfKnot(xs[i % xs.length], opening), p[1], v)
3366
+ );
3302
3367
  }
3303
3368
  function mouthRestShift(byRole) {
3304
3369
  const rig = buildMouthRig(byRole);
@@ -3311,18 +3376,31 @@ function mouthRestShift(byRole) {
3311
3376
  fields.set(role, {
3312
3377
  box,
3313
3378
  xs,
3314
- dy: xs.map((_, i) => offsets[2 * i + 1] * bh(box))
3379
+ rows: offsets.length / 2 / xs.length - 1,
3380
+ dy: offsets.filter((_, i) => i % 2 === 1).map((o) => o * bh(box))
3315
3381
  });
3316
3382
  }
3317
- const half = rig.opening.canvasW / 2;
3318
- return (role, col) => {
3319
- const { box, xs, dy } = fields.get(role);
3320
- const x = col + 0.5 - half;
3321
- if (x < box.x0 || x > box.x1) return 0;
3383
+ const { canvasW, canvasH } = byRole.get("lip_upper");
3384
+ return (role, col, row) => {
3385
+ const { box, xs, rows, dy } = fields.get(role);
3386
+ const x = col + 0.5 - canvasW / 2;
3387
+ const y = canvasH / 2 - (row + 0.5);
3388
+ if (x < box.x0 || x > box.x1 || y < box.y0 || y > box.y1) return void 0;
3322
3389
  let k = 1;
3323
3390
  while (k < xs.length - 1 && xs[k] < x) k++;
3324
- const t = (x - xs[k - 1]) / (xs[k] - xs[k - 1]);
3325
- return dy[k - 1] + (dy[k] - dy[k - 1]) * t;
3391
+ const down = (box.y1 - y) / bh(box) * rows;
3392
+ const r = Math.min(rows - 1, Math.floor(down));
3393
+ const fx = (x - xs[k - 1]) / (xs[k] - xs[k - 1]);
3394
+ const fy = down - r;
3395
+ const at = (i, j) => dy[i * xs.length + j];
3396
+ const tl = at(r, k - 1);
3397
+ const br = at(r + 1, k);
3398
+ if (fx <= fy) {
3399
+ const bl = at(r + 1, k - 1);
3400
+ return tl + fy * (bl - tl) + fx * (br - bl);
3401
+ }
3402
+ const tr = at(r, k);
3403
+ return tl + fx * (tr - tl) + fy * (br - tr);
3326
3404
  };
3327
3405
  }
3328
3406
  function mouthKnots(union, opening) {
@@ -5917,7 +5995,7 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid, lips) {
5917
5995
  headStart = fm.headStart;
5918
5996
  } else if (spec.role === "hair_front") {
5919
5997
  mesh = columnMesh(b, frontGrid);
5920
- } else if (lips !== void 0 && isLipRole(spec.role)) {
5998
+ } else if (lips !== void 0 && (isLipRole(spec.role) || isLipInsideRole(spec.role))) {
5921
5999
  ({ mesh, xs: knotXs } = mouthMesh(b, lips.knots));
5922
6000
  } else if (spec.family !== "body") {
5923
6001
  const cells = MESH_CELLS[spec.role] ?? FEATURE_MESH_CELLS;
@@ -6034,6 +6112,8 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid, lips) {
6034
6112
  extra = [(AMPLITUDE.mouthOpenWidth - 1) * (bw(b) / 2), 0];
6035
6113
  break;
6036
6114
  case "mouth_inner":
6115
+ case "mouth_tongue":
6116
+ case "mouth_teeth":
6037
6117
  case "lip_lower":
6038
6118
  case "lip_upper":
6039
6119
  warps.push(
@@ -6041,6 +6121,7 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid, lips) {
6041
6121
  mouthForm(mesh, b, lips.frame),
6042
6122
  mouthWiden(mesh, b, lips.frame)
6043
6123
  );
6124
+ if (isLipInsideRole(spec.role)) part.clip = { masks: ["mouth_inner"] };
6044
6125
  break;
6045
6126
  case "blush_L":
6046
6127
  case "blush_R":
@@ -6242,6 +6323,7 @@ export {
6242
6323
  DeletePhysicsRig,
6243
6324
  EditorDocument,
6244
6325
  HEAD_BAND,
6326
+ LIP_INSIDE_ROLES,
6245
6327
  LIP_ROLES,
6246
6328
  LayerGeometryError,
6247
6329
  SPECK_CORE_FRACTION,
@@ -6278,6 +6360,7 @@ export {
6278
6360
  generateIkiFromLayerSet,
6279
6361
  headHalfOf,
6280
6362
  interpolateGridOffsets,
6363
+ isLipInsideRole,
6281
6364
  isSpeckCore,
6282
6365
  mouthFold,
6283
6366
  mouthOpening,