pixelkiln 0.81.0 → 0.83.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/cli.js +132 -54
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.css +13 -0
- package/dist/client/gallery.js +375 -14
- package/dist/index.cjs +106 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +516 -8
- package/dist/index.d.ts +516 -8
- package/dist/index.js +101 -23
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +4 -2
- package/docs/CHARACTERS.md +11 -3
- package/docs/CLI.md +7 -0
- package/docs/PIXELLAB.md +9 -1
- package/docs/REVISIONS.md +9 -4
- package/package.json +1 -1
- package/skills/pixelkiln/references/pixellab.md +11 -8
- package/skills/pixelkiln/references/revisions.md +13 -10
package/dist/client/gallery.css
CHANGED
|
@@ -525,3 +525,16 @@ body[data-size="l"] .cell { height:210px; }
|
|
|
525
525
|
.keys-sheet dd { margin:0; color:var(--dim); }
|
|
526
526
|
.keys-sheet kbd { font:600 11.5px/1 var(--mono); border:1px solid var(--line-strong); border-bottom-width:2px; padding:2px 6px; }
|
|
527
527
|
@media (max-width: 900px) { .bar { flex-wrap:wrap; } }
|
|
528
|
+
|
|
529
|
+
/* ---- toast, studio template preview ---- */
|
|
530
|
+
.toast { position:fixed; left:22px; bottom:22px; z-index:40; display:flex; align-items:center; gap:10px; max-width:min(560px, calc(100vw - 44px));
|
|
531
|
+
padding:9px 10px 9px 14px; background:var(--panel); border:1px solid var(--line-strong); border-left:3px solid var(--ok);
|
|
532
|
+
box-shadow:0 10px 30px rgba(0,0,0,.45); font-size:13px; }
|
|
533
|
+
.toast[hidden] { display:none; }
|
|
534
|
+
.toast.bad { border-left-color:var(--bad); }
|
|
535
|
+
.toast button { padding:4px 10px; font-size:12.5px; }
|
|
536
|
+
.toast button.x { border:0; color:var(--dim); padding:4px 6px; }
|
|
537
|
+
.studio-tpl-preview { display:flex; align-items:center; gap:10px; }
|
|
538
|
+
.studio-tpl-preview[hidden] { display:none; }
|
|
539
|
+
.studio-tpl-preview img { width:48px; height:48px; image-rendering:pixelated; object-fit:contain; background-color:var(--stage-bg); background-image:var(--stage-pattern); background-size:8px 8px; border:1px solid var(--line); }
|
|
540
|
+
.studio-draft { display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
|
package/dist/client/gallery.js
CHANGED
|
@@ -150,7 +150,15 @@
|
|
|
150
150
|
return s >= 1 ? Math.floor(s) : s;
|
|
151
151
|
};
|
|
152
152
|
var projectOf = (item) => S.snap.workspace ? S.snap.workspace.projects.find((pr) => pr.id === item.project) : S.snap.project;
|
|
153
|
+
var editHooks = { saved: null };
|
|
154
|
+
var manifestShaOf = (snap, project) => snap.workspace ? snap.workspace.projects.find((pr) => pr.id === project)?.manifestSha256 ?? null : snap.project?.manifestSha256 ?? null;
|
|
153
155
|
async function postEdit(body) {
|
|
156
|
+
const before = manifestShaOf(S.snap, body && body.project);
|
|
157
|
+
const snap = await sendEdit(body);
|
|
158
|
+
editHooks.saved?.(body, manifestShaOf(snap, body && body.project) !== before);
|
|
159
|
+
return snap;
|
|
160
|
+
}
|
|
161
|
+
async function sendEdit(body) {
|
|
154
162
|
const res = await fetch("/api/edit", {
|
|
155
163
|
method: "POST",
|
|
156
164
|
headers: { "Content-Type": "application/json", "X-Pixelkiln-Session": SESSION ?? "" },
|
|
@@ -879,6 +887,23 @@
|
|
|
879
887
|
setTimeout(() => id.focus(), 0);
|
|
880
888
|
return form;
|
|
881
889
|
}
|
|
890
|
+
function revisionModesFor(item) {
|
|
891
|
+
if (item.provider !== "pixellab") return [["image-to-image", "edit with a text instruction"]];
|
|
892
|
+
if (item.outputs.length > 1) {
|
|
893
|
+
return [
|
|
894
|
+
["edit-animation", "one text edit across every frame"],
|
|
895
|
+
["reduce-colors", "fewer colours, across every frame"],
|
|
896
|
+
["correct-pixelart", "clean up the pixel grid, across every frame"]
|
|
897
|
+
];
|
|
898
|
+
}
|
|
899
|
+
return [
|
|
900
|
+
["image-to-image", "edit with a text instruction"],
|
|
901
|
+
["animate", "animate it from a text description"],
|
|
902
|
+
["animate-pixminimax", "animate it, holding the facing direction"],
|
|
903
|
+
["reduce-colors", "fewer colours, optional dithering"],
|
|
904
|
+
["correct-pixelart", "clean up the pixel grid"]
|
|
905
|
+
];
|
|
906
|
+
}
|
|
882
907
|
function newRevisionForm(item) {
|
|
883
908
|
const pr = projectOf(item);
|
|
884
909
|
const form = el("form", "edit");
|
|
@@ -889,18 +914,83 @@
|
|
|
889
914
|
id.required = true;
|
|
890
915
|
id.autocomplete = "off";
|
|
891
916
|
id.pattern = "[^\\/\\\\]+";
|
|
917
|
+
const modes = revisionModesFor(item);
|
|
918
|
+
const mode = el("select");
|
|
919
|
+
for (const [value, label] of modes) mode.append(new Option(value + ": " + label, value));
|
|
892
920
|
const prompt = el("textarea");
|
|
893
|
-
prompt.placeholder = 'Edit instruction, e.g. "add snow on the roof".';
|
|
894
|
-
prompt.required = true;
|
|
895
921
|
const strength = el("input");
|
|
896
922
|
strength.type = "number";
|
|
897
923
|
strength.min = "0";
|
|
898
924
|
strength.max = "1";
|
|
899
925
|
strength.step = "0.05";
|
|
900
926
|
strength.placeholder = "provider default";
|
|
927
|
+
const frames = el("input");
|
|
928
|
+
frames.type = "number";
|
|
929
|
+
frames.min = "4";
|
|
930
|
+
frames.max = "40";
|
|
931
|
+
frames.step = "2";
|
|
932
|
+
frames.placeholder = "provider default";
|
|
933
|
+
const fps = el("input");
|
|
934
|
+
fps.type = "number";
|
|
935
|
+
fps.min = "1";
|
|
936
|
+
fps.max = "60";
|
|
937
|
+
fps.step = "1";
|
|
938
|
+
fps.placeholder = "playback rate";
|
|
939
|
+
const enhance = el("input");
|
|
940
|
+
enhance.type = "checkbox";
|
|
941
|
+
const enhanceField = el("label", "field check");
|
|
942
|
+
enhanceField.append(enhance, el("span", null, "let PixelLab expand the motion description first"));
|
|
943
|
+
const direction = el("select");
|
|
944
|
+
for (const d of ["south", "south-east", "east", "north-east", "north", "north-west", "west", "south-west"]) direction.append(new Option(d, d));
|
|
945
|
+
const numColors = el("input");
|
|
946
|
+
numColors.type = "number";
|
|
947
|
+
numColors.min = "2";
|
|
948
|
+
numColors.max = "256";
|
|
949
|
+
numColors.step = "1";
|
|
950
|
+
numColors.placeholder = "PixelLab default";
|
|
951
|
+
const dithering = el("select");
|
|
952
|
+
for (const d of ["", "none", "2x2", "4x4", "8x8"]) dithering.append(new Option(d || "default", d));
|
|
901
953
|
const row1 = el("div", "row");
|
|
902
|
-
row1.append(field("new asset id", id), field("
|
|
903
|
-
|
|
954
|
+
row1.append(field("new asset id", id), field("mode", mode));
|
|
955
|
+
const promptField = field("instruction", prompt);
|
|
956
|
+
const strengthField = field("strength", strength, "PixelLab rejects an explicit strength for image-to-image; leave this blank there. Optional for correct-pixelart.");
|
|
957
|
+
const framesField = field("frames", frames, "Even, 4 to 40; animate allows up to 16.");
|
|
958
|
+
const fpsField = field("fps", fps, "Recorded with the frames; PixelLab does not store one.");
|
|
959
|
+
const directionField = field("facing", direction, "Holds the sprite's facing while the motion is expanded.");
|
|
960
|
+
const colorsField = field("colours", numColors);
|
|
961
|
+
const ditherField = field("dithering", dithering);
|
|
962
|
+
const grids = el("div", "row");
|
|
963
|
+
grids.append(framesField, fpsField);
|
|
964
|
+
const cleanup = el("div", "row");
|
|
965
|
+
cleanup.append(colorsField, ditherField);
|
|
966
|
+
form.append(row1, promptField, strengthField, grids, directionField, enhanceField, cleanup);
|
|
967
|
+
const hints = {
|
|
968
|
+
"image-to-image": ["edit instruction", "add snow on the roof", "The style still adds its prefix and suffix."],
|
|
969
|
+
animate: ["motion", "the chest wobbling gently", "Describes the movement; the parent image is the first frame."],
|
|
970
|
+
"animate-pixminimax": ["motion", "the chest wobbling gently", "Describes the movement; the parent image is the first frame."],
|
|
971
|
+
"edit-animation": ["edit instruction", "add a red cape", "Applied across every frame in one call, so the change stays consistent."],
|
|
972
|
+
"reduce-colors": ["note", "optional", "Cleanup only: the text is kept as a label, not sent."],
|
|
973
|
+
"correct-pixelart": ["note", "optional", "Cleanup only: the text is kept as a label, not sent."]
|
|
974
|
+
};
|
|
975
|
+
const sync = () => {
|
|
976
|
+
const m = mode.value;
|
|
977
|
+
const [label, placeholder, hint] = hints[m];
|
|
978
|
+
promptField.querySelector("span").textContent = label;
|
|
979
|
+
promptField.querySelector("small")?.remove();
|
|
980
|
+
promptField.append(el("small", null, hint));
|
|
981
|
+
prompt.placeholder = placeholder;
|
|
982
|
+
prompt.required = m !== "reduce-colors" && m !== "correct-pixelart";
|
|
983
|
+
strengthField.hidden = m !== "image-to-image" && m !== "correct-pixelart";
|
|
984
|
+
grids.hidden = !(m === "animate" || m === "animate-pixminimax" || m === "edit-animation");
|
|
985
|
+
framesField.hidden = !(m === "animate" || m === "animate-pixminimax");
|
|
986
|
+
fpsField.hidden = grids.hidden;
|
|
987
|
+
directionField.hidden = !(m === "animate-pixminimax" && enhance.checked);
|
|
988
|
+
enhanceField.hidden = !(m === "animate" || m === "animate-pixminimax");
|
|
989
|
+
cleanup.hidden = m !== "reduce-colors";
|
|
990
|
+
};
|
|
991
|
+
mode.onchange = sync;
|
|
992
|
+
enhance.onchange = sync;
|
|
993
|
+
sync();
|
|
904
994
|
const actions = el("div", "actions");
|
|
905
995
|
const save = el("button", "primary", "Create revision");
|
|
906
996
|
save.type = "submit";
|
|
@@ -918,9 +1008,19 @@
|
|
|
918
1008
|
save.disabled = true;
|
|
919
1009
|
msg.className = "msg";
|
|
920
1010
|
msg.textContent = "saving…";
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
|
|
1011
|
+
const m = mode.value;
|
|
1012
|
+
const revision = { mode: m, from: item.assetId };
|
|
1013
|
+
if ((m === "image-to-image" || m === "correct-pixelart") && strength.value.trim() !== "") revision.strength = Number(strength.value);
|
|
1014
|
+
if ((m === "animate" || m === "animate-pixminimax") && frames.value.trim() !== "") revision.frames = Number(frames.value);
|
|
1015
|
+
if ((m === "animate" || m === "animate-pixminimax" || m === "edit-animation") && fps.value.trim() !== "") revision.fps = Number(fps.value);
|
|
1016
|
+
if ((m === "animate" || m === "animate-pixminimax") && enhance.checked) revision.enhancePrompt = true;
|
|
1017
|
+
if (m === "animate-pixminimax" && enhance.checked) revision.direction = direction.value;
|
|
1018
|
+
if (m === "reduce-colors") {
|
|
1019
|
+
if (numColors.value.trim() !== "") revision.numColors = Number(numColors.value);
|
|
1020
|
+
if (dithering.value) revision.dithering = dithering.value;
|
|
1021
|
+
}
|
|
1022
|
+
const text = prompt.value.trim() || (m === "reduce-colors" ? "reduce colours" : m === "correct-pixelart" ? "correct pixel art" : "");
|
|
1023
|
+
const asset = { prompt: text, styles: [item.styleId], revision };
|
|
924
1024
|
try {
|
|
925
1025
|
const body = { action: "add-asset", assetId: id.value.trim(), expectedSha256: pr.manifestSha256, asset };
|
|
926
1026
|
if (item.project) body.project = item.project;
|
|
@@ -4365,6 +4465,12 @@
|
|
|
4365
4465
|
if (item.revision.maskSha256) row(dl, "mask sha256", item.revision.maskSha256.slice(0, 16) + "…", { mono: true, copy: item.revision.maskSha256 });
|
|
4366
4466
|
if (item.revision.strength !== void 0) row(dl, "strength", item.revision.strength);
|
|
4367
4467
|
}
|
|
4468
|
+
const declared = item.asset && item.asset.revision;
|
|
4469
|
+
if (declared) {
|
|
4470
|
+
for (const [label, key] of [["facing", "direction"], ["looks like", "description"], ["body template", "skeletonTemplate"], ["frames", "frames"], ["fps", "fps"], ["colours", "numColors"], ["dithering", "dithering"]]) {
|
|
4471
|
+
if (declared[key] !== void 0) row(dl, label, declared[key]);
|
|
4472
|
+
}
|
|
4473
|
+
}
|
|
4368
4474
|
if (item.skeleton) {
|
|
4369
4475
|
row(dl, "keypoints", item.skeleton.keypointsFile, { mono: true });
|
|
4370
4476
|
if (item.skeleton.set) {
|
|
@@ -4645,6 +4751,48 @@
|
|
|
4645
4751
|
{ id: "pro", label: "pro: described in words, sequential (20-40 per direction)" }
|
|
4646
4752
|
];
|
|
4647
4753
|
var templated = (mode) => mode === "skeleton-v3" || mode === "template";
|
|
4754
|
+
var KNOWN_TEMPLATES = [
|
|
4755
|
+
"walk",
|
|
4756
|
+
"walking-8-frames",
|
|
4757
|
+
"running-8-frames",
|
|
4758
|
+
"breathing-idle",
|
|
4759
|
+
"crouched-walking",
|
|
4760
|
+
"attack",
|
|
4761
|
+
"attack-back",
|
|
4762
|
+
"attack-left",
|
|
4763
|
+
"attack-right",
|
|
4764
|
+
"cross-punch",
|
|
4765
|
+
"backflip",
|
|
4766
|
+
"angry",
|
|
4767
|
+
"bark"
|
|
4768
|
+
];
|
|
4769
|
+
var draftKey = () => "pixelkiln.studio.draft:" + (S.snap.workspace ? S.snap.workspace.path : S.snap.project ? S.snap.project.manifest : "");
|
|
4770
|
+
function readDraft() {
|
|
4771
|
+
try {
|
|
4772
|
+
const d = JSON.parse(localStorage.getItem(draftKey()) || "null");
|
|
4773
|
+
return d && typeof d === "object" ? d : null;
|
|
4774
|
+
} catch {
|
|
4775
|
+
return null;
|
|
4776
|
+
}
|
|
4777
|
+
}
|
|
4778
|
+
function writeDraft(draft) {
|
|
4779
|
+
try {
|
|
4780
|
+
if (draft) localStorage.setItem(draftKey(), JSON.stringify(draft));
|
|
4781
|
+
else localStorage.removeItem(draftKey());
|
|
4782
|
+
} catch {
|
|
4783
|
+
}
|
|
4784
|
+
}
|
|
4785
|
+
function usedTemplates(projectId) {
|
|
4786
|
+
const out = /* @__PURE__ */ new Map();
|
|
4787
|
+
for (const i of S.snap.items) {
|
|
4788
|
+
const t = i.asset && i.asset.animation && i.asset.animation.template;
|
|
4789
|
+
if (!t || i.project !== projectId) continue;
|
|
4790
|
+
const frames = i.outputs.filter((o) => o.url);
|
|
4791
|
+
if (frames.length > 1 && !out.get(t)) out.set(t, i);
|
|
4792
|
+
else if (!out.has(t)) out.set(t, null);
|
|
4793
|
+
}
|
|
4794
|
+
return out;
|
|
4795
|
+
}
|
|
4648
4796
|
var idPattern = "[^\\/\\\\:]+";
|
|
4649
4797
|
function openStudio() {
|
|
4650
4798
|
const host = $("dialog-host");
|
|
@@ -4657,10 +4805,12 @@
|
|
|
4657
4805
|
const scrim = el("div", "sheet-scrim");
|
|
4658
4806
|
const panel = el("div", "sheet studio");
|
|
4659
4807
|
let closed = false;
|
|
4808
|
+
const previewTimers = /* @__PURE__ */ new Set();
|
|
4660
4809
|
const close = () => {
|
|
4661
4810
|
closed = true;
|
|
4662
4811
|
if (priceTimer) clearTimeout(priceTimer);
|
|
4663
4812
|
priceSeq++;
|
|
4813
|
+
for (const t of previewTimers) clearInterval(t);
|
|
4664
4814
|
host.textContent = "";
|
|
4665
4815
|
};
|
|
4666
4816
|
const bar = el("div", "rbar");
|
|
@@ -4734,7 +4884,9 @@
|
|
|
4734
4884
|
const loopList = el("div", "studio-loops");
|
|
4735
4885
|
const addLoop = el("button", "add", "+ Add a loop");
|
|
4736
4886
|
addLoop.type = "button";
|
|
4737
|
-
|
|
4887
|
+
const templateList = el("datalist");
|
|
4888
|
+
templateList.id = "studio-templates";
|
|
4889
|
+
loopsSection.append(el("h3", null, "Loops"), el("small", "state-dim", "Each loop is drawn once per ticked direction; a direction whose mirror is ticked is flipped from it for free."), loopList, addLoop, templateList);
|
|
4738
4890
|
const portrait = el("input");
|
|
4739
4891
|
portrait.type = "checkbox";
|
|
4740
4892
|
const portraitSize = el("select");
|
|
@@ -4805,7 +4957,54 @@
|
|
|
4805
4957
|
function syncLoopMode(row2) {
|
|
4806
4958
|
const t = templated(row2.mode.value);
|
|
4807
4959
|
row2.what.placeholder = t ? "template, e.g. walk, breathing-idle, running-8-frames" : 'the motion, e.g. "swings a sword overhead"';
|
|
4808
|
-
row2.what.title = t ? "A PixelLab template id" : "What the loop does";
|
|
4960
|
+
row2.what.title = t ? "A PixelLab template id: pick one, or type any id PixelLab has" : "What the loop does";
|
|
4961
|
+
if (t) row2.what.setAttribute("list", templateList.id);
|
|
4962
|
+
else row2.what.removeAttribute("list");
|
|
4963
|
+
syncPreview(row2);
|
|
4964
|
+
}
|
|
4965
|
+
function syncPreview(row2) {
|
|
4966
|
+
const old = row2.preview.__timer;
|
|
4967
|
+
if (old) {
|
|
4968
|
+
clearInterval(old);
|
|
4969
|
+
previewTimers.delete(old);
|
|
4970
|
+
}
|
|
4971
|
+
row2.preview.textContent = "";
|
|
4972
|
+
const name = row2.what.value.trim();
|
|
4973
|
+
const sample = templated(row2.mode.value) && name ? usedTemplates(projectId()).get(name) : null;
|
|
4974
|
+
if (!sample) {
|
|
4975
|
+
row2.preview.hidden = true;
|
|
4976
|
+
return;
|
|
4977
|
+
}
|
|
4978
|
+
const urls = sample.outputs.filter((o) => o.url).map((o) => o.url);
|
|
4979
|
+
const img = el("img");
|
|
4980
|
+
img.src = urls[0];
|
|
4981
|
+
img.alt = name;
|
|
4982
|
+
let i = 0;
|
|
4983
|
+
const timer = setInterval(() => {
|
|
4984
|
+
if (!img.isConnected) {
|
|
4985
|
+
clearInterval(timer);
|
|
4986
|
+
previewTimers.delete(timer);
|
|
4987
|
+
return;
|
|
4988
|
+
}
|
|
4989
|
+
if (!document.hidden) {
|
|
4990
|
+
i = (i + 1) % urls.length;
|
|
4991
|
+
img.src = urls[i];
|
|
4992
|
+
}
|
|
4993
|
+
}, Math.round(1e3 / (sample.fps || 12)));
|
|
4994
|
+
row2.preview.__timer = timer;
|
|
4995
|
+
previewTimers.add(timer);
|
|
4996
|
+
row2.preview.append(img, el("small", "state-dim", name + ", as drawn for " + sample.assetId));
|
|
4997
|
+
row2.preview.hidden = false;
|
|
4998
|
+
}
|
|
4999
|
+
function fillTemplateList() {
|
|
5000
|
+
templateList.textContent = "";
|
|
5001
|
+
const used = usedTemplates(projectId());
|
|
5002
|
+
for (const t of [.../* @__PURE__ */ new Set([...used.keys(), ...KNOWN_TEMPLATES])]) {
|
|
5003
|
+
const o = el("option");
|
|
5004
|
+
o.value = t;
|
|
5005
|
+
if (used.has(t)) o.label = "used in this project";
|
|
5006
|
+
templateList.append(o);
|
|
5007
|
+
}
|
|
4809
5008
|
}
|
|
4810
5009
|
function addLoopRow(defaults = {}) {
|
|
4811
5010
|
const wrap = el("div", "studio-loop");
|
|
@@ -4835,11 +5034,15 @@
|
|
|
4835
5034
|
}
|
|
4836
5035
|
const remove = el("button", null, "Remove");
|
|
4837
5036
|
remove.type = "button";
|
|
4838
|
-
const
|
|
5037
|
+
const preview = el("div", "studio-tpl-preview");
|
|
5038
|
+
preview.hidden = true;
|
|
5039
|
+
const row2 = { id, mode, what, directions: boxes, wrap, preview };
|
|
5040
|
+
what.addEventListener("input", () => syncPreview(row2));
|
|
4839
5041
|
remove.onclick = () => {
|
|
4840
5042
|
loops2.splice(loops2.indexOf(row2), 1);
|
|
4841
5043
|
wrap.remove();
|
|
4842
5044
|
schedulePrice();
|
|
5045
|
+
keepDraft();
|
|
4843
5046
|
};
|
|
4844
5047
|
mode.onchange = () => {
|
|
4845
5048
|
syncLoopMode(row2);
|
|
@@ -4847,7 +5050,7 @@
|
|
|
4847
5050
|
};
|
|
4848
5051
|
const line = el("div", "row");
|
|
4849
5052
|
line.append(field("loop id", id), field("mode", mode), field("template or motion", what));
|
|
4850
|
-
wrap.append(line, dirs, remove);
|
|
5053
|
+
wrap.append(line, preview, dirs, remove);
|
|
4851
5054
|
loopList.append(wrap);
|
|
4852
5055
|
loops2.push(row2);
|
|
4853
5056
|
syncLoopMode(row2);
|
|
@@ -4985,6 +5188,7 @@
|
|
|
4985
5188
|
const keys = price ? price.items.filter((i) => ours(i.key)).map((i) => i.key) : [];
|
|
4986
5189
|
try {
|
|
4987
5190
|
S.snap = await postEdit(request());
|
|
5191
|
+
writeDraft(null);
|
|
4988
5192
|
const newId = (pid ? pid + ":" : "") + sid + "/" + base;
|
|
4989
5193
|
if (generate && keys.length) {
|
|
4990
5194
|
await postGenerate({ keys, ...pid ? { project: pid } : {} });
|
|
@@ -5078,11 +5282,84 @@
|
|
|
5078
5282
|
};
|
|
5079
5283
|
form.addEventListener("input", schedulePrice);
|
|
5080
5284
|
form.addEventListener("change", schedulePrice);
|
|
5285
|
+
const capture = () => ({
|
|
5286
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5287
|
+
project: project.value,
|
|
5288
|
+
style: style.value,
|
|
5289
|
+
styleId: styleId.value,
|
|
5290
|
+
engine: engine.value,
|
|
5291
|
+
size: size.value,
|
|
5292
|
+
directions: directions.value,
|
|
5293
|
+
body: body.value,
|
|
5294
|
+
view: view.value,
|
|
5295
|
+
outDir: outDir.value,
|
|
5296
|
+
outDirTouched,
|
|
5297
|
+
baseId: baseId.value,
|
|
5298
|
+
prompt: prompt.value,
|
|
5299
|
+
reference,
|
|
5300
|
+
loops: loops2.map((r) => ({ id: r.id.value, mode: r.mode.value, what: r.what.value, directions: [...r.directions].filter(([, b]) => b.checked).map(([d]) => d) })),
|
|
5301
|
+
portrait: portrait.checked,
|
|
5302
|
+
portraitSize: portraitSize.value
|
|
5303
|
+
});
|
|
5304
|
+
let draftTimer = null;
|
|
5305
|
+
const keepDraft = () => {
|
|
5306
|
+
if (closed) return;
|
|
5307
|
+
if (draftTimer) clearTimeout(draftTimer);
|
|
5308
|
+
draftTimer = setTimeout(() => {
|
|
5309
|
+
if (!closed) writeDraft(baseId.value.trim() || prompt.value.trim() ? capture() : null);
|
|
5310
|
+
}, 300);
|
|
5311
|
+
};
|
|
5312
|
+
form.addEventListener("input", keepDraft);
|
|
5313
|
+
form.addEventListener("change", keepDraft);
|
|
5314
|
+
addLoop.addEventListener("click", keepDraft);
|
|
5081
5315
|
fillStyles();
|
|
5316
|
+
fillTemplateList();
|
|
5082
5317
|
syncEngine();
|
|
5083
5318
|
syncStyle();
|
|
5084
|
-
|
|
5085
|
-
|
|
5319
|
+
const draft = readDraft();
|
|
5320
|
+
if (draft && (draft.baseId || draft.prompt)) {
|
|
5321
|
+
const set = (input, value) => {
|
|
5322
|
+
if (typeof value !== "string") return;
|
|
5323
|
+
if (input instanceof HTMLSelectElement && ![...input.options].some((o) => o.value === value)) return;
|
|
5324
|
+
input.value = value;
|
|
5325
|
+
};
|
|
5326
|
+
set(project, draft.project);
|
|
5327
|
+
fillStyles();
|
|
5328
|
+
set(style, draft.style);
|
|
5329
|
+
set(engine, draft.engine);
|
|
5330
|
+
syncEngine();
|
|
5331
|
+
set(styleId, draft.styleId);
|
|
5332
|
+
set(size, draft.size);
|
|
5333
|
+
set(directions, draft.directions);
|
|
5334
|
+
set(body, draft.body);
|
|
5335
|
+
set(view, draft.view);
|
|
5336
|
+
outDirTouched = Boolean(draft.outDirTouched);
|
|
5337
|
+
set(outDir, draft.outDir);
|
|
5338
|
+
set(baseId, draft.baseId);
|
|
5339
|
+
set(prompt, draft.prompt);
|
|
5340
|
+
if (typeof draft.reference === "string") {
|
|
5341
|
+
reference = draft.reference;
|
|
5342
|
+
refField.append(el("small", "state-dim studio-ref-kept", "Using " + draft.reference + ", uploaded with this draft. Choose a file to replace it."));
|
|
5343
|
+
}
|
|
5344
|
+
portrait.checked = Boolean(draft.portrait);
|
|
5345
|
+
set(portraitSize, draft.portraitSize);
|
|
5346
|
+
syncStyle();
|
|
5347
|
+
for (const l of Array.isArray(draft.loops) ? draft.loops : []) addLoopRow({ id: l.id, mode: l.mode, what: l.what, directions: l.directions });
|
|
5348
|
+
const note = el("div", "notice studio-draft");
|
|
5349
|
+
note.append(document.createTextNode("Picked up the draft you left " + (fmtWhen(draft.savedAt) || "earlier") + ". "));
|
|
5350
|
+
const discard = el("button", "linkish", "Start over");
|
|
5351
|
+
discard.type = "button";
|
|
5352
|
+
discard.onclick = () => {
|
|
5353
|
+
writeDraft(null);
|
|
5354
|
+
close();
|
|
5355
|
+
openStudio();
|
|
5356
|
+
};
|
|
5357
|
+
note.append(discard);
|
|
5358
|
+
form.prepend(note);
|
|
5359
|
+
} else {
|
|
5360
|
+
addLoopRow({ id: "walk", mode: "skeleton-v3", what: "walk", directions: ["south", "west", "north"] });
|
|
5361
|
+
addLoopRow({ id: "idle", mode: "skeleton-v3", what: "breathing-idle", directions: ["south"] });
|
|
5362
|
+
}
|
|
5086
5363
|
updatePrice();
|
|
5087
5364
|
setTimeout(() => baseId.focus(), 0);
|
|
5088
5365
|
}
|
|
@@ -5094,7 +5371,8 @@
|
|
|
5094
5371
|
keys: [
|
|
5095
5372
|
[["/"], "jump to search"],
|
|
5096
5373
|
[["?"], "this list"],
|
|
5097
|
-
[["Esc"], "close whatever is open: a sheet, the record, the View menu, or select mode"]
|
|
5374
|
+
[["Esc"], "close whatever is open: a sheet, the record, the View menu, or select mode"],
|
|
5375
|
+
[["⌘/ctrl", "Z"], "undo the gallery's last manifest edit (with --edit)"]
|
|
5098
5376
|
]
|
|
5099
5377
|
},
|
|
5100
5378
|
{
|
|
@@ -5162,8 +5440,86 @@
|
|
|
5162
5440
|
close.focus();
|
|
5163
5441
|
}
|
|
5164
5442
|
|
|
5443
|
+
// src/gallery/client/src/undo.ts
|
|
5444
|
+
var lastProject = null;
|
|
5445
|
+
var hideTimer = null;
|
|
5446
|
+
function describe(body) {
|
|
5447
|
+
switch (body.action) {
|
|
5448
|
+
case "batch":
|
|
5449
|
+
return body.edits.length + " changes to the manifest";
|
|
5450
|
+
case "add-style":
|
|
5451
|
+
return "added style " + body.styleId;
|
|
5452
|
+
case "patch-style":
|
|
5453
|
+
return "changed style " + body.styleId;
|
|
5454
|
+
case "add-asset":
|
|
5455
|
+
return "added " + body.assetId;
|
|
5456
|
+
case "patch-asset":
|
|
5457
|
+
return "changed " + body.assetId;
|
|
5458
|
+
case "set-source":
|
|
5459
|
+
case "clear-source":
|
|
5460
|
+
return "changed the source of " + body.assetId;
|
|
5461
|
+
case "create-skeleton-animation":
|
|
5462
|
+
return "added " + body.assetId;
|
|
5463
|
+
case "start-edit":
|
|
5464
|
+
return "started a hand edit of " + body.assetId;
|
|
5465
|
+
case "save-edit":
|
|
5466
|
+
return "saved a hand edit of " + body.assetId;
|
|
5467
|
+
case "detach-edit":
|
|
5468
|
+
return "detached the hand edit of " + body.assetId;
|
|
5469
|
+
default:
|
|
5470
|
+
return "the manifest changed";
|
|
5471
|
+
}
|
|
5472
|
+
}
|
|
5473
|
+
function toast(text, canUndo, tone = "") {
|
|
5474
|
+
const host = $("toast");
|
|
5475
|
+
host.textContent = "";
|
|
5476
|
+
host.className = "toast" + (tone ? " " + tone : "");
|
|
5477
|
+
host.append(el("span", null, text));
|
|
5478
|
+
if (canUndo) {
|
|
5479
|
+
const b = el("button", null, "Undo");
|
|
5480
|
+
b.type = "button";
|
|
5481
|
+
b.title = "Put the manifest back as it was (⌘Z / ctrl+Z)";
|
|
5482
|
+
b.onclick = () => void undoLast();
|
|
5483
|
+
host.append(b);
|
|
5484
|
+
}
|
|
5485
|
+
const x = el("button", "x", "×");
|
|
5486
|
+
x.type = "button";
|
|
5487
|
+
x.setAttribute("aria-label", "Dismiss");
|
|
5488
|
+
x.onclick = () => {
|
|
5489
|
+
host.hidden = true;
|
|
5490
|
+
};
|
|
5491
|
+
host.append(x);
|
|
5492
|
+
host.hidden = false;
|
|
5493
|
+
if (hideTimer) clearTimeout(hideTimer);
|
|
5494
|
+
hideTimer = setTimeout(() => {
|
|
5495
|
+
host.hidden = true;
|
|
5496
|
+
}, 12e3);
|
|
5497
|
+
}
|
|
5498
|
+
function installUndo() {
|
|
5499
|
+
editHooks.saved = (body, changed) => {
|
|
5500
|
+
if (!body || body.action === "undo-edit" || !changed) return;
|
|
5501
|
+
lastProject = body.project ?? null;
|
|
5502
|
+
toast("Saved: " + describe(body) + ".", true);
|
|
5503
|
+
};
|
|
5504
|
+
}
|
|
5505
|
+
async function undoLast() {
|
|
5506
|
+
if (!EDITABLE) return;
|
|
5507
|
+
const expected = manifestShaOf(S.snap, lastProject);
|
|
5508
|
+
if (!expected) return;
|
|
5509
|
+
const body = { action: "undo-edit", expectedSha256: expected };
|
|
5510
|
+
if (lastProject) body.project = lastProject;
|
|
5511
|
+
try {
|
|
5512
|
+
S.snap = await postEdit(body);
|
|
5513
|
+
render();
|
|
5514
|
+
toast("Undone. The manifest is back as it was before that edit.", true);
|
|
5515
|
+
} catch (err) {
|
|
5516
|
+
toast(/nothing to undo/.test(err.message) ? "Nothing left to undo." : "Could not undo: " + err.message, false, "bad");
|
|
5517
|
+
}
|
|
5518
|
+
}
|
|
5519
|
+
|
|
5165
5520
|
// src/gallery/client/src/main.ts
|
|
5166
5521
|
loadPrefs();
|
|
5522
|
+
installUndo();
|
|
5167
5523
|
$("refresh").onclick = refresh;
|
|
5168
5524
|
$("select").onclick = () => setSelecting(!ui.selecting);
|
|
5169
5525
|
$("keys").onclick = openShortcuts;
|
|
@@ -5234,6 +5590,11 @@
|
|
|
5234
5590
|
openShortcuts();
|
|
5235
5591
|
return;
|
|
5236
5592
|
}
|
|
5593
|
+
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === "z" && !typing && EDITABLE && !$("dialog-host").childNodes.length) {
|
|
5594
|
+
e.preventDefault();
|
|
5595
|
+
void undoLast();
|
|
5596
|
+
return;
|
|
5597
|
+
}
|
|
5237
5598
|
if (e.key === "Escape") {
|
|
5238
5599
|
if ($("view-menu").open) {
|
|
5239
5600
|
e.preventDefault();
|