pixelkiln 0.80.0 → 0.82.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 +131 -68
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.css +55 -1
- package/dist/client/gallery.js +405 -11
- package/dist/index.cjs +105 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +100 -37
- package/dist/index.js.map +1 -1
- package/docs/CHARACTERS.md +7 -0
- package/docs/CLI.md +20 -3
- package/package.json +1 -1
package/dist/client/gallery.css
CHANGED
|
@@ -84,7 +84,7 @@ section.project { border-bottom:1px solid var(--line-strong); background:var(--p
|
|
|
84
84
|
.card:hover { border-color:var(--line-strong); }
|
|
85
85
|
.card.active { border-color:var(--accent); box-shadow:0 0 0 2px color-mix(in srgb, var(--accent) 25%, transparent); }
|
|
86
86
|
.card.ghost { border-style:dashed; background:transparent; }
|
|
87
|
-
.cell { height:132px; display:grid; place-items:center; background-color:var(--stage-bg);
|
|
87
|
+
.cell { height:132px; flex:none; display:grid; place-items:center; background-color:var(--stage-bg);
|
|
88
88
|
background-image:var(--stage-pattern); background-size:12px 12px; background-position:0 0,6px 6px;
|
|
89
89
|
overflow:hidden; position:relative; padding:8px; }
|
|
90
90
|
.card.ghost .cell { background-image:none; background-color:var(--panel-deep); color:var(--dim); font-size:12px; }
|
|
@@ -484,3 +484,57 @@ td.col-on { background:rgba(255,107,53,.08); }
|
|
|
484
484
|
.dtabs .tablist button.on { color:var(--accent-soft); border-bottom-color:var(--accent); }
|
|
485
485
|
.dtabs .tabpanel[hidden] { display:none; }
|
|
486
486
|
.dtabs .tabpanel > section.meta:first-child { border-top:0; }
|
|
487
|
+
|
|
488
|
+
/* ---- View menu, card sizes, tag chips, shortcut sheet ---- */
|
|
489
|
+
.bar { flex-wrap:nowrap; }
|
|
490
|
+
.bar > button, .bar > .editing { white-space:nowrap; flex:none; }
|
|
491
|
+
.bar .search { flex:0 1 360px; min-width:180px; }
|
|
492
|
+
.menu { position:relative; }
|
|
493
|
+
.menu > summary { list-style:none; cursor:pointer; padding:6px 11px; border:1px solid var(--line); white-space:nowrap; }
|
|
494
|
+
.menu > summary::-webkit-details-marker { display:none; }
|
|
495
|
+
.menu > summary::after { content:' ▾'; color:var(--dim); font-size:11px; }
|
|
496
|
+
.menu[open] > summary { border-color:var(--accent); color:var(--accent-soft); }
|
|
497
|
+
.menu-panel { position:absolute; right:0; top:calc(100% + 6px); z-index:15; width:300px; display:grid; gap:10px;
|
|
498
|
+
padding:12px; background:var(--panel); border:1px solid var(--line-strong); box-shadow:0 10px 30px rgba(0,0,0,.45); }
|
|
499
|
+
.menu-row { display:grid; grid-template-columns:78px 1fr; align-items:center; gap:10px; font-size:12.5px; }
|
|
500
|
+
.menu-row > span:first-child { color:var(--dim); font:600 11.5px/1 var(--mono); }
|
|
501
|
+
.menu-row.check { display:flex; gap:8px; color:var(--text); }
|
|
502
|
+
.menu-row[hidden] { display:none; }
|
|
503
|
+
.menu-row select { width:100%; }
|
|
504
|
+
.seg { display:inline-flex; }
|
|
505
|
+
.seg button { padding:4px 12px; font:600 12px/1 var(--mono); }
|
|
506
|
+
.seg button + button { border-left:0; }
|
|
507
|
+
.seg button.on { background:var(--panel-deep); color:var(--accent-soft); border-color:var(--accent); }
|
|
508
|
+
#keys { width:32px; padding:6px 0; font:700 13px/1 var(--mono); }
|
|
509
|
+
body[data-size="s"] .grid { grid-template-columns:repeat(auto-fill, minmax(132px, 1fr)); gap:8px; }
|
|
510
|
+
body[data-size="s"] .cell { height:96px; }
|
|
511
|
+
/* A card not yet drawn holds this size (content-visibility), so it has to match the card size. */
|
|
512
|
+
body[data-size="s"] .card { contain-intrinsic-size:auto 132px auto 160px; }
|
|
513
|
+
body[data-size="l"] .card { contain-intrinsic-size:auto 260px auto 275px; }
|
|
514
|
+
body[data-size="s"] .cbody { padding:6px 8px; }
|
|
515
|
+
body[data-size="l"] .grid { grid-template-columns:repeat(auto-fill, minmax(260px, 1fr)); }
|
|
516
|
+
body[data-size="l"] .cell { height:210px; }
|
|
517
|
+
.chip.tag { font-family:var(--mono); }
|
|
518
|
+
.keys-sheet { background:var(--panel); border:1px solid var(--accent); width:min(620px, calc(100vw - 32px)); max-height:calc(100vh - 64px);
|
|
519
|
+
overflow-y:auto; padding:16px 18px; display:grid; gap:14px; }
|
|
520
|
+
.keys-head { display:flex; align-items:center; justify-content:space-between; }
|
|
521
|
+
.keys-sheet h3 { margin:0; font:650 14px/1.2 var(--mono); }
|
|
522
|
+
.keys-sheet h4 { margin:0 0 6px; font:650 12px/1 var(--mono); color:var(--accent-soft); }
|
|
523
|
+
.keys-sheet dl { display:grid; grid-template-columns:150px 1fr; gap:6px 16px; margin:0; font-size:13px; }
|
|
524
|
+
.keys-sheet dt { white-space:nowrap; }
|
|
525
|
+
.keys-sheet dd { margin:0; color:var(--dim); }
|
|
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
|
+
@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
|
@@ -47,16 +47,26 @@
|
|
|
47
47
|
onion: false,
|
|
48
48
|
selecting: false,
|
|
49
49
|
selected: /* @__PURE__ */ new Set(),
|
|
50
|
-
drawerTab: "overview"
|
|
50
|
+
drawerTab: "overview",
|
|
51
|
+
tags: /* @__PURE__ */ new Set(),
|
|
52
|
+
size: "m"
|
|
51
53
|
};
|
|
54
|
+
var CARD_SIZES = {
|
|
55
|
+
s: { box: [112, 80], tile: [48, 36] },
|
|
56
|
+
m: { box: [156, 116], tile: [66, 52] },
|
|
57
|
+
l: { box: [240, 190], tile: [108, 88] }
|
|
58
|
+
};
|
|
59
|
+
var declaredTags = (item) => item.asset && Array.isArray(item.asset.tags) ? item.asset.tags : [];
|
|
52
60
|
var PREFS_KEY = "pixelkiln.gallery.prefs";
|
|
53
|
-
var PREF_NAMES = ["playLoops", "backdrop", "notify", "onion", "drawerTab"];
|
|
61
|
+
var PREF_NAMES = ["playLoops", "backdrop", "notify", "onion", "drawerTab", "size"];
|
|
54
62
|
function loadPrefs() {
|
|
55
63
|
try {
|
|
56
64
|
const saved = JSON.parse(localStorage.getItem(PREFS_KEY) || "{}");
|
|
57
65
|
for (const name of PREF_NAMES) if (typeof saved[name] === typeof ui[name]) ui[name] = saved[name];
|
|
58
66
|
} catch {
|
|
59
67
|
}
|
|
68
|
+
if (!CARD_SIZES[ui.size]) ui.size = "m";
|
|
69
|
+
document.body.dataset.size = ui.size;
|
|
60
70
|
applyBackdrop();
|
|
61
71
|
}
|
|
62
72
|
function savePrefs() {
|
|
@@ -140,7 +150,15 @@
|
|
|
140
150
|
return s >= 1 ? Math.floor(s) : s;
|
|
141
151
|
};
|
|
142
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;
|
|
143
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) {
|
|
144
162
|
const res = await fetch("/api/edit", {
|
|
145
163
|
method: "POST",
|
|
146
164
|
headers: { "Content-Type": "application/json", "X-Pixelkiln-Session": SESSION ?? "" },
|
|
@@ -1197,6 +1215,7 @@
|
|
|
1197
1215
|
if (ui.providers.size && !ui.providers.has(item.provider)) return false;
|
|
1198
1216
|
if (ui.generators.size && !ui.generators.has(item.generator)) return false;
|
|
1199
1217
|
if (ui.projects.size && !ui.projects.has(item.project ?? "")) return false;
|
|
1218
|
+
if (ui.tags.size && !declaredTags(item).some((t) => ui.tags.has(t))) return false;
|
|
1200
1219
|
if (!q) return true;
|
|
1201
1220
|
const hay = [
|
|
1202
1221
|
item.id,
|
|
@@ -1304,7 +1323,19 @@
|
|
|
1304
1323
|
chips.append(el("span", "sep"));
|
|
1305
1324
|
for (const [k, v] of [...byGen].sort()) chips.append(chip(k, v, ui.generators, k));
|
|
1306
1325
|
}
|
|
1307
|
-
|
|
1326
|
+
const byTag = /* @__PURE__ */ new Map();
|
|
1327
|
+
for (const item of S.snap.items) for (const t2 of declaredTags(item)) byTag.set(t2, (byTag.get(t2) || 0) + 1);
|
|
1328
|
+
for (const t2 of ui.tags) if (!byTag.has(t2)) byTag.set(t2, 0);
|
|
1329
|
+
if (byTag.size) {
|
|
1330
|
+
chips.append(el("span", "sep"));
|
|
1331
|
+
for (const [k, v] of [...byTag].sort()) {
|
|
1332
|
+
const c = chip("#" + k, v, ui.tags, k);
|
|
1333
|
+
c.classList.add("tag");
|
|
1334
|
+
c.title = "Assets tagged “" + k + "” in the manifest";
|
|
1335
|
+
chips.append(c);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (ui.states.size || ui.providers.size || ui.generators.size || ui.projects.size || ui.tags.size || ui.q) {
|
|
1308
1339
|
const clear = el("button", "chip", "clear filters");
|
|
1309
1340
|
clear.type = "button";
|
|
1310
1341
|
clear.onclick = () => clearFilters();
|
|
@@ -1316,6 +1347,7 @@
|
|
|
1316
1347
|
ui.providers.clear();
|
|
1317
1348
|
ui.generators.clear();
|
|
1318
1349
|
ui.projects.clear();
|
|
1350
|
+
ui.tags.clear();
|
|
1319
1351
|
ui.q = "";
|
|
1320
1352
|
$("q").value = "";
|
|
1321
1353
|
render();
|
|
@@ -1337,7 +1369,7 @@
|
|
|
1337
1369
|
img2.src = o.url;
|
|
1338
1370
|
img2.alt = o.role || item.assetId;
|
|
1339
1371
|
img2.loading = "lazy";
|
|
1340
|
-
const s2 = displayScale(w, h,
|
|
1372
|
+
const s2 = displayScale(w, h, ...CARD_SIZES[ui.size].tile);
|
|
1341
1373
|
if (s2 >= 1) {
|
|
1342
1374
|
img2.width = w * s2;
|
|
1343
1375
|
img2.height = h * s2;
|
|
@@ -1352,7 +1384,7 @@
|
|
|
1352
1384
|
img.src = shown[0].url;
|
|
1353
1385
|
img.alt = item.assetId;
|
|
1354
1386
|
img.loading = "lazy";
|
|
1355
|
-
const s = displayScale(w, h,
|
|
1387
|
+
const s = displayScale(w, h, ...CARD_SIZES[ui.size].box);
|
|
1356
1388
|
if (s >= 1) {
|
|
1357
1389
|
img.width = w * s;
|
|
1358
1390
|
img.height = h * s;
|
|
@@ -4489,6 +4521,7 @@
|
|
|
4489
4521
|
ui.states = new Set(list("state"));
|
|
4490
4522
|
ui.providers = new Set(list("provider"));
|
|
4491
4523
|
ui.generators = new Set(list("generator"));
|
|
4524
|
+
ui.tags = new Set(list("tag"));
|
|
4492
4525
|
ui.projects = new Set(list("project"));
|
|
4493
4526
|
ui.compare = list("compare").slice(0, 4);
|
|
4494
4527
|
if (["key", "newest", "oldest", "cost", "size"].includes(params.get("sort") ?? "")) ui.sort = params.get("sort");
|
|
@@ -4500,6 +4533,7 @@
|
|
|
4500
4533
|
if (ui.states.size) params.set("state", [...ui.states].join(","));
|
|
4501
4534
|
if (ui.providers.size) params.set("provider", [...ui.providers].join(","));
|
|
4502
4535
|
if (ui.generators.size) params.set("generator", [...ui.generators].join(","));
|
|
4536
|
+
if (ui.tags.size) params.set("tag", [...ui.tags].join(","));
|
|
4503
4537
|
if (ui.projects.size) params.set("project", [...ui.projects].join(","));
|
|
4504
4538
|
if (ui.compare.length) params.set("compare", ui.compare.join(","));
|
|
4505
4539
|
if (ui.sort !== "key") params.set("sort", ui.sort);
|
|
@@ -4619,6 +4653,48 @@
|
|
|
4619
4653
|
{ id: "pro", label: "pro: described in words, sequential (20-40 per direction)" }
|
|
4620
4654
|
];
|
|
4621
4655
|
var templated = (mode) => mode === "skeleton-v3" || mode === "template";
|
|
4656
|
+
var KNOWN_TEMPLATES = [
|
|
4657
|
+
"walk",
|
|
4658
|
+
"walking-8-frames",
|
|
4659
|
+
"running-8-frames",
|
|
4660
|
+
"breathing-idle",
|
|
4661
|
+
"crouched-walking",
|
|
4662
|
+
"attack",
|
|
4663
|
+
"attack-back",
|
|
4664
|
+
"attack-left",
|
|
4665
|
+
"attack-right",
|
|
4666
|
+
"cross-punch",
|
|
4667
|
+
"backflip",
|
|
4668
|
+
"angry",
|
|
4669
|
+
"bark"
|
|
4670
|
+
];
|
|
4671
|
+
var draftKey = () => "pixelkiln.studio.draft:" + (S.snap.workspace ? S.snap.workspace.path : S.snap.project ? S.snap.project.manifest : "");
|
|
4672
|
+
function readDraft() {
|
|
4673
|
+
try {
|
|
4674
|
+
const d = JSON.parse(localStorage.getItem(draftKey()) || "null");
|
|
4675
|
+
return d && typeof d === "object" ? d : null;
|
|
4676
|
+
} catch {
|
|
4677
|
+
return null;
|
|
4678
|
+
}
|
|
4679
|
+
}
|
|
4680
|
+
function writeDraft(draft) {
|
|
4681
|
+
try {
|
|
4682
|
+
if (draft) localStorage.setItem(draftKey(), JSON.stringify(draft));
|
|
4683
|
+
else localStorage.removeItem(draftKey());
|
|
4684
|
+
} catch {
|
|
4685
|
+
}
|
|
4686
|
+
}
|
|
4687
|
+
function usedTemplates(projectId) {
|
|
4688
|
+
const out = /* @__PURE__ */ new Map();
|
|
4689
|
+
for (const i of S.snap.items) {
|
|
4690
|
+
const t = i.asset && i.asset.animation && i.asset.animation.template;
|
|
4691
|
+
if (!t || i.project !== projectId) continue;
|
|
4692
|
+
const frames = i.outputs.filter((o) => o.url);
|
|
4693
|
+
if (frames.length > 1 && !out.get(t)) out.set(t, i);
|
|
4694
|
+
else if (!out.has(t)) out.set(t, null);
|
|
4695
|
+
}
|
|
4696
|
+
return out;
|
|
4697
|
+
}
|
|
4622
4698
|
var idPattern = "[^\\/\\\\:]+";
|
|
4623
4699
|
function openStudio() {
|
|
4624
4700
|
const host = $("dialog-host");
|
|
@@ -4631,10 +4707,12 @@
|
|
|
4631
4707
|
const scrim = el("div", "sheet-scrim");
|
|
4632
4708
|
const panel = el("div", "sheet studio");
|
|
4633
4709
|
let closed = false;
|
|
4710
|
+
const previewTimers = /* @__PURE__ */ new Set();
|
|
4634
4711
|
const close = () => {
|
|
4635
4712
|
closed = true;
|
|
4636
4713
|
if (priceTimer) clearTimeout(priceTimer);
|
|
4637
4714
|
priceSeq++;
|
|
4715
|
+
for (const t of previewTimers) clearInterval(t);
|
|
4638
4716
|
host.textContent = "";
|
|
4639
4717
|
};
|
|
4640
4718
|
const bar = el("div", "rbar");
|
|
@@ -4708,7 +4786,9 @@
|
|
|
4708
4786
|
const loopList = el("div", "studio-loops");
|
|
4709
4787
|
const addLoop = el("button", "add", "+ Add a loop");
|
|
4710
4788
|
addLoop.type = "button";
|
|
4711
|
-
|
|
4789
|
+
const templateList = el("datalist");
|
|
4790
|
+
templateList.id = "studio-templates";
|
|
4791
|
+
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);
|
|
4712
4792
|
const portrait = el("input");
|
|
4713
4793
|
portrait.type = "checkbox";
|
|
4714
4794
|
const portraitSize = el("select");
|
|
@@ -4779,7 +4859,54 @@
|
|
|
4779
4859
|
function syncLoopMode(row2) {
|
|
4780
4860
|
const t = templated(row2.mode.value);
|
|
4781
4861
|
row2.what.placeholder = t ? "template, e.g. walk, breathing-idle, running-8-frames" : 'the motion, e.g. "swings a sword overhead"';
|
|
4782
|
-
row2.what.title = t ? "A PixelLab template id" : "What the loop does";
|
|
4862
|
+
row2.what.title = t ? "A PixelLab template id: pick one, or type any id PixelLab has" : "What the loop does";
|
|
4863
|
+
if (t) row2.what.setAttribute("list", templateList.id);
|
|
4864
|
+
else row2.what.removeAttribute("list");
|
|
4865
|
+
syncPreview(row2);
|
|
4866
|
+
}
|
|
4867
|
+
function syncPreview(row2) {
|
|
4868
|
+
const old = row2.preview.__timer;
|
|
4869
|
+
if (old) {
|
|
4870
|
+
clearInterval(old);
|
|
4871
|
+
previewTimers.delete(old);
|
|
4872
|
+
}
|
|
4873
|
+
row2.preview.textContent = "";
|
|
4874
|
+
const name = row2.what.value.trim();
|
|
4875
|
+
const sample = templated(row2.mode.value) && name ? usedTemplates(projectId()).get(name) : null;
|
|
4876
|
+
if (!sample) {
|
|
4877
|
+
row2.preview.hidden = true;
|
|
4878
|
+
return;
|
|
4879
|
+
}
|
|
4880
|
+
const urls = sample.outputs.filter((o) => o.url).map((o) => o.url);
|
|
4881
|
+
const img = el("img");
|
|
4882
|
+
img.src = urls[0];
|
|
4883
|
+
img.alt = name;
|
|
4884
|
+
let i = 0;
|
|
4885
|
+
const timer = setInterval(() => {
|
|
4886
|
+
if (!img.isConnected) {
|
|
4887
|
+
clearInterval(timer);
|
|
4888
|
+
previewTimers.delete(timer);
|
|
4889
|
+
return;
|
|
4890
|
+
}
|
|
4891
|
+
if (!document.hidden) {
|
|
4892
|
+
i = (i + 1) % urls.length;
|
|
4893
|
+
img.src = urls[i];
|
|
4894
|
+
}
|
|
4895
|
+
}, Math.round(1e3 / (sample.fps || 12)));
|
|
4896
|
+
row2.preview.__timer = timer;
|
|
4897
|
+
previewTimers.add(timer);
|
|
4898
|
+
row2.preview.append(img, el("small", "state-dim", name + ", as drawn for " + sample.assetId));
|
|
4899
|
+
row2.preview.hidden = false;
|
|
4900
|
+
}
|
|
4901
|
+
function fillTemplateList() {
|
|
4902
|
+
templateList.textContent = "";
|
|
4903
|
+
const used = usedTemplates(projectId());
|
|
4904
|
+
for (const t of [.../* @__PURE__ */ new Set([...used.keys(), ...KNOWN_TEMPLATES])]) {
|
|
4905
|
+
const o = el("option");
|
|
4906
|
+
o.value = t;
|
|
4907
|
+
if (used.has(t)) o.label = "used in this project";
|
|
4908
|
+
templateList.append(o);
|
|
4909
|
+
}
|
|
4783
4910
|
}
|
|
4784
4911
|
function addLoopRow(defaults = {}) {
|
|
4785
4912
|
const wrap = el("div", "studio-loop");
|
|
@@ -4809,11 +4936,15 @@
|
|
|
4809
4936
|
}
|
|
4810
4937
|
const remove = el("button", null, "Remove");
|
|
4811
4938
|
remove.type = "button";
|
|
4812
|
-
const
|
|
4939
|
+
const preview = el("div", "studio-tpl-preview");
|
|
4940
|
+
preview.hidden = true;
|
|
4941
|
+
const row2 = { id, mode, what, directions: boxes, wrap, preview };
|
|
4942
|
+
what.addEventListener("input", () => syncPreview(row2));
|
|
4813
4943
|
remove.onclick = () => {
|
|
4814
4944
|
loops2.splice(loops2.indexOf(row2), 1);
|
|
4815
4945
|
wrap.remove();
|
|
4816
4946
|
schedulePrice();
|
|
4947
|
+
keepDraft();
|
|
4817
4948
|
};
|
|
4818
4949
|
mode.onchange = () => {
|
|
4819
4950
|
syncLoopMode(row2);
|
|
@@ -4821,7 +4952,7 @@
|
|
|
4821
4952
|
};
|
|
4822
4953
|
const line = el("div", "row");
|
|
4823
4954
|
line.append(field("loop id", id), field("mode", mode), field("template or motion", what));
|
|
4824
|
-
wrap.append(line, dirs, remove);
|
|
4955
|
+
wrap.append(line, preview, dirs, remove);
|
|
4825
4956
|
loopList.append(wrap);
|
|
4826
4957
|
loops2.push(row2);
|
|
4827
4958
|
syncLoopMode(row2);
|
|
@@ -4959,6 +5090,7 @@
|
|
|
4959
5090
|
const keys = price ? price.items.filter((i) => ours(i.key)).map((i) => i.key) : [];
|
|
4960
5091
|
try {
|
|
4961
5092
|
S.snap = await postEdit(request());
|
|
5093
|
+
writeDraft(null);
|
|
4962
5094
|
const newId = (pid ? pid + ":" : "") + sid + "/" + base;
|
|
4963
5095
|
if (generate && keys.length) {
|
|
4964
5096
|
await postGenerate({ keys, ...pid ? { project: pid } : {} });
|
|
@@ -5052,19 +5184,265 @@
|
|
|
5052
5184
|
};
|
|
5053
5185
|
form.addEventListener("input", schedulePrice);
|
|
5054
5186
|
form.addEventListener("change", schedulePrice);
|
|
5187
|
+
const capture = () => ({
|
|
5188
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5189
|
+
project: project.value,
|
|
5190
|
+
style: style.value,
|
|
5191
|
+
styleId: styleId.value,
|
|
5192
|
+
engine: engine.value,
|
|
5193
|
+
size: size.value,
|
|
5194
|
+
directions: directions.value,
|
|
5195
|
+
body: body.value,
|
|
5196
|
+
view: view.value,
|
|
5197
|
+
outDir: outDir.value,
|
|
5198
|
+
outDirTouched,
|
|
5199
|
+
baseId: baseId.value,
|
|
5200
|
+
prompt: prompt.value,
|
|
5201
|
+
reference,
|
|
5202
|
+
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) })),
|
|
5203
|
+
portrait: portrait.checked,
|
|
5204
|
+
portraitSize: portraitSize.value
|
|
5205
|
+
});
|
|
5206
|
+
let draftTimer = null;
|
|
5207
|
+
const keepDraft = () => {
|
|
5208
|
+
if (closed) return;
|
|
5209
|
+
if (draftTimer) clearTimeout(draftTimer);
|
|
5210
|
+
draftTimer = setTimeout(() => {
|
|
5211
|
+
if (!closed) writeDraft(baseId.value.trim() || prompt.value.trim() ? capture() : null);
|
|
5212
|
+
}, 300);
|
|
5213
|
+
};
|
|
5214
|
+
form.addEventListener("input", keepDraft);
|
|
5215
|
+
form.addEventListener("change", keepDraft);
|
|
5216
|
+
addLoop.addEventListener("click", keepDraft);
|
|
5055
5217
|
fillStyles();
|
|
5218
|
+
fillTemplateList();
|
|
5056
5219
|
syncEngine();
|
|
5057
5220
|
syncStyle();
|
|
5058
|
-
|
|
5059
|
-
|
|
5221
|
+
const draft = readDraft();
|
|
5222
|
+
if (draft && (draft.baseId || draft.prompt)) {
|
|
5223
|
+
const set = (input, value) => {
|
|
5224
|
+
if (typeof value !== "string") return;
|
|
5225
|
+
if (input instanceof HTMLSelectElement && ![...input.options].some((o) => o.value === value)) return;
|
|
5226
|
+
input.value = value;
|
|
5227
|
+
};
|
|
5228
|
+
set(project, draft.project);
|
|
5229
|
+
fillStyles();
|
|
5230
|
+
set(style, draft.style);
|
|
5231
|
+
set(engine, draft.engine);
|
|
5232
|
+
syncEngine();
|
|
5233
|
+
set(styleId, draft.styleId);
|
|
5234
|
+
set(size, draft.size);
|
|
5235
|
+
set(directions, draft.directions);
|
|
5236
|
+
set(body, draft.body);
|
|
5237
|
+
set(view, draft.view);
|
|
5238
|
+
outDirTouched = Boolean(draft.outDirTouched);
|
|
5239
|
+
set(outDir, draft.outDir);
|
|
5240
|
+
set(baseId, draft.baseId);
|
|
5241
|
+
set(prompt, draft.prompt);
|
|
5242
|
+
if (typeof draft.reference === "string") {
|
|
5243
|
+
reference = draft.reference;
|
|
5244
|
+
refField.append(el("small", "state-dim studio-ref-kept", "Using " + draft.reference + ", uploaded with this draft. Choose a file to replace it."));
|
|
5245
|
+
}
|
|
5246
|
+
portrait.checked = Boolean(draft.portrait);
|
|
5247
|
+
set(portraitSize, draft.portraitSize);
|
|
5248
|
+
syncStyle();
|
|
5249
|
+
for (const l of Array.isArray(draft.loops) ? draft.loops : []) addLoopRow({ id: l.id, mode: l.mode, what: l.what, directions: l.directions });
|
|
5250
|
+
const note = el("div", "notice studio-draft");
|
|
5251
|
+
note.append(document.createTextNode("Picked up the draft you left " + (fmtWhen(draft.savedAt) || "earlier") + ". "));
|
|
5252
|
+
const discard = el("button", "linkish", "Start over");
|
|
5253
|
+
discard.type = "button";
|
|
5254
|
+
discard.onclick = () => {
|
|
5255
|
+
writeDraft(null);
|
|
5256
|
+
close();
|
|
5257
|
+
openStudio();
|
|
5258
|
+
};
|
|
5259
|
+
note.append(discard);
|
|
5260
|
+
form.prepend(note);
|
|
5261
|
+
} else {
|
|
5262
|
+
addLoopRow({ id: "walk", mode: "skeleton-v3", what: "walk", directions: ["south", "west", "north"] });
|
|
5263
|
+
addLoopRow({ id: "idle", mode: "skeleton-v3", what: "breathing-idle", directions: ["south"] });
|
|
5264
|
+
}
|
|
5060
5265
|
updatePrice();
|
|
5061
5266
|
setTimeout(() => baseId.focus(), 0);
|
|
5062
5267
|
}
|
|
5063
5268
|
|
|
5269
|
+
// src/gallery/client/src/keys.ts
|
|
5270
|
+
var GROUPS = [
|
|
5271
|
+
{
|
|
5272
|
+
title: "Anywhere",
|
|
5273
|
+
keys: [
|
|
5274
|
+
[["/"], "jump to search"],
|
|
5275
|
+
[["?"], "this list"],
|
|
5276
|
+
[["Esc"], "close whatever is open: a sheet, the record, the View menu, or select mode"],
|
|
5277
|
+
[["⌘/ctrl", "Z"], "undo the gallery's last manifest edit (with --edit)"]
|
|
5278
|
+
]
|
|
5279
|
+
},
|
|
5280
|
+
{
|
|
5281
|
+
title: "On the grid",
|
|
5282
|
+
keys: [
|
|
5283
|
+
[["click"], "open a record"],
|
|
5284
|
+
[["shift", "click"], "add it to a side-by-side comparison (up to four)"],
|
|
5285
|
+
[["ctrl/⌘", "click"], "select it; the bar at the bottom acts on the selection"],
|
|
5286
|
+
[["hover"], "play a loop"]
|
|
5287
|
+
]
|
|
5288
|
+
},
|
|
5289
|
+
{
|
|
5290
|
+
title: "With a record open",
|
|
5291
|
+
keys: [
|
|
5292
|
+
[["←", "→"], "step through the records in view"],
|
|
5293
|
+
[[",", "."], "step a loop one frame back or on"],
|
|
5294
|
+
[["space"], "play or pause a loop"],
|
|
5295
|
+
[["←", "→"], "switch tabs, once a tab has focus"]
|
|
5296
|
+
]
|
|
5297
|
+
},
|
|
5298
|
+
{
|
|
5299
|
+
title: "In the family view",
|
|
5300
|
+
keys: [
|
|
5301
|
+
[["drag"], "turn the sprite on the turntable"],
|
|
5302
|
+
[["←", "→"], "turn it a step, once the turntable has focus"]
|
|
5303
|
+
]
|
|
5304
|
+
}
|
|
5305
|
+
];
|
|
5306
|
+
function openShortcuts() {
|
|
5307
|
+
const host = $("dialog-host");
|
|
5308
|
+
host.textContent = "";
|
|
5309
|
+
const wrap = el("div", "dialog");
|
|
5310
|
+
wrap.onclick = (e) => {
|
|
5311
|
+
if (e.target === wrap) host.textContent = "";
|
|
5312
|
+
};
|
|
5313
|
+
const box = el("div", "keys-sheet");
|
|
5314
|
+
box.setAttribute("role", "dialog");
|
|
5315
|
+
box.setAttribute("aria-label", "Keyboard shortcuts");
|
|
5316
|
+
const head = el("div", "keys-head");
|
|
5317
|
+
head.append(el("h3", null, "Keyboard shortcuts"));
|
|
5318
|
+
const close = el("button", null, "Close");
|
|
5319
|
+
close.type = "button";
|
|
5320
|
+
close.onclick = () => {
|
|
5321
|
+
host.textContent = "";
|
|
5322
|
+
};
|
|
5323
|
+
head.append(close);
|
|
5324
|
+
box.append(head);
|
|
5325
|
+
for (const group of GROUPS) {
|
|
5326
|
+
const section2 = el("section");
|
|
5327
|
+
section2.append(el("h4", null, group.title));
|
|
5328
|
+
const dl = el("dl");
|
|
5329
|
+
for (const [keys, what] of group.keys) {
|
|
5330
|
+
const dt = el("dt");
|
|
5331
|
+
keys.forEach((k, i) => {
|
|
5332
|
+
if (i) dt.append(document.createTextNode(k === "click" ? "+" : " "));
|
|
5333
|
+
dt.append(el("kbd", null, k));
|
|
5334
|
+
});
|
|
5335
|
+
dl.append(dt, el("dd", null, what));
|
|
5336
|
+
}
|
|
5337
|
+
section2.append(dl);
|
|
5338
|
+
box.append(section2);
|
|
5339
|
+
}
|
|
5340
|
+
wrap.append(box);
|
|
5341
|
+
host.append(wrap);
|
|
5342
|
+
close.focus();
|
|
5343
|
+
}
|
|
5344
|
+
|
|
5345
|
+
// src/gallery/client/src/undo.ts
|
|
5346
|
+
var lastProject = null;
|
|
5347
|
+
var hideTimer = null;
|
|
5348
|
+
function describe(body) {
|
|
5349
|
+
switch (body.action) {
|
|
5350
|
+
case "batch":
|
|
5351
|
+
return body.edits.length + " changes to the manifest";
|
|
5352
|
+
case "add-style":
|
|
5353
|
+
return "added style " + body.styleId;
|
|
5354
|
+
case "patch-style":
|
|
5355
|
+
return "changed style " + body.styleId;
|
|
5356
|
+
case "add-asset":
|
|
5357
|
+
return "added " + body.assetId;
|
|
5358
|
+
case "patch-asset":
|
|
5359
|
+
return "changed " + body.assetId;
|
|
5360
|
+
case "set-source":
|
|
5361
|
+
case "clear-source":
|
|
5362
|
+
return "changed the source of " + body.assetId;
|
|
5363
|
+
case "create-skeleton-animation":
|
|
5364
|
+
return "added " + body.assetId;
|
|
5365
|
+
case "start-edit":
|
|
5366
|
+
return "started a hand edit of " + body.assetId;
|
|
5367
|
+
case "save-edit":
|
|
5368
|
+
return "saved a hand edit of " + body.assetId;
|
|
5369
|
+
case "detach-edit":
|
|
5370
|
+
return "detached the hand edit of " + body.assetId;
|
|
5371
|
+
default:
|
|
5372
|
+
return "the manifest changed";
|
|
5373
|
+
}
|
|
5374
|
+
}
|
|
5375
|
+
function toast(text, canUndo, tone = "") {
|
|
5376
|
+
const host = $("toast");
|
|
5377
|
+
host.textContent = "";
|
|
5378
|
+
host.className = "toast" + (tone ? " " + tone : "");
|
|
5379
|
+
host.append(el("span", null, text));
|
|
5380
|
+
if (canUndo) {
|
|
5381
|
+
const b = el("button", null, "Undo");
|
|
5382
|
+
b.type = "button";
|
|
5383
|
+
b.title = "Put the manifest back as it was (⌘Z / ctrl+Z)";
|
|
5384
|
+
b.onclick = () => void undoLast();
|
|
5385
|
+
host.append(b);
|
|
5386
|
+
}
|
|
5387
|
+
const x = el("button", "x", "×");
|
|
5388
|
+
x.type = "button";
|
|
5389
|
+
x.setAttribute("aria-label", "Dismiss");
|
|
5390
|
+
x.onclick = () => {
|
|
5391
|
+
host.hidden = true;
|
|
5392
|
+
};
|
|
5393
|
+
host.append(x);
|
|
5394
|
+
host.hidden = false;
|
|
5395
|
+
if (hideTimer) clearTimeout(hideTimer);
|
|
5396
|
+
hideTimer = setTimeout(() => {
|
|
5397
|
+
host.hidden = true;
|
|
5398
|
+
}, 12e3);
|
|
5399
|
+
}
|
|
5400
|
+
function installUndo() {
|
|
5401
|
+
editHooks.saved = (body, changed) => {
|
|
5402
|
+
if (!body || body.action === "undo-edit" || !changed) return;
|
|
5403
|
+
lastProject = body.project ?? null;
|
|
5404
|
+
toast("Saved: " + describe(body) + ".", true);
|
|
5405
|
+
};
|
|
5406
|
+
}
|
|
5407
|
+
async function undoLast() {
|
|
5408
|
+
if (!EDITABLE) return;
|
|
5409
|
+
const expected = manifestShaOf(S.snap, lastProject);
|
|
5410
|
+
if (!expected) return;
|
|
5411
|
+
const body = { action: "undo-edit", expectedSha256: expected };
|
|
5412
|
+
if (lastProject) body.project = lastProject;
|
|
5413
|
+
try {
|
|
5414
|
+
S.snap = await postEdit(body);
|
|
5415
|
+
render();
|
|
5416
|
+
toast("Undone. The manifest is back as it was before that edit.", true);
|
|
5417
|
+
} catch (err) {
|
|
5418
|
+
toast(/nothing to undo/.test(err.message) ? "Nothing left to undo." : "Could not undo: " + err.message, false, "bad");
|
|
5419
|
+
}
|
|
5420
|
+
}
|
|
5421
|
+
|
|
5064
5422
|
// src/gallery/client/src/main.ts
|
|
5065
5423
|
loadPrefs();
|
|
5424
|
+
installUndo();
|
|
5066
5425
|
$("refresh").onclick = refresh;
|
|
5067
5426
|
$("select").onclick = () => setSelecting(!ui.selecting);
|
|
5427
|
+
$("keys").onclick = openShortcuts;
|
|
5428
|
+
var syncSize = () => {
|
|
5429
|
+
for (const b of $("size").querySelectorAll("button")) b.classList.toggle("on", b.dataset.size === ui.size);
|
|
5430
|
+
};
|
|
5431
|
+
for (const b of $("size").querySelectorAll("button")) {
|
|
5432
|
+
b.onclick = () => {
|
|
5433
|
+
if (!CARD_SIZES[b.dataset.size]) return;
|
|
5434
|
+
ui.size = b.dataset.size;
|
|
5435
|
+
document.body.dataset.size = ui.size;
|
|
5436
|
+
savePrefs();
|
|
5437
|
+
syncSize();
|
|
5438
|
+
render();
|
|
5439
|
+
};
|
|
5440
|
+
}
|
|
5441
|
+
syncSize();
|
|
5442
|
+
document.addEventListener("click", (e) => {
|
|
5443
|
+
const menu = $("view-menu");
|
|
5444
|
+
if (menu.open && !menu.contains(e.target)) menu.open = false;
|
|
5445
|
+
});
|
|
5068
5446
|
$("playloops").checked = ui.playLoops;
|
|
5069
5447
|
$("playloops").onchange = (e) => {
|
|
5070
5448
|
setPlayLoops(e.target.checked);
|
|
@@ -5109,7 +5487,23 @@
|
|
|
5109
5487
|
$("q").focus();
|
|
5110
5488
|
return;
|
|
5111
5489
|
}
|
|
5490
|
+
if (e.key === "?" && !typing) {
|
|
5491
|
+
e.preventDefault();
|
|
5492
|
+
openShortcuts();
|
|
5493
|
+
return;
|
|
5494
|
+
}
|
|
5495
|
+
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === "z" && !typing && EDITABLE && !$("dialog-host").childNodes.length) {
|
|
5496
|
+
e.preventDefault();
|
|
5497
|
+
void undoLast();
|
|
5498
|
+
return;
|
|
5499
|
+
}
|
|
5112
5500
|
if (e.key === "Escape") {
|
|
5501
|
+
if ($("view-menu").open) {
|
|
5502
|
+
e.preventDefault();
|
|
5503
|
+
$("view-menu").open = false;
|
|
5504
|
+
$("view-menu").querySelector("summary").focus();
|
|
5505
|
+
return;
|
|
5506
|
+
}
|
|
5113
5507
|
if (typing && document.activeElement?.id === "q") {
|
|
5114
5508
|
document.activeElement.blur();
|
|
5115
5509
|
return;
|