@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.js
CHANGED
|
@@ -1632,7 +1632,6 @@ function createLayerSetMeasurer(canvas) {
|
|
|
1632
1632
|
const opaque = new Uint8Array(canvasW * canvasH);
|
|
1633
1633
|
const layerInputs = [];
|
|
1634
1634
|
const rowSpansByRole = [];
|
|
1635
|
-
let hairFrontMask;
|
|
1636
1635
|
function add({ role, fileName, rgba }) {
|
|
1637
1636
|
if (rgba.length !== rgbaLength) {
|
|
1638
1637
|
throw new Error(
|
|
@@ -1643,19 +1642,29 @@ function createLayerSetMeasurer(canvas) {
|
|
|
1643
1642
|
if (bbox === null) return null;
|
|
1644
1643
|
const rowLeft = new Int32Array(canvasH).fill(canvasW);
|
|
1645
1644
|
const rowRight = new Int32Array(canvasH).fill(-1);
|
|
1646
|
-
const
|
|
1645
|
+
const rowRuns = role === "face" || role === "hair_front" || role === "hair_back" ? [] : void 0;
|
|
1647
1646
|
for (let y = 0; y < canvasH; y++) {
|
|
1647
|
+
let runs;
|
|
1648
|
+
if (rowRuns !== void 0 && y >= bbox.y && y < bbox.y + bbox.h) {
|
|
1649
|
+
runs = [];
|
|
1650
|
+
rowRuns.push(runs);
|
|
1651
|
+
}
|
|
1652
|
+
let inRun = false;
|
|
1648
1653
|
for (let x = 0; x < canvasW; x++) {
|
|
1649
1654
|
const p = y * canvasW + x;
|
|
1650
|
-
|
|
1655
|
+
const on = rgba[p * 4 + 3] >= ALPHA_OPAQUE;
|
|
1656
|
+
if (runs !== void 0 && on !== inRun) {
|
|
1657
|
+
runs.push(x);
|
|
1658
|
+
inRun = on;
|
|
1659
|
+
}
|
|
1660
|
+
if (!on) continue;
|
|
1651
1661
|
opaque[p] = 1;
|
|
1652
|
-
if (ownMask !== void 0) ownMask[p] = 1;
|
|
1653
1662
|
if (x < rowLeft[y]) rowLeft[y] = x;
|
|
1654
1663
|
if (x > rowRight[y]) rowRight[y] = x;
|
|
1655
1664
|
}
|
|
1665
|
+
if (runs !== void 0 && inRun) runs.push(canvasW);
|
|
1656
1666
|
}
|
|
1657
1667
|
rowSpansByRole.push({ role, rowLeft, rowRight });
|
|
1658
|
-
if (ownMask !== void 0) hairFrontMask = ownMask;
|
|
1659
1668
|
const layer = {
|
|
1660
1669
|
role,
|
|
1661
1670
|
fileName,
|
|
@@ -1665,6 +1674,7 @@ function createLayerSetMeasurer(canvas) {
|
|
|
1665
1674
|
cropW: bbox.w,
|
|
1666
1675
|
cropH: bbox.h
|
|
1667
1676
|
};
|
|
1677
|
+
if (rowRuns !== void 0) layer.rowRuns = rowRuns;
|
|
1668
1678
|
if (role === "face") {
|
|
1669
1679
|
const rowHalfWidths = [];
|
|
1670
1680
|
for (let y = bbox.y; y < bbox.y + bbox.h; y++) {
|
|
@@ -1709,8 +1719,10 @@ function createLayerSetMeasurer(canvas) {
|
|
|
1709
1719
|
}
|
|
1710
1720
|
let strandEdges;
|
|
1711
1721
|
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));
|
|
1712
|
-
|
|
1713
|
-
|
|
1722
|
+
const hairFront = layerInputs.find((l) => l.role === "hair_front");
|
|
1723
|
+
const hairRuns = hairFront?.rowRuns;
|
|
1724
|
+
if (hairFront !== void 0 && hairRuns !== void 0 && irisLayers.length === 2) {
|
|
1725
|
+
const hairTop = hairFront.bbox.y;
|
|
1714
1726
|
const half = canvasW / 2;
|
|
1715
1727
|
const strandOn = (iris, other, side) => {
|
|
1716
1728
|
const r = Math.floor(iris.bbox.y + iris.bbox.h / 2);
|
|
@@ -1721,15 +1733,11 @@ function createLayerSetMeasurer(canvas) {
|
|
|
1721
1733
|
if (span2.rowRight[r] < span2.rowLeft[r]) return void 0;
|
|
1722
1734
|
const minRun = STRAND_MIN_RUN_FRACTION * (span2.rowRight[r] + 1 - span2.rowLeft[r]);
|
|
1723
1735
|
const strand = new Uint8Array(canvasW);
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1736
|
+
const runs = r >= hairTop && r < hairTop + hairRuns.length ? hairRuns[r - hairTop] : [];
|
|
1737
|
+
for (let k = 0; k < runs.length; k += 2) {
|
|
1738
|
+
if (runs[k + 1] - runs[k] >= minRun) {
|
|
1739
|
+
strand.fill(1, runs[k], runs[k + 1]);
|
|
1728
1740
|
}
|
|
1729
|
-
let b = a;
|
|
1730
|
-
while (b < canvasW && mask[r * canvasW + b] === 1) b++;
|
|
1731
|
-
if (b - a >= minRun) strand.fill(1, a, b);
|
|
1732
|
-
a = b;
|
|
1733
1741
|
}
|
|
1734
1742
|
const strandAt = (x2) => x2 >= 0 && x2 < canvasW && strand[x2] === 1;
|
|
1735
1743
|
const outerBoundary = (x2) => (side < 0 ? x2 : x2 + 1) - half;
|
|
@@ -2171,6 +2179,23 @@ function checkLayers(layers, canvas) {
|
|
|
2171
2179
|
}
|
|
2172
2180
|
});
|
|
2173
2181
|
}
|
|
2182
|
+
if (l.rowRuns !== void 0) {
|
|
2183
|
+
const rows = l.rowRuns;
|
|
2184
|
+
if (!Array.isArray(rows) || rows.length !== h) {
|
|
2185
|
+
throw new Error(
|
|
2186
|
+
`${at}: rowRuns must have one entry per crop row (${h})`
|
|
2187
|
+
);
|
|
2188
|
+
}
|
|
2189
|
+
rows.forEach((runs, i) => {
|
|
2190
|
+
if (!Array.isArray(runs) || runs.length % 2 !== 0 || !runs.every(
|
|
2191
|
+
(c, k) => Number.isInteger(c) && c >= x && c <= x + w && (k === 0 || c > runs[k - 1])
|
|
2192
|
+
)) {
|
|
2193
|
+
throw new Error(
|
|
2194
|
+
`${at}: rowRuns[${i}] ${JSON.stringify(runs)} is not an even-length, strictly increasing list of columns inside its crop (${x}..${x + w})`
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2174
2199
|
if (l.jawRows !== void 0) {
|
|
2175
2200
|
const cols = l.jawRows;
|
|
2176
2201
|
if (!Array.isArray(cols) || cols.length !== w) {
|
|
@@ -2428,7 +2453,11 @@ var TURN = {
|
|
|
2428
2453
|
* its translation (bangs 1.18×, side locks at the eye row ≈ 1.0×). */
|
|
2429
2454
|
hairFollow: 1.1,
|
|
2430
2455
|
/** The head's outline at the eye row holds (−0.014 / −0.004 hh): where the
|
|
2431
|
-
* front hair draws it, its outer edge follows the face this much
|
|
2456
|
+
* front hair draws it, its outer edge follows the face this much on a row
|
|
2457
|
+
* where no back hair paints behind that edge. Where back hair does, the
|
|
2458
|
+
* edge rides further, as far as that back hair stays behind it through
|
|
2459
|
+
* the turn and the nod, up to the face's follow there (`hairFollow`;
|
|
2460
|
+
* over the far eye's rows, as far as that eye's outer corner goes). */
|
|
2432
2461
|
outlineFollow: 0,
|
|
2433
2462
|
/** The back hair's slight counter-motion. */
|
|
2434
2463
|
hairBack: -0.027,
|
|
@@ -2448,6 +2477,13 @@ var NOD = {
|
|
|
2448
2477
|
nose: { up: -0.207, down: 0.193 },
|
|
2449
2478
|
mouth: { up: -0.172, down: 0.177 },
|
|
2450
2479
|
hairFront: { up: -0.11, down: 0.189 },
|
|
2480
|
+
/** The front hair's cap top. The samples' cap top moves 1.215× their face's
|
|
2481
|
+
* centroid looking down (Mao 1.236, Haru 1.626, Hiyori 1.194, Natori
|
|
2482
|
+
* 0.730) and 0.415× looking up (0.385, 0.430, 0.400, 0.487); each ratio
|
|
2483
|
+
* times the plate's own nod halfway between its top and the chin (0.1435
|
|
2484
|
+
* down, 0.084 up). Looking down, the rig slides it only as far as the
|
|
2485
|
+
* back hair covers the crown behind it. */
|
|
2486
|
+
hairFrontTop: { up: -0.035, down: 0.174 },
|
|
2451
2487
|
hairBack: { up: -0.011, down: 0.025 }
|
|
2452
2488
|
};
|
|
2453
2489
|
var ROLL_DEG = 10;
|
|
@@ -2495,433 +2531,82 @@ function resolveStyle(style, check) {
|
|
|
2495
2531
|
};
|
|
2496
2532
|
}
|
|
2497
2533
|
|
|
2498
|
-
// src/auto-rig/
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
const
|
|
2502
|
-
|
|
2503
|
-
return -(up * v.up + down * v.down) * hh;
|
|
2504
|
-
}
|
|
2505
|
-
function spin(x, y, ox, oy, a) {
|
|
2506
|
-
if (a === 0) return [0, 0];
|
|
2507
|
-
const px = x - ox;
|
|
2508
|
-
const py = y - oy;
|
|
2509
|
-
return [
|
|
2510
|
-
px * Math.cos(a) - py * Math.sin(a) - px,
|
|
2511
|
-
px * Math.sin(a) + py * Math.cos(a) - py
|
|
2512
|
-
];
|
|
2513
|
-
}
|
|
2514
|
-
function faceField(m) {
|
|
2515
|
-
const f = m.frame;
|
|
2516
|
-
return (x, y, ax, ay) => {
|
|
2517
|
-
const s = Math.sign(ax);
|
|
2518
|
-
const a = Math.abs(ax) / 30;
|
|
2519
|
-
const chin = smoothstep(f.mouthY, f.chinY, y);
|
|
2520
|
-
const width = m.widthUpper + (m.widthJaw - m.widthUpper) * smoothstep(f.cheekY, f.jawY, y);
|
|
2521
|
-
const shell = f.hairShell ? 0 : m.shellScale - 1;
|
|
2522
|
-
const dx = s * a * (m.face + m.chinLead * chin) + a * (width - 1 + shell) * (x - f.axisX);
|
|
2523
|
-
const t = Math.min(
|
|
2524
|
-
1,
|
|
2525
|
-
Math.max(0, (f.face.y1 - y) / Math.max(1, f.face.y1 - f.chinY))
|
|
2526
|
-
);
|
|
2527
|
-
const v = {
|
|
2528
|
-
up: NOD.faceTop.up + (NOD.chin.up - NOD.faceTop.up) * t,
|
|
2529
|
-
down: NOD.faceTop.down + (NOD.chin.down - NOD.faceTop.down) * t
|
|
2530
|
-
};
|
|
2531
|
-
return [dx, nodDy(v, ay, f.hh)];
|
|
2532
|
-
};
|
|
2533
|
-
}
|
|
2534
|
-
function earField(m) {
|
|
2535
|
-
const f = m.frame;
|
|
2536
|
-
const ears = f.ears;
|
|
2537
|
-
if (ears === void 0) return () => [0, 0];
|
|
2538
|
-
const face = faceField(m);
|
|
2539
|
-
return (x, y, ax, ay) => {
|
|
2540
|
-
const s = Math.sign(ax);
|
|
2541
|
-
const a = Math.abs(ax) / 30;
|
|
2542
|
-
const dy = face(x, y, 0, ay)[1];
|
|
2543
|
-
if (sideOf(m, x, s) === "far") return [s * a * TURN.earFar * m.face, dy];
|
|
2544
|
-
const d = Math.abs(x - f.axisX);
|
|
2545
|
-
const attach = ears.attachAt(y);
|
|
2546
|
-
if (d <= attach) return [face(x, y, ax, 0)[0], dy];
|
|
2547
|
-
const root = face(f.axisX + Math.sign(x - f.axisX) * attach, y, ax, 0)[0];
|
|
2548
|
-
const rim = s * a * TURN.earNear * m.face;
|
|
2549
|
-
const t = Math.min(1, (d - attach) / Math.max(1, ears.outer - attach));
|
|
2550
|
-
return [root + (rim - root) * t, dy];
|
|
2551
|
-
};
|
|
2552
|
-
}
|
|
2553
|
-
function chinSlide(m) {
|
|
2554
|
-
const f = m.frame;
|
|
2555
|
-
return Math.abs(faceField(m)(f.axisX, f.chinY, 30, 0)[0]);
|
|
2556
|
-
}
|
|
2557
|
-
var NECK_SHADE_MAX = 0.45;
|
|
2558
|
-
var NECK_SHADE_FULL = 0.2;
|
|
2559
|
-
function neckField(m) {
|
|
2560
|
-
const f = m.frame;
|
|
2561
|
-
const nk = f.neck;
|
|
2562
|
-
if (nk === void 0) return () => [0, 0];
|
|
2563
|
-
const face = faceField(m);
|
|
2564
|
-
const full = chinSlide(m);
|
|
2565
|
-
const share = Math.min(1, NECK_SHADE_MAX * nk.waist / Math.max(1e-6, full));
|
|
2566
|
-
const bottom = f.face.y0;
|
|
2567
|
-
const tip = f.cutAt(f.axisX);
|
|
2568
|
-
const top = tip - NECK_SHADE_FULL * (tip - bottom);
|
|
2569
|
-
return (x, y, ax) => {
|
|
2570
|
-
const wy = Math.min(
|
|
2571
|
-
1,
|
|
2572
|
-
Math.max(0, (y - bottom) / Math.max(1, top - bottom))
|
|
2573
|
-
);
|
|
2574
|
-
const wx = Math.max(0, 1 - Math.abs(x - f.axisX) / nk.waist);
|
|
2575
|
-
return [share * face(f.axisX, f.chinY, ax, 0)[0] * wx * wy, 0];
|
|
2576
|
-
};
|
|
2577
|
-
}
|
|
2578
|
-
function sideOf(m, x, s) {
|
|
2579
|
-
return s * (x - m.frame.axisX) > 0 ? "far" : "near";
|
|
2534
|
+
// src/auto-rig/head.ts
|
|
2535
|
+
function outerEdge(runs, axisX, side) {
|
|
2536
|
+
if (runs.length === 0) return void 0;
|
|
2537
|
+
const x = side > 0 ? runs[runs.length - 1] : runs[0];
|
|
2538
|
+
return side * (x - axisX) > 0 ? x : void 0;
|
|
2580
2539
|
}
|
|
2581
|
-
function
|
|
2582
|
-
const
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
const up = Math.max(0, ay) / 30;
|
|
2591
|
-
const down = Math.max(0, -ay) / 30;
|
|
2592
|
-
const squash = up * (NOD.eyeHeight.up - 1) + down * (NOD.eyeHeight.down - 1);
|
|
2593
|
-
return [dx, nodDy(NOD.eye, ay, hh) + squash * (y - ye)];
|
|
2594
|
-
};
|
|
2540
|
+
function runReach(runs, x, side) {
|
|
2541
|
+
const px = side > 0 ? x - 1 : x;
|
|
2542
|
+
for (let k = 0; k < runs.length; k += 2) {
|
|
2543
|
+
const [a, b] = [runs[k], runs[k + 1]];
|
|
2544
|
+
if (a <= px && px + 1 <= b) {
|
|
2545
|
+
return side > 0 ? { inward: x - a, outward: b - x } : { inward: b - x, outward: x - a };
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
return { inward: 0, outward: 0 };
|
|
2595
2549
|
}
|
|
2596
|
-
function
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2550
|
+
function filledProfile(rows) {
|
|
2551
|
+
if (rows === void 0 || !rows.some((v) => v > 0)) return null;
|
|
2552
|
+
const out = rows.slice();
|
|
2553
|
+
const first = out.findIndex((v) => v > 0);
|
|
2554
|
+
for (let i = 0; i < first; i++) out[i] = out[first];
|
|
2555
|
+
for (let i = first + 1; i < out.length; i++)
|
|
2556
|
+
if (!(out[i] > 0)) out[i] = out[i - 1];
|
|
2557
|
+
return out;
|
|
2603
2558
|
}
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2559
|
+
var smoothProfile = (rows) => boxSmooth(rows, Math.max(1, Math.round(rows.length * 0.01)));
|
|
2560
|
+
var EAR_STEP = 0.03;
|
|
2561
|
+
var EAR_ROWS = 3;
|
|
2562
|
+
function findEars(raw, wMax, firstRow, lastRow, minRows, maxRows) {
|
|
2563
|
+
const step = Math.max(4, EAR_STEP * wMax);
|
|
2564
|
+
for (let t = Math.max(firstRow, EAR_ROWS); t < lastRow; t++) {
|
|
2565
|
+
if (raw[t] - raw[t - EAR_ROWS] < step) continue;
|
|
2566
|
+
for (let b = t + minRows; b <= Math.min(lastRow, t + maxRows); b++) {
|
|
2567
|
+
if (raw[b - EAR_ROWS] - raw[b] >= step) {
|
|
2568
|
+
return { top: t - EAR_ROWS, bottom: b };
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
return void 0;
|
|
2572
|
+
}
|
|
2573
|
+
return void 0;
|
|
2619
2574
|
}
|
|
2620
|
-
function
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
s * a * m.tilt * TURN.mouthTiltDeg * DEG
|
|
2632
|
-
);
|
|
2633
|
-
return [
|
|
2634
|
-
s * a * m.mouth + wx - x + tx,
|
|
2635
|
-
nodDy(NOD.mouth, ay, m.frame.hh) + ty
|
|
2636
|
-
];
|
|
2637
|
-
};
|
|
2575
|
+
function boxSmooth(v, k) {
|
|
2576
|
+
return v.map((_, i) => {
|
|
2577
|
+
let s = 0;
|
|
2578
|
+
let n = 0;
|
|
2579
|
+
for (let j = i - k; j <= i + k; j++) {
|
|
2580
|
+
if (j < 0 || j >= v.length) continue;
|
|
2581
|
+
s += v[j];
|
|
2582
|
+
n++;
|
|
2583
|
+
}
|
|
2584
|
+
return s / n;
|
|
2585
|
+
});
|
|
2638
2586
|
}
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
const a = Math.abs(ax) / 30;
|
|
2644
|
-
return [
|
|
2645
|
-
s * a * shift + a * (m.shellScale - 1) * (x - f.axisX),
|
|
2646
|
-
nodDy(nod, ay, f.hh)
|
|
2647
|
-
];
|
|
2648
|
-
};
|
|
2587
|
+
var CUT_MARGIN = 2;
|
|
2588
|
+
var CUT_LOW = 3;
|
|
2589
|
+
function fadeToOutline(d, nk) {
|
|
2590
|
+
return Math.max(0, 1 - d / nk.waist);
|
|
2649
2591
|
}
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
);
|
|
2655
|
-
const
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
inner,
|
|
2664
|
-
left: Math.max(inner + 1, f.axisX - f.shellLeft),
|
|
2665
|
-
right: Math.max(inner + 1, f.shellRight - f.axisX)
|
|
2592
|
+
var STROKE_RUN = 4;
|
|
2593
|
+
var STROKE_SLACK = 6;
|
|
2594
|
+
var JAW_SHADE = 0.05;
|
|
2595
|
+
function buildHeadFrame(layers, opts) {
|
|
2596
|
+
const byRole = new Map(layers.map((l) => [l.role, l]));
|
|
2597
|
+
const faceLayer = byRole.get("face");
|
|
2598
|
+
const { canvasW, canvasH } = faceLayer;
|
|
2599
|
+
const face = boxOfLayer(faceLayer);
|
|
2600
|
+
const axisX = cx(face);
|
|
2601
|
+
const plateHalf = faceLayer.cropW / 2;
|
|
2602
|
+
const centreY = (role) => {
|
|
2603
|
+
const l = byRole.get(role);
|
|
2604
|
+
return l === void 0 ? void 0 : cy(boxOfLayer(l));
|
|
2666
2605
|
};
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
const top = f.face.y1;
|
|
2672
|
-
const eyes = [m.boxes.eye_L, m.boxes.eye_R].filter(
|
|
2673
|
-
(e) => e !== void 0
|
|
2674
|
-
);
|
|
2675
|
-
const { inner, left, right } = hairFrontBends(f, m.boxes);
|
|
2676
|
-
const eyeY = eyes.length > 0 ? eyes.reduce((t, e) => t + cy(e), 0) / eyes.length : f.eyeY;
|
|
2677
|
-
const eyeH = eyes.length > 0 ? eyes.reduce((t, e) => t + (e.y1 - e.y0), 0) / eyes.length : 1;
|
|
2678
|
-
return (x, y, ax, ay) => {
|
|
2679
|
-
const s = Math.sign(ax);
|
|
2680
|
-
const a = Math.abs(ax) / 30;
|
|
2681
|
-
const u = s * (x - f.axisX);
|
|
2682
|
-
const across = Math.min(1, Math.max(0, (u + inner) / (2 * inner)));
|
|
2683
|
-
const band = 1 - smoothstep(eyeH / 2, 1.5 * eyeH, Math.abs(y - eyeY));
|
|
2684
|
-
const core = m.hairFront + (m.hairFar - m.hairFront) * across * band;
|
|
2685
|
-
const out = Math.min(
|
|
2686
|
-
1,
|
|
2687
|
-
Math.max(
|
|
2688
|
-
0,
|
|
2689
|
-
(Math.abs(x - f.axisX) - inner) / ((x < f.axisX ? left : right) - inner)
|
|
2690
|
-
)
|
|
2691
|
-
);
|
|
2692
|
-
const follow = core + (m.hairOuter - core) * out;
|
|
2693
|
-
const rx = s * a * follow + a * (m.shellScale - 1) * (x - f.axisX);
|
|
2694
|
-
const ry = nodDy(NOD.hairFront, ay, f.hh);
|
|
2695
|
-
const [bx, by] = back(x, y, ax, ay);
|
|
2696
|
-
const w = 1 - smoothstep(top, Math.max(top + 1, m.hairTop), y);
|
|
2697
|
-
return [bx + w * (rx - bx), by + w * (ry - by)];
|
|
2698
|
-
};
|
|
2699
|
-
}
|
|
2700
|
-
function familyField(m, family) {
|
|
2701
|
-
switch (family) {
|
|
2702
|
-
case "face":
|
|
2703
|
-
return faceField(m);
|
|
2704
|
-
case "eye_L":
|
|
2705
|
-
case "eye_R":
|
|
2706
|
-
return eyeField(m, m.boxes[family]);
|
|
2707
|
-
case "brow_L":
|
|
2708
|
-
case "brow_R":
|
|
2709
|
-
return browField(m, m.boxes[family]);
|
|
2710
|
-
case "nose":
|
|
2711
|
-
return noseField(m);
|
|
2712
|
-
case "mouth":
|
|
2713
|
-
return mouthField(m);
|
|
2714
|
-
case "hair_front":
|
|
2715
|
-
return hairFrontField(m);
|
|
2716
|
-
case "hair_back":
|
|
2717
|
-
return hairField(m, m.hairBack, NOD.hairBack);
|
|
2718
|
-
case "body":
|
|
2719
|
-
return () => [0, 0];
|
|
2720
|
-
}
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
|
-
// src/auto-rig/context.ts
|
|
2724
|
-
function sidePair(byRole, a, b) {
|
|
2725
|
-
const la = byRole.get(a);
|
|
2726
|
-
const lb = byRole.get(b);
|
|
2727
|
-
if (la === void 0 || lb === void 0) return void 0;
|
|
2728
|
-
const [lo, hi] = [
|
|
2729
|
-
{ role: a, box: boxOfLayer(la) },
|
|
2730
|
-
{ role: b, box: boxOfLayer(lb) }
|
|
2731
|
-
].sort((p, q) => cx(p.box) - cx(q.box));
|
|
2732
|
-
return { left: lo, right: hi };
|
|
2733
|
-
}
|
|
2734
|
-
function sideIrises(byRole) {
|
|
2735
|
-
const pair = sidePair(byRole, "iris_L", "iris_R");
|
|
2736
|
-
return pair === void 0 ? {} : { left: pair.left.box, right: pair.right.box };
|
|
2737
|
-
}
|
|
2738
|
-
function solveContext(frame, byRole, options, hasNose) {
|
|
2739
|
-
const box = (role) => boxOfLayer(byRole.get(role));
|
|
2740
|
-
const row = (s, strand2, sideSign) => {
|
|
2741
|
-
if (strand2 !== void 0) {
|
|
2742
|
-
const lo = sideSign < 0 ? strand2.irisOuter : strand2.irisInner;
|
|
2743
|
-
const hi = sideSign < 0 ? strand2.irisInner : strand2.irisOuter;
|
|
2744
|
-
return { role: s.role, lo, hi, y: strand2.y };
|
|
2745
|
-
}
|
|
2746
|
-
const inset = bw(s.box) > 4 ? 1 : 0;
|
|
2747
|
-
return {
|
|
2748
|
-
role: s.role,
|
|
2749
|
-
lo: s.box.x0 + inset,
|
|
2750
|
-
hi: s.box.x1 - inset,
|
|
2751
|
-
y: cy(s.box)
|
|
2752
|
-
};
|
|
2753
|
-
};
|
|
2754
|
-
const eyes = sidePair(byRole, "eye_L", "eye_R");
|
|
2755
|
-
const irises = sidePair(byRole, "iris_L", "iris_R");
|
|
2756
|
-
const pair = irises ?? eyes;
|
|
2757
|
-
const strand = irises ? options.strandEdges : void 0;
|
|
2758
|
-
const left = row(pair.left, strand?.left, -1);
|
|
2759
|
-
const right = row(pair.right, strand?.right, 1);
|
|
2760
|
-
let candidates;
|
|
2761
|
-
const edges = options.headEdges;
|
|
2762
|
-
if (frame.hairShell && edges && edges.left.length > 0 && edges.right.length > 0) {
|
|
2763
|
-
candidates = {
|
|
2764
|
-
left: edges.left.map((e) => ({ role: e.role, x: e.x })),
|
|
2765
|
-
// A right edge names its outermost pixel column; the boundary is one on.
|
|
2766
|
-
right: edges.right.map((e) => ({ role: e.role, x: e.x + 1 }))
|
|
2767
|
-
};
|
|
2768
|
-
} else if (frame.hairShell) {
|
|
2769
|
-
candidates = {
|
|
2770
|
-
left: [{ role: "hair_back", x: frame.shellLeft }],
|
|
2771
|
-
right: [{ role: "hair_back", x: frame.shellRight }]
|
|
2772
|
-
};
|
|
2773
|
-
} else {
|
|
2774
|
-
const w = frame.paintedHalfAt(frame.eyeY);
|
|
2775
|
-
candidates = {
|
|
2776
|
-
left: [{ role: "face", x: frame.axisX - w }],
|
|
2777
|
-
right: [{ role: "face", x: frame.axisX + w }]
|
|
2778
|
-
};
|
|
2779
|
-
}
|
|
2780
|
-
const restHalf = (Math.max(...candidates.right.map((c) => c.x)) - Math.min(...candidates.left.map((c) => c.x))) / 2;
|
|
2781
|
-
const nose = byRole.get("nose");
|
|
2782
|
-
let noseAt;
|
|
2783
|
-
let noseBox;
|
|
2784
|
-
if (hasNose && nose !== void 0) {
|
|
2785
|
-
noseBox = nose.denseCore ? boxOfPixels(nose.denseCore, nose.canvasW, nose.canvasH) : boxOfLayer(nose);
|
|
2786
|
-
noseAt = { x: cx(noseBox), y: cy(noseBox) };
|
|
2787
|
-
}
|
|
2788
|
-
const mouthBox = byRole.has("mouth_open") ? unionBoxes([box("mouth"), box("mouth_open")]) : box("mouth");
|
|
2789
|
-
const m = box("mouth");
|
|
2790
|
-
return {
|
|
2791
|
-
frame,
|
|
2792
|
-
left,
|
|
2793
|
-
right,
|
|
2794
|
-
candidates,
|
|
2795
|
-
restHalf,
|
|
2796
|
-
strand,
|
|
2797
|
-
hasHairFront: byRole.has("hair_front"),
|
|
2798
|
-
eyeBoxes: { left: eyes.left.box, right: eyes.right.box },
|
|
2799
|
-
noseAt,
|
|
2800
|
-
noseBox,
|
|
2801
|
-
mouthAt: { x: cx(m), y: cy(m) },
|
|
2802
|
-
mouthBox
|
|
2803
|
-
};
|
|
2804
|
-
}
|
|
2805
|
-
function renderedLander(turn, parts, grids, from = /* @__PURE__ */ new Map()) {
|
|
2806
|
-
const analytic = /* @__PURE__ */ new Map();
|
|
2807
|
-
const landed = /* @__PURE__ */ new Map();
|
|
2808
|
-
const field = (role) => {
|
|
2809
|
-
const family = roleSpec(role).family;
|
|
2810
|
-
let f = analytic.get(family);
|
|
2811
|
-
if (f === void 0) analytic.set(family, f = familyField(turn, family));
|
|
2812
|
-
return f;
|
|
2813
|
-
};
|
|
2814
|
-
return (role, x, y, ax) => {
|
|
2815
|
-
const family = roleSpec(role).family;
|
|
2816
|
-
const part = parts.find((p) => p.id === role);
|
|
2817
|
-
const g = grids.get(family);
|
|
2818
|
-
const warp = part?.warps?.find((w) => w.parameter === import_format3.StandardParameter.AngleX);
|
|
2819
|
-
if (part === void 0 || part.mesh === void 0 || !g && !warp) {
|
|
2820
|
-
return x + field(role)(x, y, ax, 0)[0];
|
|
2821
|
-
}
|
|
2822
|
-
const box = {
|
|
2823
|
-
x0: part.transform.x - part.width / 2,
|
|
2824
|
-
x1: part.transform.x + part.width / 2,
|
|
2825
|
-
y0: part.transform.y - part.height / 2,
|
|
2826
|
-
y1: part.transform.y + part.height / 2
|
|
2827
|
-
};
|
|
2828
|
-
const rest = meshPoints(part.mesh, box);
|
|
2829
|
-
const key = `${role}@${ax}`;
|
|
2830
|
-
let xs = landed.get(key);
|
|
2831
|
-
if (xs === void 0) {
|
|
2832
|
-
if (g) {
|
|
2833
|
-
const offsets = keyformAt(g.keyforms, ax);
|
|
2834
|
-
xs = rest.map(([px, py]) => land(g.lattice, offsets, px, py)[0]);
|
|
2835
|
-
} else {
|
|
2836
|
-
const k = warp.keyforms;
|
|
2837
|
-
const offsets = blendStops(
|
|
2838
|
-
k.map((kf) => kf.value),
|
|
2839
|
-
k.map((kf) => kf.offsets),
|
|
2840
|
-
ax
|
|
2841
|
-
);
|
|
2842
|
-
xs = rest.map(([px], i) => px + offsets[2 * i] * bw(box));
|
|
2843
|
-
}
|
|
2844
|
-
landed.set(key, xs);
|
|
2845
|
-
}
|
|
2846
|
-
const idx = part.mesh.indices;
|
|
2847
|
-
for (let t = from.get(role) ?? 0; t < idx.length; t += 3) {
|
|
2848
|
-
const [a, b, c] = [idx[t], idx[t + 1], idx[t + 2]];
|
|
2849
|
-
const [ax0, ay0] = rest[a];
|
|
2850
|
-
const [bx0, by0] = rest[b];
|
|
2851
|
-
const [cx0, cy0] = rest[c];
|
|
2852
|
-
const d = (by0 - cy0) * (ax0 - cx0) + (cx0 - bx0) * (ay0 - cy0);
|
|
2853
|
-
if (Math.abs(d) < 1e-9) continue;
|
|
2854
|
-
const u = ((by0 - cy0) * (x - cx0) + (cx0 - bx0) * (y - cy0)) / d;
|
|
2855
|
-
const v = ((cy0 - ay0) * (x - cx0) + (ax0 - cx0) * (y - cy0)) / d;
|
|
2856
|
-
const w = 1 - u - v;
|
|
2857
|
-
if (u < -1e-6 || v < -1e-6 || w < -1e-6) continue;
|
|
2858
|
-
return u * xs[a] + v * xs[b] + w * xs[c];
|
|
2859
|
-
}
|
|
2860
|
-
return x + field(role)(x, y, ax, 0)[0];
|
|
2861
|
-
};
|
|
2862
|
-
}
|
|
2863
|
-
|
|
2864
|
-
// src/auto-rig/head.ts
|
|
2865
|
-
function filledProfile(rows) {
|
|
2866
|
-
if (rows === void 0 || !rows.some((v) => v > 0)) return null;
|
|
2867
|
-
const out = rows.slice();
|
|
2868
|
-
const first = out.findIndex((v) => v > 0);
|
|
2869
|
-
for (let i = 0; i < first; i++) out[i] = out[first];
|
|
2870
|
-
for (let i = first + 1; i < out.length; i++)
|
|
2871
|
-
if (!(out[i] > 0)) out[i] = out[i - 1];
|
|
2872
|
-
return out;
|
|
2873
|
-
}
|
|
2874
|
-
var smoothProfile = (rows) => boxSmooth(rows, Math.max(1, Math.round(rows.length * 0.01)));
|
|
2875
|
-
var EAR_STEP = 0.03;
|
|
2876
|
-
var EAR_ROWS = 3;
|
|
2877
|
-
function findEars(raw, wMax, firstRow, lastRow, minRows, maxRows) {
|
|
2878
|
-
const step = Math.max(4, EAR_STEP * wMax);
|
|
2879
|
-
for (let t = Math.max(firstRow, EAR_ROWS); t < lastRow; t++) {
|
|
2880
|
-
if (raw[t] - raw[t - EAR_ROWS] < step) continue;
|
|
2881
|
-
for (let b = t + minRows; b <= Math.min(lastRow, t + maxRows); b++) {
|
|
2882
|
-
if (raw[b - EAR_ROWS] - raw[b] >= step) {
|
|
2883
|
-
return { top: t - EAR_ROWS, bottom: b };
|
|
2884
|
-
}
|
|
2885
|
-
}
|
|
2886
|
-
return void 0;
|
|
2887
|
-
}
|
|
2888
|
-
return void 0;
|
|
2889
|
-
}
|
|
2890
|
-
function boxSmooth(v, k) {
|
|
2891
|
-
return v.map((_, i) => {
|
|
2892
|
-
let s = 0;
|
|
2893
|
-
let n = 0;
|
|
2894
|
-
for (let j = i - k; j <= i + k; j++) {
|
|
2895
|
-
if (j < 0 || j >= v.length) continue;
|
|
2896
|
-
s += v[j];
|
|
2897
|
-
n++;
|
|
2898
|
-
}
|
|
2899
|
-
return s / n;
|
|
2900
|
-
});
|
|
2901
|
-
}
|
|
2902
|
-
var CUT_MARGIN = 2;
|
|
2903
|
-
var CUT_LOW = 3;
|
|
2904
|
-
function fadeToOutline(d, nk) {
|
|
2905
|
-
return Math.max(0, 1 - d / nk.waist);
|
|
2906
|
-
}
|
|
2907
|
-
var STROKE_RUN = 4;
|
|
2908
|
-
var STROKE_SLACK = 6;
|
|
2909
|
-
var JAW_SHADE = 0.05;
|
|
2910
|
-
function buildHeadFrame(layers, opts) {
|
|
2911
|
-
const byRole = new Map(layers.map((l) => [l.role, l]));
|
|
2912
|
-
const faceLayer = byRole.get("face");
|
|
2913
|
-
const { canvasW, canvasH } = faceLayer;
|
|
2914
|
-
const face = boxOfLayer(faceLayer);
|
|
2915
|
-
const axisX = cx(face);
|
|
2916
|
-
const plateHalf = faceLayer.cropW / 2;
|
|
2917
|
-
const centreY = (role) => {
|
|
2918
|
-
const l = byRole.get(role);
|
|
2919
|
-
return l === void 0 ? void 0 : cy(boxOfLayer(l));
|
|
2920
|
-
};
|
|
2921
|
-
const pair = (a, b) => {
|
|
2922
|
-
const ya = centreY(a);
|
|
2923
|
-
const yb = centreY(b);
|
|
2924
|
-
return ya === void 0 || yb === void 0 ? void 0 : (ya + yb) / 2;
|
|
2606
|
+
const pair = (a, b) => {
|
|
2607
|
+
const ya = centreY(a);
|
|
2608
|
+
const yb = centreY(b);
|
|
2609
|
+
return ya === void 0 || yb === void 0 ? void 0 : (ya + yb) / 2;
|
|
2925
2610
|
};
|
|
2926
2611
|
const eyeY = pair("iris_L", "iris_R") ?? pair("eye_L", "eye_R");
|
|
2927
2612
|
const mouthY = centreY("mouth");
|
|
@@ -3081,68 +2766,768 @@ function buildHeadFrame(layers, opts) {
|
|
|
3081
2766
|
}
|
|
3082
2767
|
return m;
|
|
3083
2768
|
});
|
|
3084
|
-
if (opts.chinSlide !== void 0 && banded >= 0)
|
|
3085
|
-
nk.bandEnd = banded + CUT_LOW + 1;
|
|
3086
|
-
cutAt = (x) => {
|
|
3087
|
-
const f = x - (faceLayer.bbox.x - canvasW / 2) - 0.5;
|
|
3088
|
-
const i = clamp(Math.floor(f), 0, low.length - 1);
|
|
3089
|
-
const j = Math.min(i + 1, low.length - 1);
|
|
3090
|
-
const t = clamp(f - i, 0, 1);
|
|
3091
|
-
return Math.max(face.y0, low[i] + (low[j] - low[i]) * t);
|
|
3092
|
-
};
|
|
3093
|
-
} else {
|
|
3094
|
-
const head = earless(chinY);
|
|
3095
|
-
headMax = Math.max(...head);
|
|
3096
|
-
if (ears !== void 0) {
|
|
3097
|
-
const bottomOf = (d) => {
|
|
3098
|
-
for (let r = head.length - 1; r >= 0; r--) {
|
|
3099
|
-
if (head[r] >= d) return rowY(r) - 0.5;
|
|
2769
|
+
if (opts.chinSlide !== void 0 && banded >= 0)
|
|
2770
|
+
nk.bandEnd = banded + CUT_LOW + 1;
|
|
2771
|
+
cutAt = (x) => {
|
|
2772
|
+
const f = x - (faceLayer.bbox.x - canvasW / 2) - 0.5;
|
|
2773
|
+
const i = clamp(Math.floor(f), 0, low.length - 1);
|
|
2774
|
+
const j = Math.min(i + 1, low.length - 1);
|
|
2775
|
+
const t = clamp(f - i, 0, 1);
|
|
2776
|
+
return Math.max(face.y0, low[i] + (low[j] - low[i]) * t);
|
|
2777
|
+
};
|
|
2778
|
+
} else {
|
|
2779
|
+
const head = earless(chinY);
|
|
2780
|
+
headMax = Math.max(...head);
|
|
2781
|
+
if (ears !== void 0) {
|
|
2782
|
+
const bottomOf = (d) => {
|
|
2783
|
+
for (let r = head.length - 1; r >= 0; r--) {
|
|
2784
|
+
if (head[r] >= d) return rowY(r) - 0.5;
|
|
2785
|
+
}
|
|
2786
|
+
return face.y1;
|
|
2787
|
+
};
|
|
2788
|
+
cutAt = (x) => {
|
|
2789
|
+
const y = bottomOf(Math.abs(x - axisX));
|
|
2790
|
+
return y >= face.y1 ? face.y1 : Math.max(face.y0, y - CUT_MARGIN);
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
const hh = HH_PER_EYE_TO_CHIN * Math.max(1, eyeY - chinY);
|
|
2796
|
+
const headHalf = opts.headHalfWidth;
|
|
2797
|
+
const hairShell = headHalf !== void 0;
|
|
2798
|
+
let shellLeft = axisX - plateHalf;
|
|
2799
|
+
let shellRight = axisX + plateHalf;
|
|
2800
|
+
if (hairShell) {
|
|
2801
|
+
if (opts.headEdges && opts.headEdges.left.length > 0 && opts.headEdges.right.length > 0) {
|
|
2802
|
+
shellLeft = Math.min(...opts.headEdges.left.map((e) => e.x));
|
|
2803
|
+
shellRight = Math.max(...opts.headEdges.right.map((e) => e.x)) + 1;
|
|
2804
|
+
} else {
|
|
2805
|
+
shellLeft = axisX - headHalf;
|
|
2806
|
+
shellRight = axisX + headHalf;
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
const runsOf = (role, y) => {
|
|
2810
|
+
const l = byRole.get(role);
|
|
2811
|
+
if (l?.rowRuns === void 0) return void 0;
|
|
2812
|
+
const runs = l.rowRuns[Math.floor(canvasH / 2 - y) - l.bbox.y];
|
|
2813
|
+
return runs === void 0 ? [] : runs.map((c) => c - canvasW / 2);
|
|
2814
|
+
};
|
|
2815
|
+
const hairRuns = (role, y) => runsOf(role, y);
|
|
2816
|
+
const faceRuns = (y) => runsOf("face", y);
|
|
2817
|
+
return {
|
|
2818
|
+
canvasW,
|
|
2819
|
+
canvasH,
|
|
2820
|
+
face,
|
|
2821
|
+
axisX,
|
|
2822
|
+
eyeY,
|
|
2823
|
+
mouthY,
|
|
2824
|
+
chinY,
|
|
2825
|
+
hh,
|
|
2826
|
+
paintedHalfAt,
|
|
2827
|
+
wMax,
|
|
2828
|
+
headMax,
|
|
2829
|
+
hasProfile: profile !== null,
|
|
2830
|
+
neck,
|
|
2831
|
+
ears,
|
|
2832
|
+
cutAt,
|
|
2833
|
+
cheekY: eyeY - 0.5 * (eyeY - chinY),
|
|
2834
|
+
jawY: eyeY - 0.8 * (eyeY - chinY),
|
|
2835
|
+
shellLeft,
|
|
2836
|
+
shellRight,
|
|
2837
|
+
hairShell,
|
|
2838
|
+
holdBase: headHalf ?? plateHalf,
|
|
2839
|
+
hairRuns,
|
|
2840
|
+
faceRuns
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
// src/auto-rig/fields.ts
|
|
2845
|
+
var NOSE_TILT_DEG = 6;
|
|
2846
|
+
function nodDy(v, ay, hh) {
|
|
2847
|
+
const up = Math.max(0, ay) / 30;
|
|
2848
|
+
const down = Math.max(0, -ay) / 30;
|
|
2849
|
+
return -(up * v.up + down * v.down) * hh;
|
|
2850
|
+
}
|
|
2851
|
+
function spin(x, y, ox, oy, a) {
|
|
2852
|
+
if (a === 0) return [0, 0];
|
|
2853
|
+
const px = x - ox;
|
|
2854
|
+
const py = y - oy;
|
|
2855
|
+
return [
|
|
2856
|
+
px * Math.cos(a) - py * Math.sin(a) - px,
|
|
2857
|
+
px * Math.sin(a) + py * Math.cos(a) - py
|
|
2858
|
+
];
|
|
2859
|
+
}
|
|
2860
|
+
function faceField(m) {
|
|
2861
|
+
const f = m.frame;
|
|
2862
|
+
return (x, y, ax, ay) => {
|
|
2863
|
+
const s = Math.sign(ax);
|
|
2864
|
+
const a = Math.abs(ax) / 30;
|
|
2865
|
+
const chin = smoothstep(f.mouthY, f.chinY, y);
|
|
2866
|
+
const width = m.widthUpper + (m.widthJaw - m.widthUpper) * smoothstep(f.cheekY, f.jawY, y);
|
|
2867
|
+
const shell = f.hairShell ? 0 : m.shellScale - 1;
|
|
2868
|
+
const dx = s * a * (m.face + m.chinLead * chin) + a * (width - 1 + shell) * (x - f.axisX);
|
|
2869
|
+
const t = Math.min(
|
|
2870
|
+
1,
|
|
2871
|
+
Math.max(0, (f.face.y1 - y) / Math.max(1, f.face.y1 - f.chinY))
|
|
2872
|
+
);
|
|
2873
|
+
const v = {
|
|
2874
|
+
up: NOD.faceTop.up + (NOD.chin.up - NOD.faceTop.up) * t,
|
|
2875
|
+
down: NOD.faceTop.down + (NOD.chin.down - NOD.faceTop.down) * t
|
|
2876
|
+
};
|
|
2877
|
+
return [dx, nodDy(v, ay, f.hh)];
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2880
|
+
function earField(m) {
|
|
2881
|
+
const f = m.frame;
|
|
2882
|
+
const ears = f.ears;
|
|
2883
|
+
if (ears === void 0) return () => [0, 0];
|
|
2884
|
+
const face = faceField(m);
|
|
2885
|
+
return (x, y, ax, ay) => {
|
|
2886
|
+
const s = Math.sign(ax);
|
|
2887
|
+
const a = Math.abs(ax) / 30;
|
|
2888
|
+
const dy = face(x, y, 0, ay)[1];
|
|
2889
|
+
if (sideOf(m, x, s) === "far") return [s * a * TURN.earFar * m.face, dy];
|
|
2890
|
+
const d = Math.abs(x - f.axisX);
|
|
2891
|
+
const attach = ears.attachAt(y);
|
|
2892
|
+
if (d <= attach) return [face(x, y, ax, 0)[0], dy];
|
|
2893
|
+
const root = face(f.axisX + Math.sign(x - f.axisX) * attach, y, ax, 0)[0];
|
|
2894
|
+
const rim = s * a * TURN.earNear * m.face;
|
|
2895
|
+
const t = Math.min(1, (d - attach) / Math.max(1, ears.outer - attach));
|
|
2896
|
+
return [root + (rim - root) * t, dy];
|
|
2897
|
+
};
|
|
2898
|
+
}
|
|
2899
|
+
function chinSlide(m) {
|
|
2900
|
+
const f = m.frame;
|
|
2901
|
+
return Math.abs(faceField(m)(f.axisX, f.chinY, 30, 0)[0]);
|
|
2902
|
+
}
|
|
2903
|
+
var NECK_SHADE_MAX = 0.45;
|
|
2904
|
+
var NECK_SHADE_FULL = 0.2;
|
|
2905
|
+
function neckField(m) {
|
|
2906
|
+
const f = m.frame;
|
|
2907
|
+
const nk = f.neck;
|
|
2908
|
+
if (nk === void 0) return () => [0, 0];
|
|
2909
|
+
const face = faceField(m);
|
|
2910
|
+
const full = chinSlide(m);
|
|
2911
|
+
const share = Math.min(1, NECK_SHADE_MAX * nk.waist / Math.max(1e-6, full));
|
|
2912
|
+
const bottom = f.face.y0;
|
|
2913
|
+
const tip = f.cutAt(f.axisX);
|
|
2914
|
+
const top = tip - NECK_SHADE_FULL * (tip - bottom);
|
|
2915
|
+
return (x, y, ax) => {
|
|
2916
|
+
const wy = Math.min(
|
|
2917
|
+
1,
|
|
2918
|
+
Math.max(0, (y - bottom) / Math.max(1, top - bottom))
|
|
2919
|
+
);
|
|
2920
|
+
const wx = Math.max(0, 1 - Math.abs(x - f.axisX) / nk.waist);
|
|
2921
|
+
return [share * face(f.axisX, f.chinY, ax, 0)[0] * wx * wy, 0];
|
|
2922
|
+
};
|
|
2923
|
+
}
|
|
2924
|
+
function sideOf(m, x, s) {
|
|
2925
|
+
return s * (x - m.frame.axisX) > 0 ? "far" : "near";
|
|
2926
|
+
}
|
|
2927
|
+
function eyeField(m, box) {
|
|
2928
|
+
const xe = cx(box);
|
|
2929
|
+
const ye = cy(box);
|
|
2930
|
+
const hh = m.frame.hh;
|
|
2931
|
+
return (x, y, ax, ay) => {
|
|
2932
|
+
const s = Math.sign(ax);
|
|
2933
|
+
const a = Math.abs(ax) / 30;
|
|
2934
|
+
const side = sideOf(m, xe, s);
|
|
2935
|
+
const dx = s * a * m.eye[side] + a * (m.eyeScale[side] - 1) * (x - xe);
|
|
2936
|
+
const up = Math.max(0, ay) / 30;
|
|
2937
|
+
const down = Math.max(0, -ay) / 30;
|
|
2938
|
+
const squash = up * (NOD.eyeHeight.up - 1) + down * (NOD.eyeHeight.down - 1);
|
|
2939
|
+
return [dx, nodDy(NOD.eye, ay, hh) + squash * (y - ye)];
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2942
|
+
function browField(m, box) {
|
|
2943
|
+
const xb = cx(box);
|
|
2944
|
+
return (_x, _y, ax, ay) => {
|
|
2945
|
+
const s = Math.sign(ax);
|
|
2946
|
+
const a = Math.abs(ax) / 30;
|
|
2947
|
+
return [s * a * m.brow[sideOf(m, xb, s)], nodDy(NOD.brow, ay, m.frame.hh)];
|
|
2948
|
+
};
|
|
2949
|
+
}
|
|
2950
|
+
function noseField(m) {
|
|
2951
|
+
const at = m.noseAt;
|
|
2952
|
+
const top = m.noseTopY ?? at.y;
|
|
2953
|
+
return (x, y, ax, ay) => {
|
|
2954
|
+
const s = Math.sign(ax);
|
|
2955
|
+
const a = Math.abs(ax) / 30;
|
|
2956
|
+
const [tx, ty] = spin(
|
|
2957
|
+
x,
|
|
2958
|
+
y,
|
|
2959
|
+
at.x,
|
|
2960
|
+
top,
|
|
2961
|
+
s * a * m.tilt * NOSE_TILT_DEG * DEG
|
|
2962
|
+
);
|
|
2963
|
+
return [s * a * m.nose + tx, nodDy(NOD.nose, ay, m.frame.hh) + ty];
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
function mouthField(m) {
|
|
2967
|
+
const c = m.mouthAt;
|
|
2968
|
+
return (x, y, ax, ay) => {
|
|
2969
|
+
const s = Math.sign(ax);
|
|
2970
|
+
const a = Math.abs(ax) / 30;
|
|
2971
|
+
const wx = c.x + (x - c.x) * (1 + a * (m.mouthWidth - 1));
|
|
2972
|
+
const [tx, ty] = spin(
|
|
2973
|
+
wx,
|
|
2974
|
+
y,
|
|
2975
|
+
c.x,
|
|
2976
|
+
c.y,
|
|
2977
|
+
s * a * m.tilt * TURN.mouthTiltDeg * DEG
|
|
2978
|
+
);
|
|
2979
|
+
return [
|
|
2980
|
+
s * a * m.mouth + wx - x + tx,
|
|
2981
|
+
nodDy(NOD.mouth, ay, m.frame.hh) + ty
|
|
2982
|
+
];
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2985
|
+
function hairField(m, shift, nod) {
|
|
2986
|
+
const f = m.frame;
|
|
2987
|
+
return (x, _y, ax, ay) => {
|
|
2988
|
+
const s = Math.sign(ax);
|
|
2989
|
+
const a = Math.abs(ax) / 30;
|
|
2990
|
+
return [
|
|
2991
|
+
s * a * shift + a * (m.shellScale - 1) * (x - f.axisX),
|
|
2992
|
+
nodDy(nod, ay, f.hh)
|
|
2993
|
+
];
|
|
2994
|
+
};
|
|
2995
|
+
}
|
|
2996
|
+
function hairFrontBends(frame, boxes) {
|
|
2997
|
+
const f = frame;
|
|
2998
|
+
const eyes = [boxes.eye_L, boxes.eye_R].filter(
|
|
2999
|
+
(e) => e !== void 0
|
|
3000
|
+
);
|
|
3001
|
+
const inner = Math.max(
|
|
3002
|
+
1,
|
|
3003
|
+
Math.min(
|
|
3004
|
+
f.paintedHalfAt(f.eyeY),
|
|
3005
|
+
...eyes.map((e) => Math.max(f.axisX - e.x0, e.x1 - f.axisX))
|
|
3006
|
+
)
|
|
3007
|
+
);
|
|
3008
|
+
return {
|
|
3009
|
+
inner,
|
|
3010
|
+
left: Math.max(inner + 1, f.axisX - f.shellLeft),
|
|
3011
|
+
right: Math.max(inner + 1, f.shellRight - f.axisX)
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
function hairFrontField(m) {
|
|
3015
|
+
const f = m.frame;
|
|
3016
|
+
const back = hairField(m, m.hairBack, NOD.hairBack);
|
|
3017
|
+
const cap = { up: NOD.hairFrontTop.up, down: capSlide(m) };
|
|
3018
|
+
const eyes = [m.boxes.eye_L, m.boxes.eye_R].filter(
|
|
3019
|
+
(e) => e !== void 0
|
|
3020
|
+
);
|
|
3021
|
+
const { inner, left, right } = hairFrontBends(f, m.boxes);
|
|
3022
|
+
const eyeY = eyes.length > 0 ? eyes.reduce((t, e) => t + cy(e), 0) / eyes.length : f.eyeY;
|
|
3023
|
+
const eyeH = eyes.length > 0 ? eyes.reduce((t, e) => t + (e.y1 - e.y0), 0) / eyes.length : 1;
|
|
3024
|
+
const coreAt = (x, y, s) => {
|
|
3025
|
+
const u = s * (x - f.axisX);
|
|
3026
|
+
const across = Math.min(1, Math.max(0, (u + inner) / (2 * inner)));
|
|
3027
|
+
const band = 1 - smoothstep(eyeH / 2, 1.5 * eyeH, Math.abs(y - eyeY));
|
|
3028
|
+
return m.hairFront + (m.hairFar - m.hairFront) * across * band;
|
|
3029
|
+
};
|
|
3030
|
+
const followAt = (x, y, s) => {
|
|
3031
|
+
const core = coreAt(x, y, s);
|
|
3032
|
+
const out = Math.min(
|
|
3033
|
+
1,
|
|
3034
|
+
Math.max(
|
|
3035
|
+
0,
|
|
3036
|
+
(Math.abs(x - f.axisX) - inner) / ((x < f.axisX ? left : right) - inner)
|
|
3037
|
+
)
|
|
3038
|
+
);
|
|
3039
|
+
return core + (m.hairOuter - core) * out;
|
|
3040
|
+
};
|
|
3041
|
+
const ride = outerRide(m, inner, coreAt, followAt);
|
|
3042
|
+
const followOf = (x, y, s) => followAt(x, y, s) + (ride === void 0 ? 0 : ride(x, y, s));
|
|
3043
|
+
const drift = Math.abs(m.hairBack);
|
|
3044
|
+
const crown = crownRide(m, cap, (x, y, s) => followOf(x, y, s) + drift);
|
|
3045
|
+
return (x, y, ax, ay) => {
|
|
3046
|
+
const s = Math.sign(ax);
|
|
3047
|
+
const a = Math.abs(ax) / 30;
|
|
3048
|
+
const rx = s * a * followOf(x, y, s) + a * (m.shellScale - 1) * (x - f.axisX);
|
|
3049
|
+
const ry = nodDy(NOD.hairFront, ay, f.hh);
|
|
3050
|
+
const bx = back(x, y, ax, ay)[0];
|
|
3051
|
+
const by = nodDy(cap, ay, f.hh);
|
|
3052
|
+
const w = crownBlend(m, y);
|
|
3053
|
+
const wx = crown === void 0 ? w : w + crown(y, s);
|
|
3054
|
+
return [bx + wx * (rx - bx), by + w * (ry - by)];
|
|
3055
|
+
};
|
|
3056
|
+
}
|
|
3057
|
+
function crownBlend(m, y) {
|
|
3058
|
+
const top = m.frame.face.y1;
|
|
3059
|
+
return 1 - smoothstep(top, Math.max(top + 1, m.hairTop), y);
|
|
3060
|
+
}
|
|
3061
|
+
function gridRow(ys, y, snap = false) {
|
|
3062
|
+
const n = ys.length;
|
|
3063
|
+
let v = clamp((ys[0] - y) / (ys[0] - ys[n - 1]) * (n - 1), 0, n - 1);
|
|
3064
|
+
if (snap && Math.abs(v - Math.round(v)) < 1e-3) v = Math.round(v);
|
|
3065
|
+
const r = Math.min(n - 2, Math.floor(v));
|
|
3066
|
+
return { r, t: v - r };
|
|
3067
|
+
}
|
|
3068
|
+
function renderedCrownBlend(m, ys) {
|
|
3069
|
+
const wr = ys.map((y) => crownBlend(m, y));
|
|
3070
|
+
return {
|
|
3071
|
+
wr,
|
|
3072
|
+
blendAt: (y) => {
|
|
3073
|
+
const { r, t } = gridRow(ys, y);
|
|
3074
|
+
return wr[r] + (wr[r + 1] - wr[r]) * t;
|
|
3075
|
+
}
|
|
3076
|
+
};
|
|
3077
|
+
}
|
|
3078
|
+
var CAP_COVER_LOSS = 0.01;
|
|
3079
|
+
function capSlide(m) {
|
|
3080
|
+
const f = m.frame;
|
|
3081
|
+
const least = NOD.hairBack.down;
|
|
3082
|
+
const grid = m.hairFrontGrid;
|
|
3083
|
+
if (grid === void 0 || f.hairRuns("hair_front", grid.ys[0]) === void 0 || f.hairRuns("hair_back", grid.ys[0]) === void 0) {
|
|
3084
|
+
return least;
|
|
3085
|
+
}
|
|
3086
|
+
const { xs, ys } = grid;
|
|
3087
|
+
const top = ys[0];
|
|
3088
|
+
const n = ys.length;
|
|
3089
|
+
const bangs = NOD.hairFront.down;
|
|
3090
|
+
const { wr, blendAt } = renderedCrownBlend(m, ys);
|
|
3091
|
+
const flat = ys[wr.findIndex((w) => w >= 1)] ?? ys[n - 1];
|
|
3092
|
+
const x0 = xs[0];
|
|
3093
|
+
const tops = new Float64Array(Math.round(xs[xs.length - 1] - x0)).fill(NaN);
|
|
3094
|
+
for (let i = 0; i < top - flat; i++) {
|
|
3095
|
+
const runs = f.hairRuns("hair_front", top - i - 0.5);
|
|
3096
|
+
for (let k = 0; k < runs.length; k += 2) {
|
|
3097
|
+
for (let px = runs[k]; px < runs[k + 1]; px++) {
|
|
3098
|
+
const j = Math.round(px - x0);
|
|
3099
|
+
if (Number.isNaN(tops[j])) tops[j] = i;
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
const hh = f.hh;
|
|
3104
|
+
const most = Math.ceil(Math.max(bangs, NOD.hairFrontTop.down) * hh);
|
|
3105
|
+
const bared = (s) => clamp(Math.ceil(s - 0.5), 0, most);
|
|
3106
|
+
const dropAt = (w, slide) => (w * bangs + (1 - w) * slide) * hh;
|
|
3107
|
+
const backNod = NOD.hairBack.down * hh;
|
|
3108
|
+
const backRows = /* @__PURE__ */ new Map();
|
|
3109
|
+
const columns = [];
|
|
3110
|
+
tops.forEach((i, j) => {
|
|
3111
|
+
if (Number.isNaN(i)) return;
|
|
3112
|
+
const w = blendAt(top - i);
|
|
3113
|
+
const empty = new Int32Array(most + 1);
|
|
3114
|
+
for (let r = 0; r < most; r++) {
|
|
3115
|
+
const at = Math.floor(i + r + 0.5 - backNod);
|
|
3116
|
+
let runs = backRows.get(at);
|
|
3117
|
+
if (runs === void 0) {
|
|
3118
|
+
runs = f.hairRuns("hair_back", top - at - 0.5);
|
|
3119
|
+
backRows.set(at, runs);
|
|
3120
|
+
}
|
|
3121
|
+
empty[r + 1] = empty[r] + (paints(runs, x0 + j) ? 0 : 1);
|
|
3122
|
+
}
|
|
3123
|
+
columns.push({ w, empty, base: empty[bared(dropAt(w, least))] });
|
|
3124
|
+
});
|
|
3125
|
+
const budget = CAP_COVER_LOSS * hh;
|
|
3126
|
+
const fits = (slide) => columns.every((c) => c.empty[bared(dropAt(c.w, slide))] - c.base <= budget);
|
|
3127
|
+
let lo = least;
|
|
3128
|
+
let hi = NOD.hairFrontTop.down;
|
|
3129
|
+
if (fits(hi)) return hi;
|
|
3130
|
+
for (let k = 0; k < 30; k++) {
|
|
3131
|
+
const mid = (lo + hi) / 2;
|
|
3132
|
+
if (fits(mid)) lo = mid;
|
|
3133
|
+
else hi = mid;
|
|
3134
|
+
}
|
|
3135
|
+
return lo;
|
|
3136
|
+
}
|
|
3137
|
+
function paints(runs, px) {
|
|
3138
|
+
for (let k = 0; k < runs.length; k += 2) {
|
|
3139
|
+
if (runs[k] <= px && px + 1 <= runs[k + 1]) return true;
|
|
3140
|
+
}
|
|
3141
|
+
return false;
|
|
3142
|
+
}
|
|
3143
|
+
function outerRide(m, inner, coreAt, followAt) {
|
|
3144
|
+
const f = m.frame;
|
|
3145
|
+
const ys = m.hairFrontGrid?.ys;
|
|
3146
|
+
if (ys === void 0) return void 0;
|
|
3147
|
+
const top = ys[0];
|
|
3148
|
+
if (f.hairRuns("hair_front", top) === void 0 || f.hairRuns("hair_back", top) === void 0) {
|
|
3149
|
+
return void 0;
|
|
3150
|
+
}
|
|
3151
|
+
const drift = Math.abs(m.hairBack);
|
|
3152
|
+
const sides = [-1, 1];
|
|
3153
|
+
const n = ys.length;
|
|
3154
|
+
const count = Math.round(top - ys[n - 1]);
|
|
3155
|
+
const below = (NOD.hairFront.down - NOD.hairBack.down) * f.hh;
|
|
3156
|
+
const above = (NOD.hairBack.up - NOD.hairFront.up) * f.hh;
|
|
3157
|
+
const j0 = Math.floor(-above);
|
|
3158
|
+
const backRows = Array.from(
|
|
3159
|
+
{ length: Math.ceil(count + below) - j0 },
|
|
3160
|
+
(_, j) => f.hairRuns("hair_back", top - (j + j0) - 0.5)
|
|
3161
|
+
);
|
|
3162
|
+
const offset = sides.map(() => new Float64Array(count).fill(NaN));
|
|
3163
|
+
const far = sides.map(() => new Float64Array(count));
|
|
3164
|
+
const near = sides.map(() => new Float64Array(count));
|
|
3165
|
+
for (let i = 0; i < count; i++) {
|
|
3166
|
+
const y = top - i - 0.5;
|
|
3167
|
+
const front = f.hairRuns("hair_front", y);
|
|
3168
|
+
sides.forEach((side, k) => {
|
|
3169
|
+
const edge = outerEdge(front, f.axisX, side);
|
|
3170
|
+
if (edge === void 0) return;
|
|
3171
|
+
let outward = Infinity;
|
|
3172
|
+
let inward = Infinity;
|
|
3173
|
+
for (let j = Math.floor(i - above); j < Math.ceil(i + 1 + below); j++) {
|
|
3174
|
+
const c = runReach(backRows[j - j0], edge, side);
|
|
3175
|
+
outward = Math.min(outward, c.outward);
|
|
3176
|
+
inward = Math.min(inward, c.inward);
|
|
3177
|
+
}
|
|
3178
|
+
offset[k][i] = side * (edge - f.axisX);
|
|
3179
|
+
far[k][i] = m.shellScale * outward - drift;
|
|
3180
|
+
near[k][i] = m.shellScale * inward - drift;
|
|
3181
|
+
});
|
|
3182
|
+
}
|
|
3183
|
+
const end = new Float64Array(n * 2);
|
|
3184
|
+
const lift = new Float64Array(n * 4);
|
|
3185
|
+
for (let r = 0; r < n; r++) {
|
|
3186
|
+
const i0 = Math.max(0, Math.floor(top - ys[Math.max(0, r - 1)]));
|
|
3187
|
+
const i1 = Math.min(count, Math.ceil(top - ys[Math.min(n - 1, r + 1)]));
|
|
3188
|
+
sides.forEach((side, k) => {
|
|
3189
|
+
let d = Infinity;
|
|
3190
|
+
let allowFar = Infinity;
|
|
3191
|
+
let allowNear = Infinity;
|
|
3192
|
+
for (let i = i0; i < i1; i++) {
|
|
3193
|
+
if (Number.isNaN(offset[k][i])) continue;
|
|
3194
|
+
d = Math.min(d, offset[k][i]);
|
|
3195
|
+
allowFar = Math.min(allowFar, far[k][i]);
|
|
3196
|
+
allowNear = Math.min(allowNear, near[k][i]);
|
|
3197
|
+
}
|
|
3198
|
+
end[r * 2 + k] = d;
|
|
3199
|
+
if (d === Infinity || d <= inner) return;
|
|
3200
|
+
const x = f.axisX + side * d;
|
|
3201
|
+
sides.forEach((s, j) => {
|
|
3202
|
+
const allow = Math.min(
|
|
3203
|
+
coreAt(x, ys[r], s),
|
|
3204
|
+
s === side ? allowFar : allowNear
|
|
3205
|
+
);
|
|
3206
|
+
lift[(r * 2 + k) * 2 + j] = Math.max(0, allow - followAt(x, ys[r], s));
|
|
3207
|
+
});
|
|
3208
|
+
});
|
|
3209
|
+
}
|
|
3210
|
+
const rowRide = (r, k, j, d) => {
|
|
3211
|
+
const v = lift[(r * 2 + k) * 2 + j];
|
|
3212
|
+
return v === 0 ? 0 : v * Math.min(1, (d - inner) / (end[r * 2 + k] - inner));
|
|
3213
|
+
};
|
|
3214
|
+
return (x, y, s) => {
|
|
3215
|
+
const d = Math.abs(x - f.axisX);
|
|
3216
|
+
if (s === 0 || d <= inner) return 0;
|
|
3217
|
+
const k = x < f.axisX ? 0 : 1;
|
|
3218
|
+
const j = s < 0 ? 0 : 1;
|
|
3219
|
+
const { r, t } = gridRow(ys, y, true);
|
|
3220
|
+
return rowRide(r, k, j, d) * (1 - t) + rowRide(r + 1, k, j, d) * t;
|
|
3221
|
+
};
|
|
3222
|
+
}
|
|
3223
|
+
function crownRide(m, cap, relAt) {
|
|
3224
|
+
const f = m.frame;
|
|
3225
|
+
const grid = m.hairFrontGrid;
|
|
3226
|
+
if (grid === void 0) return void 0;
|
|
3227
|
+
const { xs, ys } = grid;
|
|
3228
|
+
const top = ys[0];
|
|
3229
|
+
if (f.hairRuns("hair_front", top) === void 0 || f.hairRuns("hair_back", top) === void 0) {
|
|
3230
|
+
return void 0;
|
|
3231
|
+
}
|
|
3232
|
+
const n = ys.length;
|
|
3233
|
+
const count = Math.round(top - ys[n - 1]);
|
|
3234
|
+
const hh = f.hh;
|
|
3235
|
+
const face = faceField(m);
|
|
3236
|
+
const { wr, blendAt } = renderedCrownBlend(m, ys);
|
|
3237
|
+
const backRows = /* @__PURE__ */ new Map();
|
|
3238
|
+
const backAt = (j) => {
|
|
3239
|
+
let runs = backRows.get(j);
|
|
3240
|
+
if (runs === void 0) {
|
|
3241
|
+
runs = f.hairRuns("hair_back", top - j - 0.5);
|
|
3242
|
+
backRows.set(j, runs);
|
|
3243
|
+
}
|
|
3244
|
+
return runs;
|
|
3245
|
+
};
|
|
3246
|
+
const faceAt = (j) => {
|
|
3247
|
+
const y = top - j - 0.5;
|
|
3248
|
+
return f.faceRuns(y) ?? (y > f.face.y0 && y < f.face.y1 ? [f.face.x0, f.face.x1] : []);
|
|
3249
|
+
};
|
|
3250
|
+
const passed = (i, dys) => [
|
|
3251
|
+
Math.floor(i - Math.max(0, ...dys)),
|
|
3252
|
+
Math.ceil(i + 1 - Math.min(0, ...dys))
|
|
3253
|
+
];
|
|
3254
|
+
const rel = new Float64Array(n * xs.length * 2).fill(NaN);
|
|
3255
|
+
const relOf = (r, c, k) => {
|
|
3256
|
+
const at = (r * xs.length + c) * 2 + k;
|
|
3257
|
+
if (Number.isNaN(rel[at])) rel[at] = relAt(xs[c], ys[r], k === 0 ? -1 : 1);
|
|
3258
|
+
return rel[at];
|
|
3259
|
+
};
|
|
3260
|
+
const cellR = (x, y, k) => {
|
|
3261
|
+
let c = 0;
|
|
3262
|
+
while (c < xs.length - 2 && xs[c + 1] < x) c++;
|
|
3263
|
+
const { r } = gridRow(ys, y);
|
|
3264
|
+
return Math.max(
|
|
3265
|
+
relOf(r, c, k),
|
|
3266
|
+
relOf(r, c + 1, k),
|
|
3267
|
+
relOf(r + 1, c, k),
|
|
3268
|
+
relOf(r + 1, c + 1, k)
|
|
3269
|
+
);
|
|
3270
|
+
};
|
|
3271
|
+
const someAngle = (pairs) => {
|
|
3272
|
+
let lo = 0;
|
|
3273
|
+
let hi = 1;
|
|
3274
|
+
for (const [c0, c1] of pairs) {
|
|
3275
|
+
if (c1 === 0) {
|
|
3276
|
+
if (c0 <= 0) return false;
|
|
3277
|
+
} else if (c1 > 0) lo = Math.max(lo, -c0 / c1);
|
|
3278
|
+
else hi = Math.min(hi, -c0 / c1);
|
|
3279
|
+
}
|
|
3280
|
+
return lo < hi;
|
|
3281
|
+
};
|
|
3282
|
+
const slack = [-1, 1].map(() => new Float64Array(count).fill(NaN));
|
|
3283
|
+
const slackAt = (i, k) => {
|
|
3284
|
+
if (!Number.isNaN(slack[k][i])) return slack[k][i];
|
|
3285
|
+
const y = top - i - 0.5;
|
|
3286
|
+
const w = blendAt(y);
|
|
3287
|
+
const nod = {
|
|
3288
|
+
up: w * NOD.hairFront.up + (1 - w) * cap.up,
|
|
3289
|
+
down: w * NOD.hairFront.down + (1 - w) * cap.down
|
|
3290
|
+
};
|
|
3291
|
+
const ays = [-30, 30];
|
|
3292
|
+
const [b0, b1] = passed(
|
|
3293
|
+
i,
|
|
3294
|
+
ays.map((ay) => nodDy(nod, ay, hh) - nodDy(NOD.hairBack, ay, hh))
|
|
3295
|
+
);
|
|
3296
|
+
const band = [];
|
|
3297
|
+
for (let j = b0; j < b1; j++) band.push(backAt(j));
|
|
3298
|
+
const faces = [];
|
|
3299
|
+
const [f0, f1] = passed(
|
|
3300
|
+
i,
|
|
3301
|
+
ays.map((ay) => nodDy(nod, ay, hh) - face(f.axisX, y, 0, ay)[1])
|
|
3302
|
+
);
|
|
3303
|
+
for (let j = f0; j < f1; j++)
|
|
3304
|
+
faces.push({ y: top - j - 0.5, runs: faceAt(j) });
|
|
3305
|
+
const front = f.hairRuns("hair_front", y);
|
|
3306
|
+
[-1, 1].forEach((s, side) => {
|
|
3307
|
+
let least = Infinity;
|
|
3308
|
+
const far = outerEdge(front, f.axisX, s);
|
|
3309
|
+
if (far !== void 0) {
|
|
3310
|
+
const R = cellR(far, y, side);
|
|
3311
|
+
const c = Math.min(...band.map((b) => runReach(b, far, s).outward));
|
|
3312
|
+
if (R > 0) least = Math.min(least, m.shellScale * c / R);
|
|
3313
|
+
}
|
|
3314
|
+
for (let t = s > 0 ? 0 : 1; t < front.length; t += 2) {
|
|
3315
|
+
const x = front[t];
|
|
3316
|
+
const R = cellR(x, y, side);
|
|
3317
|
+
if (R <= 0) continue;
|
|
3318
|
+
let c = Math.min(
|
|
3319
|
+
...band.map((b) => runReach(b, x, s > 0 ? -1 : 1).inward)
|
|
3320
|
+
);
|
|
3321
|
+
const sh = (m.shellScale - 1) * (x - f.axisX);
|
|
3322
|
+
const d0 = sh + s * m.hairBack;
|
|
3323
|
+
const d1 = d0 + s * R;
|
|
3324
|
+
const lo = Math.min(d0, d1);
|
|
3325
|
+
const hi = Math.max(d0, d1);
|
|
3326
|
+
const bares = faces.some(({ y: fy, runs }) => {
|
|
3327
|
+
for (let r = 0; r < runs.length; r += 2) {
|
|
3328
|
+
const e0 = runs[r];
|
|
3329
|
+
const e1 = runs[r + 1];
|
|
3330
|
+
const de0 = face(e0, fy, 30 * s, 0)[0];
|
|
3331
|
+
const de1 = face(e1, fy, 30 * s, 0)[0];
|
|
3332
|
+
if (someAngle([
|
|
3333
|
+
[e1 - x, de1 - lo],
|
|
3334
|
+
[x - e0, hi - de0]
|
|
3335
|
+
])) {
|
|
3336
|
+
return true;
|
|
3337
|
+
}
|
|
3100
3338
|
}
|
|
3101
|
-
return
|
|
3102
|
-
};
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
return y >= face.y1 ? face.y1 : Math.max(face.y0, y - CUT_MARGIN);
|
|
3106
|
-
};
|
|
3339
|
+
return false;
|
|
3340
|
+
});
|
|
3341
|
+
if (bares) c = 0;
|
|
3342
|
+
least = Math.min(least, m.shellScale * c / R);
|
|
3107
3343
|
}
|
|
3108
|
-
|
|
3344
|
+
slack[side][i] = least - w;
|
|
3345
|
+
});
|
|
3346
|
+
return slack[k][i];
|
|
3347
|
+
};
|
|
3348
|
+
const extra = [0, 1].map((k) => {
|
|
3349
|
+
const out = wr.slice();
|
|
3350
|
+
for (let r = 0; r < n; r++) {
|
|
3351
|
+
if (wr[r] >= 1) continue;
|
|
3352
|
+
const i0 = Math.max(0, Math.floor(top - ys[Math.max(0, r - 1)]));
|
|
3353
|
+
const i1 = Math.min(count, Math.ceil(top - ys[Math.min(n - 1, r + 1)]));
|
|
3354
|
+
let least = Infinity;
|
|
3355
|
+
for (let i = i0; i < i1; i++) least = Math.min(least, slackAt(i, k));
|
|
3356
|
+
out[r] = wr[r] + clamp(least, 0, 1 - wr[r]);
|
|
3357
|
+
}
|
|
3358
|
+
for (let r = n - 2; r >= 0; r--) {
|
|
3359
|
+
if (wr[r] < 1) out[r] = Math.max(wr[r], Math.min(out[r], out[r + 1]));
|
|
3360
|
+
}
|
|
3361
|
+
return out.map((v, r) => v - wr[r]);
|
|
3362
|
+
});
|
|
3363
|
+
return (y, s) => {
|
|
3364
|
+
if (s === 0) return 0;
|
|
3365
|
+
const e = extra[s < 0 ? 0 : 1];
|
|
3366
|
+
const { r, t } = gridRow(ys, y, true);
|
|
3367
|
+
return e[r] * (1 - t) + e[r + 1] * t;
|
|
3368
|
+
};
|
|
3369
|
+
}
|
|
3370
|
+
function familyField(m, family) {
|
|
3371
|
+
switch (family) {
|
|
3372
|
+
case "face":
|
|
3373
|
+
return faceField(m);
|
|
3374
|
+
case "eye_L":
|
|
3375
|
+
case "eye_R":
|
|
3376
|
+
return eyeField(m, m.boxes[family]);
|
|
3377
|
+
case "brow_L":
|
|
3378
|
+
case "brow_R":
|
|
3379
|
+
return browField(m, m.boxes[family]);
|
|
3380
|
+
case "nose":
|
|
3381
|
+
return noseField(m);
|
|
3382
|
+
case "mouth":
|
|
3383
|
+
return mouthField(m);
|
|
3384
|
+
case "hair_front":
|
|
3385
|
+
return hairFrontField(m);
|
|
3386
|
+
case "hair_back":
|
|
3387
|
+
return hairField(m, m.hairBack, NOD.hairBack);
|
|
3388
|
+
case "body":
|
|
3389
|
+
return () => [0, 0];
|
|
3109
3390
|
}
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
// src/auto-rig/context.ts
|
|
3394
|
+
function sidePair(byRole, a, b) {
|
|
3395
|
+
const la = byRole.get(a);
|
|
3396
|
+
const lb = byRole.get(b);
|
|
3397
|
+
if (la === void 0 || lb === void 0) return void 0;
|
|
3398
|
+
const [lo, hi] = [
|
|
3399
|
+
{ role: a, box: boxOfLayer(la) },
|
|
3400
|
+
{ role: b, box: boxOfLayer(lb) }
|
|
3401
|
+
].sort((p, q) => cx(p.box) - cx(q.box));
|
|
3402
|
+
return { left: lo, right: hi };
|
|
3403
|
+
}
|
|
3404
|
+
function sideIrises(byRole) {
|
|
3405
|
+
const pair = sidePair(byRole, "iris_L", "iris_R");
|
|
3406
|
+
return pair === void 0 ? {} : { left: pair.left.box, right: pair.right.box };
|
|
3407
|
+
}
|
|
3408
|
+
function solveContext(frame, byRole, options, hasNose) {
|
|
3409
|
+
const box = (role) => boxOfLayer(byRole.get(role));
|
|
3410
|
+
const row = (s, strand2, sideSign) => {
|
|
3411
|
+
if (strand2 !== void 0) {
|
|
3412
|
+
const lo = sideSign < 0 ? strand2.irisOuter : strand2.irisInner;
|
|
3413
|
+
const hi = sideSign < 0 ? strand2.irisInner : strand2.irisOuter;
|
|
3414
|
+
return { role: s.role, lo, hi, y: strand2.y };
|
|
3122
3415
|
}
|
|
3416
|
+
const inset = bw(s.box) > 4 ? 1 : 0;
|
|
3417
|
+
return {
|
|
3418
|
+
role: s.role,
|
|
3419
|
+
lo: s.box.x0 + inset,
|
|
3420
|
+
hi: s.box.x1 - inset,
|
|
3421
|
+
y: cy(s.box)
|
|
3422
|
+
};
|
|
3423
|
+
};
|
|
3424
|
+
const eyes = sidePair(byRole, "eye_L", "eye_R");
|
|
3425
|
+
const irises = sidePair(byRole, "iris_L", "iris_R");
|
|
3426
|
+
const pair = irises ?? eyes;
|
|
3427
|
+
const strand = irises ? options.strandEdges : void 0;
|
|
3428
|
+
const left = row(pair.left, strand?.left, -1);
|
|
3429
|
+
const right = row(pair.right, strand?.right, 1);
|
|
3430
|
+
let candidates;
|
|
3431
|
+
const edges = options.headEdges;
|
|
3432
|
+
if (frame.hairShell && edges && edges.left.length > 0 && edges.right.length > 0) {
|
|
3433
|
+
candidates = {
|
|
3434
|
+
left: edges.left.map((e) => ({ role: e.role, x: e.x })),
|
|
3435
|
+
// A right edge names its outermost pixel column; the boundary is one on.
|
|
3436
|
+
right: edges.right.map((e) => ({ role: e.role, x: e.x + 1 }))
|
|
3437
|
+
};
|
|
3438
|
+
} else if (frame.hairShell) {
|
|
3439
|
+
candidates = {
|
|
3440
|
+
left: [{ role: "hair_back", x: frame.shellLeft }],
|
|
3441
|
+
right: [{ role: "hair_back", x: frame.shellRight }]
|
|
3442
|
+
};
|
|
3443
|
+
} else {
|
|
3444
|
+
const w = frame.paintedHalfAt(frame.eyeY);
|
|
3445
|
+
candidates = {
|
|
3446
|
+
left: [{ role: "face", x: frame.axisX - w }],
|
|
3447
|
+
right: [{ role: "face", x: frame.axisX + w }]
|
|
3448
|
+
};
|
|
3449
|
+
}
|
|
3450
|
+
const restHalf = (Math.max(...candidates.right.map((c) => c.x)) - Math.min(...candidates.left.map((c) => c.x))) / 2;
|
|
3451
|
+
const nose = byRole.get("nose");
|
|
3452
|
+
let noseAt;
|
|
3453
|
+
let noseBox;
|
|
3454
|
+
if (hasNose && nose !== void 0) {
|
|
3455
|
+
noseBox = nose.denseCore ? boxOfPixels(nose.denseCore, nose.canvasW, nose.canvasH) : boxOfLayer(nose);
|
|
3456
|
+
noseAt = { x: cx(noseBox), y: cy(noseBox) };
|
|
3123
3457
|
}
|
|
3458
|
+
const mouthBox = byRole.has("mouth_open") ? unionBoxes([box("mouth"), box("mouth_open")]) : box("mouth");
|
|
3459
|
+
const m = box("mouth");
|
|
3124
3460
|
return {
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3461
|
+
frame,
|
|
3462
|
+
left,
|
|
3463
|
+
right,
|
|
3464
|
+
candidates,
|
|
3465
|
+
restHalf,
|
|
3466
|
+
strand,
|
|
3467
|
+
hasHairFront: byRole.has("hair_front"),
|
|
3468
|
+
eyeBoxes: { left: eyes.left.box, right: eyes.right.box },
|
|
3469
|
+
noseAt,
|
|
3470
|
+
noseBox,
|
|
3471
|
+
mouthAt: { x: cx(m), y: cy(m) },
|
|
3472
|
+
mouthBox
|
|
3473
|
+
};
|
|
3474
|
+
}
|
|
3475
|
+
function renderedLander(turn, parts, grids, from = /* @__PURE__ */ new Map()) {
|
|
3476
|
+
const analytic = /* @__PURE__ */ new Map();
|
|
3477
|
+
const landed = /* @__PURE__ */ new Map();
|
|
3478
|
+
const field = (role) => {
|
|
3479
|
+
const family = roleSpec(role).family;
|
|
3480
|
+
let f = analytic.get(family);
|
|
3481
|
+
if (f === void 0) analytic.set(family, f = familyField(turn, family));
|
|
3482
|
+
return f;
|
|
3483
|
+
};
|
|
3484
|
+
return (role, x, y, ax) => {
|
|
3485
|
+
const family = roleSpec(role).family;
|
|
3486
|
+
const part = parts.find((p) => p.id === role);
|
|
3487
|
+
const g = grids.get(family);
|
|
3488
|
+
const warp = part?.warps?.find((w) => w.parameter === import_format3.StandardParameter.AngleX);
|
|
3489
|
+
if (part === void 0 || part.mesh === void 0 || !g && !warp) {
|
|
3490
|
+
return x + field(role)(x, y, ax, 0)[0];
|
|
3491
|
+
}
|
|
3492
|
+
const box = {
|
|
3493
|
+
x0: part.transform.x - part.width / 2,
|
|
3494
|
+
x1: part.transform.x + part.width / 2,
|
|
3495
|
+
y0: part.transform.y - part.height / 2,
|
|
3496
|
+
y1: part.transform.y + part.height / 2
|
|
3497
|
+
};
|
|
3498
|
+
const rest = meshPoints(part.mesh, box);
|
|
3499
|
+
const key = `${role}@${ax}`;
|
|
3500
|
+
let xs = landed.get(key);
|
|
3501
|
+
if (xs === void 0) {
|
|
3502
|
+
if (g) {
|
|
3503
|
+
const offsets = keyformAt(g.keyforms, ax);
|
|
3504
|
+
xs = rest.map(([px, py]) => land(g.lattice, offsets, px, py)[0]);
|
|
3505
|
+
} else {
|
|
3506
|
+
const k = warp.keyforms;
|
|
3507
|
+
const offsets = blendStops(
|
|
3508
|
+
k.map((kf) => kf.value),
|
|
3509
|
+
k.map((kf) => kf.offsets),
|
|
3510
|
+
ax
|
|
3511
|
+
);
|
|
3512
|
+
xs = rest.map(([px], i) => px + offsets[2 * i] * bw(box));
|
|
3513
|
+
}
|
|
3514
|
+
landed.set(key, xs);
|
|
3515
|
+
}
|
|
3516
|
+
const idx = part.mesh.indices;
|
|
3517
|
+
for (let t = from.get(role) ?? 0; t < idx.length; t += 3) {
|
|
3518
|
+
const [a, b, c] = [idx[t], idx[t + 1], idx[t + 2]];
|
|
3519
|
+
const [ax0, ay0] = rest[a];
|
|
3520
|
+
const [bx0, by0] = rest[b];
|
|
3521
|
+
const [cx0, cy0] = rest[c];
|
|
3522
|
+
const d = (by0 - cy0) * (ax0 - cx0) + (cx0 - bx0) * (ay0 - cy0);
|
|
3523
|
+
if (Math.abs(d) < 1e-9) continue;
|
|
3524
|
+
const u = ((by0 - cy0) * (x - cx0) + (cx0 - bx0) * (y - cy0)) / d;
|
|
3525
|
+
const v = ((cy0 - ay0) * (x - cx0) + (ax0 - cx0) * (y - cy0)) / d;
|
|
3526
|
+
const w = 1 - u - v;
|
|
3527
|
+
if (u < -1e-6 || v < -1e-6 || w < -1e-6) continue;
|
|
3528
|
+
return u * xs[a] + v * xs[b] + w * xs[c];
|
|
3529
|
+
}
|
|
3530
|
+
return x + field(role)(x, y, ax, 0)[0];
|
|
3146
3531
|
};
|
|
3147
3532
|
}
|
|
3148
3533
|
|
|
@@ -4021,14 +4406,18 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
4021
4406
|
};
|
|
4022
4407
|
const solveFrame = buildHeadFrame(layers, frameOptions);
|
|
4023
4408
|
const ctx = solveContext(solveFrame, byRole, turnOptions, hasNose);
|
|
4409
|
+
const boxes = Object.fromEntries(
|
|
4410
|
+
["eye_L", "eye_R", "brow_L", "brow_R"].filter(has).map((r) => [r, box(r)])
|
|
4411
|
+
);
|
|
4024
4412
|
const landmarks = {
|
|
4025
|
-
boxes
|
|
4026
|
-
["eye_L", "eye_R", "brow_L", "brow_R"].filter(has).map((r) => [r, box(r)])
|
|
4027
|
-
),
|
|
4413
|
+
boxes,
|
|
4028
4414
|
noseAt: ctx.noseAt,
|
|
4029
4415
|
noseTopY: ctx.noseBox?.y1,
|
|
4030
4416
|
mouthAt: ctx.mouthAt,
|
|
4031
|
-
hairTop: has("hair_front") ? box("hair_front").y1 : solveFrame.face.y1
|
|
4417
|
+
hairTop: has("hair_front") ? box("hair_front").y1 : solveFrame.face.y1,
|
|
4418
|
+
// The field reads the grid the mesh is built on (the rebuilt frame below
|
|
4419
|
+
// differs only in the jaw's cut, which the grid does not read).
|
|
4420
|
+
hairFrontGrid: has("hair_front") ? hairFrontGrid(box("hair_front"), solveFrame, boxes) : void 0
|
|
4032
4421
|
};
|
|
4033
4422
|
const build = (q) => buildTurn(solveFrame, landmarks, q);
|
|
4034
4423
|
const fit = fitTurn(ctx, targets, style, build);
|
|
@@ -4050,7 +4439,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
4050
4439
|
parts.length,
|
|
4051
4440
|
frame,
|
|
4052
4441
|
byRole,
|
|
4053
|
-
style
|
|
4442
|
+
style,
|
|
4443
|
+
landmarks.hairFrontGrid
|
|
4054
4444
|
);
|
|
4055
4445
|
if (built.region !== void 0) {
|
|
4056
4446
|
regionOf = built.region;
|
|
@@ -4141,7 +4531,7 @@ function plateFields(turn, region, head) {
|
|
|
4141
4531
|
const neck = neckField(turn);
|
|
4142
4532
|
return (i) => region[i] === "ear" ? ear : region[i] === "neck" ? neck : head;
|
|
4143
4533
|
}
|
|
4144
|
-
function buildPart(spec, b, order, frame, byRole, style) {
|
|
4534
|
+
function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
|
|
4145
4535
|
const box = (role) => boxOfLayer(byRole.get(role));
|
|
4146
4536
|
const part = {
|
|
4147
4537
|
id: spec.role,
|
|
@@ -4164,30 +4554,15 @@ function buildPart(spec, b, order, frame, byRole, style) {
|
|
|
4164
4554
|
mesh = fm.mesh;
|
|
4165
4555
|
region = fm.region;
|
|
4166
4556
|
headStart = fm.headStart;
|
|
4557
|
+
} else if (spec.role === "hair_front") {
|
|
4558
|
+
mesh = columnMesh(b, frontGrid);
|
|
4167
4559
|
} else if (spec.family !== "body") {
|
|
4168
4560
|
const cells = MESH_CELLS[spec.role] ?? FEATURE_MESH_CELLS;
|
|
4169
4561
|
const px = Math.min(cells.px, meshScale(spec.role, frame));
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
["eye_L", "eye_R"].filter((r) => byRole.has(r)).map((r) => [r, box(r)])
|
|
4175
|
-
);
|
|
4176
|
-
const { inner, left, right } = hairFrontBends(frame, boxes);
|
|
4177
|
-
mesh = columnMesh(
|
|
4178
|
-
b,
|
|
4179
|
-
[
|
|
4180
|
-
frame.axisX - left,
|
|
4181
|
-
frame.axisX - inner,
|
|
4182
|
-
frame.axisX + inner,
|
|
4183
|
-
frame.axisX + right
|
|
4184
|
-
],
|
|
4185
|
-
cols,
|
|
4186
|
-
rows
|
|
4187
|
-
);
|
|
4188
|
-
} else {
|
|
4189
|
-
mesh = gridMesh(cols, rows);
|
|
4190
|
-
}
|
|
4562
|
+
mesh = gridMesh(
|
|
4563
|
+
cellsFor(bw(b), px, cells.min, cells.max),
|
|
4564
|
+
cellsFor(bh(b), px, cells.min, cells.max)
|
|
4565
|
+
);
|
|
4191
4566
|
}
|
|
4192
4567
|
if (mesh !== void 0) part.mesh = mesh;
|
|
4193
4568
|
const side = spec.role.endsWith("_L") ? "L" : "R";
|
|
@@ -4306,16 +4681,35 @@ function buildPart(spec, b, order, frame, byRole, style) {
|
|
|
4306
4681
|
headStart
|
|
4307
4682
|
};
|
|
4308
4683
|
}
|
|
4309
|
-
function
|
|
4684
|
+
function hairFrontGrid(b, frame, boxes) {
|
|
4685
|
+
const cells = MESH_CELLS.hair_front;
|
|
4686
|
+
const px = Math.min(cells.px, meshScale("hair_front", frame));
|
|
4687
|
+
const cols = cellsFor(bw(b), px, cells.min, cells.max);
|
|
4688
|
+
const rows = cellsFor(bh(b), px, cells.min, cells.max);
|
|
4689
|
+
const { inner, left, right } = hairFrontBends(frame, boxes);
|
|
4310
4690
|
const step = bw(b) / cols;
|
|
4311
4691
|
const xs = Array.from({ length: cols + 1 }, (_, c) => b.x0 + c * step);
|
|
4312
|
-
for (const x of
|
|
4692
|
+
for (const x of [
|
|
4693
|
+
frame.axisX - left,
|
|
4694
|
+
frame.axisX - inner,
|
|
4695
|
+
frame.axisX + inner,
|
|
4696
|
+
frame.axisX + right
|
|
4697
|
+
]) {
|
|
4313
4698
|
if (x <= b.x0 + 0.25 || x >= b.x1 - 0.25) continue;
|
|
4314
4699
|
const near = xs.findIndex((v) => Math.abs(v - x) < 0.3 * step);
|
|
4315
4700
|
if (near > 0 && near < xs.length - 1) xs[near] = x;
|
|
4316
4701
|
else if (near < 0) xs.push(x);
|
|
4317
4702
|
}
|
|
4318
4703
|
xs.sort((p, q) => p - q);
|
|
4704
|
+
const ys = Array.from(
|
|
4705
|
+
{ length: rows + 1 },
|
|
4706
|
+
(_, r) => b.y1 - bh(b) * r / rows
|
|
4707
|
+
);
|
|
4708
|
+
return { xs, ys };
|
|
4709
|
+
}
|
|
4710
|
+
function columnMesh(b, grid) {
|
|
4711
|
+
const { xs } = grid;
|
|
4712
|
+
const rows = grid.ys.length - 1;
|
|
4319
4713
|
const vertices = [];
|
|
4320
4714
|
const uvs = [];
|
|
4321
4715
|
for (let r = 0; r <= rows; r++) {
|