@ikijs/editor 0.2.0 → 0.3.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 +3 -3
- package/dist/index.d.mts +6 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.js +113 -47
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +113 -47
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1609,11 +1609,12 @@ var ROLE_TABLE = {
|
|
|
1609
1609
|
// the turn through its own part warp (bakeHairBackTurnWarp) and swings its
|
|
1610
1610
|
// ends on the hair-sway warps.
|
|
1611
1611
|
hair_back: { deformer: "headDeformer", order: 0, mesh: true },
|
|
1612
|
-
// The torso.
|
|
1613
|
-
// about the neck pivot while the shoulders
|
|
1614
|
-
//
|
|
1615
|
-
//
|
|
1616
|
-
|
|
1612
|
+
// The torso. It rides its own rigid deformer, not the head's: the head turns
|
|
1613
|
+
// about the neck pivot while the shoulders follow at BODY_TURN_FOLLOW and
|
|
1614
|
+
// follow the head's breath bob, which is what keeps the character from
|
|
1615
|
+
// reading as a floating head. Drawn over the back hair so long hair falls
|
|
1616
|
+
// behind the shoulders.
|
|
1617
|
+
body: { deformer: "bodyDeformer", order: 5, mesh: false },
|
|
1617
1618
|
face: { deformer: "faceWarp", order: 10, mesh: true },
|
|
1618
1619
|
nose: { deformer: "faceWarp", order: 15, mesh: true },
|
|
1619
1620
|
blush_L: { deformer: "faceWarp", order: 20, mesh: true },
|
|
@@ -1774,28 +1775,39 @@ function createPixelGridMesh(cols, rows, w, h) {
|
|
|
1774
1775
|
}
|
|
1775
1776
|
return { vertices, uvs, indices };
|
|
1776
1777
|
}
|
|
1778
|
+
var MESH_CELL_PX = 64;
|
|
1779
|
+
var MESH_CELLS_MIN = 4;
|
|
1780
|
+
var MESH_CELLS_MAX = 8;
|
|
1781
|
+
function meshCellsFor(w, h) {
|
|
1782
|
+
const cells = (px) => Math.max(
|
|
1783
|
+
MESH_CELLS_MIN,
|
|
1784
|
+
Math.min(MESH_CELLS_MAX, Math.round(px / MESH_CELL_PX))
|
|
1785
|
+
);
|
|
1786
|
+
return { cols: cells(w), rows: cells(h) };
|
|
1787
|
+
}
|
|
1788
|
+
var FACE_GRID_CELLS = 6;
|
|
1777
1789
|
var HEAD_CYLINDER_RADIUS_FACTOR = 0.6 / 0.5;
|
|
1778
1790
|
var HEAD_TURN_MAX_DEG = 30;
|
|
1791
|
+
var HEAD_TURN_STOPS = [-30, -15, 0, 15, 30];
|
|
1779
1792
|
function headTurnParallaxUnit(gridHalfWidth) {
|
|
1780
1793
|
return gridHalfWidth * HEAD_CYLINDER_RADIUS_FACTOR * Math.sin(HEAD_TURN_MAX_DEG * (Math.PI / 180));
|
|
1781
1794
|
}
|
|
1795
|
+
function gridReach(grid, axis, center) {
|
|
1796
|
+
let reach = 0;
|
|
1797
|
+
for (let i = axis; i < grid.points.length; i += 2) {
|
|
1798
|
+
reach = Math.max(reach, Math.abs(grid.points[i] - center));
|
|
1799
|
+
}
|
|
1800
|
+
return reach;
|
|
1801
|
+
}
|
|
1782
1802
|
function pinnedCylinderBend(local, radius, theta) {
|
|
1783
1803
|
const alpha = Math.asin(Math.max(-1, Math.min(1, local / radius)));
|
|
1784
1804
|
return radius * Math.sin(alpha + theta) - local - radius * Math.sin(theta);
|
|
1785
1805
|
}
|
|
1786
1806
|
function bakeHeadTurnGridWarp2DCentered(grid, parameterX, parameterY, centerX, centerY) {
|
|
1787
|
-
const STOPS = [
|
|
1788
|
-
const
|
|
1789
|
-
const
|
|
1807
|
+
const STOPS = [...HEAD_TURN_STOPS];
|
|
1808
|
+
const RADIUS_X = gridReach(grid, 0, centerX) * HEAD_CYLINDER_RADIUS_FACTOR;
|
|
1809
|
+
const RADIUS_Y = gridReach(grid, 1, centerY) * HEAD_CYLINDER_RADIUS_FACTOR;
|
|
1790
1810
|
const pointCount = grid.points.length / 2;
|
|
1791
|
-
let halfHeight = 0;
|
|
1792
|
-
for (let i = 0; i < pointCount; i++) {
|
|
1793
|
-
halfHeight = Math.max(
|
|
1794
|
-
halfHeight,
|
|
1795
|
-
Math.abs(grid.points[i * 2 + 1] - centerY)
|
|
1796
|
-
);
|
|
1797
|
-
}
|
|
1798
|
-
const RADIUS_Y = halfHeight * HEAD_CYLINDER_RADIUS_FACTOR;
|
|
1799
1811
|
const DEG_TO_RAD = Math.PI / 180;
|
|
1800
1812
|
const keyforms2d = [];
|
|
1801
1813
|
for (const angleY of STOPS) {
|
|
@@ -1932,16 +1944,17 @@ function bindingsForRole(spec, role, cropW, cropH, options = {}) {
|
|
|
1932
1944
|
}
|
|
1933
1945
|
if (role === "hair_front" || role === "hair_back") {
|
|
1934
1946
|
const isFront = role === "hair_front";
|
|
1935
|
-
const depth = isFront ? HAIR_FRONT_DEPTH : HAIR_BACK_DEPTH;
|
|
1936
1947
|
const parallax = [];
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1948
|
+
if (!isFront) {
|
|
1949
|
+
const shiftX = HAIR_BACK_DEPTH * (options.parallaxUnit ?? 0);
|
|
1950
|
+
if (shiftX !== 0) {
|
|
1951
|
+
parallax.push({
|
|
1952
|
+
parameter: StandardParameter.AngleX,
|
|
1953
|
+
channel: "translateX",
|
|
1954
|
+
from: -shiftX,
|
|
1955
|
+
to: shiftX
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1945
1958
|
}
|
|
1946
1959
|
const shiftY = (isFront ? 0 : HAIR_BACK_NOD_DEPTH) * (options.parallaxUnitY ?? 0);
|
|
1947
1960
|
if (shiftY !== 0) {
|
|
@@ -2004,6 +2017,7 @@ function bakeHairBackTurnWarp(mesh, parameter) {
|
|
|
2004
2017
|
var HAIR_SWAY_TIP_FRACTION = 0.09;
|
|
2005
2018
|
var HAIR_SWAY_CURL = 1.5;
|
|
2006
2019
|
var HAIR_SWAY_RANGE = 20;
|
|
2020
|
+
var HAIR_SWAY_PEAK_FRACTION = 0.56;
|
|
2007
2021
|
function bakeHairSwayWarp(mesh, parameter, tipShift, range) {
|
|
2008
2022
|
let top = -Infinity;
|
|
2009
2023
|
let bottom = Infinity;
|
|
@@ -2026,10 +2040,15 @@ function bakeHairSwayWarp(mesh, parameter, tipShift, range) {
|
|
|
2026
2040
|
]
|
|
2027
2041
|
};
|
|
2028
2042
|
}
|
|
2043
|
+
var HEAD_TURN_TRAVEL = 50;
|
|
2044
|
+
var BODY_TURN_FOLLOW = 0.3;
|
|
2045
|
+
var HEAD_BREATH_BOB = -12;
|
|
2046
|
+
var BODY_BREATH_FOLLOW = 0.5;
|
|
2029
2047
|
function generateIkiFromLayerSet(layers, canvas) {
|
|
2030
2048
|
validateLayerInputs(layers, canvas);
|
|
2031
2049
|
const hasHair = layers.some((l) => l.role === "hair_front");
|
|
2032
2050
|
const hasMouthOpen = layers.some((l) => l.role === "mouth_open");
|
|
2051
|
+
const bodyLayer = layers.find((l) => l.role === "body");
|
|
2033
2052
|
const parameters = [
|
|
2034
2053
|
{
|
|
2035
2054
|
id: StandardParameter.MouthOpen,
|
|
@@ -2193,11 +2212,11 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2193
2212
|
const faceGridMinX = faceCenterX - halfW;
|
|
2194
2213
|
const faceGridMaxX = faceCenterX + halfW;
|
|
2195
2214
|
const faceGrid = {
|
|
2196
|
-
cols:
|
|
2197
|
-
rows:
|
|
2215
|
+
cols: FACE_GRID_CELLS,
|
|
2216
|
+
rows: FACE_GRID_CELLS,
|
|
2198
2217
|
points: generateGridPoints(
|
|
2199
|
-
|
|
2200
|
-
|
|
2218
|
+
FACE_GRID_CELLS,
|
|
2219
|
+
FACE_GRID_CELLS,
|
|
2201
2220
|
faceGridMinX,
|
|
2202
2221
|
faceGridMaxX,
|
|
2203
2222
|
unionMinY,
|
|
@@ -2228,21 +2247,20 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2228
2247
|
id: "headDeformer",
|
|
2229
2248
|
pivot: neckPivot,
|
|
2230
2249
|
bindings: [
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
},
|
|
2250
|
+
// The turn is a pure yaw: no roll rides on AngleX. The sample model's
|
|
2251
|
+
// ±6° "lean into the turn" was tried here and dropped — rotating about
|
|
2252
|
+
// the neck pivot swings the crown (~500px above it) far more than the
|
|
2253
|
+
// chin, so the top of the head appeared to lunge ahead of the face on
|
|
2254
|
+
// every turn. Roll is AngleZ's job, below.
|
|
2237
2255
|
{
|
|
2238
2256
|
parameter: StandardParameter.AngleX,
|
|
2239
2257
|
channel: "translateX",
|
|
2240
|
-
from: -
|
|
2241
|
-
to:
|
|
2258
|
+
from: -HEAD_TURN_TRAVEL,
|
|
2259
|
+
to: HEAD_TURN_TRAVEL
|
|
2242
2260
|
},
|
|
2243
2261
|
// Nod: a vertical translate only. No rotate — a pitch expressed as a
|
|
2244
|
-
// rigid rotation would sum with the roll below at diagonal
|
|
2245
|
-
// collapsing pitch into roll.
|
|
2262
|
+
// rigid rotation would sum with the AngleZ roll below at diagonal
|
|
2263
|
+
// poses, collapsing pitch into roll.
|
|
2246
2264
|
{
|
|
2247
2265
|
parameter: StandardParameter.AngleY,
|
|
2248
2266
|
channel: "translateY",
|
|
@@ -2251,10 +2269,10 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2251
2269
|
},
|
|
2252
2270
|
// Tilt: the whole head rolls about the neck pivot, one degree per
|
|
2253
2271
|
// degree. Positive AngleZ is clockwise on screen (engine rotate is
|
|
2254
|
-
// CCW-positive, hence the flipped from/to) — Live2D's convention
|
|
2255
|
-
//
|
|
2256
|
-
//
|
|
2257
|
-
//
|
|
2272
|
+
// CCW-positive, hence the flipped from/to) — Live2D's convention: in
|
|
2273
|
+
// its own sample motions AngleZ carries the sign of AngleX 214 times
|
|
2274
|
+
// out of 222, i.e. a head that leans into its turn tilts clockwise on
|
|
2275
|
+
// a turn to the viewer's right. The only rotate binding on the head.
|
|
2258
2276
|
{
|
|
2259
2277
|
parameter: StandardParameter.AngleZ,
|
|
2260
2278
|
channel: "rotate",
|
|
@@ -2265,7 +2283,7 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2265
2283
|
parameter: StandardParameter.Breath,
|
|
2266
2284
|
channel: "translateY",
|
|
2267
2285
|
from: 0,
|
|
2268
|
-
to:
|
|
2286
|
+
to: HEAD_BREATH_BOB
|
|
2269
2287
|
}
|
|
2270
2288
|
]
|
|
2271
2289
|
},
|
|
@@ -2281,6 +2299,34 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2281
2299
|
warp2d: faceWarp2d
|
|
2282
2300
|
}
|
|
2283
2301
|
];
|
|
2302
|
+
if (bodyLayer) {
|
|
2303
|
+
const bt = bboxToTransform(
|
|
2304
|
+
bodyLayer.bbox,
|
|
2305
|
+
bodyLayer.canvasW,
|
|
2306
|
+
bodyLayer.canvasH,
|
|
2307
|
+
"body"
|
|
2308
|
+
);
|
|
2309
|
+
deformers.push({
|
|
2310
|
+
id: "bodyDeformer",
|
|
2311
|
+
pivot: { x: bt.x, y: bt.y - bodyLayer.cropH / 2 },
|
|
2312
|
+
bindings: [
|
|
2313
|
+
// Low-weight follow of the turn, same direction as the head.
|
|
2314
|
+
{
|
|
2315
|
+
parameter: StandardParameter.AngleX,
|
|
2316
|
+
channel: "translateX",
|
|
2317
|
+
from: -BODY_TURN_FOLLOW * HEAD_TURN_TRAVEL,
|
|
2318
|
+
to: BODY_TURN_FOLLOW * HEAD_TURN_TRAVEL
|
|
2319
|
+
},
|
|
2320
|
+
// Breath: follow the head's bob, same direction, half amplitude.
|
|
2321
|
+
{
|
|
2322
|
+
parameter: StandardParameter.Breath,
|
|
2323
|
+
channel: "translateY",
|
|
2324
|
+
from: 0,
|
|
2325
|
+
to: BODY_BREATH_FOLLOW * HEAD_BREATH_BOB
|
|
2326
|
+
}
|
|
2327
|
+
]
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2284
2330
|
const eyeCreaseBySide = {};
|
|
2285
2331
|
for (const layer of layers) {
|
|
2286
2332
|
const side = ROLE_TABLE[layer.role].eyeSide;
|
|
@@ -2305,7 +2351,8 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2305
2351
|
});
|
|
2306
2352
|
const deformerId = spec.deformer === "none" ? void 0 : spec.deformer;
|
|
2307
2353
|
if (spec.mesh) {
|
|
2308
|
-
const
|
|
2354
|
+
const { cols, rows } = meshCellsFor(cropW, cropH);
|
|
2355
|
+
const mesh = createPixelGridMesh(cols, rows, cropW, cropH);
|
|
2309
2356
|
const part = {
|
|
2310
2357
|
id: role,
|
|
2311
2358
|
color: [1, 1, 1, 1],
|
|
@@ -2320,7 +2367,25 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2320
2367
|
part.bindings = roleBindings;
|
|
2321
2368
|
}
|
|
2322
2369
|
if ((role === "hair_front" || role === "hair_back") && hasHair) {
|
|
2323
|
-
|
|
2370
|
+
let tipShift = HAIR_SWAY_TIP_FRACTION * cropH;
|
|
2371
|
+
let leadWarp;
|
|
2372
|
+
if (role === "hair_front") {
|
|
2373
|
+
const shift = HAIR_FRONT_DEPTH * parallaxUnit;
|
|
2374
|
+
const headroom = Math.min(
|
|
2375
|
+
faceGridMaxX - (t.x + cropW / 2),
|
|
2376
|
+
t.x - cropW / 2 - faceGridMinX
|
|
2377
|
+
) - shift;
|
|
2378
|
+
tipShift = Math.min(
|
|
2379
|
+
tipShift,
|
|
2380
|
+
Math.max(0, headroom) / HAIR_SWAY_PEAK_FRACTION
|
|
2381
|
+
);
|
|
2382
|
+
leadWarp = bakeHairSwayWarp(
|
|
2383
|
+
mesh,
|
|
2384
|
+
StandardParameter.AngleX,
|
|
2385
|
+
shift,
|
|
2386
|
+
HEAD_TURN_MAX_DEG
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2324
2389
|
part.warps = [
|
|
2325
2390
|
bakeHairSwayWarp(
|
|
2326
2391
|
mesh,
|
|
@@ -2333,7 +2398,8 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2333
2398
|
StandardParameter.HairSwayZ,
|
|
2334
2399
|
tipShift,
|
|
2335
2400
|
HAIR_SWAY_RANGE
|
|
2336
|
-
)
|
|
2401
|
+
),
|
|
2402
|
+
...leadWarp ? [leadWarp] : []
|
|
2337
2403
|
];
|
|
2338
2404
|
}
|
|
2339
2405
|
if (role === "hair_back") {
|