@ikijs/editor 0.10.1 → 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 +12 -7
- package/dist/index.d.mts +20 -8
- package/dist/index.d.ts +20 -8
- package/dist/index.js +915 -521
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +915 -521
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1559,7 +1559,6 @@ function createLayerSetMeasurer(canvas) {
|
|
|
1559
1559
|
const opaque = new Uint8Array(canvasW * canvasH);
|
|
1560
1560
|
const layerInputs = [];
|
|
1561
1561
|
const rowSpansByRole = [];
|
|
1562
|
-
let hairFrontMask;
|
|
1563
1562
|
function add({ role, fileName, rgba }) {
|
|
1564
1563
|
if (rgba.length !== rgbaLength) {
|
|
1565
1564
|
throw new Error(
|
|
@@ -1570,19 +1569,29 @@ function createLayerSetMeasurer(canvas) {
|
|
|
1570
1569
|
if (bbox === null) return null;
|
|
1571
1570
|
const rowLeft = new Int32Array(canvasH).fill(canvasW);
|
|
1572
1571
|
const rowRight = new Int32Array(canvasH).fill(-1);
|
|
1573
|
-
const
|
|
1572
|
+
const rowRuns = role === "face" || role === "hair_front" || role === "hair_back" ? [] : void 0;
|
|
1574
1573
|
for (let y = 0; y < canvasH; y++) {
|
|
1574
|
+
let runs;
|
|
1575
|
+
if (rowRuns !== void 0 && y >= bbox.y && y < bbox.y + bbox.h) {
|
|
1576
|
+
runs = [];
|
|
1577
|
+
rowRuns.push(runs);
|
|
1578
|
+
}
|
|
1579
|
+
let inRun = false;
|
|
1575
1580
|
for (let x = 0; x < canvasW; x++) {
|
|
1576
1581
|
const p = y * canvasW + x;
|
|
1577
|
-
|
|
1582
|
+
const on = rgba[p * 4 + 3] >= ALPHA_OPAQUE;
|
|
1583
|
+
if (runs !== void 0 && on !== inRun) {
|
|
1584
|
+
runs.push(x);
|
|
1585
|
+
inRun = on;
|
|
1586
|
+
}
|
|
1587
|
+
if (!on) continue;
|
|
1578
1588
|
opaque[p] = 1;
|
|
1579
|
-
if (ownMask !== void 0) ownMask[p] = 1;
|
|
1580
1589
|
if (x < rowLeft[y]) rowLeft[y] = x;
|
|
1581
1590
|
if (x > rowRight[y]) rowRight[y] = x;
|
|
1582
1591
|
}
|
|
1592
|
+
if (runs !== void 0 && inRun) runs.push(canvasW);
|
|
1583
1593
|
}
|
|
1584
1594
|
rowSpansByRole.push({ role, rowLeft, rowRight });
|
|
1585
|
-
if (ownMask !== void 0) hairFrontMask = ownMask;
|
|
1586
1595
|
const layer = {
|
|
1587
1596
|
role,
|
|
1588
1597
|
fileName,
|
|
@@ -1592,6 +1601,7 @@ function createLayerSetMeasurer(canvas) {
|
|
|
1592
1601
|
cropW: bbox.w,
|
|
1593
1602
|
cropH: bbox.h
|
|
1594
1603
|
};
|
|
1604
|
+
if (rowRuns !== void 0) layer.rowRuns = rowRuns;
|
|
1595
1605
|
if (role === "face") {
|
|
1596
1606
|
const rowHalfWidths = [];
|
|
1597
1607
|
for (let y = bbox.y; y < bbox.y + bbox.h; y++) {
|
|
@@ -1636,8 +1646,10 @@ function createLayerSetMeasurer(canvas) {
|
|
|
1636
1646
|
}
|
|
1637
1647
|
let strandEdges;
|
|
1638
1648
|
const irisLayers = layerInputs.filter((l) => l.role === "iris_L" || l.role === "iris_R").sort((a, b) => a.bbox.x + a.bbox.w / 2 - (b.bbox.x + b.bbox.w / 2));
|
|
1639
|
-
|
|
1640
|
-
|
|
1649
|
+
const hairFront = layerInputs.find((l) => l.role === "hair_front");
|
|
1650
|
+
const hairRuns = hairFront?.rowRuns;
|
|
1651
|
+
if (hairFront !== void 0 && hairRuns !== void 0 && irisLayers.length === 2) {
|
|
1652
|
+
const hairTop = hairFront.bbox.y;
|
|
1641
1653
|
const half = canvasW / 2;
|
|
1642
1654
|
const strandOn = (iris, other, side) => {
|
|
1643
1655
|
const r = Math.floor(iris.bbox.y + iris.bbox.h / 2);
|
|
@@ -1648,15 +1660,11 @@ function createLayerSetMeasurer(canvas) {
|
|
|
1648
1660
|
if (span2.rowRight[r] < span2.rowLeft[r]) return void 0;
|
|
1649
1661
|
const minRun = STRAND_MIN_RUN_FRACTION * (span2.rowRight[r] + 1 - span2.rowLeft[r]);
|
|
1650
1662
|
const strand = new Uint8Array(canvasW);
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1663
|
+
const runs = r >= hairTop && r < hairTop + hairRuns.length ? hairRuns[r - hairTop] : [];
|
|
1664
|
+
for (let k = 0; k < runs.length; k += 2) {
|
|
1665
|
+
if (runs[k + 1] - runs[k] >= minRun) {
|
|
1666
|
+
strand.fill(1, runs[k], runs[k + 1]);
|
|
1655
1667
|
}
|
|
1656
|
-
let b = a;
|
|
1657
|
-
while (b < canvasW && mask[r * canvasW + b] === 1) b++;
|
|
1658
|
-
if (b - a >= minRun) strand.fill(1, a, b);
|
|
1659
|
-
a = b;
|
|
1660
1668
|
}
|
|
1661
1669
|
const strandAt = (x2) => x2 >= 0 && x2 < canvasW && strand[x2] === 1;
|
|
1662
1670
|
const outerBoundary = (x2) => (side < 0 ? x2 : x2 + 1) - half;
|
|
@@ -2102,6 +2110,23 @@ function checkLayers(layers, canvas) {
|
|
|
2102
2110
|
}
|
|
2103
2111
|
});
|
|
2104
2112
|
}
|
|
2113
|
+
if (l.rowRuns !== void 0) {
|
|
2114
|
+
const rows = l.rowRuns;
|
|
2115
|
+
if (!Array.isArray(rows) || rows.length !== h) {
|
|
2116
|
+
throw new Error(
|
|
2117
|
+
`${at}: rowRuns must have one entry per crop row (${h})`
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2120
|
+
rows.forEach((runs, i) => {
|
|
2121
|
+
if (!Array.isArray(runs) || runs.length % 2 !== 0 || !runs.every(
|
|
2122
|
+
(c, k) => Number.isInteger(c) && c >= x && c <= x + w && (k === 0 || c > runs[k - 1])
|
|
2123
|
+
)) {
|
|
2124
|
+
throw new Error(
|
|
2125
|
+
`${at}: rowRuns[${i}] ${JSON.stringify(runs)} is not an even-length, strictly increasing list of columns inside its crop (${x}..${x + w})`
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2105
2130
|
if (l.jawRows !== void 0) {
|
|
2106
2131
|
const cols = l.jawRows;
|
|
2107
2132
|
if (!Array.isArray(cols) || cols.length !== w) {
|
|
@@ -2359,7 +2384,11 @@ var TURN = {
|
|
|
2359
2384
|
* its translation (bangs 1.18×, side locks at the eye row ≈ 1.0×). */
|
|
2360
2385
|
hairFollow: 1.1,
|
|
2361
2386
|
/** The head's outline at the eye row holds (−0.014 / −0.004 hh): where the
|
|
2362
|
-
* front hair draws it, its outer edge follows the face this much
|
|
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). */
|
|
2363
2392
|
outlineFollow: 0,
|
|
2364
2393
|
/** The back hair's slight counter-motion. */
|
|
2365
2394
|
hairBack: -0.027,
|
|
@@ -2379,6 +2408,13 @@ var NOD = {
|
|
|
2379
2408
|
nose: { up: -0.207, down: 0.193 },
|
|
2380
2409
|
mouth: { up: -0.172, down: 0.177 },
|
|
2381
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 },
|
|
2382
2418
|
hairBack: { up: -0.011, down: 0.025 }
|
|
2383
2419
|
};
|
|
2384
2420
|
var ROLL_DEG = 10;
|
|
@@ -2426,433 +2462,82 @@ function resolveStyle(style, check) {
|
|
|
2426
2462
|
};
|
|
2427
2463
|
}
|
|
2428
2464
|
|
|
2429
|
-
// src/auto-rig/
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
const
|
|
2433
|
-
|
|
2434
|
-
return -(up * v.up + down * v.down) * hh;
|
|
2435
|
-
}
|
|
2436
|
-
function spin(x, y, ox, oy, a) {
|
|
2437
|
-
if (a === 0) return [0, 0];
|
|
2438
|
-
const px = x - ox;
|
|
2439
|
-
const py = y - oy;
|
|
2440
|
-
return [
|
|
2441
|
-
px * Math.cos(a) - py * Math.sin(a) - px,
|
|
2442
|
-
px * Math.sin(a) + py * Math.cos(a) - py
|
|
2443
|
-
];
|
|
2444
|
-
}
|
|
2445
|
-
function faceField(m) {
|
|
2446
|
-
const f = m.frame;
|
|
2447
|
-
return (x, y, ax, ay) => {
|
|
2448
|
-
const s = Math.sign(ax);
|
|
2449
|
-
const a = Math.abs(ax) / 30;
|
|
2450
|
-
const chin = smoothstep(f.mouthY, f.chinY, y);
|
|
2451
|
-
const width = m.widthUpper + (m.widthJaw - m.widthUpper) * smoothstep(f.cheekY, f.jawY, y);
|
|
2452
|
-
const shell = f.hairShell ? 0 : m.shellScale - 1;
|
|
2453
|
-
const dx = s * a * (m.face + m.chinLead * chin) + a * (width - 1 + shell) * (x - f.axisX);
|
|
2454
|
-
const t = Math.min(
|
|
2455
|
-
1,
|
|
2456
|
-
Math.max(0, (f.face.y1 - y) / Math.max(1, f.face.y1 - f.chinY))
|
|
2457
|
-
);
|
|
2458
|
-
const v = {
|
|
2459
|
-
up: NOD.faceTop.up + (NOD.chin.up - NOD.faceTop.up) * t,
|
|
2460
|
-
down: NOD.faceTop.down + (NOD.chin.down - NOD.faceTop.down) * t
|
|
2461
|
-
};
|
|
2462
|
-
return [dx, nodDy(v, ay, f.hh)];
|
|
2463
|
-
};
|
|
2464
|
-
}
|
|
2465
|
-
function earField(m) {
|
|
2466
|
-
const f = m.frame;
|
|
2467
|
-
const ears = f.ears;
|
|
2468
|
-
if (ears === void 0) return () => [0, 0];
|
|
2469
|
-
const face = faceField(m);
|
|
2470
|
-
return (x, y, ax, ay) => {
|
|
2471
|
-
const s = Math.sign(ax);
|
|
2472
|
-
const a = Math.abs(ax) / 30;
|
|
2473
|
-
const dy = face(x, y, 0, ay)[1];
|
|
2474
|
-
if (sideOf(m, x, s) === "far") return [s * a * TURN.earFar * m.face, dy];
|
|
2475
|
-
const d = Math.abs(x - f.axisX);
|
|
2476
|
-
const attach = ears.attachAt(y);
|
|
2477
|
-
if (d <= attach) return [face(x, y, ax, 0)[0], dy];
|
|
2478
|
-
const root = face(f.axisX + Math.sign(x - f.axisX) * attach, y, ax, 0)[0];
|
|
2479
|
-
const rim = s * a * TURN.earNear * m.face;
|
|
2480
|
-
const t = Math.min(1, (d - attach) / Math.max(1, ears.outer - attach));
|
|
2481
|
-
return [root + (rim - root) * t, dy];
|
|
2482
|
-
};
|
|
2483
|
-
}
|
|
2484
|
-
function chinSlide(m) {
|
|
2485
|
-
const f = m.frame;
|
|
2486
|
-
return Math.abs(faceField(m)(f.axisX, f.chinY, 30, 0)[0]);
|
|
2487
|
-
}
|
|
2488
|
-
var NECK_SHADE_MAX = 0.45;
|
|
2489
|
-
var NECK_SHADE_FULL = 0.2;
|
|
2490
|
-
function neckField(m) {
|
|
2491
|
-
const f = m.frame;
|
|
2492
|
-
const nk = f.neck;
|
|
2493
|
-
if (nk === void 0) return () => [0, 0];
|
|
2494
|
-
const face = faceField(m);
|
|
2495
|
-
const full = chinSlide(m);
|
|
2496
|
-
const share = Math.min(1, NECK_SHADE_MAX * nk.waist / Math.max(1e-6, full));
|
|
2497
|
-
const bottom = f.face.y0;
|
|
2498
|
-
const tip = f.cutAt(f.axisX);
|
|
2499
|
-
const top = tip - NECK_SHADE_FULL * (tip - bottom);
|
|
2500
|
-
return (x, y, ax) => {
|
|
2501
|
-
const wy = Math.min(
|
|
2502
|
-
1,
|
|
2503
|
-
Math.max(0, (y - bottom) / Math.max(1, top - bottom))
|
|
2504
|
-
);
|
|
2505
|
-
const wx = Math.max(0, 1 - Math.abs(x - f.axisX) / nk.waist);
|
|
2506
|
-
return [share * face(f.axisX, f.chinY, ax, 0)[0] * wx * wy, 0];
|
|
2507
|
-
};
|
|
2508
|
-
}
|
|
2509
|
-
function sideOf(m, x, s) {
|
|
2510
|
-
return s * (x - m.frame.axisX) > 0 ? "far" : "near";
|
|
2465
|
+
// src/auto-rig/head.ts
|
|
2466
|
+
function outerEdge(runs, axisX, side) {
|
|
2467
|
+
if (runs.length === 0) return void 0;
|
|
2468
|
+
const x = side > 0 ? runs[runs.length - 1] : runs[0];
|
|
2469
|
+
return side * (x - axisX) > 0 ? x : void 0;
|
|
2511
2470
|
}
|
|
2512
|
-
function
|
|
2513
|
-
const
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
const up = Math.max(0, ay) / 30;
|
|
2522
|
-
const down = Math.max(0, -ay) / 30;
|
|
2523
|
-
const squash = up * (NOD.eyeHeight.up - 1) + down * (NOD.eyeHeight.down - 1);
|
|
2524
|
-
return [dx, nodDy(NOD.eye, ay, hh) + squash * (y - ye)];
|
|
2525
|
-
};
|
|
2471
|
+
function runReach(runs, x, side) {
|
|
2472
|
+
const px = side > 0 ? x - 1 : x;
|
|
2473
|
+
for (let k = 0; k < runs.length; k += 2) {
|
|
2474
|
+
const [a, b] = [runs[k], runs[k + 1]];
|
|
2475
|
+
if (a <= px && px + 1 <= b) {
|
|
2476
|
+
return side > 0 ? { inward: x - a, outward: b - x } : { inward: b - x, outward: x - a };
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
return { inward: 0, outward: 0 };
|
|
2526
2480
|
}
|
|
2527
|
-
function
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2481
|
+
function filledProfile(rows) {
|
|
2482
|
+
if (rows === void 0 || !rows.some((v) => v > 0)) return null;
|
|
2483
|
+
const out = rows.slice();
|
|
2484
|
+
const first = out.findIndex((v) => v > 0);
|
|
2485
|
+
for (let i = 0; i < first; i++) out[i] = out[first];
|
|
2486
|
+
for (let i = first + 1; i < out.length; i++)
|
|
2487
|
+
if (!(out[i] > 0)) out[i] = out[i - 1];
|
|
2488
|
+
return out;
|
|
2534
2489
|
}
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2490
|
+
var smoothProfile = (rows) => boxSmooth(rows, Math.max(1, Math.round(rows.length * 0.01)));
|
|
2491
|
+
var EAR_STEP = 0.03;
|
|
2492
|
+
var EAR_ROWS = 3;
|
|
2493
|
+
function findEars(raw, wMax, firstRow, lastRow, minRows, maxRows) {
|
|
2494
|
+
const step = Math.max(4, EAR_STEP * wMax);
|
|
2495
|
+
for (let t = Math.max(firstRow, EAR_ROWS); t < lastRow; t++) {
|
|
2496
|
+
if (raw[t] - raw[t - EAR_ROWS] < step) continue;
|
|
2497
|
+
for (let b = t + minRows; b <= Math.min(lastRow, t + maxRows); b++) {
|
|
2498
|
+
if (raw[b - EAR_ROWS] - raw[b] >= step) {
|
|
2499
|
+
return { top: t - EAR_ROWS, bottom: b };
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
return void 0;
|
|
2503
|
+
}
|
|
2504
|
+
return void 0;
|
|
2550
2505
|
}
|
|
2551
|
-
function
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
s * a * m.tilt * TURN.mouthTiltDeg * DEG
|
|
2563
|
-
);
|
|
2564
|
-
return [
|
|
2565
|
-
s * a * m.mouth + wx - x + tx,
|
|
2566
|
-
nodDy(NOD.mouth, ay, m.frame.hh) + ty
|
|
2567
|
-
];
|
|
2568
|
-
};
|
|
2506
|
+
function boxSmooth(v, k) {
|
|
2507
|
+
return v.map((_, i) => {
|
|
2508
|
+
let s = 0;
|
|
2509
|
+
let n = 0;
|
|
2510
|
+
for (let j = i - k; j <= i + k; j++) {
|
|
2511
|
+
if (j < 0 || j >= v.length) continue;
|
|
2512
|
+
s += v[j];
|
|
2513
|
+
n++;
|
|
2514
|
+
}
|
|
2515
|
+
return s / n;
|
|
2516
|
+
});
|
|
2569
2517
|
}
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
const a = Math.abs(ax) / 30;
|
|
2575
|
-
return [
|
|
2576
|
-
s * a * shift + a * (m.shellScale - 1) * (x - f.axisX),
|
|
2577
|
-
nodDy(nod, ay, f.hh)
|
|
2578
|
-
];
|
|
2579
|
-
};
|
|
2518
|
+
var CUT_MARGIN = 2;
|
|
2519
|
+
var CUT_LOW = 3;
|
|
2520
|
+
function fadeToOutline(d, nk) {
|
|
2521
|
+
return Math.max(0, 1 - d / nk.waist);
|
|
2580
2522
|
}
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
);
|
|
2586
|
-
const
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
inner,
|
|
2595
|
-
left: Math.max(inner + 1, f.axisX - f.shellLeft),
|
|
2596
|
-
right: Math.max(inner + 1, f.shellRight - f.axisX)
|
|
2523
|
+
var STROKE_RUN = 4;
|
|
2524
|
+
var STROKE_SLACK = 6;
|
|
2525
|
+
var JAW_SHADE = 0.05;
|
|
2526
|
+
function buildHeadFrame(layers, opts) {
|
|
2527
|
+
const byRole = new Map(layers.map((l) => [l.role, l]));
|
|
2528
|
+
const faceLayer = byRole.get("face");
|
|
2529
|
+
const { canvasW, canvasH } = faceLayer;
|
|
2530
|
+
const face = boxOfLayer(faceLayer);
|
|
2531
|
+
const axisX = cx(face);
|
|
2532
|
+
const plateHalf = faceLayer.cropW / 2;
|
|
2533
|
+
const centreY = (role) => {
|
|
2534
|
+
const l = byRole.get(role);
|
|
2535
|
+
return l === void 0 ? void 0 : cy(boxOfLayer(l));
|
|
2597
2536
|
};
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
const top = f.face.y1;
|
|
2603
|
-
const eyes = [m.boxes.eye_L, m.boxes.eye_R].filter(
|
|
2604
|
-
(e) => e !== void 0
|
|
2605
|
-
);
|
|
2606
|
-
const { inner, left, right } = hairFrontBends(f, m.boxes);
|
|
2607
|
-
const eyeY = eyes.length > 0 ? eyes.reduce((t, e) => t + cy(e), 0) / eyes.length : f.eyeY;
|
|
2608
|
-
const eyeH = eyes.length > 0 ? eyes.reduce((t, e) => t + (e.y1 - e.y0), 0) / eyes.length : 1;
|
|
2609
|
-
return (x, y, ax, ay) => {
|
|
2610
|
-
const s = Math.sign(ax);
|
|
2611
|
-
const a = Math.abs(ax) / 30;
|
|
2612
|
-
const u = s * (x - f.axisX);
|
|
2613
|
-
const across = Math.min(1, Math.max(0, (u + inner) / (2 * inner)));
|
|
2614
|
-
const band = 1 - smoothstep(eyeH / 2, 1.5 * eyeH, Math.abs(y - eyeY));
|
|
2615
|
-
const core = m.hairFront + (m.hairFar - m.hairFront) * across * band;
|
|
2616
|
-
const out = Math.min(
|
|
2617
|
-
1,
|
|
2618
|
-
Math.max(
|
|
2619
|
-
0,
|
|
2620
|
-
(Math.abs(x - f.axisX) - inner) / ((x < f.axisX ? left : right) - inner)
|
|
2621
|
-
)
|
|
2622
|
-
);
|
|
2623
|
-
const follow = core + (m.hairOuter - core) * out;
|
|
2624
|
-
const rx = s * a * follow + a * (m.shellScale - 1) * (x - f.axisX);
|
|
2625
|
-
const ry = nodDy(NOD.hairFront, ay, f.hh);
|
|
2626
|
-
const [bx, by] = back(x, y, ax, ay);
|
|
2627
|
-
const w = 1 - smoothstep(top, Math.max(top + 1, m.hairTop), y);
|
|
2628
|
-
return [bx + w * (rx - bx), by + w * (ry - by)];
|
|
2629
|
-
};
|
|
2630
|
-
}
|
|
2631
|
-
function familyField(m, family) {
|
|
2632
|
-
switch (family) {
|
|
2633
|
-
case "face":
|
|
2634
|
-
return faceField(m);
|
|
2635
|
-
case "eye_L":
|
|
2636
|
-
case "eye_R":
|
|
2637
|
-
return eyeField(m, m.boxes[family]);
|
|
2638
|
-
case "brow_L":
|
|
2639
|
-
case "brow_R":
|
|
2640
|
-
return browField(m, m.boxes[family]);
|
|
2641
|
-
case "nose":
|
|
2642
|
-
return noseField(m);
|
|
2643
|
-
case "mouth":
|
|
2644
|
-
return mouthField(m);
|
|
2645
|
-
case "hair_front":
|
|
2646
|
-
return hairFrontField(m);
|
|
2647
|
-
case "hair_back":
|
|
2648
|
-
return hairField(m, m.hairBack, NOD.hairBack);
|
|
2649
|
-
case "body":
|
|
2650
|
-
return () => [0, 0];
|
|
2651
|
-
}
|
|
2652
|
-
}
|
|
2653
|
-
|
|
2654
|
-
// src/auto-rig/context.ts
|
|
2655
|
-
function sidePair(byRole, a, b) {
|
|
2656
|
-
const la = byRole.get(a);
|
|
2657
|
-
const lb = byRole.get(b);
|
|
2658
|
-
if (la === void 0 || lb === void 0) return void 0;
|
|
2659
|
-
const [lo, hi] = [
|
|
2660
|
-
{ role: a, box: boxOfLayer(la) },
|
|
2661
|
-
{ role: b, box: boxOfLayer(lb) }
|
|
2662
|
-
].sort((p, q) => cx(p.box) - cx(q.box));
|
|
2663
|
-
return { left: lo, right: hi };
|
|
2664
|
-
}
|
|
2665
|
-
function sideIrises(byRole) {
|
|
2666
|
-
const pair = sidePair(byRole, "iris_L", "iris_R");
|
|
2667
|
-
return pair === void 0 ? {} : { left: pair.left.box, right: pair.right.box };
|
|
2668
|
-
}
|
|
2669
|
-
function solveContext(frame, byRole, options, hasNose) {
|
|
2670
|
-
const box = (role) => boxOfLayer(byRole.get(role));
|
|
2671
|
-
const row = (s, strand2, sideSign) => {
|
|
2672
|
-
if (strand2 !== void 0) {
|
|
2673
|
-
const lo = sideSign < 0 ? strand2.irisOuter : strand2.irisInner;
|
|
2674
|
-
const hi = sideSign < 0 ? strand2.irisInner : strand2.irisOuter;
|
|
2675
|
-
return { role: s.role, lo, hi, y: strand2.y };
|
|
2676
|
-
}
|
|
2677
|
-
const inset = bw(s.box) > 4 ? 1 : 0;
|
|
2678
|
-
return {
|
|
2679
|
-
role: s.role,
|
|
2680
|
-
lo: s.box.x0 + inset,
|
|
2681
|
-
hi: s.box.x1 - inset,
|
|
2682
|
-
y: cy(s.box)
|
|
2683
|
-
};
|
|
2684
|
-
};
|
|
2685
|
-
const eyes = sidePair(byRole, "eye_L", "eye_R");
|
|
2686
|
-
const irises = sidePair(byRole, "iris_L", "iris_R");
|
|
2687
|
-
const pair = irises ?? eyes;
|
|
2688
|
-
const strand = irises ? options.strandEdges : void 0;
|
|
2689
|
-
const left = row(pair.left, strand?.left, -1);
|
|
2690
|
-
const right = row(pair.right, strand?.right, 1);
|
|
2691
|
-
let candidates;
|
|
2692
|
-
const edges = options.headEdges;
|
|
2693
|
-
if (frame.hairShell && edges && edges.left.length > 0 && edges.right.length > 0) {
|
|
2694
|
-
candidates = {
|
|
2695
|
-
left: edges.left.map((e) => ({ role: e.role, x: e.x })),
|
|
2696
|
-
// A right edge names its outermost pixel column; the boundary is one on.
|
|
2697
|
-
right: edges.right.map((e) => ({ role: e.role, x: e.x + 1 }))
|
|
2698
|
-
};
|
|
2699
|
-
} else if (frame.hairShell) {
|
|
2700
|
-
candidates = {
|
|
2701
|
-
left: [{ role: "hair_back", x: frame.shellLeft }],
|
|
2702
|
-
right: [{ role: "hair_back", x: frame.shellRight }]
|
|
2703
|
-
};
|
|
2704
|
-
} else {
|
|
2705
|
-
const w = frame.paintedHalfAt(frame.eyeY);
|
|
2706
|
-
candidates = {
|
|
2707
|
-
left: [{ role: "face", x: frame.axisX - w }],
|
|
2708
|
-
right: [{ role: "face", x: frame.axisX + w }]
|
|
2709
|
-
};
|
|
2710
|
-
}
|
|
2711
|
-
const restHalf = (Math.max(...candidates.right.map((c) => c.x)) - Math.min(...candidates.left.map((c) => c.x))) / 2;
|
|
2712
|
-
const nose = byRole.get("nose");
|
|
2713
|
-
let noseAt;
|
|
2714
|
-
let noseBox;
|
|
2715
|
-
if (hasNose && nose !== void 0) {
|
|
2716
|
-
noseBox = nose.denseCore ? boxOfPixels(nose.denseCore, nose.canvasW, nose.canvasH) : boxOfLayer(nose);
|
|
2717
|
-
noseAt = { x: cx(noseBox), y: cy(noseBox) };
|
|
2718
|
-
}
|
|
2719
|
-
const mouthBox = byRole.has("mouth_open") ? unionBoxes([box("mouth"), box("mouth_open")]) : box("mouth");
|
|
2720
|
-
const m = box("mouth");
|
|
2721
|
-
return {
|
|
2722
|
-
frame,
|
|
2723
|
-
left,
|
|
2724
|
-
right,
|
|
2725
|
-
candidates,
|
|
2726
|
-
restHalf,
|
|
2727
|
-
strand,
|
|
2728
|
-
hasHairFront: byRole.has("hair_front"),
|
|
2729
|
-
eyeBoxes: { left: eyes.left.box, right: eyes.right.box },
|
|
2730
|
-
noseAt,
|
|
2731
|
-
noseBox,
|
|
2732
|
-
mouthAt: { x: cx(m), y: cy(m) },
|
|
2733
|
-
mouthBox
|
|
2734
|
-
};
|
|
2735
|
-
}
|
|
2736
|
-
function renderedLander(turn, parts, grids, from = /* @__PURE__ */ new Map()) {
|
|
2737
|
-
const analytic = /* @__PURE__ */ new Map();
|
|
2738
|
-
const landed = /* @__PURE__ */ new Map();
|
|
2739
|
-
const field = (role) => {
|
|
2740
|
-
const family = roleSpec(role).family;
|
|
2741
|
-
let f = analytic.get(family);
|
|
2742
|
-
if (f === void 0) analytic.set(family, f = familyField(turn, family));
|
|
2743
|
-
return f;
|
|
2744
|
-
};
|
|
2745
|
-
return (role, x, y, ax) => {
|
|
2746
|
-
const family = roleSpec(role).family;
|
|
2747
|
-
const part = parts.find((p) => p.id === role);
|
|
2748
|
-
const g = grids.get(family);
|
|
2749
|
-
const warp = part?.warps?.find((w) => w.parameter === P.AngleX);
|
|
2750
|
-
if (part === void 0 || part.mesh === void 0 || !g && !warp) {
|
|
2751
|
-
return x + field(role)(x, y, ax, 0)[0];
|
|
2752
|
-
}
|
|
2753
|
-
const box = {
|
|
2754
|
-
x0: part.transform.x - part.width / 2,
|
|
2755
|
-
x1: part.transform.x + part.width / 2,
|
|
2756
|
-
y0: part.transform.y - part.height / 2,
|
|
2757
|
-
y1: part.transform.y + part.height / 2
|
|
2758
|
-
};
|
|
2759
|
-
const rest = meshPoints(part.mesh, box);
|
|
2760
|
-
const key = `${role}@${ax}`;
|
|
2761
|
-
let xs = landed.get(key);
|
|
2762
|
-
if (xs === void 0) {
|
|
2763
|
-
if (g) {
|
|
2764
|
-
const offsets = keyformAt(g.keyforms, ax);
|
|
2765
|
-
xs = rest.map(([px, py]) => land(g.lattice, offsets, px, py)[0]);
|
|
2766
|
-
} else {
|
|
2767
|
-
const k = warp.keyforms;
|
|
2768
|
-
const offsets = blendStops(
|
|
2769
|
-
k.map((kf) => kf.value),
|
|
2770
|
-
k.map((kf) => kf.offsets),
|
|
2771
|
-
ax
|
|
2772
|
-
);
|
|
2773
|
-
xs = rest.map(([px], i) => px + offsets[2 * i] * bw(box));
|
|
2774
|
-
}
|
|
2775
|
-
landed.set(key, xs);
|
|
2776
|
-
}
|
|
2777
|
-
const idx = part.mesh.indices;
|
|
2778
|
-
for (let t = from.get(role) ?? 0; t < idx.length; t += 3) {
|
|
2779
|
-
const [a, b, c] = [idx[t], idx[t + 1], idx[t + 2]];
|
|
2780
|
-
const [ax0, ay0] = rest[a];
|
|
2781
|
-
const [bx0, by0] = rest[b];
|
|
2782
|
-
const [cx0, cy0] = rest[c];
|
|
2783
|
-
const d = (by0 - cy0) * (ax0 - cx0) + (cx0 - bx0) * (ay0 - cy0);
|
|
2784
|
-
if (Math.abs(d) < 1e-9) continue;
|
|
2785
|
-
const u = ((by0 - cy0) * (x - cx0) + (cx0 - bx0) * (y - cy0)) / d;
|
|
2786
|
-
const v = ((cy0 - ay0) * (x - cx0) + (ax0 - cx0) * (y - cy0)) / d;
|
|
2787
|
-
const w = 1 - u - v;
|
|
2788
|
-
if (u < -1e-6 || v < -1e-6 || w < -1e-6) continue;
|
|
2789
|
-
return u * xs[a] + v * xs[b] + w * xs[c];
|
|
2790
|
-
}
|
|
2791
|
-
return x + field(role)(x, y, ax, 0)[0];
|
|
2792
|
-
};
|
|
2793
|
-
}
|
|
2794
|
-
|
|
2795
|
-
// src/auto-rig/head.ts
|
|
2796
|
-
function filledProfile(rows) {
|
|
2797
|
-
if (rows === void 0 || !rows.some((v) => v > 0)) return null;
|
|
2798
|
-
const out = rows.slice();
|
|
2799
|
-
const first = out.findIndex((v) => v > 0);
|
|
2800
|
-
for (let i = 0; i < first; i++) out[i] = out[first];
|
|
2801
|
-
for (let i = first + 1; i < out.length; i++)
|
|
2802
|
-
if (!(out[i] > 0)) out[i] = out[i - 1];
|
|
2803
|
-
return out;
|
|
2804
|
-
}
|
|
2805
|
-
var smoothProfile = (rows) => boxSmooth(rows, Math.max(1, Math.round(rows.length * 0.01)));
|
|
2806
|
-
var EAR_STEP = 0.03;
|
|
2807
|
-
var EAR_ROWS = 3;
|
|
2808
|
-
function findEars(raw, wMax, firstRow, lastRow, minRows, maxRows) {
|
|
2809
|
-
const step = Math.max(4, EAR_STEP * wMax);
|
|
2810
|
-
for (let t = Math.max(firstRow, EAR_ROWS); t < lastRow; t++) {
|
|
2811
|
-
if (raw[t] - raw[t - EAR_ROWS] < step) continue;
|
|
2812
|
-
for (let b = t + minRows; b <= Math.min(lastRow, t + maxRows); b++) {
|
|
2813
|
-
if (raw[b - EAR_ROWS] - raw[b] >= step) {
|
|
2814
|
-
return { top: t - EAR_ROWS, bottom: b };
|
|
2815
|
-
}
|
|
2816
|
-
}
|
|
2817
|
-
return void 0;
|
|
2818
|
-
}
|
|
2819
|
-
return void 0;
|
|
2820
|
-
}
|
|
2821
|
-
function boxSmooth(v, k) {
|
|
2822
|
-
return v.map((_, i) => {
|
|
2823
|
-
let s = 0;
|
|
2824
|
-
let n = 0;
|
|
2825
|
-
for (let j = i - k; j <= i + k; j++) {
|
|
2826
|
-
if (j < 0 || j >= v.length) continue;
|
|
2827
|
-
s += v[j];
|
|
2828
|
-
n++;
|
|
2829
|
-
}
|
|
2830
|
-
return s / n;
|
|
2831
|
-
});
|
|
2832
|
-
}
|
|
2833
|
-
var CUT_MARGIN = 2;
|
|
2834
|
-
var CUT_LOW = 3;
|
|
2835
|
-
function fadeToOutline(d, nk) {
|
|
2836
|
-
return Math.max(0, 1 - d / nk.waist);
|
|
2837
|
-
}
|
|
2838
|
-
var STROKE_RUN = 4;
|
|
2839
|
-
var STROKE_SLACK = 6;
|
|
2840
|
-
var JAW_SHADE = 0.05;
|
|
2841
|
-
function buildHeadFrame(layers, opts) {
|
|
2842
|
-
const byRole = new Map(layers.map((l) => [l.role, l]));
|
|
2843
|
-
const faceLayer = byRole.get("face");
|
|
2844
|
-
const { canvasW, canvasH } = faceLayer;
|
|
2845
|
-
const face = boxOfLayer(faceLayer);
|
|
2846
|
-
const axisX = cx(face);
|
|
2847
|
-
const plateHalf = faceLayer.cropW / 2;
|
|
2848
|
-
const centreY = (role) => {
|
|
2849
|
-
const l = byRole.get(role);
|
|
2850
|
-
return l === void 0 ? void 0 : cy(boxOfLayer(l));
|
|
2851
|
-
};
|
|
2852
|
-
const pair = (a, b) => {
|
|
2853
|
-
const ya = centreY(a);
|
|
2854
|
-
const yb = centreY(b);
|
|
2855
|
-
return ya === void 0 || yb === void 0 ? void 0 : (ya + yb) / 2;
|
|
2537
|
+
const pair = (a, b) => {
|
|
2538
|
+
const ya = centreY(a);
|
|
2539
|
+
const yb = centreY(b);
|
|
2540
|
+
return ya === void 0 || yb === void 0 ? void 0 : (ya + yb) / 2;
|
|
2856
2541
|
};
|
|
2857
2542
|
const eyeY = pair("iris_L", "iris_R") ?? pair("eye_L", "eye_R");
|
|
2858
2543
|
const mouthY = centreY("mouth");
|
|
@@ -3012,68 +2697,768 @@ function buildHeadFrame(layers, opts) {
|
|
|
3012
2697
|
}
|
|
3013
2698
|
return m;
|
|
3014
2699
|
});
|
|
3015
|
-
if (opts.chinSlide !== void 0 && banded >= 0)
|
|
3016
|
-
nk.bandEnd = banded + CUT_LOW + 1;
|
|
3017
|
-
cutAt = (x) => {
|
|
3018
|
-
const f = x - (faceLayer.bbox.x - canvasW / 2) - 0.5;
|
|
3019
|
-
const i = clamp(Math.floor(f), 0, low.length - 1);
|
|
3020
|
-
const j = Math.min(i + 1, low.length - 1);
|
|
3021
|
-
const t = clamp(f - i, 0, 1);
|
|
3022
|
-
return Math.max(face.y0, low[i] + (low[j] - low[i]) * t);
|
|
3023
|
-
};
|
|
3024
|
-
} else {
|
|
3025
|
-
const head = earless(chinY);
|
|
3026
|
-
headMax = Math.max(...head);
|
|
3027
|
-
if (ears !== void 0) {
|
|
3028
|
-
const bottomOf = (d) => {
|
|
3029
|
-
for (let r = head.length - 1; r >= 0; r--) {
|
|
3030
|
-
if (head[r] >= d) return rowY(r) - 0.5;
|
|
2700
|
+
if (opts.chinSlide !== void 0 && banded >= 0)
|
|
2701
|
+
nk.bandEnd = banded + CUT_LOW + 1;
|
|
2702
|
+
cutAt = (x) => {
|
|
2703
|
+
const f = x - (faceLayer.bbox.x - canvasW / 2) - 0.5;
|
|
2704
|
+
const i = clamp(Math.floor(f), 0, low.length - 1);
|
|
2705
|
+
const j = Math.min(i + 1, low.length - 1);
|
|
2706
|
+
const t = clamp(f - i, 0, 1);
|
|
2707
|
+
return Math.max(face.y0, low[i] + (low[j] - low[i]) * t);
|
|
2708
|
+
};
|
|
2709
|
+
} else {
|
|
2710
|
+
const head = earless(chinY);
|
|
2711
|
+
headMax = Math.max(...head);
|
|
2712
|
+
if (ears !== void 0) {
|
|
2713
|
+
const bottomOf = (d) => {
|
|
2714
|
+
for (let r = head.length - 1; r >= 0; r--) {
|
|
2715
|
+
if (head[r] >= d) return rowY(r) - 0.5;
|
|
2716
|
+
}
|
|
2717
|
+
return face.y1;
|
|
2718
|
+
};
|
|
2719
|
+
cutAt = (x) => {
|
|
2720
|
+
const y = bottomOf(Math.abs(x - axisX));
|
|
2721
|
+
return y >= face.y1 ? face.y1 : Math.max(face.y0, y - CUT_MARGIN);
|
|
2722
|
+
};
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
const hh = HH_PER_EYE_TO_CHIN * Math.max(1, eyeY - chinY);
|
|
2727
|
+
const headHalf = opts.headHalfWidth;
|
|
2728
|
+
const hairShell = headHalf !== void 0;
|
|
2729
|
+
let shellLeft = axisX - plateHalf;
|
|
2730
|
+
let shellRight = axisX + plateHalf;
|
|
2731
|
+
if (hairShell) {
|
|
2732
|
+
if (opts.headEdges && opts.headEdges.left.length > 0 && opts.headEdges.right.length > 0) {
|
|
2733
|
+
shellLeft = Math.min(...opts.headEdges.left.map((e) => e.x));
|
|
2734
|
+
shellRight = Math.max(...opts.headEdges.right.map((e) => e.x)) + 1;
|
|
2735
|
+
} else {
|
|
2736
|
+
shellLeft = axisX - headHalf;
|
|
2737
|
+
shellRight = axisX + headHalf;
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
const runsOf = (role, y) => {
|
|
2741
|
+
const l = byRole.get(role);
|
|
2742
|
+
if (l?.rowRuns === void 0) return void 0;
|
|
2743
|
+
const runs = l.rowRuns[Math.floor(canvasH / 2 - y) - l.bbox.y];
|
|
2744
|
+
return runs === void 0 ? [] : runs.map((c) => c - canvasW / 2);
|
|
2745
|
+
};
|
|
2746
|
+
const hairRuns = (role, y) => runsOf(role, y);
|
|
2747
|
+
const faceRuns = (y) => runsOf("face", y);
|
|
2748
|
+
return {
|
|
2749
|
+
canvasW,
|
|
2750
|
+
canvasH,
|
|
2751
|
+
face,
|
|
2752
|
+
axisX,
|
|
2753
|
+
eyeY,
|
|
2754
|
+
mouthY,
|
|
2755
|
+
chinY,
|
|
2756
|
+
hh,
|
|
2757
|
+
paintedHalfAt,
|
|
2758
|
+
wMax,
|
|
2759
|
+
headMax,
|
|
2760
|
+
hasProfile: profile !== null,
|
|
2761
|
+
neck,
|
|
2762
|
+
ears,
|
|
2763
|
+
cutAt,
|
|
2764
|
+
cheekY: eyeY - 0.5 * (eyeY - chinY),
|
|
2765
|
+
jawY: eyeY - 0.8 * (eyeY - chinY),
|
|
2766
|
+
shellLeft,
|
|
2767
|
+
shellRight,
|
|
2768
|
+
hairShell,
|
|
2769
|
+
holdBase: headHalf ?? plateHalf,
|
|
2770
|
+
hairRuns,
|
|
2771
|
+
faceRuns
|
|
2772
|
+
};
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
// src/auto-rig/fields.ts
|
|
2776
|
+
var NOSE_TILT_DEG = 6;
|
|
2777
|
+
function nodDy(v, ay, hh) {
|
|
2778
|
+
const up = Math.max(0, ay) / 30;
|
|
2779
|
+
const down = Math.max(0, -ay) / 30;
|
|
2780
|
+
return -(up * v.up + down * v.down) * hh;
|
|
2781
|
+
}
|
|
2782
|
+
function spin(x, y, ox, oy, a) {
|
|
2783
|
+
if (a === 0) return [0, 0];
|
|
2784
|
+
const px = x - ox;
|
|
2785
|
+
const py = y - oy;
|
|
2786
|
+
return [
|
|
2787
|
+
px * Math.cos(a) - py * Math.sin(a) - px,
|
|
2788
|
+
px * Math.sin(a) + py * Math.cos(a) - py
|
|
2789
|
+
];
|
|
2790
|
+
}
|
|
2791
|
+
function faceField(m) {
|
|
2792
|
+
const f = m.frame;
|
|
2793
|
+
return (x, y, ax, ay) => {
|
|
2794
|
+
const s = Math.sign(ax);
|
|
2795
|
+
const a = Math.abs(ax) / 30;
|
|
2796
|
+
const chin = smoothstep(f.mouthY, f.chinY, y);
|
|
2797
|
+
const width = m.widthUpper + (m.widthJaw - m.widthUpper) * smoothstep(f.cheekY, f.jawY, y);
|
|
2798
|
+
const shell = f.hairShell ? 0 : m.shellScale - 1;
|
|
2799
|
+
const dx = s * a * (m.face + m.chinLead * chin) + a * (width - 1 + shell) * (x - f.axisX);
|
|
2800
|
+
const t = Math.min(
|
|
2801
|
+
1,
|
|
2802
|
+
Math.max(0, (f.face.y1 - y) / Math.max(1, f.face.y1 - f.chinY))
|
|
2803
|
+
);
|
|
2804
|
+
const v = {
|
|
2805
|
+
up: NOD.faceTop.up + (NOD.chin.up - NOD.faceTop.up) * t,
|
|
2806
|
+
down: NOD.faceTop.down + (NOD.chin.down - NOD.faceTop.down) * t
|
|
2807
|
+
};
|
|
2808
|
+
return [dx, nodDy(v, ay, f.hh)];
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2811
|
+
function earField(m) {
|
|
2812
|
+
const f = m.frame;
|
|
2813
|
+
const ears = f.ears;
|
|
2814
|
+
if (ears === void 0) return () => [0, 0];
|
|
2815
|
+
const face = faceField(m);
|
|
2816
|
+
return (x, y, ax, ay) => {
|
|
2817
|
+
const s = Math.sign(ax);
|
|
2818
|
+
const a = Math.abs(ax) / 30;
|
|
2819
|
+
const dy = face(x, y, 0, ay)[1];
|
|
2820
|
+
if (sideOf(m, x, s) === "far") return [s * a * TURN.earFar * m.face, dy];
|
|
2821
|
+
const d = Math.abs(x - f.axisX);
|
|
2822
|
+
const attach = ears.attachAt(y);
|
|
2823
|
+
if (d <= attach) return [face(x, y, ax, 0)[0], dy];
|
|
2824
|
+
const root = face(f.axisX + Math.sign(x - f.axisX) * attach, y, ax, 0)[0];
|
|
2825
|
+
const rim = s * a * TURN.earNear * m.face;
|
|
2826
|
+
const t = Math.min(1, (d - attach) / Math.max(1, ears.outer - attach));
|
|
2827
|
+
return [root + (rim - root) * t, dy];
|
|
2828
|
+
};
|
|
2829
|
+
}
|
|
2830
|
+
function chinSlide(m) {
|
|
2831
|
+
const f = m.frame;
|
|
2832
|
+
return Math.abs(faceField(m)(f.axisX, f.chinY, 30, 0)[0]);
|
|
2833
|
+
}
|
|
2834
|
+
var NECK_SHADE_MAX = 0.45;
|
|
2835
|
+
var NECK_SHADE_FULL = 0.2;
|
|
2836
|
+
function neckField(m) {
|
|
2837
|
+
const f = m.frame;
|
|
2838
|
+
const nk = f.neck;
|
|
2839
|
+
if (nk === void 0) return () => [0, 0];
|
|
2840
|
+
const face = faceField(m);
|
|
2841
|
+
const full = chinSlide(m);
|
|
2842
|
+
const share = Math.min(1, NECK_SHADE_MAX * nk.waist / Math.max(1e-6, full));
|
|
2843
|
+
const bottom = f.face.y0;
|
|
2844
|
+
const tip = f.cutAt(f.axisX);
|
|
2845
|
+
const top = tip - NECK_SHADE_FULL * (tip - bottom);
|
|
2846
|
+
return (x, y, ax) => {
|
|
2847
|
+
const wy = Math.min(
|
|
2848
|
+
1,
|
|
2849
|
+
Math.max(0, (y - bottom) / Math.max(1, top - bottom))
|
|
2850
|
+
);
|
|
2851
|
+
const wx = Math.max(0, 1 - Math.abs(x - f.axisX) / nk.waist);
|
|
2852
|
+
return [share * face(f.axisX, f.chinY, ax, 0)[0] * wx * wy, 0];
|
|
2853
|
+
};
|
|
2854
|
+
}
|
|
2855
|
+
function sideOf(m, x, s) {
|
|
2856
|
+
return s * (x - m.frame.axisX) > 0 ? "far" : "near";
|
|
2857
|
+
}
|
|
2858
|
+
function eyeField(m, box) {
|
|
2859
|
+
const xe = cx(box);
|
|
2860
|
+
const ye = cy(box);
|
|
2861
|
+
const hh = m.frame.hh;
|
|
2862
|
+
return (x, y, ax, ay) => {
|
|
2863
|
+
const s = Math.sign(ax);
|
|
2864
|
+
const a = Math.abs(ax) / 30;
|
|
2865
|
+
const side = sideOf(m, xe, s);
|
|
2866
|
+
const dx = s * a * m.eye[side] + a * (m.eyeScale[side] - 1) * (x - xe);
|
|
2867
|
+
const up = Math.max(0, ay) / 30;
|
|
2868
|
+
const down = Math.max(0, -ay) / 30;
|
|
2869
|
+
const squash = up * (NOD.eyeHeight.up - 1) + down * (NOD.eyeHeight.down - 1);
|
|
2870
|
+
return [dx, nodDy(NOD.eye, ay, hh) + squash * (y - ye)];
|
|
2871
|
+
};
|
|
2872
|
+
}
|
|
2873
|
+
function browField(m, box) {
|
|
2874
|
+
const xb = cx(box);
|
|
2875
|
+
return (_x, _y, ax, ay) => {
|
|
2876
|
+
const s = Math.sign(ax);
|
|
2877
|
+
const a = Math.abs(ax) / 30;
|
|
2878
|
+
return [s * a * m.brow[sideOf(m, xb, s)], nodDy(NOD.brow, ay, m.frame.hh)];
|
|
2879
|
+
};
|
|
2880
|
+
}
|
|
2881
|
+
function noseField(m) {
|
|
2882
|
+
const at = m.noseAt;
|
|
2883
|
+
const top = m.noseTopY ?? at.y;
|
|
2884
|
+
return (x, y, ax, ay) => {
|
|
2885
|
+
const s = Math.sign(ax);
|
|
2886
|
+
const a = Math.abs(ax) / 30;
|
|
2887
|
+
const [tx, ty] = spin(
|
|
2888
|
+
x,
|
|
2889
|
+
y,
|
|
2890
|
+
at.x,
|
|
2891
|
+
top,
|
|
2892
|
+
s * a * m.tilt * NOSE_TILT_DEG * DEG
|
|
2893
|
+
);
|
|
2894
|
+
return [s * a * m.nose + tx, nodDy(NOD.nose, ay, m.frame.hh) + ty];
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
function mouthField(m) {
|
|
2898
|
+
const c = m.mouthAt;
|
|
2899
|
+
return (x, y, ax, ay) => {
|
|
2900
|
+
const s = Math.sign(ax);
|
|
2901
|
+
const a = Math.abs(ax) / 30;
|
|
2902
|
+
const wx = c.x + (x - c.x) * (1 + a * (m.mouthWidth - 1));
|
|
2903
|
+
const [tx, ty] = spin(
|
|
2904
|
+
wx,
|
|
2905
|
+
y,
|
|
2906
|
+
c.x,
|
|
2907
|
+
c.y,
|
|
2908
|
+
s * a * m.tilt * TURN.mouthTiltDeg * DEG
|
|
2909
|
+
);
|
|
2910
|
+
return [
|
|
2911
|
+
s * a * m.mouth + wx - x + tx,
|
|
2912
|
+
nodDy(NOD.mouth, ay, m.frame.hh) + ty
|
|
2913
|
+
];
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
function hairField(m, shift, nod) {
|
|
2917
|
+
const f = m.frame;
|
|
2918
|
+
return (x, _y, ax, ay) => {
|
|
2919
|
+
const s = Math.sign(ax);
|
|
2920
|
+
const a = Math.abs(ax) / 30;
|
|
2921
|
+
return [
|
|
2922
|
+
s * a * shift + a * (m.shellScale - 1) * (x - f.axisX),
|
|
2923
|
+
nodDy(nod, ay, f.hh)
|
|
2924
|
+
];
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
function hairFrontBends(frame, boxes) {
|
|
2928
|
+
const f = frame;
|
|
2929
|
+
const eyes = [boxes.eye_L, boxes.eye_R].filter(
|
|
2930
|
+
(e) => e !== void 0
|
|
2931
|
+
);
|
|
2932
|
+
const inner = Math.max(
|
|
2933
|
+
1,
|
|
2934
|
+
Math.min(
|
|
2935
|
+
f.paintedHalfAt(f.eyeY),
|
|
2936
|
+
...eyes.map((e) => Math.max(f.axisX - e.x0, e.x1 - f.axisX))
|
|
2937
|
+
)
|
|
2938
|
+
);
|
|
2939
|
+
return {
|
|
2940
|
+
inner,
|
|
2941
|
+
left: Math.max(inner + 1, f.axisX - f.shellLeft),
|
|
2942
|
+
right: Math.max(inner + 1, f.shellRight - f.axisX)
|
|
2943
|
+
};
|
|
2944
|
+
}
|
|
2945
|
+
function hairFrontField(m) {
|
|
2946
|
+
const f = m.frame;
|
|
2947
|
+
const back = hairField(m, m.hairBack, NOD.hairBack);
|
|
2948
|
+
const cap = { up: NOD.hairFrontTop.up, down: capSlide(m) };
|
|
2949
|
+
const eyes = [m.boxes.eye_L, m.boxes.eye_R].filter(
|
|
2950
|
+
(e) => e !== void 0
|
|
2951
|
+
);
|
|
2952
|
+
const { inner, left, right } = hairFrontBends(f, m.boxes);
|
|
2953
|
+
const eyeY = eyes.length > 0 ? eyes.reduce((t, e) => t + cy(e), 0) / eyes.length : f.eyeY;
|
|
2954
|
+
const eyeH = eyes.length > 0 ? eyes.reduce((t, e) => t + (e.y1 - e.y0), 0) / eyes.length : 1;
|
|
2955
|
+
const coreAt = (x, y, s) => {
|
|
2956
|
+
const u = s * (x - f.axisX);
|
|
2957
|
+
const across = Math.min(1, Math.max(0, (u + inner) / (2 * inner)));
|
|
2958
|
+
const band = 1 - smoothstep(eyeH / 2, 1.5 * eyeH, Math.abs(y - eyeY));
|
|
2959
|
+
return m.hairFront + (m.hairFar - m.hairFront) * across * band;
|
|
2960
|
+
};
|
|
2961
|
+
const followAt = (x, y, s) => {
|
|
2962
|
+
const core = coreAt(x, y, s);
|
|
2963
|
+
const out = Math.min(
|
|
2964
|
+
1,
|
|
2965
|
+
Math.max(
|
|
2966
|
+
0,
|
|
2967
|
+
(Math.abs(x - f.axisX) - inner) / ((x < f.axisX ? left : right) - inner)
|
|
2968
|
+
)
|
|
2969
|
+
);
|
|
2970
|
+
return core + (m.hairOuter - core) * out;
|
|
2971
|
+
};
|
|
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));
|
|
2974
|
+
const drift = Math.abs(m.hairBack);
|
|
2975
|
+
const crown = crownRide(m, cap, (x, y, s) => followOf(x, y, s) + drift);
|
|
2976
|
+
return (x, y, ax, ay) => {
|
|
2977
|
+
const s = Math.sign(ax);
|
|
2978
|
+
const a = Math.abs(ax) / 30;
|
|
2979
|
+
const rx = s * a * followOf(x, y, s) + a * (m.shellScale - 1) * (x - f.axisX);
|
|
2980
|
+
const ry = nodDy(NOD.hairFront, ay, f.hh);
|
|
2981
|
+
const bx = back(x, y, ax, ay)[0];
|
|
2982
|
+
const by = nodDy(cap, ay, f.hh);
|
|
2983
|
+
const w = crownBlend(m, y);
|
|
2984
|
+
const wx = crown === void 0 ? w : w + crown(y, s);
|
|
2985
|
+
return [bx + wx * (rx - bx), by + w * (ry - by)];
|
|
2986
|
+
};
|
|
2987
|
+
}
|
|
2988
|
+
function crownBlend(m, y) {
|
|
2989
|
+
const top = m.frame.face.y1;
|
|
2990
|
+
return 1 - smoothstep(top, Math.max(top + 1, m.hairTop), y);
|
|
2991
|
+
}
|
|
2992
|
+
function gridRow(ys, y, snap = false) {
|
|
2993
|
+
const n = ys.length;
|
|
2994
|
+
let v = clamp((ys[0] - y) / (ys[0] - ys[n - 1]) * (n - 1), 0, n - 1);
|
|
2995
|
+
if (snap && Math.abs(v - Math.round(v)) < 1e-3) v = Math.round(v);
|
|
2996
|
+
const r = Math.min(n - 2, Math.floor(v));
|
|
2997
|
+
return { r, t: v - r };
|
|
2998
|
+
}
|
|
2999
|
+
function renderedCrownBlend(m, ys) {
|
|
3000
|
+
const wr = ys.map((y) => crownBlend(m, y));
|
|
3001
|
+
return {
|
|
3002
|
+
wr,
|
|
3003
|
+
blendAt: (y) => {
|
|
3004
|
+
const { r, t } = gridRow(ys, y);
|
|
3005
|
+
return wr[r] + (wr[r + 1] - wr[r]) * t;
|
|
3006
|
+
}
|
|
3007
|
+
};
|
|
3008
|
+
}
|
|
3009
|
+
var CAP_COVER_LOSS = 0.01;
|
|
3010
|
+
function capSlide(m) {
|
|
3011
|
+
const f = m.frame;
|
|
3012
|
+
const least = NOD.hairBack.down;
|
|
3013
|
+
const grid = m.hairFrontGrid;
|
|
3014
|
+
if (grid === void 0 || f.hairRuns("hair_front", grid.ys[0]) === void 0 || f.hairRuns("hair_back", grid.ys[0]) === void 0) {
|
|
3015
|
+
return least;
|
|
3016
|
+
}
|
|
3017
|
+
const { xs, ys } = grid;
|
|
3018
|
+
const top = ys[0];
|
|
3019
|
+
const n = ys.length;
|
|
3020
|
+
const bangs = NOD.hairFront.down;
|
|
3021
|
+
const { wr, blendAt } = renderedCrownBlend(m, ys);
|
|
3022
|
+
const flat = ys[wr.findIndex((w) => w >= 1)] ?? ys[n - 1];
|
|
3023
|
+
const x0 = xs[0];
|
|
3024
|
+
const tops = new Float64Array(Math.round(xs[xs.length - 1] - x0)).fill(NaN);
|
|
3025
|
+
for (let i = 0; i < top - flat; i++) {
|
|
3026
|
+
const runs = f.hairRuns("hair_front", top - i - 0.5);
|
|
3027
|
+
for (let k = 0; k < runs.length; k += 2) {
|
|
3028
|
+
for (let px = runs[k]; px < runs[k + 1]; px++) {
|
|
3029
|
+
const j = Math.round(px - x0);
|
|
3030
|
+
if (Number.isNaN(tops[j])) tops[j] = i;
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
const hh = f.hh;
|
|
3035
|
+
const most = Math.ceil(Math.max(bangs, NOD.hairFrontTop.down) * hh);
|
|
3036
|
+
const bared = (s) => clamp(Math.ceil(s - 0.5), 0, most);
|
|
3037
|
+
const dropAt = (w, slide) => (w * bangs + (1 - w) * slide) * hh;
|
|
3038
|
+
const backNod = NOD.hairBack.down * hh;
|
|
3039
|
+
const backRows = /* @__PURE__ */ new Map();
|
|
3040
|
+
const columns = [];
|
|
3041
|
+
tops.forEach((i, j) => {
|
|
3042
|
+
if (Number.isNaN(i)) return;
|
|
3043
|
+
const w = blendAt(top - i);
|
|
3044
|
+
const empty = new Int32Array(most + 1);
|
|
3045
|
+
for (let r = 0; r < most; r++) {
|
|
3046
|
+
const at = Math.floor(i + r + 0.5 - backNod);
|
|
3047
|
+
let runs = backRows.get(at);
|
|
3048
|
+
if (runs === void 0) {
|
|
3049
|
+
runs = f.hairRuns("hair_back", top - at - 0.5);
|
|
3050
|
+
backRows.set(at, runs);
|
|
3051
|
+
}
|
|
3052
|
+
empty[r + 1] = empty[r] + (paints(runs, x0 + j) ? 0 : 1);
|
|
3053
|
+
}
|
|
3054
|
+
columns.push({ w, empty, base: empty[bared(dropAt(w, least))] });
|
|
3055
|
+
});
|
|
3056
|
+
const budget = CAP_COVER_LOSS * hh;
|
|
3057
|
+
const fits = (slide) => columns.every((c) => c.empty[bared(dropAt(c.w, slide))] - c.base <= budget);
|
|
3058
|
+
let lo = least;
|
|
3059
|
+
let hi = NOD.hairFrontTop.down;
|
|
3060
|
+
if (fits(hi)) return hi;
|
|
3061
|
+
for (let k = 0; k < 30; k++) {
|
|
3062
|
+
const mid = (lo + hi) / 2;
|
|
3063
|
+
if (fits(mid)) lo = mid;
|
|
3064
|
+
else hi = mid;
|
|
3065
|
+
}
|
|
3066
|
+
return lo;
|
|
3067
|
+
}
|
|
3068
|
+
function paints(runs, px) {
|
|
3069
|
+
for (let k = 0; k < runs.length; k += 2) {
|
|
3070
|
+
if (runs[k] <= px && px + 1 <= runs[k + 1]) return true;
|
|
3071
|
+
}
|
|
3072
|
+
return false;
|
|
3073
|
+
}
|
|
3074
|
+
function outerRide(m, inner, coreAt, followAt) {
|
|
3075
|
+
const f = m.frame;
|
|
3076
|
+
const ys = m.hairFrontGrid?.ys;
|
|
3077
|
+
if (ys === void 0) return void 0;
|
|
3078
|
+
const top = ys[0];
|
|
3079
|
+
if (f.hairRuns("hair_front", top) === void 0 || f.hairRuns("hair_back", top) === void 0) {
|
|
3080
|
+
return void 0;
|
|
3081
|
+
}
|
|
3082
|
+
const drift = Math.abs(m.hairBack);
|
|
3083
|
+
const sides = [-1, 1];
|
|
3084
|
+
const n = ys.length;
|
|
3085
|
+
const count = Math.round(top - ys[n - 1]);
|
|
3086
|
+
const below = (NOD.hairFront.down - NOD.hairBack.down) * f.hh;
|
|
3087
|
+
const above = (NOD.hairBack.up - NOD.hairFront.up) * f.hh;
|
|
3088
|
+
const j0 = Math.floor(-above);
|
|
3089
|
+
const backRows = Array.from(
|
|
3090
|
+
{ length: Math.ceil(count + below) - j0 },
|
|
3091
|
+
(_, j) => f.hairRuns("hair_back", top - (j + j0) - 0.5)
|
|
3092
|
+
);
|
|
3093
|
+
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));
|
|
3096
|
+
for (let i = 0; i < count; i++) {
|
|
3097
|
+
const y = top - i - 0.5;
|
|
3098
|
+
const front = f.hairRuns("hair_front", y);
|
|
3099
|
+
sides.forEach((side, k) => {
|
|
3100
|
+
const edge = outerEdge(front, f.axisX, side);
|
|
3101
|
+
if (edge === void 0) return;
|
|
3102
|
+
let outward = Infinity;
|
|
3103
|
+
let inward = Infinity;
|
|
3104
|
+
for (let j = Math.floor(i - above); j < Math.ceil(i + 1 + below); j++) {
|
|
3105
|
+
const c = runReach(backRows[j - j0], edge, side);
|
|
3106
|
+
outward = Math.min(outward, c.outward);
|
|
3107
|
+
inward = Math.min(inward, c.inward);
|
|
3108
|
+
}
|
|
3109
|
+
offset[k][i] = side * (edge - f.axisX);
|
|
3110
|
+
far[k][i] = m.shellScale * outward - drift;
|
|
3111
|
+
near[k][i] = m.shellScale * inward - drift;
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
3114
|
+
const end = new Float64Array(n * 2);
|
|
3115
|
+
const lift = new Float64Array(n * 4);
|
|
3116
|
+
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)]));
|
|
3119
|
+
sides.forEach((side, k) => {
|
|
3120
|
+
let d = Infinity;
|
|
3121
|
+
let allowFar = Infinity;
|
|
3122
|
+
let allowNear = Infinity;
|
|
3123
|
+
for (let i = i0; i < i1; i++) {
|
|
3124
|
+
if (Number.isNaN(offset[k][i])) continue;
|
|
3125
|
+
d = Math.min(d, offset[k][i]);
|
|
3126
|
+
allowFar = Math.min(allowFar, far[k][i]);
|
|
3127
|
+
allowNear = Math.min(allowNear, near[k][i]);
|
|
3128
|
+
}
|
|
3129
|
+
end[r * 2 + k] = d;
|
|
3130
|
+
if (d === Infinity || d <= inner) return;
|
|
3131
|
+
const x = f.axisX + side * d;
|
|
3132
|
+
sides.forEach((s, j) => {
|
|
3133
|
+
const allow = Math.min(
|
|
3134
|
+
coreAt(x, ys[r], s),
|
|
3135
|
+
s === side ? allowFar : allowNear
|
|
3136
|
+
);
|
|
3137
|
+
lift[(r * 2 + k) * 2 + j] = Math.max(0, allow - followAt(x, ys[r], s));
|
|
3138
|
+
});
|
|
3139
|
+
});
|
|
3140
|
+
}
|
|
3141
|
+
const rowRide = (r, k, j, d) => {
|
|
3142
|
+
const v = lift[(r * 2 + k) * 2 + j];
|
|
3143
|
+
return v === 0 ? 0 : v * Math.min(1, (d - inner) / (end[r * 2 + k] - inner));
|
|
3144
|
+
};
|
|
3145
|
+
return (x, y, s) => {
|
|
3146
|
+
const d = Math.abs(x - f.axisX);
|
|
3147
|
+
if (s === 0 || d <= inner) return 0;
|
|
3148
|
+
const k = x < f.axisX ? 0 : 1;
|
|
3149
|
+
const j = s < 0 ? 0 : 1;
|
|
3150
|
+
const { r, t } = gridRow(ys, y, true);
|
|
3151
|
+
return rowRide(r, k, j, d) * (1 - t) + rowRide(r + 1, k, j, d) * t;
|
|
3152
|
+
};
|
|
3153
|
+
}
|
|
3154
|
+
function crownRide(m, cap, relAt) {
|
|
3155
|
+
const f = m.frame;
|
|
3156
|
+
const grid = m.hairFrontGrid;
|
|
3157
|
+
if (grid === void 0) return void 0;
|
|
3158
|
+
const { xs, ys } = grid;
|
|
3159
|
+
const top = ys[0];
|
|
3160
|
+
if (f.hairRuns("hair_front", top) === void 0 || f.hairRuns("hair_back", top) === void 0) {
|
|
3161
|
+
return void 0;
|
|
3162
|
+
}
|
|
3163
|
+
const n = ys.length;
|
|
3164
|
+
const count = Math.round(top - ys[n - 1]);
|
|
3165
|
+
const hh = f.hh;
|
|
3166
|
+
const face = faceField(m);
|
|
3167
|
+
const { wr, blendAt } = renderedCrownBlend(m, ys);
|
|
3168
|
+
const backRows = /* @__PURE__ */ new Map();
|
|
3169
|
+
const backAt = (j) => {
|
|
3170
|
+
let runs = backRows.get(j);
|
|
3171
|
+
if (runs === void 0) {
|
|
3172
|
+
runs = f.hairRuns("hair_back", top - j - 0.5);
|
|
3173
|
+
backRows.set(j, runs);
|
|
3174
|
+
}
|
|
3175
|
+
return runs;
|
|
3176
|
+
};
|
|
3177
|
+
const faceAt = (j) => {
|
|
3178
|
+
const y = top - j - 0.5;
|
|
3179
|
+
return f.faceRuns(y) ?? (y > f.face.y0 && y < f.face.y1 ? [f.face.x0, f.face.x1] : []);
|
|
3180
|
+
};
|
|
3181
|
+
const passed = (i, dys) => [
|
|
3182
|
+
Math.floor(i - Math.max(0, ...dys)),
|
|
3183
|
+
Math.ceil(i + 1 - Math.min(0, ...dys))
|
|
3184
|
+
];
|
|
3185
|
+
const rel = new Float64Array(n * xs.length * 2).fill(NaN);
|
|
3186
|
+
const relOf = (r, c, k) => {
|
|
3187
|
+
const at = (r * xs.length + c) * 2 + k;
|
|
3188
|
+
if (Number.isNaN(rel[at])) rel[at] = relAt(xs[c], ys[r], k === 0 ? -1 : 1);
|
|
3189
|
+
return rel[at];
|
|
3190
|
+
};
|
|
3191
|
+
const cellR = (x, y, k) => {
|
|
3192
|
+
let c = 0;
|
|
3193
|
+
while (c < xs.length - 2 && xs[c + 1] < x) c++;
|
|
3194
|
+
const { r } = gridRow(ys, y);
|
|
3195
|
+
return Math.max(
|
|
3196
|
+
relOf(r, c, k),
|
|
3197
|
+
relOf(r, c + 1, k),
|
|
3198
|
+
relOf(r + 1, c, k),
|
|
3199
|
+
relOf(r + 1, c + 1, k)
|
|
3200
|
+
);
|
|
3201
|
+
};
|
|
3202
|
+
const someAngle = (pairs) => {
|
|
3203
|
+
let lo = 0;
|
|
3204
|
+
let hi = 1;
|
|
3205
|
+
for (const [c0, c1] of pairs) {
|
|
3206
|
+
if (c1 === 0) {
|
|
3207
|
+
if (c0 <= 0) return false;
|
|
3208
|
+
} else if (c1 > 0) lo = Math.max(lo, -c0 / c1);
|
|
3209
|
+
else hi = Math.min(hi, -c0 / c1);
|
|
3210
|
+
}
|
|
3211
|
+
return lo < hi;
|
|
3212
|
+
};
|
|
3213
|
+
const slack = [-1, 1].map(() => new Float64Array(count).fill(NaN));
|
|
3214
|
+
const slackAt = (i, k) => {
|
|
3215
|
+
if (!Number.isNaN(slack[k][i])) return slack[k][i];
|
|
3216
|
+
const y = top - i - 0.5;
|
|
3217
|
+
const w = blendAt(y);
|
|
3218
|
+
const nod = {
|
|
3219
|
+
up: w * NOD.hairFront.up + (1 - w) * cap.up,
|
|
3220
|
+
down: w * NOD.hairFront.down + (1 - w) * cap.down
|
|
3221
|
+
};
|
|
3222
|
+
const ays = [-30, 30];
|
|
3223
|
+
const [b0, b1] = passed(
|
|
3224
|
+
i,
|
|
3225
|
+
ays.map((ay) => nodDy(nod, ay, hh) - nodDy(NOD.hairBack, ay, hh))
|
|
3226
|
+
);
|
|
3227
|
+
const band = [];
|
|
3228
|
+
for (let j = b0; j < b1; j++) band.push(backAt(j));
|
|
3229
|
+
const faces = [];
|
|
3230
|
+
const [f0, f1] = passed(
|
|
3231
|
+
i,
|
|
3232
|
+
ays.map((ay) => nodDy(nod, ay, hh) - face(f.axisX, y, 0, ay)[1])
|
|
3233
|
+
);
|
|
3234
|
+
for (let j = f0; j < f1; j++)
|
|
3235
|
+
faces.push({ y: top - j - 0.5, runs: faceAt(j) });
|
|
3236
|
+
const front = f.hairRuns("hair_front", y);
|
|
3237
|
+
[-1, 1].forEach((s, side) => {
|
|
3238
|
+
let least = Infinity;
|
|
3239
|
+
const far = outerEdge(front, f.axisX, s);
|
|
3240
|
+
if (far !== void 0) {
|
|
3241
|
+
const R = cellR(far, y, side);
|
|
3242
|
+
const c = Math.min(...band.map((b) => runReach(b, far, s).outward));
|
|
3243
|
+
if (R > 0) least = Math.min(least, m.shellScale * c / R);
|
|
3244
|
+
}
|
|
3245
|
+
for (let t = s > 0 ? 0 : 1; t < front.length; t += 2) {
|
|
3246
|
+
const x = front[t];
|
|
3247
|
+
const R = cellR(x, y, side);
|
|
3248
|
+
if (R <= 0) continue;
|
|
3249
|
+
let c = Math.min(
|
|
3250
|
+
...band.map((b) => runReach(b, x, s > 0 ? -1 : 1).inward)
|
|
3251
|
+
);
|
|
3252
|
+
const sh = (m.shellScale - 1) * (x - f.axisX);
|
|
3253
|
+
const d0 = sh + s * m.hairBack;
|
|
3254
|
+
const d1 = d0 + s * R;
|
|
3255
|
+
const lo = Math.min(d0, d1);
|
|
3256
|
+
const hi = Math.max(d0, d1);
|
|
3257
|
+
const bares = faces.some(({ y: fy, runs }) => {
|
|
3258
|
+
for (let r = 0; r < runs.length; r += 2) {
|
|
3259
|
+
const e0 = runs[r];
|
|
3260
|
+
const e1 = runs[r + 1];
|
|
3261
|
+
const de0 = face(e0, fy, 30 * s, 0)[0];
|
|
3262
|
+
const de1 = face(e1, fy, 30 * s, 0)[0];
|
|
3263
|
+
if (someAngle([
|
|
3264
|
+
[e1 - x, de1 - lo],
|
|
3265
|
+
[x - e0, hi - de0]
|
|
3266
|
+
])) {
|
|
3267
|
+
return true;
|
|
3268
|
+
}
|
|
3031
3269
|
}
|
|
3032
|
-
return
|
|
3033
|
-
};
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
return y >= face.y1 ? face.y1 : Math.max(face.y0, y - CUT_MARGIN);
|
|
3037
|
-
};
|
|
3270
|
+
return false;
|
|
3271
|
+
});
|
|
3272
|
+
if (bares) c = 0;
|
|
3273
|
+
least = Math.min(least, m.shellScale * c / R);
|
|
3038
3274
|
}
|
|
3039
|
-
|
|
3275
|
+
slack[side][i] = least - w;
|
|
3276
|
+
});
|
|
3277
|
+
return slack[k][i];
|
|
3278
|
+
};
|
|
3279
|
+
const extra = [0, 1].map((k) => {
|
|
3280
|
+
const out = wr.slice();
|
|
3281
|
+
for (let r = 0; r < n; r++) {
|
|
3282
|
+
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)]));
|
|
3285
|
+
let least = Infinity;
|
|
3286
|
+
for (let i = i0; i < i1; i++) least = Math.min(least, slackAt(i, k));
|
|
3287
|
+
out[r] = wr[r] + clamp(least, 0, 1 - wr[r]);
|
|
3288
|
+
}
|
|
3289
|
+
for (let r = n - 2; r >= 0; r--) {
|
|
3290
|
+
if (wr[r] < 1) out[r] = Math.max(wr[r], Math.min(out[r], out[r + 1]));
|
|
3291
|
+
}
|
|
3292
|
+
return out.map((v, r) => v - wr[r]);
|
|
3293
|
+
});
|
|
3294
|
+
return (y, s) => {
|
|
3295
|
+
if (s === 0) return 0;
|
|
3296
|
+
const e = extra[s < 0 ? 0 : 1];
|
|
3297
|
+
const { r, t } = gridRow(ys, y, true);
|
|
3298
|
+
return e[r] * (1 - t) + e[r + 1] * t;
|
|
3299
|
+
};
|
|
3300
|
+
}
|
|
3301
|
+
function familyField(m, family) {
|
|
3302
|
+
switch (family) {
|
|
3303
|
+
case "face":
|
|
3304
|
+
return faceField(m);
|
|
3305
|
+
case "eye_L":
|
|
3306
|
+
case "eye_R":
|
|
3307
|
+
return eyeField(m, m.boxes[family]);
|
|
3308
|
+
case "brow_L":
|
|
3309
|
+
case "brow_R":
|
|
3310
|
+
return browField(m, m.boxes[family]);
|
|
3311
|
+
case "nose":
|
|
3312
|
+
return noseField(m);
|
|
3313
|
+
case "mouth":
|
|
3314
|
+
return mouthField(m);
|
|
3315
|
+
case "hair_front":
|
|
3316
|
+
return hairFrontField(m);
|
|
3317
|
+
case "hair_back":
|
|
3318
|
+
return hairField(m, m.hairBack, NOD.hairBack);
|
|
3319
|
+
case "body":
|
|
3320
|
+
return () => [0, 0];
|
|
3040
3321
|
}
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
// src/auto-rig/context.ts
|
|
3325
|
+
function sidePair(byRole, a, b) {
|
|
3326
|
+
const la = byRole.get(a);
|
|
3327
|
+
const lb = byRole.get(b);
|
|
3328
|
+
if (la === void 0 || lb === void 0) return void 0;
|
|
3329
|
+
const [lo, hi] = [
|
|
3330
|
+
{ role: a, box: boxOfLayer(la) },
|
|
3331
|
+
{ role: b, box: boxOfLayer(lb) }
|
|
3332
|
+
].sort((p, q) => cx(p.box) - cx(q.box));
|
|
3333
|
+
return { left: lo, right: hi };
|
|
3334
|
+
}
|
|
3335
|
+
function sideIrises(byRole) {
|
|
3336
|
+
const pair = sidePair(byRole, "iris_L", "iris_R");
|
|
3337
|
+
return pair === void 0 ? {} : { left: pair.left.box, right: pair.right.box };
|
|
3338
|
+
}
|
|
3339
|
+
function solveContext(frame, byRole, options, hasNose) {
|
|
3340
|
+
const box = (role) => boxOfLayer(byRole.get(role));
|
|
3341
|
+
const row = (s, strand2, sideSign) => {
|
|
3342
|
+
if (strand2 !== void 0) {
|
|
3343
|
+
const lo = sideSign < 0 ? strand2.irisOuter : strand2.irisInner;
|
|
3344
|
+
const hi = sideSign < 0 ? strand2.irisInner : strand2.irisOuter;
|
|
3345
|
+
return { role: s.role, lo, hi, y: strand2.y };
|
|
3053
3346
|
}
|
|
3347
|
+
const inset = bw(s.box) > 4 ? 1 : 0;
|
|
3348
|
+
return {
|
|
3349
|
+
role: s.role,
|
|
3350
|
+
lo: s.box.x0 + inset,
|
|
3351
|
+
hi: s.box.x1 - inset,
|
|
3352
|
+
y: cy(s.box)
|
|
3353
|
+
};
|
|
3354
|
+
};
|
|
3355
|
+
const eyes = sidePair(byRole, "eye_L", "eye_R");
|
|
3356
|
+
const irises = sidePair(byRole, "iris_L", "iris_R");
|
|
3357
|
+
const pair = irises ?? eyes;
|
|
3358
|
+
const strand = irises ? options.strandEdges : void 0;
|
|
3359
|
+
const left = row(pair.left, strand?.left, -1);
|
|
3360
|
+
const right = row(pair.right, strand?.right, 1);
|
|
3361
|
+
let candidates;
|
|
3362
|
+
const edges = options.headEdges;
|
|
3363
|
+
if (frame.hairShell && edges && edges.left.length > 0 && edges.right.length > 0) {
|
|
3364
|
+
candidates = {
|
|
3365
|
+
left: edges.left.map((e) => ({ role: e.role, x: e.x })),
|
|
3366
|
+
// A right edge names its outermost pixel column; the boundary is one on.
|
|
3367
|
+
right: edges.right.map((e) => ({ role: e.role, x: e.x + 1 }))
|
|
3368
|
+
};
|
|
3369
|
+
} else if (frame.hairShell) {
|
|
3370
|
+
candidates = {
|
|
3371
|
+
left: [{ role: "hair_back", x: frame.shellLeft }],
|
|
3372
|
+
right: [{ role: "hair_back", x: frame.shellRight }]
|
|
3373
|
+
};
|
|
3374
|
+
} else {
|
|
3375
|
+
const w = frame.paintedHalfAt(frame.eyeY);
|
|
3376
|
+
candidates = {
|
|
3377
|
+
left: [{ role: "face", x: frame.axisX - w }],
|
|
3378
|
+
right: [{ role: "face", x: frame.axisX + w }]
|
|
3379
|
+
};
|
|
3380
|
+
}
|
|
3381
|
+
const restHalf = (Math.max(...candidates.right.map((c) => c.x)) - Math.min(...candidates.left.map((c) => c.x))) / 2;
|
|
3382
|
+
const nose = byRole.get("nose");
|
|
3383
|
+
let noseAt;
|
|
3384
|
+
let noseBox;
|
|
3385
|
+
if (hasNose && nose !== void 0) {
|
|
3386
|
+
noseBox = nose.denseCore ? boxOfPixels(nose.denseCore, nose.canvasW, nose.canvasH) : boxOfLayer(nose);
|
|
3387
|
+
noseAt = { x: cx(noseBox), y: cy(noseBox) };
|
|
3054
3388
|
}
|
|
3389
|
+
const mouthBox = byRole.has("mouth_open") ? unionBoxes([box("mouth"), box("mouth_open")]) : box("mouth");
|
|
3390
|
+
const m = box("mouth");
|
|
3055
3391
|
return {
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3392
|
+
frame,
|
|
3393
|
+
left,
|
|
3394
|
+
right,
|
|
3395
|
+
candidates,
|
|
3396
|
+
restHalf,
|
|
3397
|
+
strand,
|
|
3398
|
+
hasHairFront: byRole.has("hair_front"),
|
|
3399
|
+
eyeBoxes: { left: eyes.left.box, right: eyes.right.box },
|
|
3400
|
+
noseAt,
|
|
3401
|
+
noseBox,
|
|
3402
|
+
mouthAt: { x: cx(m), y: cy(m) },
|
|
3403
|
+
mouthBox
|
|
3404
|
+
};
|
|
3405
|
+
}
|
|
3406
|
+
function renderedLander(turn, parts, grids, from = /* @__PURE__ */ new Map()) {
|
|
3407
|
+
const analytic = /* @__PURE__ */ new Map();
|
|
3408
|
+
const landed = /* @__PURE__ */ new Map();
|
|
3409
|
+
const field = (role) => {
|
|
3410
|
+
const family = roleSpec(role).family;
|
|
3411
|
+
let f = analytic.get(family);
|
|
3412
|
+
if (f === void 0) analytic.set(family, f = familyField(turn, family));
|
|
3413
|
+
return f;
|
|
3414
|
+
};
|
|
3415
|
+
return (role, x, y, ax) => {
|
|
3416
|
+
const family = roleSpec(role).family;
|
|
3417
|
+
const part = parts.find((p) => p.id === role);
|
|
3418
|
+
const g = grids.get(family);
|
|
3419
|
+
const warp = part?.warps?.find((w) => w.parameter === P.AngleX);
|
|
3420
|
+
if (part === void 0 || part.mesh === void 0 || !g && !warp) {
|
|
3421
|
+
return x + field(role)(x, y, ax, 0)[0];
|
|
3422
|
+
}
|
|
3423
|
+
const box = {
|
|
3424
|
+
x0: part.transform.x - part.width / 2,
|
|
3425
|
+
x1: part.transform.x + part.width / 2,
|
|
3426
|
+
y0: part.transform.y - part.height / 2,
|
|
3427
|
+
y1: part.transform.y + part.height / 2
|
|
3428
|
+
};
|
|
3429
|
+
const rest = meshPoints(part.mesh, box);
|
|
3430
|
+
const key = `${role}@${ax}`;
|
|
3431
|
+
let xs = landed.get(key);
|
|
3432
|
+
if (xs === void 0) {
|
|
3433
|
+
if (g) {
|
|
3434
|
+
const offsets = keyformAt(g.keyforms, ax);
|
|
3435
|
+
xs = rest.map(([px, py]) => land(g.lattice, offsets, px, py)[0]);
|
|
3436
|
+
} else {
|
|
3437
|
+
const k = warp.keyforms;
|
|
3438
|
+
const offsets = blendStops(
|
|
3439
|
+
k.map((kf) => kf.value),
|
|
3440
|
+
k.map((kf) => kf.offsets),
|
|
3441
|
+
ax
|
|
3442
|
+
);
|
|
3443
|
+
xs = rest.map(([px], i) => px + offsets[2 * i] * bw(box));
|
|
3444
|
+
}
|
|
3445
|
+
landed.set(key, xs);
|
|
3446
|
+
}
|
|
3447
|
+
const idx = part.mesh.indices;
|
|
3448
|
+
for (let t = from.get(role) ?? 0; t < idx.length; t += 3) {
|
|
3449
|
+
const [a, b, c] = [idx[t], idx[t + 1], idx[t + 2]];
|
|
3450
|
+
const [ax0, ay0] = rest[a];
|
|
3451
|
+
const [bx0, by0] = rest[b];
|
|
3452
|
+
const [cx0, cy0] = rest[c];
|
|
3453
|
+
const d = (by0 - cy0) * (ax0 - cx0) + (cx0 - bx0) * (ay0 - cy0);
|
|
3454
|
+
if (Math.abs(d) < 1e-9) continue;
|
|
3455
|
+
const u = ((by0 - cy0) * (x - cx0) + (cx0 - bx0) * (y - cy0)) / d;
|
|
3456
|
+
const v = ((cy0 - ay0) * (x - cx0) + (ax0 - cx0) * (y - cy0)) / d;
|
|
3457
|
+
const w = 1 - u - v;
|
|
3458
|
+
if (u < -1e-6 || v < -1e-6 || w < -1e-6) continue;
|
|
3459
|
+
return u * xs[a] + v * xs[b] + w * xs[c];
|
|
3460
|
+
}
|
|
3461
|
+
return x + field(role)(x, y, ax, 0)[0];
|
|
3077
3462
|
};
|
|
3078
3463
|
}
|
|
3079
3464
|
|
|
@@ -3954,14 +4339,18 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3954
4339
|
};
|
|
3955
4340
|
const solveFrame = buildHeadFrame(layers, frameOptions);
|
|
3956
4341
|
const ctx = solveContext(solveFrame, byRole, turnOptions, hasNose);
|
|
4342
|
+
const boxes = Object.fromEntries(
|
|
4343
|
+
["eye_L", "eye_R", "brow_L", "brow_R"].filter(has).map((r) => [r, box(r)])
|
|
4344
|
+
);
|
|
3957
4345
|
const landmarks = {
|
|
3958
|
-
boxes
|
|
3959
|
-
["eye_L", "eye_R", "brow_L", "brow_R"].filter(has).map((r) => [r, box(r)])
|
|
3960
|
-
),
|
|
4346
|
+
boxes,
|
|
3961
4347
|
noseAt: ctx.noseAt,
|
|
3962
4348
|
noseTopY: ctx.noseBox?.y1,
|
|
3963
4349
|
mouthAt: ctx.mouthAt,
|
|
3964
|
-
hairTop: has("hair_front") ? box("hair_front").y1 : solveFrame.face.y1
|
|
4350
|
+
hairTop: has("hair_front") ? box("hair_front").y1 : solveFrame.face.y1,
|
|
4351
|
+
// The field reads the grid the mesh is built on (the rebuilt frame below
|
|
4352
|
+
// differs only in the jaw's cut, which the grid does not read).
|
|
4353
|
+
hairFrontGrid: has("hair_front") ? hairFrontGrid(box("hair_front"), solveFrame, boxes) : void 0
|
|
3965
4354
|
};
|
|
3966
4355
|
const build = (q) => buildTurn(solveFrame, landmarks, q);
|
|
3967
4356
|
const fit = fitTurn(ctx, targets, style, build);
|
|
@@ -3983,7 +4372,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3983
4372
|
parts.length,
|
|
3984
4373
|
frame,
|
|
3985
4374
|
byRole,
|
|
3986
|
-
style
|
|
4375
|
+
style,
|
|
4376
|
+
landmarks.hairFrontGrid
|
|
3987
4377
|
);
|
|
3988
4378
|
if (built.region !== void 0) {
|
|
3989
4379
|
regionOf = built.region;
|
|
@@ -4074,7 +4464,7 @@ function plateFields(turn, region, head) {
|
|
|
4074
4464
|
const neck = neckField(turn);
|
|
4075
4465
|
return (i) => region[i] === "ear" ? ear : region[i] === "neck" ? neck : head;
|
|
4076
4466
|
}
|
|
4077
|
-
function buildPart(spec, b, order, frame, byRole, style) {
|
|
4467
|
+
function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
|
|
4078
4468
|
const box = (role) => boxOfLayer(byRole.get(role));
|
|
4079
4469
|
const part = {
|
|
4080
4470
|
id: spec.role,
|
|
@@ -4097,30 +4487,15 @@ function buildPart(spec, b, order, frame, byRole, style) {
|
|
|
4097
4487
|
mesh = fm.mesh;
|
|
4098
4488
|
region = fm.region;
|
|
4099
4489
|
headStart = fm.headStart;
|
|
4490
|
+
} else if (spec.role === "hair_front") {
|
|
4491
|
+
mesh = columnMesh(b, frontGrid);
|
|
4100
4492
|
} else if (spec.family !== "body") {
|
|
4101
4493
|
const cells = MESH_CELLS[spec.role] ?? FEATURE_MESH_CELLS;
|
|
4102
4494
|
const px = Math.min(cells.px, meshScale(spec.role, frame));
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
["eye_L", "eye_R"].filter((r) => byRole.has(r)).map((r) => [r, box(r)])
|
|
4108
|
-
);
|
|
4109
|
-
const { inner, left, right } = hairFrontBends(frame, boxes);
|
|
4110
|
-
mesh = columnMesh(
|
|
4111
|
-
b,
|
|
4112
|
-
[
|
|
4113
|
-
frame.axisX - left,
|
|
4114
|
-
frame.axisX - inner,
|
|
4115
|
-
frame.axisX + inner,
|
|
4116
|
-
frame.axisX + right
|
|
4117
|
-
],
|
|
4118
|
-
cols,
|
|
4119
|
-
rows
|
|
4120
|
-
);
|
|
4121
|
-
} else {
|
|
4122
|
-
mesh = gridMesh(cols, rows);
|
|
4123
|
-
}
|
|
4495
|
+
mesh = gridMesh(
|
|
4496
|
+
cellsFor(bw(b), px, cells.min, cells.max),
|
|
4497
|
+
cellsFor(bh(b), px, cells.min, cells.max)
|
|
4498
|
+
);
|
|
4124
4499
|
}
|
|
4125
4500
|
if (mesh !== void 0) part.mesh = mesh;
|
|
4126
4501
|
const side = spec.role.endsWith("_L") ? "L" : "R";
|
|
@@ -4239,16 +4614,35 @@ function buildPart(spec, b, order, frame, byRole, style) {
|
|
|
4239
4614
|
headStart
|
|
4240
4615
|
};
|
|
4241
4616
|
}
|
|
4242
|
-
function
|
|
4617
|
+
function hairFrontGrid(b, frame, boxes) {
|
|
4618
|
+
const cells = MESH_CELLS.hair_front;
|
|
4619
|
+
const px = Math.min(cells.px, meshScale("hair_front", frame));
|
|
4620
|
+
const cols = cellsFor(bw(b), px, cells.min, cells.max);
|
|
4621
|
+
const rows = cellsFor(bh(b), px, cells.min, cells.max);
|
|
4622
|
+
const { inner, left, right } = hairFrontBends(frame, boxes);
|
|
4243
4623
|
const step = bw(b) / cols;
|
|
4244
4624
|
const xs = Array.from({ length: cols + 1 }, (_, c) => b.x0 + c * step);
|
|
4245
|
-
for (const x of
|
|
4625
|
+
for (const x of [
|
|
4626
|
+
frame.axisX - left,
|
|
4627
|
+
frame.axisX - inner,
|
|
4628
|
+
frame.axisX + inner,
|
|
4629
|
+
frame.axisX + right
|
|
4630
|
+
]) {
|
|
4246
4631
|
if (x <= b.x0 + 0.25 || x >= b.x1 - 0.25) continue;
|
|
4247
4632
|
const near = xs.findIndex((v) => Math.abs(v - x) < 0.3 * step);
|
|
4248
4633
|
if (near > 0 && near < xs.length - 1) xs[near] = x;
|
|
4249
4634
|
else if (near < 0) xs.push(x);
|
|
4250
4635
|
}
|
|
4251
4636
|
xs.sort((p, q) => p - q);
|
|
4637
|
+
const ys = Array.from(
|
|
4638
|
+
{ length: rows + 1 },
|
|
4639
|
+
(_, r) => b.y1 - bh(b) * r / rows
|
|
4640
|
+
);
|
|
4641
|
+
return { xs, ys };
|
|
4642
|
+
}
|
|
4643
|
+
function columnMesh(b, grid) {
|
|
4644
|
+
const { xs } = grid;
|
|
4645
|
+
const rows = grid.ys.length - 1;
|
|
4252
4646
|
const vertices = [];
|
|
4253
4647
|
const uvs = [];
|
|
4254
4648
|
for (let r = 0; r <= rows; r++) {
|