@ikijs/editor 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +36 -20
- package/dist/index.d.ts +36 -20
- package/dist/index.js +73 -48
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +73 -48
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -891,9 +891,12 @@ interface GenerateOptions {
|
|
|
891
891
|
* wedge carries the painted line, fading out in the antialias column as the
|
|
892
892
|
* painted edge does; the fan inside it is the crop's own pixels.
|
|
893
893
|
*
|
|
894
|
-
* Draw order: hair_back < body < elbow_L,
|
|
895
|
-
* forearm_R < face … < hair_front. The
|
|
896
|
-
* which draws over the cap.
|
|
894
|
+
* Draw order: hair_back < body < elbow_L, forearm_L, arm_L, elbow_R,
|
|
895
|
+
* forearm_R, arm_R < face … < hair_front. The upper arm draws over the
|
|
896
|
+
* forearm, which draws over the cap. On the inside of a bend the bands
|
|
897
|
+
* overlap, and the forearm's seam-row cut stays under the upper arm there, so
|
|
898
|
+
* the overlap shows the upper arm's bottom edge at the elbow row instead of
|
|
899
|
+
* the cut rotated up the sleeve.
|
|
897
900
|
*
|
|
898
901
|
* Signs: ParamArmX + raises the arm outward on either side, in degrees 1:1.
|
|
899
902
|
* A rotation is CCW-positive, so an arm whose shoulder lies right of the
|
|
@@ -1070,17 +1073,23 @@ declare function parseLayerRoles(fileNames: string[]): {
|
|
|
1070
1073
|
* then stay inside the line's closed ink at every column, the tapered ends
|
|
1071
1074
|
* included, and tucked skin never shows above the line.
|
|
1072
1075
|
*
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1076
|
-
* fitted
|
|
1077
|
-
*
|
|
1078
|
-
*
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1083
|
-
*
|
|
1076
|
+
* Shut, the line is a smooth parabola across the opening. Its chord joins the
|
|
1077
|
+
* line's smoothed bottom (below) at the two end columns; its depth below the
|
|
1078
|
+
* chord, half way, is the sag at the centre column of the drawn arc plus a
|
|
1079
|
+
* fitted closing travel: a cubic in the column index, pinned to zero at the
|
|
1080
|
+
* end columns and fitted by least squares to the raw per-column travel
|
|
1081
|
+
* `(T - UPPER_SHARE * H) - Tu`, how far the line's bottom is from the share's
|
|
1082
|
+
* point in the interior's span. That sag is capped at `CLOSED_SAG_MAX` of the
|
|
1083
|
+
* opening's width, the drawn arc included, so a deep opening closes into a
|
|
1084
|
+
* gentle smile and a line drawn deeper than the cap closes flatter than
|
|
1085
|
+
* drawn. The seam is `Tu` plus the column's travel: the parabola minus the
|
|
1086
|
+
* line's bottom averaged over 3 columns each side (the smoothed bottom), not
|
|
1087
|
+
* minus `Tu` itself, so the end columns do not travel. `Tu` is an integer-row read, and a curve minus that staircase
|
|
1088
|
+
* is a sawtooth the mesh would carry into the rendered line; against the
|
|
1089
|
+
* smoothed bottom the stroke (whose antialiased edge is smooth) moves as one
|
|
1090
|
+
* piece, and a kink drawn in it (a V at the centre) keeps only its offset
|
|
1091
|
+
* from that average, which the cap counts in. Every closed shape and the
|
|
1092
|
+
* preview are read off the seam.
|
|
1084
1093
|
*
|
|
1085
1094
|
* The lower lip's columns outside the opening (the corner hooks) do not fold,
|
|
1086
1095
|
* the line's are only thinned and shaped (no travel), and the interior's edge
|
|
@@ -1093,9 +1102,9 @@ declare function parseLayerRoles(fileNames: string[]): {
|
|
|
1093
1102
|
*
|
|
1094
1103
|
* Knots: the parts share column knots every `MOUTH_KNOT_PX` plus the
|
|
1095
1104
|
* opening's boundaries `x0`, `x1` and `x1 + 1`, each an integer canvas
|
|
1096
|
-
* boundary. The
|
|
1105
|
+
* boundary. The travel's x is the column index, which is the mesh's boundary
|
|
1097
1106
|
* position: a knot at boundary `b` samples column `b`, so the mesh renders the
|
|
1098
|
-
*
|
|
1107
|
+
* travel exactly at its knots and the pinned ends render as pinned (`x1` and
|
|
1099
1108
|
* `x1 + 1` both sample the last column; the second sits at its right edge).
|
|
1100
1109
|
* The preview resamples each column through the lip meshes' own landed rows
|
|
1101
1110
|
* (`mouthRestShift`). A part's mesh rounds its local x, so one knot
|
|
@@ -1129,8 +1138,8 @@ interface OpeningColumn {
|
|
|
1129
1138
|
/** How far the skin tucks up under the line shut: inside the thinned,
|
|
1130
1139
|
* squashed line's ink. */
|
|
1131
1140
|
overlap: number;
|
|
1132
|
-
/** Where the lips meet when shut: the line's bottom edge after its
|
|
1133
|
-
*
|
|
1141
|
+
/** Where the lips meet when shut: the line's bottom edge after its closing
|
|
1142
|
+
* travel. */
|
|
1134
1143
|
seam: number;
|
|
1135
1144
|
}
|
|
1136
1145
|
interface Opening {
|
|
@@ -1507,10 +1516,17 @@ interface AtlasLayout {
|
|
|
1507
1516
|
* BOTTOM only. Page left/top edges need no gutter.
|
|
1508
1517
|
*
|
|
1509
1518
|
* pageWidth/pageHeight = tight bound (max x+width+padding, max y+height+padding).
|
|
1519
|
+
* When `maxSide` is given and the first layout is taller than it but not wider,
|
|
1520
|
+
* the pack is retried at every width from the widest padded source up to `maxSide`, and the first
|
|
1521
|
+
* layout that fits the height wins (shelf height is not monotonic in width);
|
|
1522
|
+
* when none does the first is returned for the caller to refuse. The attempts
|
|
1523
|
+
* are every width for this shelf order, not a proof that no packing exists.
|
|
1524
|
+
*
|
|
1510
1525
|
* Empty sources → { pageWidth: 0, pageHeight: 0, placements: [], padding }.
|
|
1511
|
-
* Throws a plain Error naming the offending source id on a non-finite or <= 0 dimension
|
|
1526
|
+
* Throws a plain Error naming the offending source id on a non-finite or <= 0 dimension,
|
|
1527
|
+
* and on a supplied `maxSide` that is non-finite or <= 0.
|
|
1512
1528
|
*/
|
|
1513
|
-
declare function packAtlas(sources: AtlasSource[], padding?: number): AtlasLayout;
|
|
1529
|
+
declare function packAtlas(sources: AtlasSource[], padding?: number, maxSide?: number): AtlasLayout;
|
|
1514
1530
|
/**
|
|
1515
1531
|
* Convert a pixel placement into a UV rect, inset by `insetPx` on all four
|
|
1516
1532
|
* edges and clamped to [0, 1] so the validator's bounds check always passes.
|
package/dist/index.d.ts
CHANGED
|
@@ -891,9 +891,12 @@ interface GenerateOptions {
|
|
|
891
891
|
* wedge carries the painted line, fading out in the antialias column as the
|
|
892
892
|
* painted edge does; the fan inside it is the crop's own pixels.
|
|
893
893
|
*
|
|
894
|
-
* Draw order: hair_back < body < elbow_L,
|
|
895
|
-
* forearm_R < face … < hair_front. The
|
|
896
|
-
* which draws over the cap.
|
|
894
|
+
* Draw order: hair_back < body < elbow_L, forearm_L, arm_L, elbow_R,
|
|
895
|
+
* forearm_R, arm_R < face … < hair_front. The upper arm draws over the
|
|
896
|
+
* forearm, which draws over the cap. On the inside of a bend the bands
|
|
897
|
+
* overlap, and the forearm's seam-row cut stays under the upper arm there, so
|
|
898
|
+
* the overlap shows the upper arm's bottom edge at the elbow row instead of
|
|
899
|
+
* the cut rotated up the sleeve.
|
|
897
900
|
*
|
|
898
901
|
* Signs: ParamArmX + raises the arm outward on either side, in degrees 1:1.
|
|
899
902
|
* A rotation is CCW-positive, so an arm whose shoulder lies right of the
|
|
@@ -1070,17 +1073,23 @@ declare function parseLayerRoles(fileNames: string[]): {
|
|
|
1070
1073
|
* then stay inside the line's closed ink at every column, the tapered ends
|
|
1071
1074
|
* included, and tucked skin never shows above the line.
|
|
1072
1075
|
*
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1076
|
-
* fitted
|
|
1077
|
-
*
|
|
1078
|
-
*
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1083
|
-
*
|
|
1076
|
+
* Shut, the line is a smooth parabola across the opening. Its chord joins the
|
|
1077
|
+
* line's smoothed bottom (below) at the two end columns; its depth below the
|
|
1078
|
+
* chord, half way, is the sag at the centre column of the drawn arc plus a
|
|
1079
|
+
* fitted closing travel: a cubic in the column index, pinned to zero at the
|
|
1080
|
+
* end columns and fitted by least squares to the raw per-column travel
|
|
1081
|
+
* `(T - UPPER_SHARE * H) - Tu`, how far the line's bottom is from the share's
|
|
1082
|
+
* point in the interior's span. That sag is capped at `CLOSED_SAG_MAX` of the
|
|
1083
|
+
* opening's width, the drawn arc included, so a deep opening closes into a
|
|
1084
|
+
* gentle smile and a line drawn deeper than the cap closes flatter than
|
|
1085
|
+
* drawn. The seam is `Tu` plus the column's travel: the parabola minus the
|
|
1086
|
+
* line's bottom averaged over 3 columns each side (the smoothed bottom), not
|
|
1087
|
+
* minus `Tu` itself, so the end columns do not travel. `Tu` is an integer-row read, and a curve minus that staircase
|
|
1088
|
+
* is a sawtooth the mesh would carry into the rendered line; against the
|
|
1089
|
+
* smoothed bottom the stroke (whose antialiased edge is smooth) moves as one
|
|
1090
|
+
* piece, and a kink drawn in it (a V at the centre) keeps only its offset
|
|
1091
|
+
* from that average, which the cap counts in. Every closed shape and the
|
|
1092
|
+
* preview are read off the seam.
|
|
1084
1093
|
*
|
|
1085
1094
|
* The lower lip's columns outside the opening (the corner hooks) do not fold,
|
|
1086
1095
|
* the line's are only thinned and shaped (no travel), and the interior's edge
|
|
@@ -1093,9 +1102,9 @@ declare function parseLayerRoles(fileNames: string[]): {
|
|
|
1093
1102
|
*
|
|
1094
1103
|
* Knots: the parts share column knots every `MOUTH_KNOT_PX` plus the
|
|
1095
1104
|
* opening's boundaries `x0`, `x1` and `x1 + 1`, each an integer canvas
|
|
1096
|
-
* boundary. The
|
|
1105
|
+
* boundary. The travel's x is the column index, which is the mesh's boundary
|
|
1097
1106
|
* position: a knot at boundary `b` samples column `b`, so the mesh renders the
|
|
1098
|
-
*
|
|
1107
|
+
* travel exactly at its knots and the pinned ends render as pinned (`x1` and
|
|
1099
1108
|
* `x1 + 1` both sample the last column; the second sits at its right edge).
|
|
1100
1109
|
* The preview resamples each column through the lip meshes' own landed rows
|
|
1101
1110
|
* (`mouthRestShift`). A part's mesh rounds its local x, so one knot
|
|
@@ -1129,8 +1138,8 @@ interface OpeningColumn {
|
|
|
1129
1138
|
/** How far the skin tucks up under the line shut: inside the thinned,
|
|
1130
1139
|
* squashed line's ink. */
|
|
1131
1140
|
overlap: number;
|
|
1132
|
-
/** Where the lips meet when shut: the line's bottom edge after its
|
|
1133
|
-
*
|
|
1141
|
+
/** Where the lips meet when shut: the line's bottom edge after its closing
|
|
1142
|
+
* travel. */
|
|
1134
1143
|
seam: number;
|
|
1135
1144
|
}
|
|
1136
1145
|
interface Opening {
|
|
@@ -1507,10 +1516,17 @@ interface AtlasLayout {
|
|
|
1507
1516
|
* BOTTOM only. Page left/top edges need no gutter.
|
|
1508
1517
|
*
|
|
1509
1518
|
* pageWidth/pageHeight = tight bound (max x+width+padding, max y+height+padding).
|
|
1519
|
+
* When `maxSide` is given and the first layout is taller than it but not wider,
|
|
1520
|
+
* the pack is retried at every width from the widest padded source up to `maxSide`, and the first
|
|
1521
|
+
* layout that fits the height wins (shelf height is not monotonic in width);
|
|
1522
|
+
* when none does the first is returned for the caller to refuse. The attempts
|
|
1523
|
+
* are every width for this shelf order, not a proof that no packing exists.
|
|
1524
|
+
*
|
|
1510
1525
|
* Empty sources → { pageWidth: 0, pageHeight: 0, placements: [], padding }.
|
|
1511
|
-
* Throws a plain Error naming the offending source id on a non-finite or <= 0 dimension
|
|
1526
|
+
* Throws a plain Error naming the offending source id on a non-finite or <= 0 dimension,
|
|
1527
|
+
* and on a supplied `maxSide` that is non-finite or <= 0.
|
|
1512
1528
|
*/
|
|
1513
|
-
declare function packAtlas(sources: AtlasSource[], padding?: number): AtlasLayout;
|
|
1529
|
+
declare function packAtlas(sources: AtlasSource[], padding?: number, maxSide?: number): AtlasLayout;
|
|
1514
1530
|
/**
|
|
1515
1531
|
* Convert a pixel placement into a UV rect, inset by `insetPx` on all four
|
|
1516
1532
|
* edges and clamped to [0, 1] so the validator's bounds check always passes.
|
package/dist/index.js
CHANGED
|
@@ -1845,7 +1845,10 @@ function captureBindingEndpoint(channel, restValue, posedValue) {
|
|
|
1845
1845
|
// src/atlas.ts
|
|
1846
1846
|
var ATLAS_PADDING = 2;
|
|
1847
1847
|
var UV_INSET_PX = 0.5;
|
|
1848
|
-
function packAtlas(sources, padding = ATLAS_PADDING) {
|
|
1848
|
+
function packAtlas(sources, padding = ATLAS_PADDING, maxSide) {
|
|
1849
|
+
if (maxSide !== void 0 && !(isFinite(maxSide) && maxSide > 0)) {
|
|
1850
|
+
throw new Error(`packAtlas: invalid maxSide ${maxSide}`);
|
|
1851
|
+
}
|
|
1849
1852
|
for (const src of sources) {
|
|
1850
1853
|
if (!isFinite(src.width) || src.width <= 0) {
|
|
1851
1854
|
throw new Error(
|
|
@@ -1866,42 +1869,50 @@ function packAtlas(sources, padding = ATLAS_PADDING) {
|
|
|
1866
1869
|
(sum, s) => sum + (s.width + padding) * (s.height + padding),
|
|
1867
1870
|
0
|
|
1868
1871
|
);
|
|
1869
|
-
const
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1872
|
+
const widest = Math.max(...sorted.map((s) => s.width + padding));
|
|
1873
|
+
const firstWidth = Math.max(Math.ceil(Math.sqrt(totalArea)), widest);
|
|
1874
|
+
const shelve = (targetWidth) => {
|
|
1875
|
+
const placements = [];
|
|
1876
|
+
let shelfX = 0;
|
|
1877
|
+
let shelfY = 0;
|
|
1878
|
+
let shelfHeight = 0;
|
|
1879
|
+
for (const src of sorted) {
|
|
1880
|
+
const paddedW = src.width + padding;
|
|
1881
|
+
const paddedH = src.height + padding;
|
|
1882
|
+
if (shelfX > 0 && shelfX + paddedW > targetWidth) {
|
|
1883
|
+
shelfY += shelfHeight;
|
|
1884
|
+
shelfX = 0;
|
|
1885
|
+
shelfHeight = 0;
|
|
1886
|
+
}
|
|
1887
|
+
placements.push({
|
|
1888
|
+
id: src.id,
|
|
1889
|
+
x: shelfX,
|
|
1890
|
+
y: shelfY,
|
|
1891
|
+
width: src.width,
|
|
1892
|
+
height: src.height
|
|
1893
|
+
});
|
|
1894
|
+
shelfX += paddedW;
|
|
1895
|
+
if (paddedH > shelfHeight) shelfHeight = paddedH;
|
|
1896
|
+
}
|
|
1897
|
+
let pageWidth = 0;
|
|
1898
|
+
let pageHeight = 0;
|
|
1899
|
+
for (const p of placements) {
|
|
1900
|
+
const right = p.x + p.width + padding;
|
|
1901
|
+
const bottom = p.y + p.height + padding;
|
|
1902
|
+
if (right > pageWidth) pageWidth = right;
|
|
1903
|
+
if (bottom > pageHeight) pageHeight = bottom;
|
|
1904
|
+
}
|
|
1905
|
+
return { pageWidth, pageHeight, placements };
|
|
1906
|
+
};
|
|
1907
|
+
const first = shelve(firstWidth);
|
|
1908
|
+
if (maxSide === void 0 || first.pageHeight <= maxSide || first.pageWidth > maxSide) {
|
|
1909
|
+
return { ...first, padding };
|
|
1895
1910
|
}
|
|
1896
|
-
let
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
const right = p.x + p.width + padding;
|
|
1900
|
-
const bottom = p.y + p.height + padding;
|
|
1901
|
-
if (right > pageWidth) pageWidth = right;
|
|
1902
|
-
if (bottom > pageHeight) pageHeight = bottom;
|
|
1911
|
+
for (let w = widest; w <= maxSide; w++) {
|
|
1912
|
+
const layout = shelve(w);
|
|
1913
|
+
if (layout.pageHeight <= maxSide) return { ...layout, padding };
|
|
1903
1914
|
}
|
|
1904
|
-
return {
|
|
1915
|
+
return { ...first, padding };
|
|
1905
1916
|
}
|
|
1906
1917
|
function uvRectFor(placement, page, insetPx = UV_INSET_PX) {
|
|
1907
1918
|
const px = placement.x + insetPx;
|
|
@@ -3004,14 +3015,14 @@ function armParts(role, layer, g, order, poseSwitch) {
|
|
|
3004
3015
|
},
|
|
3005
3016
|
true
|
|
3006
3017
|
),
|
|
3007
|
-
part(role, ids.armDeformer, order + 1, meshOf(band(0.5, seam), quad)),
|
|
3008
3018
|
part(
|
|
3009
3019
|
ids.forearm,
|
|
3010
3020
|
ids.forearmDeformer,
|
|
3011
|
-
order +
|
|
3021
|
+
order + 1,
|
|
3012
3022
|
meshOf(band(seam, -0.5), quad),
|
|
3013
3023
|
true
|
|
3014
|
-
)
|
|
3024
|
+
),
|
|
3025
|
+
part(role, ids.armDeformer, order + 2, meshOf(band(0.5, seam), quad))
|
|
3015
3026
|
];
|
|
3016
3027
|
}
|
|
3017
3028
|
var rotateBinding = (parameter, range, sign) => ({
|
|
@@ -3105,8 +3116,8 @@ var isLipInsideRole = (role) => LIP_INSIDE_ROLES.includes(role);
|
|
|
3105
3116
|
var ROLE_TABLE = [
|
|
3106
3117
|
{ role: "hair_back", family: "hair_back" },
|
|
3107
3118
|
{ role: "body", family: "body" },
|
|
3108
|
-
{ role: "arm_L", family: "arm", parts: ["elbow_L", "
|
|
3109
|
-
{ role: "arm_R", family: "arm", parts: ["elbow_R", "
|
|
3119
|
+
{ role: "arm_L", family: "arm", parts: ["elbow_L", "forearm_L", "arm_L"] },
|
|
3120
|
+
{ role: "arm_R", family: "arm", parts: ["elbow_R", "forearm_R", "arm_R"] },
|
|
3110
3121
|
{ role: "face", family: "face", required: true },
|
|
3111
3122
|
{ role: "blush_L", family: "face" },
|
|
3112
3123
|
{ role: "blush_R", family: "face" },
|
|
@@ -3251,6 +3262,7 @@ function parseLayerRoles(fileNames) {
|
|
|
3251
3262
|
|
|
3252
3263
|
// src/auto-rig/mouth.ts
|
|
3253
3264
|
var UPPER_SHARE = 0.4;
|
|
3265
|
+
var CLOSED_SAG_MAX = 0.1;
|
|
3254
3266
|
var UPPER_THIN = 0.7;
|
|
3255
3267
|
var END_TAPER = 0.35;
|
|
3256
3268
|
var END_TAPER_FROM = 0.7;
|
|
@@ -3317,10 +3329,15 @@ function mouthOpening(byRole) {
|
|
|
3317
3329
|
for (let d = 0; (r = read(c - d) ?? read(c + d)) === void 0; d++) ;
|
|
3318
3330
|
reads.push(r);
|
|
3319
3331
|
}
|
|
3332
|
+
const n = reads.length;
|
|
3320
3333
|
const m = (x0 + x1) / 2;
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3334
|
+
const centre = Math.round(m);
|
|
3335
|
+
const smooth = reads.map((_, i) => {
|
|
3336
|
+
const near = reads.slice(Math.max(0, i - 3), i + 4);
|
|
3337
|
+
return near.reduce((s, r) => s + r.Tu, 0) / near.length;
|
|
3338
|
+
});
|
|
3339
|
+
let sag = 0;
|
|
3340
|
+
if (n >= 4) {
|
|
3324
3341
|
let s11 = 0;
|
|
3325
3342
|
let s12 = 0;
|
|
3326
3343
|
let s22 = 0;
|
|
@@ -3338,16 +3355,24 @@ function mouthOpening(byRole) {
|
|
|
3338
3355
|
t2 += b2 * d;
|
|
3339
3356
|
});
|
|
3340
3357
|
const det = s11 * s22 - s12 * s12;
|
|
3341
|
-
p = (t1 * s22 - t2 * s12) / det;
|
|
3342
|
-
q = (s11 * t2 - s12 * t1) / det;
|
|
3358
|
+
const p = (t1 * s22 - t2 * s12) / det;
|
|
3359
|
+
const q = (s11 * t2 - s12 * t1) / det;
|
|
3360
|
+
const c = centre - x0;
|
|
3361
|
+
const D = c * (centre - x1) * (p + q * (centre - m));
|
|
3362
|
+
const arc = (reads[0].Tu + reads[n - 1].Tu) / 2 - reads[c].Tu;
|
|
3363
|
+
const t = c / (n - 1);
|
|
3364
|
+
const k = 4 * t * (1 - t);
|
|
3365
|
+
const offset = arc - (smooth[0] + (smooth[n - 1] - smooth[0]) * t - smooth[c]);
|
|
3366
|
+
sag = Math.min(arc - D, CLOSED_SAG_MAX * n - Math.max(0, offset) / k);
|
|
3343
3367
|
}
|
|
3344
3368
|
const columns = reads.map((r, i) => {
|
|
3345
|
-
|
|
3346
|
-
|
|
3369
|
+
if (n < 4) return { ...r, seam: r.Tu };
|
|
3370
|
+
const t = i / (x1 - x0);
|
|
3371
|
+
const parabola = smooth[0] + (smooth[n - 1] - smooth[0]) * t - sag * 4 * t * (1 - t);
|
|
3372
|
+
return { ...r, seam: r.Tu + parabola - smooth[i] };
|
|
3347
3373
|
});
|
|
3348
3374
|
const clamp2 = (col) => Math.min(x1, Math.max(x0, col));
|
|
3349
3375
|
const base = (col) => columns[clamp2(col) - x0];
|
|
3350
|
-
const centre = Math.round((x0 + x1) / 2);
|
|
3351
3376
|
const w = base(centre).lineH;
|
|
3352
3377
|
const from = END_TAPER_FROM * ((x1 - x0 + 1) / 2);
|
|
3353
3378
|
const squash = (col) => {
|