pixelkiln 0.70.1 → 0.72.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/PROVIDERS.md +1 -1
- package/dist/cli.js +70 -15
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.css +15 -0
- package/dist/client/gallery.js +369 -9
- package/dist/index.cjs +70 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +88 -55
- package/dist/index.d.ts +88 -55
- package/dist/index.js +70 -15
- package/dist/index.js.map +1 -1
- package/docs/CHARACTERS.md +13 -0
- package/docs/GENERATORS.md +1 -1
- package/docs/REVISIONS.md +11 -3
- package/package.json +1 -1
- package/schema/manifest.schema.json +1 -0
- package/skills/pixelkiln/references/pixellab-roadmap.md +6 -0
- package/skills/pixelkiln/references/pixellab.md +8 -1
- package/skills/pixelkiln/references/revisions.md +3 -2
package/dist/client/gallery.css
CHANGED
|
@@ -326,3 +326,18 @@ form.edit .pal { margin-top:4px; }
|
|
|
326
326
|
.poses svg.pose { width:112px; height:112px; background:var(--panel-deep); border:1px solid var(--line); image-rendering:pixelated; }
|
|
327
327
|
.poses figcaption { font:10.5px/1 var(--mono); color:var(--dim); }
|
|
328
328
|
textarea.mono { font-family:var(--mono); font-size:11.5px; }
|
|
329
|
+
.pose-editor { display:grid; grid-template-columns:minmax(200px, 320px) 1fr; gap:12px; align-items:start; margin-top:10px; }
|
|
330
|
+
.pose-stage svg.stage { width:100%; aspect-ratio:1; background:var(--panel-deep); border:1px solid var(--line); image-rendering:pixelated; touch-action:none; display:block; }
|
|
331
|
+
.pose-stage svg.stage:focus { outline:1px solid var(--accent); }
|
|
332
|
+
.pose-stage .joint { cursor:grab; }
|
|
333
|
+
.pose-stage .joint:active { cursor:grabbing; }
|
|
334
|
+
.pose-side { display:grid; gap:8px; align-content:start; }
|
|
335
|
+
.pose-tools { display:flex; flex-wrap:wrap; gap:6px; }
|
|
336
|
+
.pose-tools button { font:11.5px/1 var(--mono); padding:6px 8px; }
|
|
337
|
+
.pose-status { font:11.5px/1.45 var(--mono); color:var(--dim); }
|
|
338
|
+
.pose-strip { grid-column:1 / -1; }
|
|
339
|
+
.pose-strip button { flex:none; padding:3px; display:grid; gap:3px; justify-items:center; }
|
|
340
|
+
.pose-strip button.on { border-color:var(--accent); }
|
|
341
|
+
.pose-strip svg.pose { width:64px; height:64px; }
|
|
342
|
+
.pose-strip span { font:10.5px/1 var(--mono); color:var(--dim); }
|
|
343
|
+
@media (max-width: 640px) { .pose-editor { grid-template-columns:1fr; } }
|
package/dist/client/gallery.js
CHANGED
|
@@ -363,13 +363,22 @@
|
|
|
363
363
|
prompt.placeholder = 'What the loop does, e.g. "a slow idle sway".';
|
|
364
364
|
prompt.required = true;
|
|
365
365
|
const mode = el("select");
|
|
366
|
-
if (isCharacter)
|
|
366
|
+
if (isCharacter) {
|
|
367
|
+
mode.append(
|
|
368
|
+
new Option("skeleton-v3 (a template, posed by the skeleton model; steadiest)", "skeleton-v3"),
|
|
369
|
+
new Option("template (a template, redrawn; 1 generation)", "template")
|
|
370
|
+
);
|
|
371
|
+
}
|
|
367
372
|
mode.append(new Option("v3 (text-described)", "v3"), new Option("pro (sequential, high quality)", "pro"));
|
|
368
|
-
mode.value = "v3";
|
|
373
|
+
mode.value = isCharacter ? "skeleton-v3" : "v3";
|
|
369
374
|
const template = el("input");
|
|
370
375
|
template.type = "text";
|
|
371
376
|
template.placeholder = "e.g. walk, breathing-idle";
|
|
372
|
-
const
|
|
377
|
+
const templateHint = {
|
|
378
|
+
"skeleton-v3": "A PixelLab template posed onto the character by the skeleton model, which moves it instead of redrawing it: steadier identity and colours. 2 to 4 generations and 3 to 5 minutes per direction; beta, Tier 1 plans and up.",
|
|
379
|
+
template: "A PixelLab template, redrawn frame by frame; 1 generation regardless of frame count."
|
|
380
|
+
};
|
|
381
|
+
const templateField = field("template id", template, templateHint.template);
|
|
373
382
|
const frames = el("input");
|
|
374
383
|
frames.type = "number";
|
|
375
384
|
frames.min = "4";
|
|
@@ -403,9 +412,11 @@
|
|
|
403
412
|
form.append(row2);
|
|
404
413
|
if (subject) form.append(field("subject override", subject));
|
|
405
414
|
const syncMode = () => {
|
|
406
|
-
const isTemplate = isCharacter && mode.value === "template";
|
|
415
|
+
const isTemplate = isCharacter && (mode.value === "template" || mode.value === "skeleton-v3");
|
|
407
416
|
templateField.style.display = isTemplate ? "" : "none";
|
|
417
|
+
if (isTemplate) templateField.querySelector("small").textContent = templateHint[mode.value];
|
|
408
418
|
template.required = isTemplate;
|
|
419
|
+
prompt.required = !isTemplate;
|
|
409
420
|
row2.style.display = isTemplate ? "none" : "";
|
|
410
421
|
};
|
|
411
422
|
if (isCharacter) {
|
|
@@ -433,8 +444,9 @@
|
|
|
433
444
|
msg.textContent = "saving…";
|
|
434
445
|
const animation = { of: item.assetId };
|
|
435
446
|
if (directionField) animation.direction = directionField.select.value;
|
|
436
|
-
if (isCharacter && mode.value === "template" && template.value.trim()) {
|
|
447
|
+
if (isCharacter && (mode.value === "template" || mode.value === "skeleton-v3") && template.value.trim()) {
|
|
437
448
|
animation.template = template.value.trim();
|
|
449
|
+
if (mode.value === "skeleton-v3") animation.mode = "skeleton-v3";
|
|
438
450
|
} else {
|
|
439
451
|
animation.mode = mode.value;
|
|
440
452
|
if (frames.value.trim() !== "") animation.frames = Number(frames.value);
|
|
@@ -2107,6 +2119,291 @@
|
|
|
2107
2119
|
return row2;
|
|
2108
2120
|
}
|
|
2109
2121
|
|
|
2122
|
+
// src/gallery/client/src/pose-editor.ts
|
|
2123
|
+
var MIN_FRAMES = 3;
|
|
2124
|
+
var MAX_FRAMES = 15;
|
|
2125
|
+
var HISTORY = 100;
|
|
2126
|
+
var NUDGE = 1 / 128;
|
|
2127
|
+
var PLAY_MS = 125;
|
|
2128
|
+
var clone = (set) => JSON.parse(JSON.stringify(set));
|
|
2129
|
+
var clamp = (v) => Math.min(1, Math.max(0, Math.round(v * 1e3) / 1e3));
|
|
2130
|
+
var mirrorLabel = (label) => label.startsWith("RIGHT ") ? "LEFT " + label.slice(6) : label.startsWith("LEFT ") ? "RIGHT " + label.slice(5) : label;
|
|
2131
|
+
function poseEditor(initial, imageUrl, onChange) {
|
|
2132
|
+
let set = clone(initial);
|
|
2133
|
+
let index = set.frames.length ? 1 : 0;
|
|
2134
|
+
let selected = null;
|
|
2135
|
+
let playing = null;
|
|
2136
|
+
const past = [];
|
|
2137
|
+
const future = [];
|
|
2138
|
+
const root = el("div", "pose-editor");
|
|
2139
|
+
const stageWrap = el("div", "pose-stage");
|
|
2140
|
+
const side = el("div", "pose-side");
|
|
2141
|
+
const status = el("div", "pose-status");
|
|
2142
|
+
const strip = el("div", "poses pose-strip");
|
|
2143
|
+
const tools = el("div", "pose-tools");
|
|
2144
|
+
side.append(tools, status);
|
|
2145
|
+
root.append(stageWrap, side, strip);
|
|
2146
|
+
const poseAt = (i) => i === 0 ? set.firstFrameKeypoints : set.frames[i - 1];
|
|
2147
|
+
const count = () => set.frames.length + 1;
|
|
2148
|
+
const name = (i) => i === 0 ? "start" : "frame " + i;
|
|
2149
|
+
const remember = (before) => {
|
|
2150
|
+
past.push(before);
|
|
2151
|
+
if (past.length > HISTORY) past.shift();
|
|
2152
|
+
future.length = 0;
|
|
2153
|
+
};
|
|
2154
|
+
const commit = (before) => {
|
|
2155
|
+
if (JSON.stringify(set) === before) return;
|
|
2156
|
+
remember(before);
|
|
2157
|
+
onChange(clone(set));
|
|
2158
|
+
drawStrip();
|
|
2159
|
+
drawTools();
|
|
2160
|
+
};
|
|
2161
|
+
const edit = (change) => {
|
|
2162
|
+
const before = JSON.stringify(set);
|
|
2163
|
+
change();
|
|
2164
|
+
commit(before);
|
|
2165
|
+
drawStage();
|
|
2166
|
+
};
|
|
2167
|
+
let svg;
|
|
2168
|
+
let joints = /* @__PURE__ */ new Map();
|
|
2169
|
+
let bones = [];
|
|
2170
|
+
function drawStage() {
|
|
2171
|
+
stageWrap.textContent = "";
|
|
2172
|
+
svg = svgEl("svg", { viewBox: "0 0 1 1", class: "pose stage", tabindex: 0 });
|
|
2173
|
+
svg.setAttribute("aria-label", "Pose " + name(index) + ": drag joints, arrow keys nudge the selected one");
|
|
2174
|
+
if (imageUrl) svg.append(svgEl("image", { href: imageUrl, x: 0, y: 0, width: 1, height: 1, preserveAspectRatio: "none" }));
|
|
2175
|
+
if (index > 0) {
|
|
2176
|
+
const prev = new Map(poseAt(index - 1).map((j) => [j.label, j]));
|
|
2177
|
+
for (const [a, b] of SKELETON_BONES) {
|
|
2178
|
+
const p = prev.get(a), q = prev.get(b);
|
|
2179
|
+
if (p && q) svg.append(svgEl("line", { x1: p.x, y1: p.y, x2: q.x, y2: q.y, stroke: boneColor(b), "stroke-width": 0.012, opacity: 0.3, "stroke-linecap": "round" }));
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
const at = new Map(poseAt(index).map((j) => [j.label, j]));
|
|
2183
|
+
joints = /* @__PURE__ */ new Map();
|
|
2184
|
+
bones = [];
|
|
2185
|
+
for (const [a, b] of SKELETON_BONES) {
|
|
2186
|
+
const p = at.get(a), q = at.get(b);
|
|
2187
|
+
if (!p || !q) continue;
|
|
2188
|
+
const lines = [
|
|
2189
|
+
svgEl("line", { x1: p.x, y1: p.y, x2: q.x, y2: q.y, stroke: "#000", "stroke-width": 0.03, "stroke-linecap": "round" }),
|
|
2190
|
+
svgEl("line", { x1: p.x, y1: p.y, x2: q.x, y2: q.y, stroke: boneColor(b), "stroke-width": 0.015, "stroke-linecap": "round" })
|
|
2191
|
+
];
|
|
2192
|
+
svg.append(...lines);
|
|
2193
|
+
bones.push({ a, b, lines });
|
|
2194
|
+
}
|
|
2195
|
+
for (const j of poseAt(index)) {
|
|
2196
|
+
const on = j.label === selected;
|
|
2197
|
+
const ring = svgEl("circle", { cx: j.x, cy: j.y, r: on ? 0.034 : 0.026, fill: "#000" });
|
|
2198
|
+
const dot = svgEl("circle", { cx: j.x, cy: j.y, r: on ? 0.022 : 0.016, fill: on ? "#fff" : boneColor(j.label) });
|
|
2199
|
+
const hit = svgEl("circle", { cx: j.x, cy: j.y, r: 0.045, fill: "transparent", class: "joint" });
|
|
2200
|
+
const title = svgEl("title", {});
|
|
2201
|
+
title.textContent = j.label;
|
|
2202
|
+
hit.append(title);
|
|
2203
|
+
hit.addEventListener("pointerdown", (e) => startDrag(e, j.label));
|
|
2204
|
+
svg.append(ring, dot, hit);
|
|
2205
|
+
joints.set(j.label, [ring, dot, hit]);
|
|
2206
|
+
}
|
|
2207
|
+
svg.addEventListener("keydown", onKey);
|
|
2208
|
+
stageWrap.append(svg);
|
|
2209
|
+
drawStatus();
|
|
2210
|
+
}
|
|
2211
|
+
function place(label, x, y) {
|
|
2212
|
+
for (const c of joints.get(label) || []) {
|
|
2213
|
+
c.setAttribute("cx", String(x));
|
|
2214
|
+
c.setAttribute("cy", String(y));
|
|
2215
|
+
}
|
|
2216
|
+
for (const bone of bones) {
|
|
2217
|
+
for (const line of bone.lines) {
|
|
2218
|
+
if (bone.a === label) {
|
|
2219
|
+
line.setAttribute("x1", String(x));
|
|
2220
|
+
line.setAttribute("y1", String(y));
|
|
2221
|
+
}
|
|
2222
|
+
if (bone.b === label) {
|
|
2223
|
+
line.setAttribute("x2", String(x));
|
|
2224
|
+
line.setAttribute("y2", String(y));
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
function toUnit(e) {
|
|
2230
|
+
const m = svg.getScreenCTM();
|
|
2231
|
+
if (!m) return { x: 0, y: 0 };
|
|
2232
|
+
const p = new DOMPoint(e.clientX, e.clientY).matrixTransform(m.inverse());
|
|
2233
|
+
return { x: clamp(p.x), y: clamp(p.y) };
|
|
2234
|
+
}
|
|
2235
|
+
function startDrag(e, label) {
|
|
2236
|
+
if (playing) return;
|
|
2237
|
+
e.preventDefault();
|
|
2238
|
+
const before = JSON.stringify(set);
|
|
2239
|
+
const joint = poseAt(index).find((j) => j.label === label);
|
|
2240
|
+
const target = e.target;
|
|
2241
|
+
target.setPointerCapture(e.pointerId);
|
|
2242
|
+
const wasSelected = selected;
|
|
2243
|
+
selected = label;
|
|
2244
|
+
if (wasSelected !== label) {
|
|
2245
|
+
for (const [l, [ring, dot]] of joints) {
|
|
2246
|
+
const on = l === label;
|
|
2247
|
+
ring.setAttribute("r", String(on ? 0.034 : 0.026));
|
|
2248
|
+
dot.setAttribute("r", String(on ? 0.022 : 0.016));
|
|
2249
|
+
dot.setAttribute("fill", on ? "#fff" : boneColor(l));
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
const move = (ev) => {
|
|
2253
|
+
const { x, y } = toUnit(ev);
|
|
2254
|
+
joint.x = x;
|
|
2255
|
+
joint.y = y;
|
|
2256
|
+
place(label, x, y);
|
|
2257
|
+
drawStatus();
|
|
2258
|
+
};
|
|
2259
|
+
const end = () => {
|
|
2260
|
+
target.removeEventListener("pointermove", move);
|
|
2261
|
+
target.removeEventListener("pointerup", end);
|
|
2262
|
+
target.removeEventListener("pointercancel", end);
|
|
2263
|
+
commit(before);
|
|
2264
|
+
svg.focus({ preventScroll: true });
|
|
2265
|
+
};
|
|
2266
|
+
target.addEventListener("pointermove", move);
|
|
2267
|
+
target.addEventListener("pointerup", end);
|
|
2268
|
+
target.addEventListener("pointercancel", end);
|
|
2269
|
+
drawStatus();
|
|
2270
|
+
}
|
|
2271
|
+
function onKey(e) {
|
|
2272
|
+
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "z") {
|
|
2273
|
+
e.preventDefault();
|
|
2274
|
+
if (e.shiftKey) redo();
|
|
2275
|
+
else undo();
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
if (e.key === ",") {
|
|
2279
|
+
e.preventDefault();
|
|
2280
|
+
go(index - 1);
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
if (e.key === ".") {
|
|
2284
|
+
e.preventDefault();
|
|
2285
|
+
go(index + 1);
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
const step2 = (e.shiftKey ? 4 : 1) * NUDGE;
|
|
2289
|
+
const d = { ArrowLeft: [-step2, 0], ArrowRight: [step2, 0], ArrowUp: [0, -step2], ArrowDown: [0, step2] }[e.key];
|
|
2290
|
+
if (!d || !selected || playing) return;
|
|
2291
|
+
e.preventDefault();
|
|
2292
|
+
const label = selected;
|
|
2293
|
+
edit(() => {
|
|
2294
|
+
const joint = poseAt(index).find((j) => j.label === label);
|
|
2295
|
+
joint.x = clamp(joint.x + d[0]);
|
|
2296
|
+
joint.y = clamp(joint.y + d[1]);
|
|
2297
|
+
});
|
|
2298
|
+
svg.focus({ preventScroll: true });
|
|
2299
|
+
}
|
|
2300
|
+
function undo() {
|
|
2301
|
+
const before = past.pop();
|
|
2302
|
+
if (!before) return;
|
|
2303
|
+
future.push(JSON.stringify(set));
|
|
2304
|
+
set = JSON.parse(before);
|
|
2305
|
+
index = Math.min(index, count() - 1);
|
|
2306
|
+
onChange(clone(set));
|
|
2307
|
+
redraw();
|
|
2308
|
+
}
|
|
2309
|
+
function redo() {
|
|
2310
|
+
const next = future.pop();
|
|
2311
|
+
if (!next) return;
|
|
2312
|
+
past.push(JSON.stringify(set));
|
|
2313
|
+
set = JSON.parse(next);
|
|
2314
|
+
index = Math.min(index, count() - 1);
|
|
2315
|
+
onChange(clone(set));
|
|
2316
|
+
redraw();
|
|
2317
|
+
}
|
|
2318
|
+
function go(i) {
|
|
2319
|
+
if (i < 0 || i >= count()) return;
|
|
2320
|
+
index = i;
|
|
2321
|
+
redraw();
|
|
2322
|
+
svg.focus({ preventScroll: true });
|
|
2323
|
+
}
|
|
2324
|
+
const button = (label, title, onClick, disabled = false) => {
|
|
2325
|
+
const b = el("button", null, label);
|
|
2326
|
+
b.type = "button";
|
|
2327
|
+
b.title = title;
|
|
2328
|
+
b.disabled = disabled || !!playing && label !== "Stop";
|
|
2329
|
+
b.onclick = onClick;
|
|
2330
|
+
return b;
|
|
2331
|
+
};
|
|
2332
|
+
function drawTools() {
|
|
2333
|
+
tools.textContent = "";
|
|
2334
|
+
const frameOnly = index === 0;
|
|
2335
|
+
tools.append(
|
|
2336
|
+
button("◀", "Previous pose (,)", () => go(index - 1), index === 0),
|
|
2337
|
+
button("▶", "Next pose (.)", () => go(index + 1), index >= count() - 1),
|
|
2338
|
+
button(playing ? "Stop" : "Play", "Play the frames in a loop", () => togglePlay(), set.frames.length < 2 && !playing),
|
|
2339
|
+
button("Copy previous", "Replace this pose with the one before it", () => edit(() => {
|
|
2340
|
+
set.frames[index - 1] = poseAt(index - 1).map((j) => ({ ...j }));
|
|
2341
|
+
}), frameOnly),
|
|
2342
|
+
button("Mirror", "Flip this pose left to right, swapping left and right joints", () => edit(() => {
|
|
2343
|
+
const flipped = poseAt(index).map((j) => ({ ...j, label: mirrorLabel(j.label), x: clamp(1 - j.x) }));
|
|
2344
|
+
if (index === 0) set.firstFrameKeypoints = flipped;
|
|
2345
|
+
else set.frames[index - 1] = flipped;
|
|
2346
|
+
})),
|
|
2347
|
+
button("+ Frame", "Add a copy of this pose after it", () => edit(() => {
|
|
2348
|
+
set.frames.splice(index, 0, poseAt(index).map((j) => ({ ...j })));
|
|
2349
|
+
index += 1;
|
|
2350
|
+
}), set.frames.length >= MAX_FRAMES),
|
|
2351
|
+
button("− Frame", "Remove this frame", () => edit(() => {
|
|
2352
|
+
set.frames.splice(index - 1, 1);
|
|
2353
|
+
index = Math.min(index, count() - 1);
|
|
2354
|
+
}), frameOnly || set.frames.length <= MIN_FRAMES),
|
|
2355
|
+
button("Undo", "Undo (Ctrl+Z)", undo, !past.length),
|
|
2356
|
+
button("Redo", "Redo (Ctrl+Shift+Z)", redo, !future.length)
|
|
2357
|
+
);
|
|
2358
|
+
}
|
|
2359
|
+
function togglePlay() {
|
|
2360
|
+
if (playing) {
|
|
2361
|
+
clearInterval(playing);
|
|
2362
|
+
playing = null;
|
|
2363
|
+
redraw();
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
let i = index > 0 ? index : 1;
|
|
2367
|
+
playing = setInterval(() => {
|
|
2368
|
+
if (!root.isConnected) {
|
|
2369
|
+
clearInterval(playing);
|
|
2370
|
+
playing = null;
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
i = i >= count() - 1 ? 1 : i + 1;
|
|
2374
|
+
index = i;
|
|
2375
|
+
drawStage();
|
|
2376
|
+
drawStrip();
|
|
2377
|
+
}, PLAY_MS);
|
|
2378
|
+
drawTools();
|
|
2379
|
+
}
|
|
2380
|
+
function drawStatus() {
|
|
2381
|
+
const j = selected && poseAt(index).find((x) => x.label === selected);
|
|
2382
|
+
status.textContent = name(index) + " of " + set.frames.length + " frames" + (j ? " · " + j.label + " (" + j.x.toFixed(3) + ", " + j.y.toFixed(3) + ")" : " · drag a joint to move it") + (index === 0 ? " · the starting pose must match the sprite as drawn" : "");
|
|
2383
|
+
}
|
|
2384
|
+
function drawStrip() {
|
|
2385
|
+
strip.textContent = "";
|
|
2386
|
+
for (let i = 0; i < count(); i++) {
|
|
2387
|
+
const cell = el("button", i === index ? "on" : null);
|
|
2388
|
+
cell.type = "button";
|
|
2389
|
+
cell.title = name(i);
|
|
2390
|
+
cell.append(poseSvg(poseAt(i), imageUrl, i > 0), el("span", null, i === 0 ? "start" : String(i)));
|
|
2391
|
+
const target = i;
|
|
2392
|
+
cell.onclick = () => {
|
|
2393
|
+
if (!playing) go(target);
|
|
2394
|
+
};
|
|
2395
|
+
strip.append(cell);
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
function redraw() {
|
|
2399
|
+
drawStage();
|
|
2400
|
+
drawStrip();
|
|
2401
|
+
drawTools();
|
|
2402
|
+
}
|
|
2403
|
+
redraw();
|
|
2404
|
+
return root;
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2110
2407
|
// src/gallery/client/src/skeleton.ts
|
|
2111
2408
|
var SKELETON_BONES = [
|
|
2112
2409
|
["NOSE", "NECK"],
|
|
@@ -2260,7 +2557,7 @@
|
|
|
2260
2557
|
const preview = el("div");
|
|
2261
2558
|
const row2 = el("div", "row");
|
|
2262
2559
|
row2.append(field("poses from", source), field("keypoints file", file, "Inside the project. The manifest records this path."));
|
|
2263
|
-
form.append(row2, estimateRow, field("poses (JSON)", json, "
|
|
2560
|
+
form.append(row2, estimateRow, field("poses (JSON)", json, "Drag joints in the editor below, or edit the JSON; x and y are fractions of the sprite."), overwriteField, status, preview);
|
|
2264
2561
|
const imageUrl = displayUrl(item);
|
|
2265
2562
|
let set = null;
|
|
2266
2563
|
const showPoses = () => {
|
|
@@ -2286,7 +2583,10 @@
|
|
|
2286
2583
|
set = null;
|
|
2287
2584
|
return;
|
|
2288
2585
|
}
|
|
2289
|
-
preview.append(
|
|
2586
|
+
preview.append(poseEditor(set, imageUrl, (next) => {
|
|
2587
|
+
set = next;
|
|
2588
|
+
json.value = JSON.stringify(next, null, 2);
|
|
2589
|
+
}));
|
|
2290
2590
|
};
|
|
2291
2591
|
const sync = () => {
|
|
2292
2592
|
const pasting = source.value !== "file";
|
|
@@ -2373,6 +2673,51 @@
|
|
|
2373
2673
|
setTimeout(() => id.focus(), 0);
|
|
2374
2674
|
return form;
|
|
2375
2675
|
}
|
|
2676
|
+
function skeletonPoseForm(item, parentUrl) {
|
|
2677
|
+
const form = el("form", "edit");
|
|
2678
|
+
form.append(el("h3", null, "Edit poses of " + item.assetId));
|
|
2679
|
+
let draft = item.skeleton.set;
|
|
2680
|
+
let dirty = false;
|
|
2681
|
+
const msg = el("span", "msg");
|
|
2682
|
+
const save = el("button", "primary", "Save poses");
|
|
2683
|
+
save.type = "submit";
|
|
2684
|
+
save.disabled = true;
|
|
2685
|
+
form.append(poseEditor(draft, parentUrl, (next) => {
|
|
2686
|
+
draft = next;
|
|
2687
|
+
dirty = true;
|
|
2688
|
+
save.disabled = false;
|
|
2689
|
+
msg.className = "msg";
|
|
2690
|
+
msg.textContent = "unsaved";
|
|
2691
|
+
}));
|
|
2692
|
+
form.append(el("small", "state-dim", "Saving rewrites " + item.skeleton.keypointsFile + "; the animation is then stale until it is generated again."));
|
|
2693
|
+
const cancel = el("button", null, "Cancel");
|
|
2694
|
+
cancel.type = "button";
|
|
2695
|
+
cancel.onclick = () => {
|
|
2696
|
+
if (dirty && !confirm("Discard the pose changes?")) return;
|
|
2697
|
+
ui.editing = null;
|
|
2698
|
+
renderDrawer();
|
|
2699
|
+
};
|
|
2700
|
+
const actions = el("div", "actions");
|
|
2701
|
+
actions.append(save, cancel, msg);
|
|
2702
|
+
form.append(actions);
|
|
2703
|
+
form.onsubmit = async (e) => {
|
|
2704
|
+
e.preventDefault();
|
|
2705
|
+
save.disabled = true;
|
|
2706
|
+
msg.className = "msg";
|
|
2707
|
+
msg.textContent = "saving…";
|
|
2708
|
+
const body = { action: "update-skeleton-keypoints", styleId: item.styleId, assetId: item.assetId, expectedSha256: item.skeleton.sha256, set: draft };
|
|
2709
|
+
if (item.project) body.project = item.project;
|
|
2710
|
+
try {
|
|
2711
|
+
S.snap = await postEdit(body);
|
|
2712
|
+
ui.editing = null;
|
|
2713
|
+
ui.notice = { id: item.id, text: "Poses saved to " + item.skeleton.keypointsFile + ". " + nextStep().replace("it", "the animation") };
|
|
2714
|
+
render();
|
|
2715
|
+
} catch (err) {
|
|
2716
|
+
showSaveError(msg, save, err);
|
|
2717
|
+
}
|
|
2718
|
+
};
|
|
2719
|
+
return form;
|
|
2720
|
+
}
|
|
2376
2721
|
|
|
2377
2722
|
// src/gallery/client/src/drawer.ts
|
|
2378
2723
|
function row(dl, label, value, opts = {}) {
|
|
@@ -2737,8 +3082,23 @@
|
|
|
2737
3082
|
row(dl, "keypoints", item.skeleton.keypointsFile, { mono: true });
|
|
2738
3083
|
if (item.skeleton.set) {
|
|
2739
3084
|
const parent = S.snap.items.find((i) => i.key === item.revisionParentKey && i.project === item.project);
|
|
2740
|
-
|
|
2741
|
-
|
|
3085
|
+
const posesKey = "poses:" + item.id;
|
|
3086
|
+
if (ui.editing === posesKey) {
|
|
3087
|
+
s.append(skeletonPoseForm(item, displayUrl(parent)));
|
|
3088
|
+
} else {
|
|
3089
|
+
s.append(poseStrip(item.skeleton.set, displayUrl(parent)));
|
|
3090
|
+
s.append(el("small", "state-dim", "The starting pose must match the sprite; each frame is drawn over a dimmed copy. `pixelkiln skeleton-preview " + item.assetId + "` writes the same sheet as a PNG."));
|
|
3091
|
+
if (canEdit && item.skeleton.sha256) {
|
|
3092
|
+
const b = el("button", "add", "Edit poses");
|
|
3093
|
+
b.type = "button";
|
|
3094
|
+
b.onclick = () => {
|
|
3095
|
+
ui.editing = posesKey;
|
|
3096
|
+
ui.notice = null;
|
|
3097
|
+
renderDrawer();
|
|
3098
|
+
};
|
|
3099
|
+
s.append(b);
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
2742
3102
|
} else {
|
|
2743
3103
|
row(dl, "poses", el("span", "state-warn", "the keypoints file does not exist yet"));
|
|
2744
3104
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -449,7 +449,7 @@ var CharacterProportionsSchema = import_zod.z.union([
|
|
|
449
449
|
hipWidth: import_zod.z.number().min(0.5).max(2).optional()
|
|
450
450
|
}).strict()
|
|
451
451
|
]);
|
|
452
|
-
var CharacterAnimationModeSchema = import_zod.z.enum(["template", "v3", "pro"]);
|
|
452
|
+
var CharacterAnimationModeSchema = import_zod.z.enum(["template", "skeleton-v3", "v3", "pro"]);
|
|
453
453
|
var UiPieceSchema = import_zod.z.discriminatedUnion("kind", [
|
|
454
454
|
import_zod.z.object({
|
|
455
455
|
id: import_zod.z.string().min(1),
|
|
@@ -563,7 +563,11 @@ var CharacterAnimationSchema = import_zod.z.object({
|
|
|
563
563
|
fps: import_zod.z.number().int().min(1).max(60).default(8),
|
|
564
564
|
/** v3 only: keep the resting pose as frame 0, so `frames` generated frames land as `frames + 1` files. */
|
|
565
565
|
keepFirstFrame: import_zod.z.boolean().default(true),
|
|
566
|
-
/**
|
|
566
|
+
/**
|
|
567
|
+
* `template` when a template is named, otherwise `v3`; `skeleton-v3`
|
|
568
|
+
* poses a template with PixelLab's skeleton model instead; `pro` for the
|
|
569
|
+
* sequential high-quality engine.
|
|
570
|
+
*/
|
|
567
571
|
mode: CharacterAnimationModeSchema.optional(),
|
|
568
572
|
/**
|
|
569
573
|
* v3 only: a manifest-relative image of the pose to start from, instead
|
|
@@ -600,7 +604,7 @@ var CharacterAnimationSchema = import_zod.z.object({
|
|
|
600
604
|
if (animation[key] !== void 0 && mode2 !== "v3") {
|
|
601
605
|
context.addIssue({
|
|
602
606
|
code: import_zod.z.ZodIssueCode.custom,
|
|
603
|
-
message: `${key} is for v3 loops; a ${mode2} loop ${mode2 === "template" ? "starts from the character's rotation and follows its template" : "takes neither"}`,
|
|
607
|
+
message: `${key} is for v3 loops; a ${mode2} loop ${mode2 === "template" || mode2 === "skeleton-v3" ? "starts from the character's rotation and follows its template" : "takes neither"}`,
|
|
604
608
|
path: [key]
|
|
605
609
|
});
|
|
606
610
|
}
|
|
@@ -610,10 +614,10 @@ var CharacterAnimationSchema = import_zod.z.object({
|
|
|
610
614
|
context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: `${key} overrides apply to template loops only`, path: [key] });
|
|
611
615
|
}
|
|
612
616
|
}
|
|
613
|
-
if (animation.template && animation.mode && animation.mode !== "template") {
|
|
617
|
+
if (animation.template && animation.mode && animation.mode !== "template" && animation.mode !== "skeleton-v3") {
|
|
614
618
|
context.addIssue({
|
|
615
619
|
code: import_zod.z.ZodIssueCode.custom,
|
|
616
|
-
message: `a template animation cannot use mode "${animation.mode}"`,
|
|
620
|
+
message: `a template animation cannot use mode "${animation.mode}"; use "template" or "skeleton-v3"`,
|
|
617
621
|
path: ["mode"]
|
|
618
622
|
});
|
|
619
623
|
}
|
|
@@ -624,8 +628,8 @@ var CharacterAnimationSchema = import_zod.z.object({
|
|
|
624
628
|
path: ["frames"]
|
|
625
629
|
});
|
|
626
630
|
}
|
|
627
|
-
if (!animation.template && animation.mode === "template") {
|
|
628
|
-
context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message:
|
|
631
|
+
if (!animation.template && (animation.mode === "template" || animation.mode === "skeleton-v3")) {
|
|
632
|
+
context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: `${animation.mode} mode needs a template`, path: ["template"] });
|
|
629
633
|
}
|
|
630
634
|
});
|
|
631
635
|
var RevisionDitheringSchema = import_zod.z.enum(["none", "2x2", "4x4", "8x8"]);
|
|
@@ -2559,12 +2563,20 @@ var PixelLabClient = class {
|
|
|
2559
2563
|
}
|
|
2560
2564
|
/**
|
|
2561
2565
|
* One animation of one character. Template mode costs a generation per
|
|
2562
|
-
* direction and fixes the frame count; v3
|
|
2563
|
-
* the
|
|
2566
|
+
* direction and fixes the frame count; skeleton-v3 poses the same template
|
|
2567
|
+
* with the skeleton video model (2-4 generations per direction); v3 draws
|
|
2568
|
+
* `frameCount` frames from the action text; pro is the sequential engine.
|
|
2569
|
+
* One job per direction.
|
|
2570
|
+
*
|
|
2571
|
+
* Every mode but skeleton-v3 goes to `/animate-character`, the path whose
|
|
2572
|
+
* shapes were measured live. skeleton-v3 is documented only on
|
|
2573
|
+
* `/characters/animations`, which takes the same request and answers with
|
|
2574
|
+
* the same job list (plus an `animation_group_id`).
|
|
2564
2575
|
*/
|
|
2565
2576
|
async animateCharacter(args) {
|
|
2566
2577
|
const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
|
|
2567
|
-
const
|
|
2578
|
+
const endpoint = args.mode === "skeleton-v3" ? "/characters/animations" : "/animate-character";
|
|
2579
|
+
const raw = await this.request(endpoint, {
|
|
2568
2580
|
method: "POST",
|
|
2569
2581
|
body: JSON.stringify({
|
|
2570
2582
|
character_id: args.characterId,
|
|
@@ -2588,7 +2600,7 @@ var PixelLabClient = class {
|
|
|
2588
2600
|
...args.seed != null ? { seed: args.seed } : {}
|
|
2589
2601
|
})
|
|
2590
2602
|
});
|
|
2591
|
-
return validateResponse(AnimateSubmitSchema, raw,
|
|
2603
|
+
return validateResponse(AnimateSubmitSchema, raw, endpoint.slice(1));
|
|
2592
2604
|
}
|
|
2593
2605
|
/**
|
|
2594
2606
|
* A full-body sprite in, a bust portrait out (or the reverse, unused
|
|
@@ -3881,6 +3893,7 @@ function characterCost(spec) {
|
|
|
3881
3893
|
if (character.kind === "animation") {
|
|
3882
3894
|
const animation = character.animation;
|
|
3883
3895
|
if (animation.mode === "template") return 1;
|
|
3896
|
+
if (animation.mode === "skeleton-v3") return 4;
|
|
3884
3897
|
if (animation.mode === "v3") return Math.max(1, Math.ceil(px * animation.frames / 65536));
|
|
3885
3898
|
return generationCost(spec.width, spec.height, "1dir");
|
|
3886
3899
|
}
|
|
@@ -4473,7 +4486,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4473
4486
|
`${spec.styleId}/${spec.assetId} animates "${character.animation.direction}", but its character has ${character.directions} directions: ${allowed.join(", ")}`
|
|
4474
4487
|
);
|
|
4475
4488
|
}
|
|
4476
|
-
if (character.animation.mode !== "template" && !spec.prompt.trim()) {
|
|
4489
|
+
if (character.animation.mode !== "template" && character.animation.mode !== "skeleton-v3" && !spec.prompt.trim()) {
|
|
4477
4490
|
throw new Error(`${spec.styleId}/${spec.assetId} needs a prompt describing the motion, or a template`);
|
|
4478
4491
|
}
|
|
4479
4492
|
}
|
|
@@ -9401,8 +9414,8 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
|
|
|
9401
9414
|
);
|
|
9402
9415
|
}
|
|
9403
9416
|
}
|
|
9404
|
-
if (asset.animation.mode === "template") {
|
|
9405
|
-
throw new Error(`assets.${assetId}: objectPro animations have no "
|
|
9417
|
+
if (asset.animation.mode === "template" || asset.animation.mode === "skeleton-v3") {
|
|
9418
|
+
throw new Error(`assets.${assetId}: objectPro animations have no "${asset.animation.mode}" mode; use "v3" or "pro"`);
|
|
9406
9419
|
}
|
|
9407
9420
|
const pose = async (rel, what) => {
|
|
9408
9421
|
const image = await files.load(rel, what);
|
|
@@ -15947,6 +15960,15 @@ var SkeletonAnimationRequestSchema = import_zod10.z.object({
|
|
|
15947
15960
|
/** Replace an existing `keypointsFile` with `set`. */
|
|
15948
15961
|
overwrite: import_zod10.z.boolean().optional()
|
|
15949
15962
|
}).strict();
|
|
15963
|
+
var SkeletonKeypointsUpdateSchema = import_zod10.z.object({
|
|
15964
|
+
action: import_zod10.z.literal("update-skeleton-keypoints"),
|
|
15965
|
+
project: import_zod10.z.string().min(1).optional(),
|
|
15966
|
+
styleId: import_zod10.z.string().min(1),
|
|
15967
|
+
assetId: import_zod10.z.string().min(1),
|
|
15968
|
+
/** The file's SHA-256 when the page loaded it; anything else on disk is a concurrent edit. */
|
|
15969
|
+
expectedSha256: import_zod10.z.string().regex(/^[0-9a-f]{64}$/),
|
|
15970
|
+
set: SkeletonSetSchema
|
|
15971
|
+
}).strict();
|
|
15950
15972
|
async function createSkeletonAnimation(manifestPath, request) {
|
|
15951
15973
|
const root = import_node_path29.default.dirname(import_node_path29.default.resolve(manifestPath));
|
|
15952
15974
|
const file = import_node_path29.default.resolve(root, request.keypointsFile);
|
|
@@ -15992,6 +16014,26 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
15992
16014
|
throw error;
|
|
15993
16015
|
}
|
|
15994
16016
|
}
|
|
16017
|
+
async function updateSkeletonKeypoints(ctx, request) {
|
|
16018
|
+
const key = lockKey(request.styleId, request.assetId);
|
|
16019
|
+
const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === key);
|
|
16020
|
+
if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
|
|
16021
|
+
const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
|
|
16022
|
+
if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
|
|
16023
|
+
const relative = import_node_path29.default.relative(ctx.loaded.root, file);
|
|
16024
|
+
if (relative.startsWith("..") || import_node_path29.default.isAbsolute(relative)) {
|
|
16025
|
+
throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
|
|
16026
|
+
}
|
|
16027
|
+
const current = (0, import_node_fs25.existsSync)(file) ? sha256(await (0, import_promises22.readFile)(file)) : null;
|
|
16028
|
+
if (current !== request.expectedSha256) {
|
|
16029
|
+
throw Object.assign(
|
|
16030
|
+
new Error(`${relative.split(import_node_path29.default.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
|
|
16031
|
+
{ status: 409 }
|
|
16032
|
+
);
|
|
16033
|
+
}
|
|
16034
|
+
await (0, import_promises22.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
|
|
16035
|
+
return relative.split(import_node_path29.default.sep).join("/");
|
|
16036
|
+
}
|
|
15995
16037
|
|
|
15996
16038
|
// src/gallery/edit.ts
|
|
15997
16039
|
var Base64 = import_zod11.z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
|
|
@@ -16068,6 +16110,18 @@ function createGalleryEditHandler(opts) {
|
|
|
16068
16110
|
}
|
|
16069
16111
|
return opts.reload();
|
|
16070
16112
|
}
|
|
16113
|
+
const poses = SkeletonKeypointsUpdateSchema.safeParse(body);
|
|
16114
|
+
if (poses.success) {
|
|
16115
|
+
if (!opts.loadProject) throw new ManifestEditError("pose edits are not available in this gallery");
|
|
16116
|
+
const written = await updateSkeletonKeypoints(await opts.loadProject(poses.data.project), poses.data);
|
|
16117
|
+
log(` poses saved: ${written} (${poses.data.styleId}/${poses.data.assetId} is stale until generated again)`);
|
|
16118
|
+
return opts.reload();
|
|
16119
|
+
}
|
|
16120
|
+
if (body?.action === "update-skeleton-keypoints") {
|
|
16121
|
+
throw new ManifestEditError(
|
|
16122
|
+
"invalid pose edit: " + poses.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
|
|
16123
|
+
);
|
|
16124
|
+
}
|
|
16071
16125
|
const skeleton = SkeletonAnimationRequestSchema.safeParse(body);
|
|
16072
16126
|
if (skeleton.success) {
|
|
16073
16127
|
let manifestPath2;
|
|
@@ -16441,7 +16495,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
16441
16495
|
mirrorOfKey: spec.mirror ? lockKey(spec.styleId, spec.mirror.sourceAssetId) : entry?.mirror ? lockKey(spec.styleId, entry.mirror.sourceAssetId) : null,
|
|
16442
16496
|
skeleton: spec.revision?.mode === "animate-skeleton" && spec.revision.keypointsFile ? {
|
|
16443
16497
|
keypointsFile: portableOutputPath(spec.revision.keypointsFile, root),
|
|
16444
|
-
set: spec.revision.skeleton ?? null
|
|
16498
|
+
set: spec.revision.skeleton ?? null,
|
|
16499
|
+
sha256: spec.revision.keypointsSha256 ?? null
|
|
16445
16500
|
} : null,
|
|
16446
16501
|
character: describeCharacter(spec, entry),
|
|
16447
16502
|
outputs,
|