@ikijs/editor 0.21.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/README.md +14 -14
- package/dist/index.d.mts +181 -4
- package/dist/index.d.ts +181 -4
- package/dist/index.js +355 -202
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +350 -204
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -3018,9 +3018,16 @@ function forearmPoseDeformer(role, arm) {
|
|
|
3018
3018
|
};
|
|
3019
3019
|
}
|
|
3020
3020
|
|
|
3021
|
+
// src/auto-rig/mouth.ts
|
|
3022
|
+
import {
|
|
3023
|
+
StandardParameter as P5
|
|
3024
|
+
} from "@ikijs/format";
|
|
3025
|
+
|
|
3021
3026
|
// src/auto-rig/roles.ts
|
|
3022
3027
|
var LIP_ROLES = ["mouth_inner", "lip_lower", "lip_upper"];
|
|
3023
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);
|
|
3024
3031
|
var ROLE_TABLE = [
|
|
3025
3032
|
{ role: "hair_back", family: "hair_back" },
|
|
3026
3033
|
{ role: "body", family: "body" },
|
|
@@ -3032,10 +3039,13 @@ var ROLE_TABLE = [
|
|
|
3032
3039
|
{ role: "nose", family: "nose" },
|
|
3033
3040
|
{ role: "mouth", family: "mouth" },
|
|
3034
3041
|
{ role: "mouth_open", family: "mouth" },
|
|
3035
|
-
// The lip set, back to front: the interior, the
|
|
3036
|
-
// lip and its line over both. It
|
|
3037
|
-
// (`mouth.ts`), so it stands in for
|
|
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.
|
|
3038
3046
|
{ role: "mouth_inner", family: "mouth" },
|
|
3047
|
+
{ role: "mouth_tongue", family: "mouth" },
|
|
3048
|
+
{ role: "mouth_teeth", family: "mouth" },
|
|
3039
3049
|
{ role: "lip_lower", family: "mouth" },
|
|
3040
3050
|
{ role: "lip_upper", family: "mouth" },
|
|
3041
3051
|
{ role: "eye_L", family: "eye_L", required: true },
|
|
@@ -3098,6 +3108,12 @@ function checkArmsHaveBody(roles) {
|
|
|
3098
3108
|
function checkMouth(roles) {
|
|
3099
3109
|
const has = new Set(roles);
|
|
3100
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
|
+
}
|
|
3101
3117
|
if (present.length === 0) {
|
|
3102
3118
|
if (!has.has("mouth")) {
|
|
3103
3119
|
throw new Error(
|
|
@@ -3159,6 +3175,301 @@ function parseLayerRoles(fileNames) {
|
|
|
3159
3175
|
return out;
|
|
3160
3176
|
}
|
|
3161
3177
|
|
|
3178
|
+
// src/auto-rig/mouth.ts
|
|
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;
|
|
3185
|
+
var MOUTH_KNOT_PX = 4;
|
|
3186
|
+
var isLipSet = (byRole) => LIP_ROLES.every((r) => byRole.has(r));
|
|
3187
|
+
function columnRuns(layer) {
|
|
3188
|
+
const cols = /* @__PURE__ */ new Map();
|
|
3189
|
+
(layer.rowRuns ?? []).forEach((runs, k) => {
|
|
3190
|
+
for (let i = 0; i < runs.length; i += 2) {
|
|
3191
|
+
for (let c = runs[i]; c < runs[i + 1]; c++) {
|
|
3192
|
+
const list = cols.get(c) ?? [];
|
|
3193
|
+
const last = list[list.length - 1];
|
|
3194
|
+
if (last !== void 0 && last[1] === k) last[1] = k + 1;
|
|
3195
|
+
else list.push([k, k + 1]);
|
|
3196
|
+
cols.set(c, list);
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
});
|
|
3200
|
+
return cols;
|
|
3201
|
+
}
|
|
3202
|
+
var edgesOf = (layer, run) => ({
|
|
3203
|
+
top: layer.canvasH / 2 - (layer.bbox.y + run[0]),
|
|
3204
|
+
bottom: layer.canvasH / 2 - (layer.bbox.y + run[1])
|
|
3205
|
+
});
|
|
3206
|
+
function mouthOpening(byRole) {
|
|
3207
|
+
const inner = byRole.get("mouth_inner");
|
|
3208
|
+
const upper = byRole.get("lip_upper");
|
|
3209
|
+
const lower = byRole.get("lip_lower");
|
|
3210
|
+
const innerCols = columnRuns(inner);
|
|
3211
|
+
if (innerCols.size === 0) {
|
|
3212
|
+
throw new LayerGeometryError(
|
|
3213
|
+
`auto-rig: layer "${inner.fileName}": mouth_inner has no opaque pixel (alpha \u2265 128) to read the opening off`
|
|
3214
|
+
);
|
|
3215
|
+
}
|
|
3216
|
+
const upperCols = columnRuns(upper);
|
|
3217
|
+
const lowerCols = columnRuns(lower);
|
|
3218
|
+
const cols = [...innerCols.keys()];
|
|
3219
|
+
const x0 = Math.min(...cols);
|
|
3220
|
+
const x1 = Math.max(...cols);
|
|
3221
|
+
const read = (col) => {
|
|
3222
|
+
const runs = innerCols.get(col);
|
|
3223
|
+
if (runs === void 0) return void 0;
|
|
3224
|
+
const T = edgesOf(inner, runs[0]).top;
|
|
3225
|
+
const Bb = edgesOf(inner, runs[runs.length - 1]).bottom;
|
|
3226
|
+
const line = [...upperCols.get(col) ?? []].reverse().map((r) => edgesOf(upper, r)).find((e) => e.top >= T);
|
|
3227
|
+
const Tu = line?.bottom ?? T;
|
|
3228
|
+
const lineTop = line?.top ?? T;
|
|
3229
|
+
const skin = lowerCols.get(col)?.[0];
|
|
3230
|
+
return {
|
|
3231
|
+
T,
|
|
3232
|
+
Bb,
|
|
3233
|
+
Tu,
|
|
3234
|
+
lineTop,
|
|
3235
|
+
lineH: lineTop - Tu,
|
|
3236
|
+
Bl: skin === void 0 ? Bb : edgesOf(lower, skin).top,
|
|
3237
|
+
H: T - Bb
|
|
3238
|
+
};
|
|
3239
|
+
};
|
|
3240
|
+
const reads = [];
|
|
3241
|
+
for (let c = x0; c <= x1; c++) {
|
|
3242
|
+
let r;
|
|
3243
|
+
for (let d = 0; (r = read(c - d) ?? read(c + d)) === void 0; d++) ;
|
|
3244
|
+
reads.push(r);
|
|
3245
|
+
}
|
|
3246
|
+
const m = (x0 + x1) / 2;
|
|
3247
|
+
let p = 0;
|
|
3248
|
+
let q = 0;
|
|
3249
|
+
if (reads.length >= 4) {
|
|
3250
|
+
let s11 = 0;
|
|
3251
|
+
let s12 = 0;
|
|
3252
|
+
let s22 = 0;
|
|
3253
|
+
let t1 = 0;
|
|
3254
|
+
let t2 = 0;
|
|
3255
|
+
reads.forEach((r, i) => {
|
|
3256
|
+
const x = x0 + i;
|
|
3257
|
+
const b1 = (x - x0) * (x - x1);
|
|
3258
|
+
const b2 = b1 * (x - m);
|
|
3259
|
+
const d = r.T - UPPER_SHARE * r.H - r.Tu;
|
|
3260
|
+
s11 += b1 * b1;
|
|
3261
|
+
s12 += b1 * b2;
|
|
3262
|
+
s22 += b2 * b2;
|
|
3263
|
+
t1 += b1 * d;
|
|
3264
|
+
t2 += b2 * d;
|
|
3265
|
+
});
|
|
3266
|
+
const det = s11 * s22 - s12 * s12;
|
|
3267
|
+
p = (t1 * s22 - t2 * s12) / det;
|
|
3268
|
+
q = (s11 * t2 - s12 * t1) / det;
|
|
3269
|
+
}
|
|
3270
|
+
const columns = reads.map((r, i) => {
|
|
3271
|
+
const x = x0 + i;
|
|
3272
|
+
return { ...r, seam: r.Tu + (x - x0) * (x - x1) * (p + q * (x - m)) };
|
|
3273
|
+
});
|
|
3274
|
+
const clamp2 = (col) => Math.min(x1, Math.max(x0, col));
|
|
3275
|
+
const base = (col) => columns[clamp2(col) - x0];
|
|
3276
|
+
const centre = Math.round((x0 + x1) / 2);
|
|
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
|
+
};
|
|
3285
|
+
return {
|
|
3286
|
+
x0,
|
|
3287
|
+
x1,
|
|
3288
|
+
centre,
|
|
3289
|
+
canvasW: inner.canvasW,
|
|
3290
|
+
w,
|
|
3291
|
+
at(col) {
|
|
3292
|
+
const b = base(col);
|
|
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
|
+
};
|
|
3311
|
+
}
|
|
3312
|
+
};
|
|
3313
|
+
}
|
|
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;
|
|
3317
|
+
return (col, y, v) => {
|
|
3318
|
+
const k = 1 - v;
|
|
3319
|
+
const inside = col >= x0 && col <= x1;
|
|
3320
|
+
switch (role) {
|
|
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
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
};
|
|
3353
|
+
}
|
|
3354
|
+
function columnOfKnot(knot, opening) {
|
|
3355
|
+
const boundary = Math.round(knot + opening.canvasW / 2);
|
|
3356
|
+
return boundary === opening.x1 + 1 ? opening.x1 : boundary;
|
|
3357
|
+
}
|
|
3358
|
+
function mouthFoldWarp(role, mesh, xs, box, opening) {
|
|
3359
|
+
const fold = mouthFold(role, opening);
|
|
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
|
+
);
|
|
3367
|
+
}
|
|
3368
|
+
function mouthRestShift(byRole) {
|
|
3369
|
+
const rig = buildMouthRig(byRole);
|
|
3370
|
+
const fields = /* @__PURE__ */ new Map();
|
|
3371
|
+
for (const role of ["lip_upper", "lip_lower"]) {
|
|
3372
|
+
const box = boxOfLayer(byRole.get(role));
|
|
3373
|
+
const { mesh, xs } = mouthMesh(box, rig.knots);
|
|
3374
|
+
const warp = mouthFoldWarp(role, mesh, xs, box, rig.opening);
|
|
3375
|
+
const offsets = warp.keyforms[0].offsets;
|
|
3376
|
+
fields.set(role, {
|
|
3377
|
+
box,
|
|
3378
|
+
xs,
|
|
3379
|
+
rows: offsets.length / 2 / xs.length - 1,
|
|
3380
|
+
dy: offsets.filter((_, i) => i % 2 === 1).map((o) => o * bh(box))
|
|
3381
|
+
});
|
|
3382
|
+
}
|
|
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;
|
|
3389
|
+
let k = 1;
|
|
3390
|
+
while (k < xs.length - 1 && xs[k] < x) k++;
|
|
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);
|
|
3404
|
+
};
|
|
3405
|
+
}
|
|
3406
|
+
function mouthKnots(union, opening) {
|
|
3407
|
+
const half = opening.canvasW / 2;
|
|
3408
|
+
const step = MOUTH_KNOT_PX;
|
|
3409
|
+
const xs = [union.x0];
|
|
3410
|
+
while (xs[xs.length - 1] + step < union.x1 - 0.3 * step)
|
|
3411
|
+
xs.push(xs[xs.length - 1] + step);
|
|
3412
|
+
xs.push(union.x1);
|
|
3413
|
+
const fixed = /* @__PURE__ */ new Set([union.x0, union.x1]);
|
|
3414
|
+
for (const x of [
|
|
3415
|
+
opening.x0 - half,
|
|
3416
|
+
opening.x1 - half,
|
|
3417
|
+
opening.x1 + 1 - half
|
|
3418
|
+
]) {
|
|
3419
|
+
fixed.add(x);
|
|
3420
|
+
if (xs.includes(x)) continue;
|
|
3421
|
+
const near = xs.findIndex((v) => Math.abs(v - x) < 0.3 * step);
|
|
3422
|
+
if (near >= 0 && !fixed.has(xs[near])) xs[near] = x;
|
|
3423
|
+
else xs.push(x);
|
|
3424
|
+
}
|
|
3425
|
+
xs.sort((p, q) => p - q);
|
|
3426
|
+
for (const x of xs) {
|
|
3427
|
+
if (!Number.isInteger(x + half)) {
|
|
3428
|
+
throw new Error(
|
|
3429
|
+
`auto-rig: mouth knot ${x} is not an integer canvas boundary`
|
|
3430
|
+
);
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
return xs;
|
|
3434
|
+
}
|
|
3435
|
+
function mouthMesh(box, knots) {
|
|
3436
|
+
const xs = [box.x0, ...knots.filter((k) => k > box.x0 && k < box.x1), box.x1];
|
|
3437
|
+
const rows = cellsFor(bh(box), MOUTH_KNOT_PX, 2, 40);
|
|
3438
|
+
const ys = Array.from(
|
|
3439
|
+
{ length: rows + 1 },
|
|
3440
|
+
(_, r) => box.y1 - bh(box) * r / rows
|
|
3441
|
+
);
|
|
3442
|
+
return { mesh: columnMesh(box, { xs, ys }), xs };
|
|
3443
|
+
}
|
|
3444
|
+
function buildMouthRig(byRole) {
|
|
3445
|
+
const opening = mouthOpening(byRole);
|
|
3446
|
+
const union = unionBoxes(LIP_ROLES.map((r) => boxOfLayer(byRole.get(r))));
|
|
3447
|
+
return {
|
|
3448
|
+
opening,
|
|
3449
|
+
frame: mouthFrameOf(union),
|
|
3450
|
+
knots: mouthKnots(union, opening),
|
|
3451
|
+
union
|
|
3452
|
+
};
|
|
3453
|
+
}
|
|
3454
|
+
function mouthAnchor(byRole) {
|
|
3455
|
+
const box = (role) => boxOfLayer(byRole.get(role));
|
|
3456
|
+
if (isLipSet(byRole)) {
|
|
3457
|
+
const opening = mouthOpening(byRole);
|
|
3458
|
+
return {
|
|
3459
|
+
at: {
|
|
3460
|
+
x: cx(box("mouth_inner")),
|
|
3461
|
+
y: opening.at(opening.centre).seam
|
|
3462
|
+
},
|
|
3463
|
+
box: unionBoxes(LIP_ROLES.map(box))
|
|
3464
|
+
};
|
|
3465
|
+
}
|
|
3466
|
+
const m = box("mouth");
|
|
3467
|
+
return {
|
|
3468
|
+
at: { x: cx(m), y: cy(m) },
|
|
3469
|
+
box: byRole.has("mouth_open") ? unionBoxes([m, box("mouth_open")]) : m
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3162
3473
|
// src/auto-rig/generate.ts
|
|
3163
3474
|
import {
|
|
3164
3475
|
IKI_FORMAT_VERSION as IKI_FORMAT_VERSION2,
|
|
@@ -3168,30 +3479,30 @@ import {
|
|
|
3168
3479
|
|
|
3169
3480
|
// src/auto-rig/animations.ts
|
|
3170
3481
|
import {
|
|
3171
|
-
StandardParameter as
|
|
3482
|
+
StandardParameter as P6
|
|
3172
3483
|
} from "@ikijs/format";
|
|
3173
3484
|
function browsY(y) {
|
|
3174
3485
|
return [
|
|
3175
|
-
{ parameter:
|
|
3176
|
-
{ parameter:
|
|
3486
|
+
{ parameter: P6.BrowLeftY, value: y, blend: "add" },
|
|
3487
|
+
{ parameter: P6.BrowRightY, value: y, blend: "add" }
|
|
3177
3488
|
];
|
|
3178
3489
|
}
|
|
3179
3490
|
function browsTilt(innerDown) {
|
|
3180
3491
|
return [
|
|
3181
|
-
{ parameter:
|
|
3182
|
-
{ parameter:
|
|
3492
|
+
{ parameter: P6.BrowLeftAngle, value: innerDown, blend: "add" },
|
|
3493
|
+
{ parameter: P6.BrowRightAngle, value: -innerDown, blend: "add" }
|
|
3183
3494
|
];
|
|
3184
3495
|
}
|
|
3185
3496
|
function cheek(value) {
|
|
3186
|
-
return { parameter:
|
|
3497
|
+
return { parameter: P6.Cheek, value, blend: "add" };
|
|
3187
3498
|
}
|
|
3188
|
-
var BROWS = [
|
|
3499
|
+
var BROWS = [P6.BrowLeftY, P6.BrowRightY, P6.BrowLeftAngle, P6.BrowRightAngle];
|
|
3189
3500
|
var EXPRESSIONS = [
|
|
3190
3501
|
{
|
|
3191
3502
|
id: "smile",
|
|
3192
3503
|
description: "Smiling and pleased: warm, friendly, content. For greetings, thanks, agreement and gentle happiness.",
|
|
3193
3504
|
parameters: [
|
|
3194
|
-
{ parameter:
|
|
3505
|
+
{ parameter: P6.MouthForm, value: 1, blend: "add" },
|
|
3195
3506
|
...browsY(0.25),
|
|
3196
3507
|
cheek(0.4)
|
|
3197
3508
|
]
|
|
@@ -3200,10 +3511,10 @@ var EXPRESSIONS = [
|
|
|
3200
3511
|
id: "laugh",
|
|
3201
3512
|
description: "Laughing, eyes shut and mouth open: delighted, amused. For jokes, playfulness and big joy.",
|
|
3202
3513
|
parameters: [
|
|
3203
|
-
{ parameter:
|
|
3204
|
-
{ parameter:
|
|
3205
|
-
{ parameter:
|
|
3206
|
-
{ parameter:
|
|
3514
|
+
{ parameter: P6.EyeOpenLeft, value: 0, blend: "multiply" },
|
|
3515
|
+
{ parameter: P6.EyeOpenRight, value: 0, blend: "multiply" },
|
|
3516
|
+
{ parameter: P6.MouthForm, value: 1, blend: "add" },
|
|
3517
|
+
{ parameter: P6.MouthOpen, value: 0.8, blend: "overwrite" },
|
|
3207
3518
|
...browsY(0.4),
|
|
3208
3519
|
cheek(0.65)
|
|
3209
3520
|
]
|
|
@@ -3215,7 +3526,7 @@ var EXPRESSIONS = [
|
|
|
3215
3526
|
parameters: [
|
|
3216
3527
|
...browsY(-0.5),
|
|
3217
3528
|
...browsTilt(0.8),
|
|
3218
|
-
{ parameter:
|
|
3529
|
+
{ parameter: P6.MouthForm, value: -0.9, blend: "add" }
|
|
3219
3530
|
]
|
|
3220
3531
|
},
|
|
3221
3532
|
{
|
|
@@ -3225,7 +3536,7 @@ var EXPRESSIONS = [
|
|
|
3225
3536
|
parameters: [
|
|
3226
3537
|
...browsY(0.35),
|
|
3227
3538
|
...browsTilt(-0.8),
|
|
3228
|
-
{ parameter:
|
|
3539
|
+
{ parameter: P6.MouthForm, value: -0.8, blend: "add" }
|
|
3229
3540
|
]
|
|
3230
3541
|
},
|
|
3231
3542
|
{
|
|
@@ -3233,18 +3544,18 @@ var EXPRESSIONS = [
|
|
|
3233
3544
|
description: "Surprised, mouth dropped open: startled, amazed. For shock, sudden news or disbelief.",
|
|
3234
3545
|
parameters: [
|
|
3235
3546
|
...browsY(0.8),
|
|
3236
|
-
{ parameter:
|
|
3547
|
+
{ parameter: P6.MouthOpen, value: 0.9, blend: "overwrite" }
|
|
3237
3548
|
]
|
|
3238
3549
|
},
|
|
3239
3550
|
{
|
|
3240
3551
|
id: "shy",
|
|
3241
3552
|
description: "Shy or embarrassed: bashful, flustered. For being praised, teased or caught off guard.",
|
|
3242
|
-
needs: [
|
|
3553
|
+
needs: [P6.Cheek],
|
|
3243
3554
|
parameters: [
|
|
3244
3555
|
cheek(1),
|
|
3245
3556
|
// Gaze down.
|
|
3246
|
-
{ parameter:
|
|
3247
|
-
{ parameter:
|
|
3557
|
+
{ parameter: P6.EyeballY, value: -0.65, blend: "add" },
|
|
3558
|
+
{ parameter: P6.MouthForm, value: 0.4, blend: "add" },
|
|
3248
3559
|
...browsTilt(-0.25)
|
|
3249
3560
|
]
|
|
3250
3561
|
}
|
|
@@ -3264,7 +3575,7 @@ var HEAD_MOTIONS = {
|
|
|
3264
3575
|
duration: 1,
|
|
3265
3576
|
curves: [
|
|
3266
3577
|
{
|
|
3267
|
-
parameter:
|
|
3578
|
+
parameter: P6.AngleY,
|
|
3268
3579
|
keys: [
|
|
3269
3580
|
[0, 0],
|
|
3270
3581
|
[0.3, -18],
|
|
@@ -3285,7 +3596,7 @@ var HEAD_MOTIONS = {
|
|
|
3285
3596
|
fadeIn: 0.15,
|
|
3286
3597
|
curves: [
|
|
3287
3598
|
{
|
|
3288
|
-
parameter:
|
|
3599
|
+
parameter: P6.AngleX,
|
|
3289
3600
|
keys: [
|
|
3290
3601
|
[0, 0],
|
|
3291
3602
|
[0.2, -18],
|
|
@@ -3304,7 +3615,7 @@ var HEAD_MOTIONS = {
|
|
|
3304
3615
|
duration: 1.4,
|
|
3305
3616
|
curves: [
|
|
3306
3617
|
{
|
|
3307
|
-
parameter:
|
|
3618
|
+
parameter: P6.AngleZ,
|
|
3308
3619
|
keys: [
|
|
3309
3620
|
[0, 0],
|
|
3310
3621
|
[0.35, 20],
|
|
@@ -3325,7 +3636,7 @@ var WAVE = {
|
|
|
3325
3636
|
fadeOut: 0.15,
|
|
3326
3637
|
curves: [
|
|
3327
3638
|
{
|
|
3328
|
-
parameter:
|
|
3639
|
+
parameter: P6.ArmPoseRight,
|
|
3329
3640
|
interpolation: "linear",
|
|
3330
3641
|
keys: [
|
|
3331
3642
|
[0, 0],
|
|
@@ -3335,7 +3646,7 @@ var WAVE = {
|
|
|
3335
3646
|
]
|
|
3336
3647
|
},
|
|
3337
3648
|
{
|
|
3338
|
-
parameter:
|
|
3649
|
+
parameter: P6.ArmRight,
|
|
3339
3650
|
keys: [
|
|
3340
3651
|
[0, 0],
|
|
3341
3652
|
[0.4, 10],
|
|
@@ -3344,7 +3655,7 @@ var WAVE = {
|
|
|
3344
3655
|
]
|
|
3345
3656
|
},
|
|
3346
3657
|
{
|
|
3347
|
-
parameter:
|
|
3658
|
+
parameter: P6.ArmPoseAngleRight,
|
|
3348
3659
|
keys: [
|
|
3349
3660
|
[0, 0],
|
|
3350
3661
|
[0.5, 0],
|
|
@@ -3360,7 +3671,7 @@ var WAVE = {
|
|
|
3360
3671
|
]
|
|
3361
3672
|
};
|
|
3362
3673
|
function defaultMotions(declared) {
|
|
3363
|
-
const waves = [
|
|
3674
|
+
const waves = [P6.ArmRight, P6.ArmPoseRight, P6.ArmPoseAngleRight].every(
|
|
3364
3675
|
(id) => declared.has(id)
|
|
3365
3676
|
);
|
|
3366
3677
|
return waves ? { ...HEAD_MOTIONS, ...WAVE } : HEAD_MOTIONS;
|
|
@@ -3598,181 +3909,6 @@ function checkStrandEdges(strand, irises, hair) {
|
|
|
3598
3909
|
// src/auto-rig/context.ts
|
|
3599
3910
|
import { StandardParameter as P7 } from "@ikijs/format";
|
|
3600
3911
|
|
|
3601
|
-
// src/auto-rig/mouth.ts
|
|
3602
|
-
import {
|
|
3603
|
-
StandardParameter as P6
|
|
3604
|
-
} from "@ikijs/format";
|
|
3605
|
-
var UPPER_SHARE = 0.3;
|
|
3606
|
-
var MOUTH_KNOT_PX = 4;
|
|
3607
|
-
var isLipSet = (byRole) => LIP_ROLES.every((r) => byRole.has(r));
|
|
3608
|
-
function columnRuns(layer) {
|
|
3609
|
-
const cols = /* @__PURE__ */ new Map();
|
|
3610
|
-
(layer.rowRuns ?? []).forEach((runs, k) => {
|
|
3611
|
-
for (let i = 0; i < runs.length; i += 2) {
|
|
3612
|
-
for (let c = runs[i]; c < runs[i + 1]; c++) {
|
|
3613
|
-
const list = cols.get(c) ?? [];
|
|
3614
|
-
const last = list[list.length - 1];
|
|
3615
|
-
if (last !== void 0 && last[1] === k) last[1] = k + 1;
|
|
3616
|
-
else list.push([k, k + 1]);
|
|
3617
|
-
cols.set(c, list);
|
|
3618
|
-
}
|
|
3619
|
-
}
|
|
3620
|
-
});
|
|
3621
|
-
return cols;
|
|
3622
|
-
}
|
|
3623
|
-
var edgesOf = (layer, run) => ({
|
|
3624
|
-
top: layer.canvasH / 2 - (layer.bbox.y + run[0]),
|
|
3625
|
-
bottom: layer.canvasH / 2 - (layer.bbox.y + run[1])
|
|
3626
|
-
});
|
|
3627
|
-
function mouthOpening(byRole) {
|
|
3628
|
-
const inner = byRole.get("mouth_inner");
|
|
3629
|
-
const upper = byRole.get("lip_upper");
|
|
3630
|
-
const lower = byRole.get("lip_lower");
|
|
3631
|
-
const innerCols = columnRuns(inner);
|
|
3632
|
-
if (innerCols.size === 0) {
|
|
3633
|
-
throw new LayerGeometryError(
|
|
3634
|
-
`auto-rig: layer "${inner.fileName}": mouth_inner has no opaque pixel (alpha \u2265 128) to read the opening off`
|
|
3635
|
-
);
|
|
3636
|
-
}
|
|
3637
|
-
const upperCols = columnRuns(upper);
|
|
3638
|
-
const lowerCols = columnRuns(lower);
|
|
3639
|
-
const cols = [...innerCols.keys()];
|
|
3640
|
-
const x0 = Math.min(...cols);
|
|
3641
|
-
const x1 = Math.max(...cols);
|
|
3642
|
-
const read = (col) => {
|
|
3643
|
-
const runs = innerCols.get(col);
|
|
3644
|
-
if (runs === void 0) return void 0;
|
|
3645
|
-
const T = edgesOf(inner, runs[0]).top;
|
|
3646
|
-
const Bb = edgesOf(inner, runs[runs.length - 1]).bottom;
|
|
3647
|
-
const line = [...upperCols.get(col) ?? []].reverse().map((r) => edgesOf(upper, r)).find((e) => e.top >= T);
|
|
3648
|
-
const Tu = line?.bottom ?? T;
|
|
3649
|
-
const lineTop = line?.top ?? T;
|
|
3650
|
-
const skin = lowerCols.get(col)?.[0];
|
|
3651
|
-
return {
|
|
3652
|
-
T,
|
|
3653
|
-
Bb,
|
|
3654
|
-
Tu,
|
|
3655
|
-
lineTop,
|
|
3656
|
-
lineH: lineTop - Tu,
|
|
3657
|
-
Bl: skin === void 0 ? Bb : edgesOf(lower, skin).top,
|
|
3658
|
-
H: T - Bb,
|
|
3659
|
-
seam: T - UPPER_SHARE * (T - Bb)
|
|
3660
|
-
};
|
|
3661
|
-
};
|
|
3662
|
-
const reads = [];
|
|
3663
|
-
for (let c = x0; c <= x1; c++) {
|
|
3664
|
-
let r;
|
|
3665
|
-
for (let d = 0; (r = read(c - d) ?? read(c + d)) === void 0; d++) ;
|
|
3666
|
-
reads.push(r);
|
|
3667
|
-
}
|
|
3668
|
-
const base = (col) => reads[Math.min(x1, Math.max(x0, col)) - x0];
|
|
3669
|
-
const centre = Math.round((x0 + x1) / 2);
|
|
3670
|
-
const w = base(centre).lineH;
|
|
3671
|
-
return {
|
|
3672
|
-
x0,
|
|
3673
|
-
x1,
|
|
3674
|
-
centre,
|
|
3675
|
-
canvasW: inner.canvasW,
|
|
3676
|
-
w,
|
|
3677
|
-
at(col) {
|
|
3678
|
-
const b = base(col);
|
|
3679
|
-
return { ...b, overlap: Math.max(0, Math.min(w, b.lineH)) };
|
|
3680
|
-
}
|
|
3681
|
-
};
|
|
3682
|
-
}
|
|
3683
|
-
function mouthFold(role, opening) {
|
|
3684
|
-
return (col, y, v) => {
|
|
3685
|
-
if (v === 1) return 0;
|
|
3686
|
-
if (role !== "mouth_inner" && (col < opening.x0 || col > opening.x1))
|
|
3687
|
-
return 0;
|
|
3688
|
-
const c = opening.at(col);
|
|
3689
|
-
const k = 1 - v;
|
|
3690
|
-
switch (role) {
|
|
3691
|
-
case "lip_upper":
|
|
3692
|
-
return (c.seam - c.Tu) * k;
|
|
3693
|
-
case "lip_lower":
|
|
3694
|
-
return (c.seam + c.overlap - c.Bl) * k;
|
|
3695
|
-
case "mouth_inner":
|
|
3696
|
-
return (c.seam + c.overlap - y) * k;
|
|
3697
|
-
}
|
|
3698
|
-
};
|
|
3699
|
-
}
|
|
3700
|
-
function columnOfKnot(knot, opening, role) {
|
|
3701
|
-
const boundary = Math.round(knot + opening.canvasW / 2);
|
|
3702
|
-
const col = boundary === opening.x1 + 1 ? opening.x1 : boundary;
|
|
3703
|
-
if (col >= opening.x0 && col <= opening.x1) return col;
|
|
3704
|
-
return role === "mouth_inner" ? Math.min(opening.x1, Math.max(opening.x0, col)) : void 0;
|
|
3705
|
-
}
|
|
3706
|
-
function mouthFoldWarp(role, mesh, xs, box, opening) {
|
|
3707
|
-
const fold = mouthFold(role, opening);
|
|
3708
|
-
return localWarp(P6.MouthOpen, mesh, box, [0, 1], (p, v, i) => {
|
|
3709
|
-
const col = columnOfKnot(xs[i % xs.length], opening, role);
|
|
3710
|
-
return [0, col === void 0 ? 0 : fold(col, p[1], v)];
|
|
3711
|
-
});
|
|
3712
|
-
}
|
|
3713
|
-
function mouthKnots(union, opening) {
|
|
3714
|
-
const half = opening.canvasW / 2;
|
|
3715
|
-
const step = MOUTH_KNOT_PX;
|
|
3716
|
-
const xs = [union.x0];
|
|
3717
|
-
while (xs[xs.length - 1] + step < union.x1 - 0.3 * step)
|
|
3718
|
-
xs.push(xs[xs.length - 1] + step);
|
|
3719
|
-
xs.push(union.x1);
|
|
3720
|
-
const fixed = /* @__PURE__ */ new Set([union.x0, union.x1]);
|
|
3721
|
-
for (const x of [opening.x0 - half, opening.x1 + 1 - half]) {
|
|
3722
|
-
fixed.add(x);
|
|
3723
|
-
if (xs.includes(x)) continue;
|
|
3724
|
-
const near = xs.findIndex((v) => Math.abs(v - x) < 0.3 * step);
|
|
3725
|
-
if (near >= 0 && !fixed.has(xs[near])) xs[near] = x;
|
|
3726
|
-
else xs.push(x);
|
|
3727
|
-
}
|
|
3728
|
-
xs.sort((p, q) => p - q);
|
|
3729
|
-
for (const x of xs) {
|
|
3730
|
-
if (!Number.isInteger(x + half)) {
|
|
3731
|
-
throw new Error(
|
|
3732
|
-
`auto-rig: mouth knot ${x} is not an integer canvas boundary`
|
|
3733
|
-
);
|
|
3734
|
-
}
|
|
3735
|
-
}
|
|
3736
|
-
return xs;
|
|
3737
|
-
}
|
|
3738
|
-
function mouthMesh(box, knots) {
|
|
3739
|
-
const xs = [box.x0, ...knots.filter((k) => k > box.x0 && k < box.x1), box.x1];
|
|
3740
|
-
const rows = cellsFor(bh(box), MOUTH_KNOT_PX, 2, 40);
|
|
3741
|
-
const ys = Array.from(
|
|
3742
|
-
{ length: rows + 1 },
|
|
3743
|
-
(_, r) => box.y1 - bh(box) * r / rows
|
|
3744
|
-
);
|
|
3745
|
-
return { mesh: columnMesh(box, { xs, ys }), xs };
|
|
3746
|
-
}
|
|
3747
|
-
function buildMouthRig(byRole) {
|
|
3748
|
-
const opening = mouthOpening(byRole);
|
|
3749
|
-
const union = unionBoxes(LIP_ROLES.map((r) => boxOfLayer(byRole.get(r))));
|
|
3750
|
-
return {
|
|
3751
|
-
opening,
|
|
3752
|
-
frame: mouthFrameOf(union),
|
|
3753
|
-
knots: mouthKnots(union, opening),
|
|
3754
|
-
union
|
|
3755
|
-
};
|
|
3756
|
-
}
|
|
3757
|
-
function mouthAnchor(byRole) {
|
|
3758
|
-
const box = (role) => boxOfLayer(byRole.get(role));
|
|
3759
|
-
if (isLipSet(byRole)) {
|
|
3760
|
-
const opening = mouthOpening(byRole);
|
|
3761
|
-
return {
|
|
3762
|
-
at: {
|
|
3763
|
-
x: cx(box("mouth_inner")),
|
|
3764
|
-
y: opening.at(opening.centre).seam
|
|
3765
|
-
},
|
|
3766
|
-
box: unionBoxes(LIP_ROLES.map(box))
|
|
3767
|
-
};
|
|
3768
|
-
}
|
|
3769
|
-
const m = box("mouth");
|
|
3770
|
-
return {
|
|
3771
|
-
at: { x: cx(m), y: cy(m) },
|
|
3772
|
-
box: byRole.has("mouth_open") ? unionBoxes([m, box("mouth_open")]) : m
|
|
3773
|
-
};
|
|
3774
|
-
}
|
|
3775
|
-
|
|
3776
3912
|
// src/auto-rig/head.ts
|
|
3777
3913
|
function outerEdge(runs, axisX, side) {
|
|
3778
3914
|
if (runs.length === 0) return void 0;
|
|
@@ -5859,7 +5995,7 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid, lips) {
|
|
|
5859
5995
|
headStart = fm.headStart;
|
|
5860
5996
|
} else if (spec.role === "hair_front") {
|
|
5861
5997
|
mesh = columnMesh(b, frontGrid);
|
|
5862
|
-
} else if (lips !== void 0 && isLipRole(spec.role)) {
|
|
5998
|
+
} else if (lips !== void 0 && (isLipRole(spec.role) || isLipInsideRole(spec.role))) {
|
|
5863
5999
|
({ mesh, xs: knotXs } = mouthMesh(b, lips.knots));
|
|
5864
6000
|
} else if (spec.family !== "body") {
|
|
5865
6001
|
const cells = MESH_CELLS[spec.role] ?? FEATURE_MESH_CELLS;
|
|
@@ -5976,6 +6112,8 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid, lips) {
|
|
|
5976
6112
|
extra = [(AMPLITUDE.mouthOpenWidth - 1) * (bw(b) / 2), 0];
|
|
5977
6113
|
break;
|
|
5978
6114
|
case "mouth_inner":
|
|
6115
|
+
case "mouth_tongue":
|
|
6116
|
+
case "mouth_teeth":
|
|
5979
6117
|
case "lip_lower":
|
|
5980
6118
|
case "lip_upper":
|
|
5981
6119
|
warps.push(
|
|
@@ -5983,6 +6121,7 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid, lips) {
|
|
|
5983
6121
|
mouthForm(mesh, b, lips.frame),
|
|
5984
6122
|
mouthWiden(mesh, b, lips.frame)
|
|
5985
6123
|
);
|
|
6124
|
+
if (isLipInsideRole(spec.role)) part.clip = { masks: ["mouth_inner"] };
|
|
5986
6125
|
break;
|
|
5987
6126
|
case "blush_L":
|
|
5988
6127
|
case "blush_R":
|
|
@@ -6184,6 +6323,8 @@ export {
|
|
|
6184
6323
|
DeletePhysicsRig,
|
|
6185
6324
|
EditorDocument,
|
|
6186
6325
|
HEAD_BAND,
|
|
6326
|
+
LIP_INSIDE_ROLES,
|
|
6327
|
+
LIP_ROLES,
|
|
6187
6328
|
LayerGeometryError,
|
|
6188
6329
|
SPECK_CORE_FRACTION,
|
|
6189
6330
|
SetDeformerBindings,
|
|
@@ -6205,6 +6346,7 @@ export {
|
|
|
6205
6346
|
UV_INSET_PX,
|
|
6206
6347
|
armGeometry,
|
|
6207
6348
|
captureBindingEndpoint,
|
|
6349
|
+
columnRuns,
|
|
6208
6350
|
computeGridOffsets,
|
|
6209
6351
|
createDefaultMatrixDeformer,
|
|
6210
6352
|
createDefaultPart,
|
|
@@ -6218,7 +6360,11 @@ export {
|
|
|
6218
6360
|
generateIkiFromLayerSet,
|
|
6219
6361
|
headHalfOf,
|
|
6220
6362
|
interpolateGridOffsets,
|
|
6363
|
+
isLipInsideRole,
|
|
6221
6364
|
isSpeckCore,
|
|
6365
|
+
mouthFold,
|
|
6366
|
+
mouthOpening,
|
|
6367
|
+
mouthRestShift,
|
|
6222
6368
|
packAtlas,
|
|
6223
6369
|
parseLayerRoles,
|
|
6224
6370
|
partIdsOfRole,
|