@ikijs/editor 0.2.1 → 0.3.1
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 +97 -38
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +97 -38
- 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,8 +1775,20 @@ 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
|
}
|
|
@@ -1791,7 +1804,7 @@ function pinnedCylinderBend(local, radius, theta) {
|
|
|
1791
1804
|
return radius * Math.sin(alpha + theta) - local - radius * Math.sin(theta);
|
|
1792
1805
|
}
|
|
1793
1806
|
function bakeHeadTurnGridWarp2DCentered(grid, parameterX, parameterY, centerX, centerY) {
|
|
1794
|
-
const STOPS = [
|
|
1807
|
+
const STOPS = [...HEAD_TURN_STOPS];
|
|
1795
1808
|
const RADIUS_X = gridReach(grid, 0, centerX) * HEAD_CYLINDER_RADIUS_FACTOR;
|
|
1796
1809
|
const RADIUS_Y = gridReach(grid, 1, centerY) * HEAD_CYLINDER_RADIUS_FACTOR;
|
|
1797
1810
|
const pointCount = grid.points.length / 2;
|
|
@@ -1931,16 +1944,17 @@ function bindingsForRole(spec, role, cropW, cropH, options = {}) {
|
|
|
1931
1944
|
}
|
|
1932
1945
|
if (role === "hair_front" || role === "hair_back") {
|
|
1933
1946
|
const isFront = role === "hair_front";
|
|
1934
|
-
const depth = isFront ? HAIR_FRONT_DEPTH : HAIR_BACK_DEPTH;
|
|
1935
1947
|
const parallax = [];
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
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
|
+
}
|
|
1944
1958
|
}
|
|
1945
1959
|
const shiftY = (isFront ? 0 : HAIR_BACK_NOD_DEPTH) * (options.parallaxUnitY ?? 0);
|
|
1946
1960
|
if (shiftY !== 0) {
|
|
@@ -1987,14 +2001,18 @@ function bakeHairBackTurnWarp(mesh, parameter) {
|
|
|
1987
2001
|
const radius = halfWidth * HAIR_BACK_BEND_RADIUS_FACTOR;
|
|
1988
2002
|
const bulge = HAIR_BACK_FAR_BULGE * halfWidth;
|
|
1989
2003
|
const DEG_TO_RAD = Math.PI / 180;
|
|
1990
|
-
const keyforms =
|
|
2004
|
+
const keyforms = HEAD_TURN_STOPS.map((deg) => {
|
|
1991
2005
|
const theta = deg * DEG_TO_RAD;
|
|
1992
2006
|
const s = Math.sign(deg);
|
|
2007
|
+
const turnFraction = Math.abs(deg) / HEAD_TURN_MAX_DEG;
|
|
2008
|
+
const fullTheta = s * HEAD_TURN_MAX_DEG * DEG_TO_RAD;
|
|
2009
|
+
const bulgeAtStop = bulge * deg / HEAD_TURN_MAX_DEG;
|
|
1993
2010
|
const offsets = [];
|
|
1994
2011
|
for (let i = 0; i < mesh.vertices.length; i += 2) {
|
|
1995
2012
|
const x = mesh.vertices[i];
|
|
1996
2013
|
const far = Math.max(0, -s * x / halfWidth);
|
|
1997
|
-
|
|
2014
|
+
const bend = far > 0 ? turnFraction * pinnedCylinderBend(x, radius, fullTheta) : pinnedCylinderBend(x, radius, theta);
|
|
2015
|
+
offsets.push(bend - bulgeAtStop * far, 0);
|
|
1998
2016
|
}
|
|
1999
2017
|
return { value: deg, offsets };
|
|
2000
2018
|
});
|
|
@@ -2026,10 +2044,15 @@ function bakeHairSwayWarp(mesh, parameter, tipShift, range) {
|
|
|
2026
2044
|
]
|
|
2027
2045
|
};
|
|
2028
2046
|
}
|
|
2047
|
+
var HEAD_TURN_TRAVEL = 50;
|
|
2048
|
+
var BODY_TURN_FOLLOW = 0.3;
|
|
2049
|
+
var HEAD_BREATH_BOB = -12;
|
|
2050
|
+
var BODY_BREATH_FOLLOW = 0.5;
|
|
2029
2051
|
function generateIkiFromLayerSet(layers, canvas) {
|
|
2030
2052
|
validateLayerInputs(layers, canvas);
|
|
2031
2053
|
const hasHair = layers.some((l) => l.role === "hair_front");
|
|
2032
2054
|
const hasMouthOpen = layers.some((l) => l.role === "mouth_open");
|
|
2055
|
+
const bodyLayer = layers.find((l) => l.role === "body");
|
|
2033
2056
|
const parameters = [
|
|
2034
2057
|
{
|
|
2035
2058
|
id: StandardParameter.MouthOpen,
|
|
@@ -2193,11 +2216,11 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2193
2216
|
const faceGridMinX = faceCenterX - halfW;
|
|
2194
2217
|
const faceGridMaxX = faceCenterX + halfW;
|
|
2195
2218
|
const faceGrid = {
|
|
2196
|
-
cols:
|
|
2197
|
-
rows:
|
|
2219
|
+
cols: FACE_GRID_CELLS,
|
|
2220
|
+
rows: FACE_GRID_CELLS,
|
|
2198
2221
|
points: generateGridPoints(
|
|
2199
|
-
|
|
2200
|
-
|
|
2222
|
+
FACE_GRID_CELLS,
|
|
2223
|
+
FACE_GRID_CELLS,
|
|
2201
2224
|
faceGridMinX,
|
|
2202
2225
|
faceGridMaxX,
|
|
2203
2226
|
unionMinY,
|
|
@@ -2228,21 +2251,20 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2228
2251
|
id: "headDeformer",
|
|
2229
2252
|
pivot: neckPivot,
|
|
2230
2253
|
bindings: [
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
},
|
|
2254
|
+
// The turn is a pure yaw: no roll rides on AngleX. The sample model's
|
|
2255
|
+
// ±6° "lean into the turn" was tried here and dropped — rotating about
|
|
2256
|
+
// the neck pivot swings the crown (~500px above it) far more than the
|
|
2257
|
+
// chin, so the top of the head appeared to lunge ahead of the face on
|
|
2258
|
+
// every turn. Roll is AngleZ's job, below.
|
|
2237
2259
|
{
|
|
2238
2260
|
parameter: StandardParameter.AngleX,
|
|
2239
2261
|
channel: "translateX",
|
|
2240
|
-
from: -
|
|
2241
|
-
to:
|
|
2262
|
+
from: -HEAD_TURN_TRAVEL,
|
|
2263
|
+
to: HEAD_TURN_TRAVEL
|
|
2242
2264
|
},
|
|
2243
2265
|
// 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.
|
|
2266
|
+
// rigid rotation would sum with the AngleZ roll below at diagonal
|
|
2267
|
+
// poses, collapsing pitch into roll.
|
|
2246
2268
|
{
|
|
2247
2269
|
parameter: StandardParameter.AngleY,
|
|
2248
2270
|
channel: "translateY",
|
|
@@ -2251,10 +2273,10 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2251
2273
|
},
|
|
2252
2274
|
// Tilt: the whole head rolls about the neck pivot, one degree per
|
|
2253
2275
|
// degree. Positive AngleZ is clockwise on screen (engine rotate is
|
|
2254
|
-
// CCW-positive, hence the flipped from/to) — Live2D's convention
|
|
2255
|
-
//
|
|
2256
|
-
//
|
|
2257
|
-
//
|
|
2276
|
+
// CCW-positive, hence the flipped from/to) — Live2D's convention: in
|
|
2277
|
+
// its own sample motions AngleZ carries the sign of AngleX 214 times
|
|
2278
|
+
// out of 222, i.e. a head that leans into its turn tilts clockwise on
|
|
2279
|
+
// a turn to the viewer's right. The only rotate binding on the head.
|
|
2258
2280
|
{
|
|
2259
2281
|
parameter: StandardParameter.AngleZ,
|
|
2260
2282
|
channel: "rotate",
|
|
@@ -2265,7 +2287,7 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2265
2287
|
parameter: StandardParameter.Breath,
|
|
2266
2288
|
channel: "translateY",
|
|
2267
2289
|
from: 0,
|
|
2268
|
-
to:
|
|
2290
|
+
to: HEAD_BREATH_BOB
|
|
2269
2291
|
}
|
|
2270
2292
|
]
|
|
2271
2293
|
},
|
|
@@ -2281,6 +2303,34 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2281
2303
|
warp2d: faceWarp2d
|
|
2282
2304
|
}
|
|
2283
2305
|
];
|
|
2306
|
+
if (bodyLayer) {
|
|
2307
|
+
const bt = bboxToTransform(
|
|
2308
|
+
bodyLayer.bbox,
|
|
2309
|
+
bodyLayer.canvasW,
|
|
2310
|
+
bodyLayer.canvasH,
|
|
2311
|
+
"body"
|
|
2312
|
+
);
|
|
2313
|
+
deformers.push({
|
|
2314
|
+
id: "bodyDeformer",
|
|
2315
|
+
pivot: { x: bt.x, y: bt.y - bodyLayer.cropH / 2 },
|
|
2316
|
+
bindings: [
|
|
2317
|
+
// Low-weight follow of the turn, same direction as the head.
|
|
2318
|
+
{
|
|
2319
|
+
parameter: StandardParameter.AngleX,
|
|
2320
|
+
channel: "translateX",
|
|
2321
|
+
from: -BODY_TURN_FOLLOW * HEAD_TURN_TRAVEL,
|
|
2322
|
+
to: BODY_TURN_FOLLOW * HEAD_TURN_TRAVEL
|
|
2323
|
+
},
|
|
2324
|
+
// Breath: follow the head's bob, same direction, half amplitude.
|
|
2325
|
+
{
|
|
2326
|
+
parameter: StandardParameter.Breath,
|
|
2327
|
+
channel: "translateY",
|
|
2328
|
+
from: 0,
|
|
2329
|
+
to: BODY_BREATH_FOLLOW * HEAD_BREATH_BOB
|
|
2330
|
+
}
|
|
2331
|
+
]
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2284
2334
|
const eyeCreaseBySide = {};
|
|
2285
2335
|
for (const layer of layers) {
|
|
2286
2336
|
const side = ROLE_TABLE[layer.role].eyeSide;
|
|
@@ -2305,7 +2355,8 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2305
2355
|
});
|
|
2306
2356
|
const deformerId = spec.deformer === "none" ? void 0 : spec.deformer;
|
|
2307
2357
|
if (spec.mesh) {
|
|
2308
|
-
const
|
|
2358
|
+
const { cols, rows } = meshCellsFor(cropW, cropH);
|
|
2359
|
+
const mesh = createPixelGridMesh(cols, rows, cropW, cropH);
|
|
2309
2360
|
const part = {
|
|
2310
2361
|
id: role,
|
|
2311
2362
|
color: [1, 1, 1, 1],
|
|
@@ -2321,6 +2372,7 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2321
2372
|
}
|
|
2322
2373
|
if ((role === "hair_front" || role === "hair_back") && hasHair) {
|
|
2323
2374
|
let tipShift = HAIR_SWAY_TIP_FRACTION * cropH;
|
|
2375
|
+
let leadWarp;
|
|
2324
2376
|
if (role === "hair_front") {
|
|
2325
2377
|
const shift = HAIR_FRONT_DEPTH * parallaxUnit;
|
|
2326
2378
|
const headroom = Math.min(
|
|
@@ -2331,6 +2383,12 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2331
2383
|
tipShift,
|
|
2332
2384
|
Math.max(0, headroom) / HAIR_SWAY_PEAK_FRACTION
|
|
2333
2385
|
);
|
|
2386
|
+
leadWarp = bakeHairSwayWarp(
|
|
2387
|
+
mesh,
|
|
2388
|
+
StandardParameter.AngleX,
|
|
2389
|
+
shift,
|
|
2390
|
+
HEAD_TURN_MAX_DEG
|
|
2391
|
+
);
|
|
2334
2392
|
}
|
|
2335
2393
|
part.warps = [
|
|
2336
2394
|
bakeHairSwayWarp(
|
|
@@ -2344,7 +2402,8 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2344
2402
|
StandardParameter.HairSwayZ,
|
|
2345
2403
|
tipShift,
|
|
2346
2404
|
HAIR_SWAY_RANGE
|
|
2347
|
-
)
|
|
2405
|
+
),
|
|
2406
|
+
...leadWarp ? [leadWarp] : []
|
|
2348
2407
|
];
|
|
2349
2408
|
}
|
|
2350
2409
|
if (role === "hair_back") {
|