pixelkiln 0.76.0 → 0.78.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 +9 -1
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.css +73 -15
- package/dist/client/gallery.js +647 -65
- package/dist/index.cjs +9 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/docs/CLI.md +41 -0
- package/package.json +1 -1
package/dist/client/gallery.js
CHANGED
|
@@ -39,8 +39,74 @@
|
|
|
39
39
|
compare: [],
|
|
40
40
|
compareZoom: "auto",
|
|
41
41
|
logs: /* @__PURE__ */ new Set(),
|
|
42
|
-
settled: /* @__PURE__ */ new Set()
|
|
42
|
+
settled: /* @__PURE__ */ new Set(),
|
|
43
|
+
playLoops: false,
|
|
44
|
+
backdrop: "checker",
|
|
45
|
+
notify: false,
|
|
46
|
+
fps: null,
|
|
47
|
+
onion: false,
|
|
48
|
+
selecting: false,
|
|
49
|
+
selected: /* @__PURE__ */ new Set()
|
|
43
50
|
};
|
|
51
|
+
var PREFS_KEY = "pixelkiln.gallery.prefs";
|
|
52
|
+
var PREF_NAMES = ["playLoops", "backdrop", "notify", "onion"];
|
|
53
|
+
function loadPrefs() {
|
|
54
|
+
try {
|
|
55
|
+
const saved = JSON.parse(localStorage.getItem(PREFS_KEY) || "{}");
|
|
56
|
+
for (const name of PREF_NAMES) if (typeof saved[name] === typeof ui[name]) ui[name] = saved[name];
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
applyBackdrop();
|
|
60
|
+
}
|
|
61
|
+
function savePrefs() {
|
|
62
|
+
try {
|
|
63
|
+
localStorage.setItem(PREFS_KEY, JSON.stringify(Object.fromEntries(PREF_NAMES.map((n) => [n, ui[n]]))));
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
var HEX = /^#[0-9a-f]{6}$/i;
|
|
68
|
+
function applyBackdrop() {
|
|
69
|
+
const custom = HEX.test(ui.backdrop);
|
|
70
|
+
document.body.dataset.backdrop = custom ? "custom" : ui.backdrop;
|
|
71
|
+
document.body.style.setProperty("--stage-bg", custom ? ui.backdrop : "");
|
|
72
|
+
if (!custom) document.body.style.removeProperty("--stage-bg");
|
|
73
|
+
}
|
|
74
|
+
function backdropControl(onChange) {
|
|
75
|
+
const wrap = el("span", "backdrop");
|
|
76
|
+
wrap.title = "What sprites sit on: judge them against dark, light, or your game's own background";
|
|
77
|
+
const options = [["checker", "Transparency grid"], ["dark", "Dark"], ["light", "Light"]];
|
|
78
|
+
const buttons = [];
|
|
79
|
+
const picker = el("input");
|
|
80
|
+
picker.type = "color";
|
|
81
|
+
picker.setAttribute("aria-label", "Custom backdrop colour");
|
|
82
|
+
const sync = () => {
|
|
83
|
+
for (const b of buttons) b.classList.toggle("on", b.dataset.value === ui.backdrop);
|
|
84
|
+
picker.classList.toggle("on", HEX.test(ui.backdrop));
|
|
85
|
+
if (HEX.test(ui.backdrop)) picker.value = ui.backdrop;
|
|
86
|
+
};
|
|
87
|
+
const set = (value) => {
|
|
88
|
+
ui.backdrop = value;
|
|
89
|
+
applyBackdrop();
|
|
90
|
+
savePrefs();
|
|
91
|
+
for (const other of document.querySelectorAll(".backdrop")) other.__sync?.();
|
|
92
|
+
onChange?.();
|
|
93
|
+
};
|
|
94
|
+
for (const [value, label] of options) {
|
|
95
|
+
const b = el("button", "bd-" + value);
|
|
96
|
+
b.type = "button";
|
|
97
|
+
b.dataset.value = value;
|
|
98
|
+
b.title = label;
|
|
99
|
+
b.setAttribute("aria-label", "Backdrop: " + label);
|
|
100
|
+
b.onclick = () => set(value);
|
|
101
|
+
buttons.push(b);
|
|
102
|
+
wrap.append(b);
|
|
103
|
+
}
|
|
104
|
+
picker.oninput = () => set(picker.value.toLowerCase());
|
|
105
|
+
wrap.append(picker);
|
|
106
|
+
wrap.__sync = sync;
|
|
107
|
+
sync();
|
|
108
|
+
return wrap;
|
|
109
|
+
}
|
|
44
110
|
var formRegion = () => !ui.editing ? null : /^(new|style|cand):/.test(ui.editing) ? "main" : "drawer";
|
|
45
111
|
var $ = (id) => document.getElementById(id);
|
|
46
112
|
var el = (tag, cls, text) => {
|
|
@@ -190,7 +256,7 @@
|
|
|
190
256
|
};
|
|
191
257
|
zoomBar.append(b);
|
|
192
258
|
}
|
|
193
|
-
bar.append(zoomBar);
|
|
259
|
+
bar.append(zoomBar, backdropControl());
|
|
194
260
|
const zoom = ui.compareZoom === "auto" ? auto : ui.compareZoom === "fit" ? Math.min(auto, 1) : Number(ui.compareZoom);
|
|
195
261
|
const body = el("div", "compare-body");
|
|
196
262
|
const table = el("table");
|
|
@@ -856,6 +922,272 @@
|
|
|
856
922
|
return form;
|
|
857
923
|
}
|
|
858
924
|
|
|
925
|
+
// src/gallery/client/src/motion.ts
|
|
926
|
+
var loops = /* @__PURE__ */ new Map();
|
|
927
|
+
var raf = null;
|
|
928
|
+
var t0 = 0;
|
|
929
|
+
var seen = typeof IntersectionObserver === "function" ? new IntersectionObserver((entries) => {
|
|
930
|
+
for (const e of entries) {
|
|
931
|
+
const loop = loops.get(e.target);
|
|
932
|
+
if (loop) loop.visible = e.isIntersecting;
|
|
933
|
+
}
|
|
934
|
+
wake();
|
|
935
|
+
}) : null;
|
|
936
|
+
function registerLoop(img, urls, fps, hoverTarget) {
|
|
937
|
+
if (urls.length < 2) return;
|
|
938
|
+
const loop = { img, urls, fps: fps || 12, index: 0, visible: !seen, hovered: false, first: img.src };
|
|
939
|
+
loops.set(img, loop);
|
|
940
|
+
seen?.observe(img);
|
|
941
|
+
let warmed = false;
|
|
942
|
+
const warm = () => {
|
|
943
|
+
if (warmed) return;
|
|
944
|
+
warmed = true;
|
|
945
|
+
for (const u of urls) {
|
|
946
|
+
const p = new Image();
|
|
947
|
+
p.src = u;
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
hoverTarget.addEventListener("pointerenter", () => {
|
|
951
|
+
warm();
|
|
952
|
+
loop.hovered = true;
|
|
953
|
+
wake();
|
|
954
|
+
});
|
|
955
|
+
hoverTarget.addEventListener("pointerleave", () => {
|
|
956
|
+
loop.hovered = false;
|
|
957
|
+
rest(loop);
|
|
958
|
+
});
|
|
959
|
+
hoverTarget.addEventListener("focus", () => {
|
|
960
|
+
warm();
|
|
961
|
+
loop.hovered = true;
|
|
962
|
+
wake();
|
|
963
|
+
});
|
|
964
|
+
hoverTarget.addEventListener("blur", () => {
|
|
965
|
+
loop.hovered = false;
|
|
966
|
+
rest(loop);
|
|
967
|
+
});
|
|
968
|
+
if (ui.playLoops) warm();
|
|
969
|
+
}
|
|
970
|
+
function pruneLoops() {
|
|
971
|
+
for (const [img, loop] of loops) if (!img.isConnected) {
|
|
972
|
+
seen?.unobserve(img);
|
|
973
|
+
loops.delete(img);
|
|
974
|
+
void loop;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
function setPlayLoops(on) {
|
|
978
|
+
ui.playLoops = on;
|
|
979
|
+
if (on) for (const loop of loops.values()) for (const u of loop.urls) {
|
|
980
|
+
const p = new Image();
|
|
981
|
+
p.src = u;
|
|
982
|
+
}
|
|
983
|
+
else for (const loop of loops.values()) if (!loop.hovered) rest(loop);
|
|
984
|
+
wake();
|
|
985
|
+
}
|
|
986
|
+
function rest(loop) {
|
|
987
|
+
if (ui.playLoops) return;
|
|
988
|
+
loop.index = 0;
|
|
989
|
+
if (loop.img.src !== loop.first) loop.img.src = loop.first;
|
|
990
|
+
}
|
|
991
|
+
function wake() {
|
|
992
|
+
if (raf === null && typeof requestAnimationFrame === "function") {
|
|
993
|
+
t0 = performance.now();
|
|
994
|
+
raf = requestAnimationFrame(tick);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
function tick(now) {
|
|
998
|
+
raf = null;
|
|
999
|
+
let active = false;
|
|
1000
|
+
const seconds = Math.max(0, (now - t0) / 1e3);
|
|
1001
|
+
for (const [img, loop] of loops) {
|
|
1002
|
+
if (!img.isConnected) continue;
|
|
1003
|
+
if (!loop.hovered && !(ui.playLoops && loop.visible)) continue;
|
|
1004
|
+
active = true;
|
|
1005
|
+
const i = Math.floor(seconds * loop.fps) % loop.urls.length;
|
|
1006
|
+
if (i !== loop.index) {
|
|
1007
|
+
loop.index = i;
|
|
1008
|
+
img.src = loop.urls[i];
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
if (active && !document.hidden) raf = requestAnimationFrame(tick);
|
|
1012
|
+
}
|
|
1013
|
+
document.addEventListener("visibilitychange", () => {
|
|
1014
|
+
if (!document.hidden) wake();
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
// src/gallery/client/src/selection.ts
|
|
1018
|
+
var selectedItems = () => S.snap.items.filter((i) => ui.selected.has(i.id));
|
|
1019
|
+
var regenerable = (i) => i.declared && i.currentSpecHash !== null && i.state === "ok";
|
|
1020
|
+
function setSelecting(on) {
|
|
1021
|
+
ui.selecting = on;
|
|
1022
|
+
if (!on) ui.selected.clear();
|
|
1023
|
+
$("select").textContent = on ? "Done selecting" : "Select";
|
|
1024
|
+
$("select").classList.toggle("on", on);
|
|
1025
|
+
render();
|
|
1026
|
+
}
|
|
1027
|
+
function toggleSelected(id) {
|
|
1028
|
+
if (!ui.selecting) {
|
|
1029
|
+
ui.selecting = true;
|
|
1030
|
+
$("select").textContent = "Done selecting";
|
|
1031
|
+
$("select").classList.add("on");
|
|
1032
|
+
}
|
|
1033
|
+
if (ui.selected.has(id)) ui.selected.delete(id);
|
|
1034
|
+
else ui.selected.add(id);
|
|
1035
|
+
const c = document.querySelector('.card[data-key="' + CSS.escape(id) + '"]');
|
|
1036
|
+
if (c) {
|
|
1037
|
+
c.classList.toggle("selected", ui.selected.has(id));
|
|
1038
|
+
c.setAttribute("aria-pressed", String(ui.selected.has(id)));
|
|
1039
|
+
}
|
|
1040
|
+
if (!document.querySelector(".card .sel")) render();
|
|
1041
|
+
else {
|
|
1042
|
+
refreshSelectAll();
|
|
1043
|
+
renderSelectionBar();
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function selectAllButton(items) {
|
|
1047
|
+
const b = el("button", "select-all");
|
|
1048
|
+
b.type = "button";
|
|
1049
|
+
const ids = items.map((i) => i.id);
|
|
1050
|
+
b.__ids = ids;
|
|
1051
|
+
const label = () => {
|
|
1052
|
+
b.textContent = ids.every((id) => ui.selected.has(id)) ? "Clear " + ids.length : "Select all " + ids.length;
|
|
1053
|
+
};
|
|
1054
|
+
b.__label = label;
|
|
1055
|
+
label();
|
|
1056
|
+
b.onclick = () => {
|
|
1057
|
+
const all = ids.every((id) => ui.selected.has(id));
|
|
1058
|
+
for (const id of ids) {
|
|
1059
|
+
if (all) ui.selected.delete(id);
|
|
1060
|
+
else ui.selected.add(id);
|
|
1061
|
+
}
|
|
1062
|
+
for (const id of ids) {
|
|
1063
|
+
const c = document.querySelector('.card[data-key="' + CSS.escape(id) + '"]');
|
|
1064
|
+
if (c) {
|
|
1065
|
+
c.classList.toggle("selected", ui.selected.has(id));
|
|
1066
|
+
c.setAttribute("aria-pressed", String(ui.selected.has(id)));
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
refreshSelectAll();
|
|
1070
|
+
renderSelectionBar();
|
|
1071
|
+
};
|
|
1072
|
+
return b;
|
|
1073
|
+
}
|
|
1074
|
+
function refreshSelectAll() {
|
|
1075
|
+
for (const b of document.querySelectorAll(".select-all")) b.__label?.();
|
|
1076
|
+
}
|
|
1077
|
+
function renderSelectionBar() {
|
|
1078
|
+
const bar = $("selbar");
|
|
1079
|
+
bar.textContent = "";
|
|
1080
|
+
for (const id of [...ui.selected]) if (!S.snap.items.some((i) => i.id === id)) ui.selected.delete(id);
|
|
1081
|
+
bar.hidden = !ui.selecting;
|
|
1082
|
+
if (!ui.selecting) return;
|
|
1083
|
+
const items = selectedItems();
|
|
1084
|
+
const projects = new Set(items.map((i) => i.project));
|
|
1085
|
+
const oneProject = projects.size <= 1;
|
|
1086
|
+
const project = items.length ? items[0].project : null;
|
|
1087
|
+
bar.append(el("b", null, items.length ? items.length + " selected" : "Click cards to select them"));
|
|
1088
|
+
const acts = el("div", "acts");
|
|
1089
|
+
const add = (label, run, opts = {}) => {
|
|
1090
|
+
const b = el("button", opts.primary ? "primary" : null, label);
|
|
1091
|
+
b.type = "button";
|
|
1092
|
+
b.disabled = Boolean(opts.disabled);
|
|
1093
|
+
if (opts.title) b.title = opts.title;
|
|
1094
|
+
b.onclick = run;
|
|
1095
|
+
acts.append(b);
|
|
1096
|
+
return b;
|
|
1097
|
+
};
|
|
1098
|
+
const crossProject = "Select within one project: a job and an edit each belong to one";
|
|
1099
|
+
if (GENERATION) {
|
|
1100
|
+
const todo = items.filter(actionable);
|
|
1101
|
+
if (todo.length) add("Generate " + todo.length + " · " + priceOf(todo), () => generateDialog(todo, { project }), { primary: true, disabled: !oneProject, title: oneProject ? "One job: parents first, then everything drawn from them" : crossProject });
|
|
1102
|
+
const redo = items.filter(regenerable);
|
|
1103
|
+
if (redo.length) add("Regenerate " + redo.length + " · " + priceOf(redo), () => generateDialog(redo, { project, force: true }), { disabled: !oneProject, title: oneProject ? "Replace these with new generations; each old one stays in its history" : crossProject });
|
|
1104
|
+
}
|
|
1105
|
+
const shown = items.filter((i) => i.outputs.some((o) => o.url));
|
|
1106
|
+
if (shown.length >= 2) {
|
|
1107
|
+
add("Compare" + (shown.length > 4 ? " first 4" : ""), () => {
|
|
1108
|
+
ui.compare = shown.slice(0, 4).map((i) => i.id);
|
|
1109
|
+
openCompare();
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
if (EDITABLE && items.length) {
|
|
1113
|
+
const taggable = items.filter((i) => i.declaredAs && projectOf(i)?.manifestSha256);
|
|
1114
|
+
const tag = el("input");
|
|
1115
|
+
tag.type = "text";
|
|
1116
|
+
tag.placeholder = "tag";
|
|
1117
|
+
tag.setAttribute("aria-label", "Tag to add or remove");
|
|
1118
|
+
tag.pattern = "[^,]+";
|
|
1119
|
+
const addTag = el("button", null, "Add tag");
|
|
1120
|
+
addTag.type = "button";
|
|
1121
|
+
const dropTag = el("button", null, "Remove tag");
|
|
1122
|
+
dropTag.type = "button";
|
|
1123
|
+
addTag.disabled = dropTag.disabled = !taggable.length || !oneProject;
|
|
1124
|
+
addTag.title = dropTag.title = !oneProject ? crossProject : taggable.length < items.length ? "Undeclared records have no manifest entry to tag" : "One manifest write for every selected asset";
|
|
1125
|
+
const run = (adding) => retag(taggable, tag.value.trim(), adding, tag);
|
|
1126
|
+
addTag.onclick = () => run(true);
|
|
1127
|
+
dropTag.onclick = () => run(false);
|
|
1128
|
+
tag.onkeydown = (e) => {
|
|
1129
|
+
if (e.key === "Enter") {
|
|
1130
|
+
e.preventDefault();
|
|
1131
|
+
run(true);
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
const box = el("span", "tagbox");
|
|
1135
|
+
box.append(tag, addTag, dropTag);
|
|
1136
|
+
acts.append(box);
|
|
1137
|
+
}
|
|
1138
|
+
add("Clear", () => {
|
|
1139
|
+
ui.selected.clear();
|
|
1140
|
+
render();
|
|
1141
|
+
}, { disabled: !items.length });
|
|
1142
|
+
add("Done", () => setSelecting(false));
|
|
1143
|
+
bar.append(acts);
|
|
1144
|
+
const msg = el("span", "msg");
|
|
1145
|
+
msg.id = "selmsg";
|
|
1146
|
+
bar.append(msg);
|
|
1147
|
+
}
|
|
1148
|
+
async function retag(items, tag, adding, input) {
|
|
1149
|
+
const msg = $("selmsg");
|
|
1150
|
+
if (!tag || tag.includes(",")) {
|
|
1151
|
+
msg.className = "msg bad";
|
|
1152
|
+
msg.textContent = "Type one tag (no commas).";
|
|
1153
|
+
input.focus();
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1157
|
+
for (const i of items) {
|
|
1158
|
+
if (byId.has(i.declaredAs)) continue;
|
|
1159
|
+
const now = i.asset && i.asset.tags || [];
|
|
1160
|
+
const next = adding ? now.includes(tag) ? now : [...now, tag] : now.filter((t) => t !== tag);
|
|
1161
|
+
if (next.length !== now.length) byId.set(i.declaredAs, next);
|
|
1162
|
+
}
|
|
1163
|
+
if (!byId.size) {
|
|
1164
|
+
msg.className = "msg";
|
|
1165
|
+
msg.textContent = adding ? "They all have it already." : "None of them has it.";
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
const pr = projectOf(items[0]);
|
|
1169
|
+
const body = {
|
|
1170
|
+
action: "batch",
|
|
1171
|
+
expectedSha256: pr.manifestSha256,
|
|
1172
|
+
edits: [...byId].map(([assetId, tags]) => ({ action: "patch-asset", assetId, patch: { tags } }))
|
|
1173
|
+
};
|
|
1174
|
+
if (items[0].project) body.project = items[0].project;
|
|
1175
|
+
msg.className = "msg";
|
|
1176
|
+
msg.textContent = "saving…";
|
|
1177
|
+
try {
|
|
1178
|
+
S.snap = await postEdit(body);
|
|
1179
|
+
render();
|
|
1180
|
+
const after = $("selmsg");
|
|
1181
|
+
if (after) {
|
|
1182
|
+
after.className = "msg ok";
|
|
1183
|
+
after.textContent = (adding ? "Tagged " : "Untagged ") + byId.size + (byId.size === 1 ? " asset" : " assets") + " “" + tag + "”.";
|
|
1184
|
+
}
|
|
1185
|
+
} catch (err) {
|
|
1186
|
+
msg.className = "msg bad";
|
|
1187
|
+
msg.textContent = err.status === 409 ? "The manifest changed on disk; refresh and try again." : err.message;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
859
1191
|
// src/gallery/client/src/grid.ts
|
|
860
1192
|
function visibleItems() {
|
|
861
1193
|
const q = ui.q.trim().toLowerCase();
|
|
@@ -1031,7 +1363,9 @@
|
|
|
1031
1363
|
}
|
|
1032
1364
|
function card(item) {
|
|
1033
1365
|
const slot = ui.compare.indexOf(item.id);
|
|
1034
|
-
const
|
|
1366
|
+
const picked = ui.selected.has(item.id);
|
|
1367
|
+
const c = el("button", "card" + (item.outputs.some((o) => o.url) ? "" : " ghost") + (ui.open === item.id ? " active" : "") + (slot >= 0 ? " compared" : "") + (picked ? " selected" : ""));
|
|
1368
|
+
if (ui.selecting) c.setAttribute("aria-pressed", String(picked));
|
|
1035
1369
|
c.type = "button";
|
|
1036
1370
|
c.dataset.key = item.id;
|
|
1037
1371
|
c.setAttribute("aria-label", item.id + ", " + item.state + (slot >= 0 ? ", in comparison" : ""));
|
|
@@ -1060,15 +1394,21 @@
|
|
|
1060
1394
|
cell.append(badge);
|
|
1061
1395
|
}
|
|
1062
1396
|
c.append(cell, body);
|
|
1397
|
+
if (ui.selecting) cell.append(el("span", "sel", "✓"));
|
|
1063
1398
|
c.onclick = (e) => {
|
|
1064
1399
|
if (e.shiftKey) toggleCompare(item.id);
|
|
1400
|
+
else if (ui.selecting || e.metaKey || e.ctrlKey) toggleSelected(item.id);
|
|
1065
1401
|
else openItem(item.id);
|
|
1066
1402
|
};
|
|
1403
|
+
const still = cell.querySelector(":scope > img");
|
|
1404
|
+
const shown = item.edit && item.editStatus === "edited" && item.edits.every((e) => e.url) ? item.edits : item.outputs.filter((o) => o.url);
|
|
1405
|
+
if (still && isFrameSet(item) && shown.length > 1) registerLoop(still, shown.map((o) => o.url), item.fps || 12, c);
|
|
1067
1406
|
return c;
|
|
1068
1407
|
}
|
|
1069
1408
|
function renderMain(items) {
|
|
1070
1409
|
const root = $("root");
|
|
1071
1410
|
root.textContent = "";
|
|
1411
|
+
pruneLoops();
|
|
1072
1412
|
if (!S.snap.items.length) {
|
|
1073
1413
|
const e = el("div", "empty");
|
|
1074
1414
|
e.append(el("p", null, "Nothing has been generated for this project yet."));
|
|
@@ -1093,7 +1433,7 @@
|
|
|
1093
1433
|
const sections = [];
|
|
1094
1434
|
for (const pr of projects) {
|
|
1095
1435
|
const inProject = pr ? shown.filter((i) => i.project === pr.id) : shown;
|
|
1096
|
-
const styleSections = ui.group === "style" ? S.snap.styles.filter((s) => !pr || s.project === pr.id).map((s) => ({ style: s, items: inProject.filter((i) => i.styleId === s.id) })).filter((s) => s.items.length) : inProject.length ? [{ style: null, items: inProject }] : [];
|
|
1436
|
+
const styleSections = ui.group === "style" || ui.group === "family" ? S.snap.styles.filter((s) => !pr || s.project === pr.id).map((s) => ({ style: s, items: inProject.filter((i) => i.styleId === s.id) })).filter((s) => s.items.length) : inProject.length ? [{ style: null, items: inProject }] : [];
|
|
1097
1437
|
if (pr) sections.push({ project: pr, style: null, items: null, count: inProject.length });
|
|
1098
1438
|
sections.push(...styleSections);
|
|
1099
1439
|
}
|
|
@@ -1150,6 +1490,7 @@
|
|
|
1150
1490
|
r.onclick = () => generateDialog(refreshable, { project: s.project, refresh: true });
|
|
1151
1491
|
tools.append(r);
|
|
1152
1492
|
}
|
|
1493
|
+
if (ui.selecting && ui.group !== "family") tools.append(selectAllButton(sec.items));
|
|
1153
1494
|
const styleKey = "style:" + (s.project || "") + ":" + s.id;
|
|
1154
1495
|
if (EDITABLE && s.outDir) {
|
|
1155
1496
|
const es = el("button", null, ui.editing === styleKey ? "Cancel" : "Edit style");
|
|
@@ -1174,9 +1515,12 @@
|
|
|
1174
1515
|
if (ui.editing === styleKey) wrap.append(styleForm(s));
|
|
1175
1516
|
if (ui.editing === addKey) wrap.append(addAssetForm(s));
|
|
1176
1517
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1518
|
+
if (ui.group === "family") wrap.append(...familyGroups(sec.items));
|
|
1519
|
+
else {
|
|
1520
|
+
const grid = el("div", "grid");
|
|
1521
|
+
for (const item of sec.items) grid.append(card(item));
|
|
1522
|
+
wrap.append(grid);
|
|
1523
|
+
}
|
|
1180
1524
|
root.append(wrap);
|
|
1181
1525
|
}
|
|
1182
1526
|
if (hidden) {
|
|
@@ -1192,6 +1536,93 @@
|
|
|
1192
1536
|
root.append(more);
|
|
1193
1537
|
}
|
|
1194
1538
|
}
|
|
1539
|
+
var KIND_ORDER = { base: 0, state: 1, animation: 2, portrait: 3, outfit: 4 };
|
|
1540
|
+
var TURN = ["south", "south-east", "east", "north-east", "north", "north-west", "west", "south-west"];
|
|
1541
|
+
function familyOrder(a, b) {
|
|
1542
|
+
const ka = KIND_ORDER[a.character.kind] ?? 9, kb = KIND_ORDER[b.character.kind] ?? 9;
|
|
1543
|
+
if (ka !== kb) return ka - kb;
|
|
1544
|
+
if (a.character.kind === "animation") {
|
|
1545
|
+
const la = loopName(a), lb = loopName(b);
|
|
1546
|
+
if (la !== lb) return la < lb ? -1 : 1;
|
|
1547
|
+
return TURN.indexOf(a.character.direction) - TURN.indexOf(b.character.direction);
|
|
1548
|
+
}
|
|
1549
|
+
return a.assetId < b.assetId ? -1 : a.assetId > b.assetId ? 1 : 0;
|
|
1550
|
+
}
|
|
1551
|
+
function familyGroups(items) {
|
|
1552
|
+
const families = /* @__PURE__ */ new Map();
|
|
1553
|
+
const loose = [];
|
|
1554
|
+
for (const item of items) {
|
|
1555
|
+
const root = item.character ? familyRoot(item) : null;
|
|
1556
|
+
if (!root) {
|
|
1557
|
+
loose.push(item);
|
|
1558
|
+
continue;
|
|
1559
|
+
}
|
|
1560
|
+
if (!families.has(root.id)) families.set(root.id, { root, members: [] });
|
|
1561
|
+
families.get(root.id).members.push(item);
|
|
1562
|
+
}
|
|
1563
|
+
const out = [];
|
|
1564
|
+
for (const { root, members } of families.values()) {
|
|
1565
|
+
members.sort(familyOrder);
|
|
1566
|
+
const group = el("div", "fam-group");
|
|
1567
|
+
const head = el("div", "fam-head");
|
|
1568
|
+
head.append(el("b", "mono", root.assetId));
|
|
1569
|
+
const counts = {};
|
|
1570
|
+
for (const m of members) counts[m.state] = (counts[m.state] || 0) + 1;
|
|
1571
|
+
const tally = el("span", "fam-tally");
|
|
1572
|
+
const loops2 = new Set(members.filter((m) => m.character.kind === "animation").map(loopName)).size;
|
|
1573
|
+
tally.append(document.createTextNode(members.length + (members.length === 1 ? " asset" : " assets") + (loops2 ? " · " + loops2 + (loops2 === 1 ? " loop" : " loops") : "")));
|
|
1574
|
+
for (const [state, n] of Object.entries(counts)) {
|
|
1575
|
+
const t = el("span", "st");
|
|
1576
|
+
t.append(el("i", "dot " + STATE_TONE[state]), document.createTextNode(n + " " + state));
|
|
1577
|
+
tally.append(t);
|
|
1578
|
+
}
|
|
1579
|
+
head.append(tally);
|
|
1580
|
+
const tools = el("div", "tools");
|
|
1581
|
+
if (ui.selecting) tools.append(selectAllButton(members));
|
|
1582
|
+
const view = el("button", null, "Family view");
|
|
1583
|
+
view.type = "button";
|
|
1584
|
+
view.onclick = () => openFamily(root);
|
|
1585
|
+
tools.append(view);
|
|
1586
|
+
const everything = familyMembers(root);
|
|
1587
|
+
const todo = everything.filter(actionable);
|
|
1588
|
+
if (GENERATION && todo.length) {
|
|
1589
|
+
const g = el("button", "primary", "Generate " + todo.length + " · " + priceOf(todo));
|
|
1590
|
+
g.type = "button";
|
|
1591
|
+
g.title = "One job for the family: parents first, then everything drawn from them";
|
|
1592
|
+
g.onclick = () => generateDialog(todo, { project: root.project });
|
|
1593
|
+
tools.append(g);
|
|
1594
|
+
}
|
|
1595
|
+
head.append(tools);
|
|
1596
|
+
const grid = el("div", "grid");
|
|
1597
|
+
for (const m of members) grid.append(card(m));
|
|
1598
|
+
group.append(head, grid);
|
|
1599
|
+
out.push(group);
|
|
1600
|
+
}
|
|
1601
|
+
if (loose.length) {
|
|
1602
|
+
const group = el("div", "fam-group loose");
|
|
1603
|
+
if (families.size) {
|
|
1604
|
+
const head = el("div", "fam-head");
|
|
1605
|
+
head.append(el("b", null, "Not in a family"), el("span", "fam-tally", loose.length + (loose.length === 1 ? " asset" : " assets")));
|
|
1606
|
+
if (ui.selecting) {
|
|
1607
|
+
const tools = el("div", "tools");
|
|
1608
|
+
tools.append(selectAllButton(loose));
|
|
1609
|
+
head.append(tools);
|
|
1610
|
+
}
|
|
1611
|
+
group.append(head);
|
|
1612
|
+
}
|
|
1613
|
+
const grid = el("div", "grid");
|
|
1614
|
+
for (const item of loose) grid.append(card(item));
|
|
1615
|
+
group.append(grid);
|
|
1616
|
+
out.push(group);
|
|
1617
|
+
}
|
|
1618
|
+
return out;
|
|
1619
|
+
}
|
|
1620
|
+
var actionable = (item) => item.declared && item.currentSpecHash !== null && ["missing", "stale", "failed"].includes(item.state);
|
|
1621
|
+
function priceOf(items) {
|
|
1622
|
+
const totals = {};
|
|
1623
|
+
for (const i of items) if (i.estimatedCost !== null) totals[i.costUnit] = (totals[i.costUnit] || 0) + i.estimatedCost;
|
|
1624
|
+
return Object.entries(totals).map(([unit, n]) => fmtCost(unit, Math.round(n * 100) / 100)).join(" + ") || "free";
|
|
1625
|
+
}
|
|
1195
1626
|
function projectHeader(pr, count) {
|
|
1196
1627
|
const head = el("section", "project");
|
|
1197
1628
|
const line = el("div", "phead");
|
|
@@ -1464,6 +1895,7 @@
|
|
|
1464
1895
|
if (!ACTIVE_PHASES.has(job.phase) && !ui.settled.has(job.id)) {
|
|
1465
1896
|
ui.settled.add(job.id);
|
|
1466
1897
|
changed = true;
|
|
1898
|
+
notifyJob(job);
|
|
1467
1899
|
const open = ui.open && S.snap.items.find((i) => i.id === ui.open);
|
|
1468
1900
|
if (open && job.project === open.project && job.keys.includes(open.key)) {
|
|
1469
1901
|
ui.notice = { id: open.id, text: job.phase === "failed" ? (JOB_VERB[job.mode] || job.mode).replace(/ing$/, "ing") + " this asset failed: " + (job.error || "see the log above") : job.mode === "generate" ? "Regenerated. The new result is on disk and recorded as this generation." : job.mode === "restore" ? "Restored. The recorded bytes are back on disk." : job.mode === "revert" ? "Brought back generation #" + job.revert + ". The one it replaced is now #1 in the list below." : job.mode === "refresh" ? job.counts.downloaded ? "Pulled the upstream change; the lockfile records the new bytes." : "Nothing changed upstream; the file was left alone." : job.mode === "resume" && job.counts.downloaded ? "Done. The file on disk and the record now match the manifest." : "Done." };
|
|
@@ -1472,6 +1904,7 @@
|
|
|
1472
1904
|
if (job.phase === "review" && !ui.settled.has(job.id + ":review")) {
|
|
1473
1905
|
ui.settled.add(job.id + ":review");
|
|
1474
1906
|
changed = true;
|
|
1907
|
+
notifyJob(job);
|
|
1475
1908
|
}
|
|
1476
1909
|
}
|
|
1477
1910
|
if (changed) refresh();
|
|
@@ -1489,6 +1922,34 @@
|
|
|
1489
1922
|
clearTimeout(jobsTimer ?? void 0);
|
|
1490
1923
|
if (active) jobsTimer = setTimeout(pollJobs, 2e3);
|
|
1491
1924
|
}
|
|
1925
|
+
var DONE_VERB = { generate: "Generated", resume: "Resumed", refresh: "Pulled upstream changes for", restore: "Restored", revert: "Brought back a generation of" };
|
|
1926
|
+
async function enableNotifications() {
|
|
1927
|
+
if (typeof Notification === "undefined") return false;
|
|
1928
|
+
if (Notification.permission === "granted") return true;
|
|
1929
|
+
if (Notification.permission === "denied") {
|
|
1930
|
+
$("note").textContent = " Notifications are blocked for this page in the browser's site settings.";
|
|
1931
|
+
return false;
|
|
1932
|
+
}
|
|
1933
|
+
try {
|
|
1934
|
+
return await Notification.requestPermission() === "granted";
|
|
1935
|
+
} catch {
|
|
1936
|
+
return false;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
function notifyJob(job) {
|
|
1940
|
+
if (!ui.notify || !document.hidden || typeof Notification === "undefined" || Notification.permission !== "granted") return;
|
|
1941
|
+
const n = job.keys.length;
|
|
1942
|
+
const what = n === 1 ? job.keys[0] : n + " assets";
|
|
1943
|
+
const body = job.phase === "review" ? job.review.length + " waiting for you to pick a candidate" : job.phase === "failed" ? "failed: " + (job.error || "see the log") : (DONE_VERB[job.mode] || "Finished") + " " + what;
|
|
1944
|
+
try {
|
|
1945
|
+
const note = new Notification("pixelkiln" + (job.project ? " · " + job.project : ""), { body, tag: "pixelkiln-job-" + job.id });
|
|
1946
|
+
note.onclick = () => {
|
|
1947
|
+
window.focus();
|
|
1948
|
+
note.close();
|
|
1949
|
+
};
|
|
1950
|
+
} catch {
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1492
1953
|
function renderJobs() {
|
|
1493
1954
|
const host = $("jobs");
|
|
1494
1955
|
host.textContent = "";
|
|
@@ -2135,6 +2596,7 @@
|
|
|
2135
2596
|
var SPEEDS = [0.5, 1, 2];
|
|
2136
2597
|
var SPIN_MS = 420;
|
|
2137
2598
|
var DRAG_STEP_PX = 28;
|
|
2599
|
+
var CELL_PX = 68;
|
|
2138
2600
|
var F = null;
|
|
2139
2601
|
var reducedMotion = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
2140
2602
|
var byKey = (project, key) => S.snap.items.find((i) => i.key === key && i.project === project);
|
|
@@ -2243,7 +2705,7 @@
|
|
|
2243
2705
|
t.append(el("i", "dot " + STATE_TONE[state]), document.createTextNode(n + " " + state));
|
|
2244
2706
|
tally.append(t);
|
|
2245
2707
|
}
|
|
2246
|
-
bar.append(tally);
|
|
2708
|
+
bar.append(tally, backdropControl());
|
|
2247
2709
|
if (GENERATION) {
|
|
2248
2710
|
const todo = members.filter((m) => m.declared && m.currentSpecHash !== null && ["missing", "stale", "failed"].includes(m.state));
|
|
2249
2711
|
if (todo.length) {
|
|
@@ -2270,7 +2732,7 @@
|
|
|
2270
2732
|
host.append(scrim, panel);
|
|
2271
2733
|
panel.scrollTop = scroll;
|
|
2272
2734
|
highlight(F.direction);
|
|
2273
|
-
if (F.raf === null) F.raf = requestAnimationFrame(
|
|
2735
|
+
if (F.raf === null) F.raf = requestAnimationFrame(tick2);
|
|
2274
2736
|
}
|
|
2275
2737
|
function turntable(root, turnables, source, order) {
|
|
2276
2738
|
const wrap = el("section", "turntable");
|
|
@@ -2407,11 +2869,11 @@
|
|
|
2407
2869
|
return wrap;
|
|
2408
2870
|
}
|
|
2409
2871
|
function loopGrid(root, members, order) {
|
|
2410
|
-
const
|
|
2872
|
+
const loops2 = members.filter((m) => m.character.kind === "animation");
|
|
2411
2873
|
const wrap = el("section", "loop-grid");
|
|
2412
2874
|
const head = el("div", "lg-head");
|
|
2413
|
-
head.append(el("h3", null,
|
|
2414
|
-
if (
|
|
2875
|
+
head.append(el("h3", null, loops2.length ? "Loops" : "No loops yet"));
|
|
2876
|
+
if (loops2.length) {
|
|
2415
2877
|
const play = el("button", null, F.playing ? "Pause" : "Play");
|
|
2416
2878
|
play.type = "button";
|
|
2417
2879
|
play.onclick = () => {
|
|
@@ -2431,14 +2893,14 @@
|
|
|
2431
2893
|
head.append(speed);
|
|
2432
2894
|
}
|
|
2433
2895
|
wrap.append(head);
|
|
2434
|
-
if (!
|
|
2896
|
+
if (!loops2.length) {
|
|
2435
2897
|
wrap.append(el("p", "state-dim", EDITABLE ? "Add one from the base's drawer (+ New animation), or draft a whole character in the studio." : "None declared."));
|
|
2436
2898
|
return wrap;
|
|
2437
2899
|
}
|
|
2438
|
-
const used = /* @__PURE__ */ new Set([...order, ...
|
|
2900
|
+
const used = /* @__PURE__ */ new Set([...order, ...loops2.map((l) => l.character.direction).filter(Boolean)]);
|
|
2439
2901
|
const columns = COMPASS.filter((d) => used.has(d));
|
|
2440
2902
|
const rows = /* @__PURE__ */ new Map();
|
|
2441
|
-
for (const l of
|
|
2903
|
+
for (const l of loops2) {
|
|
2442
2904
|
const k = (l.character.parentKey || "") + "|" + loopName(l);
|
|
2443
2905
|
if (!rows.has(k)) rows.set(k, []);
|
|
2444
2906
|
rows.get(k).push(l);
|
|
@@ -2505,6 +2967,7 @@
|
|
|
2505
2967
|
img.alt = item.assetId;
|
|
2506
2968
|
const urls = frameUrls(item);
|
|
2507
2969
|
if (urls.length) {
|
|
2970
|
+
fitCell(img, item.width, item.height);
|
|
2508
2971
|
img.src = urls[0];
|
|
2509
2972
|
for (const u of urls) {
|
|
2510
2973
|
const p = new Image();
|
|
@@ -2514,6 +2977,7 @@
|
|
|
2514
2977
|
} else {
|
|
2515
2978
|
const ghost = rotationUrl(parent, direction);
|
|
2516
2979
|
if (ghost) {
|
|
2980
|
+
fitCell(img, parent.width, parent.height);
|
|
2517
2981
|
img.src = ghost;
|
|
2518
2982
|
b.classList.add("ghost");
|
|
2519
2983
|
} else img.hidden = true;
|
|
@@ -2539,6 +3003,7 @@
|
|
|
2539
3003
|
img.alt = item.assetId;
|
|
2540
3004
|
const urls = frameUrls(item);
|
|
2541
3005
|
if (urls.length) {
|
|
3006
|
+
fitCell(img, item.width, item.height);
|
|
2542
3007
|
img.src = urls[0];
|
|
2543
3008
|
if (urls.length > 1) F.anims.push({ img, urls, fps: item.fps || 12, index: 0 });
|
|
2544
3009
|
} else img.hidden = true;
|
|
@@ -2551,11 +3016,16 @@
|
|
|
2551
3016
|
wrap.append(strip);
|
|
2552
3017
|
return wrap;
|
|
2553
3018
|
}
|
|
3019
|
+
function fitCell(img, width, height) {
|
|
3020
|
+
const s = displayScale(width, height, CELL_PX, CELL_PX);
|
|
3021
|
+
img.width = Math.max(1, Math.round(width * s));
|
|
3022
|
+
img.height = Math.max(1, Math.round(height * s));
|
|
3023
|
+
}
|
|
2554
3024
|
function highlight(direction) {
|
|
2555
3025
|
if (!F) return;
|
|
2556
3026
|
for (const [d, nodes] of F.columns) for (const n of nodes) n.classList.toggle("col-on", d === direction);
|
|
2557
3027
|
}
|
|
2558
|
-
function
|
|
3028
|
+
function tick2(now) {
|
|
2559
3029
|
if (!F) return;
|
|
2560
3030
|
if (!F.panel || !F.panel.isConnected) {
|
|
2561
3031
|
stopFamily();
|
|
@@ -2577,7 +3047,7 @@
|
|
|
2577
3047
|
const order = source ? turnOrder(source) : COMPASS;
|
|
2578
3048
|
F.turn(order[(order.indexOf(F.direction) + 1) % order.length]);
|
|
2579
3049
|
}
|
|
2580
|
-
F.raf = requestAnimationFrame(
|
|
3050
|
+
F.raf = requestAnimationFrame(tick2);
|
|
2581
3051
|
}
|
|
2582
3052
|
var familyState = () => F && { rootId: F.rootId, source: F.source, direction: F.direction, playing: F.playing, anims: F.anims.length };
|
|
2583
3053
|
|
|
@@ -2932,11 +3402,11 @@
|
|
|
2932
3402
|
for (const [i, pose] of [set.firstFrameKeypoints, ...set.frames].entries()) {
|
|
2933
3403
|
const where = i === 0 ? "the starting pose" : "frame " + i;
|
|
2934
3404
|
if (!Array.isArray(pose) || pose.length !== 18) return where + " needs all 18 joints";
|
|
2935
|
-
const
|
|
3405
|
+
const seen2 = /* @__PURE__ */ new Set();
|
|
2936
3406
|
for (const j of pose) {
|
|
2937
3407
|
if (!SKELETON_LABELS.includes(j && j.label)) return where + ': "' + (j && j.label) + `" is not one of PixelLab's 18 joints`;
|
|
2938
|
-
if (
|
|
2939
|
-
|
|
3408
|
+
if (seen2.has(j.label)) return where + ': "' + j.label + '" appears twice';
|
|
3409
|
+
seen2.add(j.label);
|
|
2940
3410
|
if (!(j.x >= 0 && j.x <= 1 && j.y >= 0 && j.y <= 1)) return where + ": " + j.label + " must sit inside the image (x and y 0 to 1)";
|
|
2941
3411
|
}
|
|
2942
3412
|
}
|
|
@@ -3255,25 +3725,34 @@
|
|
|
3255
3725
|
host.append(preview);
|
|
3256
3726
|
return;
|
|
3257
3727
|
}
|
|
3258
|
-
|
|
3259
|
-
const frameSet = isFrameSet(item);
|
|
3728
|
+
let idx = Math.min(ui.member, shown.length - 1);
|
|
3729
|
+
const frameSet = isFrameSet(item) && shown.length > 1;
|
|
3260
3730
|
const w = item.width, h = item.height;
|
|
3261
3731
|
const box = Math.min(window.innerWidth, 540) - 62;
|
|
3262
3732
|
const auto = displayScale(w, h, box, Math.min(window.innerHeight * 0.55, 560));
|
|
3263
3733
|
const zoom = ui.zoom === "auto" ? auto : ui.zoom === "fit" ? Math.min(auto, 1) : Number(ui.zoom);
|
|
3264
|
-
const
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
}
|
|
3276
|
-
|
|
3734
|
+
const sized = (img2) => {
|
|
3735
|
+
if (zoom >= 1) {
|
|
3736
|
+
img2.width = Math.round(w * zoom);
|
|
3737
|
+
img2.height = Math.round(h * zoom);
|
|
3738
|
+
} else {
|
|
3739
|
+
img2.style.maxWidth = "100%";
|
|
3740
|
+
img2.style.maxHeight = "55vh";
|
|
3741
|
+
img2.style.width = "auto";
|
|
3742
|
+
img2.style.height = "auto";
|
|
3743
|
+
}
|
|
3744
|
+
return img2;
|
|
3745
|
+
};
|
|
3746
|
+
const stack = el("div", "stack");
|
|
3747
|
+
const img = sized(el("img"));
|
|
3748
|
+
img.src = shown[idx].url;
|
|
3749
|
+
img.alt = shown[idx].role || item.assetId;
|
|
3750
|
+
const onion = sized(el("img", "onion"));
|
|
3751
|
+
onion.alt = "";
|
|
3752
|
+
onion.setAttribute("aria-hidden", "true");
|
|
3753
|
+
onion.hidden = !(frameSet && ui.onion);
|
|
3754
|
+
stack.append(onion, img);
|
|
3755
|
+
preview.append(stack);
|
|
3277
3756
|
const zoomBar = el("div", "zoom");
|
|
3278
3757
|
zoomBar.append(el("span", null, "zoom"));
|
|
3279
3758
|
for (const z of ["fit", 1, 2, 4, 8]) {
|
|
@@ -3286,34 +3765,98 @@
|
|
|
3286
3765
|
zoomBar.append(b);
|
|
3287
3766
|
}
|
|
3288
3767
|
const label = el("span", "grow", w + "×" + h + (zoom >= 1 ? " at " + zoom + "×" : " fitted"));
|
|
3289
|
-
zoomBar.append(label);
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3768
|
+
zoomBar.append(label, backdropControl());
|
|
3769
|
+
host.append(preview, zoomBar);
|
|
3770
|
+
let strip = null;
|
|
3771
|
+
const show = (i) => {
|
|
3772
|
+
idx = (i + shown.length) % shown.length;
|
|
3773
|
+
ui.member = idx;
|
|
3774
|
+
img.src = shown[idx].url;
|
|
3775
|
+
img.alt = shown[idx].role || item.assetId;
|
|
3776
|
+
if (frameSet) {
|
|
3777
|
+
onion.src = shown[(idx - 1 + shown.length) % shown.length].url;
|
|
3778
|
+
scrub.value = String(idx);
|
|
3779
|
+
count.textContent = idx + 1 + " / " + shown.length;
|
|
3780
|
+
}
|
|
3781
|
+
if (strip) for (const [j, b] of [...strip.querySelectorAll("button")].entries()) b.classList.toggle("on", j === idx);
|
|
3782
|
+
};
|
|
3783
|
+
const scrub = el("input");
|
|
3784
|
+
const count = el("span", "count");
|
|
3785
|
+
if (frameSet) {
|
|
3786
|
+
const bar = el("div", "frames-bar");
|
|
3787
|
+
const own = item.fps || 12;
|
|
3788
|
+
const fps = () => ui.fps || own;
|
|
3789
|
+
const prev = el("button", null, "◀");
|
|
3790
|
+
prev.type = "button";
|
|
3791
|
+
prev.title = "Previous frame (,)";
|
|
3792
|
+
const next = el("button", null, "▶");
|
|
3793
|
+
next.type = "button";
|
|
3794
|
+
next.title = "Next frame (.)";
|
|
3795
|
+
const play = el("button", "play", "Play");
|
|
3293
3796
|
play.type = "button";
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3797
|
+
play.title = "Play or pause (space)";
|
|
3798
|
+
const pause = () => {
|
|
3799
|
+
stopPlayback();
|
|
3800
|
+
play.textContent = "Play";
|
|
3801
|
+
};
|
|
3802
|
+
const start = () => {
|
|
3803
|
+
stopPlayback();
|
|
3301
3804
|
play.textContent = "Pause";
|
|
3302
3805
|
ui.playing = setInterval(() => {
|
|
3303
|
-
if (document.hidden)
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3806
|
+
if (!document.hidden) show(idx + 1);
|
|
3807
|
+
}, Math.max(16, Math.round(1e3 / fps())));
|
|
3808
|
+
};
|
|
3809
|
+
play.onclick = () => ui.playing ? pause() : start();
|
|
3810
|
+
prev.onclick = () => {
|
|
3811
|
+
pause();
|
|
3812
|
+
show(idx - 1);
|
|
3309
3813
|
};
|
|
3310
|
-
|
|
3814
|
+
next.onclick = () => {
|
|
3815
|
+
pause();
|
|
3816
|
+
show(idx + 1);
|
|
3817
|
+
};
|
|
3818
|
+
scrub.type = "range";
|
|
3819
|
+
scrub.min = "0";
|
|
3820
|
+
scrub.max = String(shown.length - 1);
|
|
3821
|
+
scrub.step = "1";
|
|
3822
|
+
scrub.setAttribute("aria-label", "Frame");
|
|
3823
|
+
scrub.oninput = () => {
|
|
3824
|
+
pause();
|
|
3825
|
+
show(Number(scrub.value));
|
|
3826
|
+
};
|
|
3827
|
+
const speed = el("select");
|
|
3828
|
+
speed.setAttribute("aria-label", "Frames per second");
|
|
3829
|
+
const rates = [.../* @__PURE__ */ new Set([own, 4, 6, 8, 10, 12, 15, 24])].sort((a, b) => a - b);
|
|
3830
|
+
for (const r of rates) speed.append(new Option(r + " fps" + (r === own ? " (its own)" : ""), String(r)));
|
|
3831
|
+
speed.value = String(fps());
|
|
3832
|
+
speed.onchange = () => {
|
|
3833
|
+
ui.fps = Number(speed.value) === own ? null : Number(speed.value);
|
|
3834
|
+
if (ui.playing) start();
|
|
3835
|
+
};
|
|
3836
|
+
const onionBox = el("label", "chip");
|
|
3837
|
+
const onionToggle = el("input");
|
|
3838
|
+
onionToggle.type = "checkbox";
|
|
3839
|
+
onionToggle.checked = ui.onion;
|
|
3840
|
+
onionToggle.onchange = () => {
|
|
3841
|
+
ui.onion = onionToggle.checked;
|
|
3842
|
+
onion.hidden = !ui.onion;
|
|
3843
|
+
savePrefs();
|
|
3844
|
+
};
|
|
3845
|
+
onionBox.title = "Show the previous frame faintly under this one";
|
|
3846
|
+
onionBox.append(onionToggle, document.createTextNode(" onion skin"));
|
|
3847
|
+
bar.append(prev, play, next, scrub, count, speed, onionBox);
|
|
3848
|
+
host.append(bar);
|
|
3849
|
+
host.__frames = { step: (n) => {
|
|
3850
|
+
pause();
|
|
3851
|
+
show(idx + n);
|
|
3852
|
+
}, toggle: () => play.click() };
|
|
3853
|
+
} else {
|
|
3854
|
+
host.__frames = null;
|
|
3311
3855
|
}
|
|
3312
|
-
host.append(preview, zoomBar);
|
|
3313
3856
|
if (shown.length > 1) {
|
|
3314
|
-
|
|
3857
|
+
strip = el("div", "members");
|
|
3315
3858
|
shown.forEach((o, i) => {
|
|
3316
|
-
const b = el("button",
|
|
3859
|
+
const b = el("button", i === idx ? "on" : null);
|
|
3317
3860
|
b.type = "button";
|
|
3318
3861
|
const t = el("img");
|
|
3319
3862
|
t.src = o.url;
|
|
@@ -3321,13 +3864,21 @@
|
|
|
3321
3864
|
t.loading = "lazy";
|
|
3322
3865
|
b.append(t, el("span", null, o.role || String(i + 1)));
|
|
3323
3866
|
b.onclick = () => {
|
|
3324
|
-
|
|
3325
|
-
|
|
3867
|
+
if (frameSet) {
|
|
3868
|
+
stopPlayback();
|
|
3869
|
+
}
|
|
3870
|
+
show(i);
|
|
3871
|
+
if (!frameSet) renderPreview(item, host);
|
|
3326
3872
|
};
|
|
3327
3873
|
strip.append(b);
|
|
3328
3874
|
});
|
|
3329
3875
|
host.append(strip);
|
|
3330
3876
|
}
|
|
3877
|
+
show(idx);
|
|
3878
|
+
}
|
|
3879
|
+
function drawerFrames() {
|
|
3880
|
+
const host = document.querySelector(".drawer .dbody > div");
|
|
3881
|
+
return host ? host.__frames ?? null : null;
|
|
3331
3882
|
}
|
|
3332
3883
|
var CHARACTER_KIND = {
|
|
3333
3884
|
base: "base",
|
|
@@ -3698,7 +4249,7 @@
|
|
|
3698
4249
|
ui.projects = new Set(list("project"));
|
|
3699
4250
|
ui.compare = list("compare").slice(0, 4);
|
|
3700
4251
|
if (["key", "newest", "oldest", "cost", "size"].includes(params.get("sort") ?? "")) ui.sort = params.get("sort");
|
|
3701
|
-
if (["style", "none"].includes(params.get("group") ?? "")) ui.group = params.get("group");
|
|
4252
|
+
if (["style", "family", "none"].includes(params.get("group") ?? "")) ui.group = params.get("group");
|
|
3702
4253
|
}
|
|
3703
4254
|
function writeUrlState() {
|
|
3704
4255
|
const params = new URLSearchParams();
|
|
@@ -3771,6 +4322,7 @@
|
|
|
3771
4322
|
renderMain(visibleItems());
|
|
3772
4323
|
renderDrawer();
|
|
3773
4324
|
renderTray();
|
|
4325
|
+
renderSelectionBar();
|
|
3774
4326
|
refreshFamily();
|
|
3775
4327
|
}
|
|
3776
4328
|
function renderQuietly() {
|
|
@@ -3780,6 +4332,7 @@
|
|
|
3780
4332
|
if (region !== "main") renderMain(visibleItems());
|
|
3781
4333
|
if (region !== "drawer") renderDrawer();
|
|
3782
4334
|
renderTray();
|
|
4335
|
+
renderSelectionBar();
|
|
3783
4336
|
refreshFamily();
|
|
3784
4337
|
}
|
|
3785
4338
|
|
|
@@ -3907,7 +4460,7 @@
|
|
|
3907
4460
|
const refField = field("own sprite (optional)", refInput, "A south-facing sprite at the style's size: PixelLab rotates it instead of drawing one, and only the rotations are billed.");
|
|
3908
4461
|
refField.append(refPreview);
|
|
3909
4462
|
baseSection.append(baseRow, field("description", prompt), refField);
|
|
3910
|
-
const
|
|
4463
|
+
const loops2 = [];
|
|
3911
4464
|
const loopsSection = el("section", "studio-part");
|
|
3912
4465
|
const loopList = el("div", "studio-loops");
|
|
3913
4466
|
const addLoop = el("button", "add", "+ Add a loop");
|
|
@@ -3970,7 +4523,7 @@
|
|
|
3970
4523
|
newStyle.hidden = !isNewStyle();
|
|
3971
4524
|
styleId.required = isNewStyle();
|
|
3972
4525
|
if (!outDirTouched) outDir.value = "art/" + (styleId.value.trim() || "characters");
|
|
3973
|
-
for (const row2 of
|
|
4526
|
+
for (const row2 of loops2) syncLoopDirections(row2);
|
|
3974
4527
|
}
|
|
3975
4528
|
function syncLoopDirections(row2) {
|
|
3976
4529
|
const allowed = styleDirections();
|
|
@@ -4015,7 +4568,7 @@
|
|
|
4015
4568
|
remove.type = "button";
|
|
4016
4569
|
const row2 = { id, mode, what, directions: boxes, wrap };
|
|
4017
4570
|
remove.onclick = () => {
|
|
4018
|
-
|
|
4571
|
+
loops2.splice(loops2.indexOf(row2), 1);
|
|
4019
4572
|
wrap.remove();
|
|
4020
4573
|
schedulePrice();
|
|
4021
4574
|
};
|
|
@@ -4027,7 +4580,7 @@
|
|
|
4027
4580
|
line.append(field("loop id", id), field("mode", mode), field("template or motion", what));
|
|
4028
4581
|
wrap.append(line, dirs, remove);
|
|
4029
4582
|
loopList.append(wrap);
|
|
4030
|
-
|
|
4583
|
+
loops2.push(row2);
|
|
4031
4584
|
syncLoopMode(row2);
|
|
4032
4585
|
syncLoopDirections(row2);
|
|
4033
4586
|
}
|
|
@@ -4044,7 +4597,7 @@
|
|
|
4044
4597
|
}
|
|
4045
4598
|
const base = baseId.value.trim();
|
|
4046
4599
|
out.push({ action: "add-asset", assetId: base, asset: { prompt: prompt.value.trim(), styles: [sid], ...reference ? { reference } : {} } });
|
|
4047
|
-
for (const row2 of
|
|
4600
|
+
for (const row2 of loops2) {
|
|
4048
4601
|
const chosen = [...row2.directions].filter(([, box]) => box.checked).map(([d]) => d);
|
|
4049
4602
|
if (!row2.id.value.trim() || !chosen.length) continue;
|
|
4050
4603
|
const animation = { of: base, directions: chosen };
|
|
@@ -4266,7 +4819,23 @@
|
|
|
4266
4819
|
}
|
|
4267
4820
|
|
|
4268
4821
|
// src/gallery/client/src/main.ts
|
|
4822
|
+
loadPrefs();
|
|
4269
4823
|
$("refresh").onclick = refresh;
|
|
4824
|
+
$("select").onclick = () => setSelecting(!ui.selecting);
|
|
4825
|
+
$("playloops").checked = ui.playLoops;
|
|
4826
|
+
$("playloops").onchange = (e) => {
|
|
4827
|
+
setPlayLoops(e.target.checked);
|
|
4828
|
+
savePrefs();
|
|
4829
|
+
};
|
|
4830
|
+
$("backdrop-slot").replaceWith(backdropControl());
|
|
4831
|
+
$("notify-chip").hidden = !GENERATION || typeof Notification === "undefined";
|
|
4832
|
+
$("notify").checked = ui.notify && typeof Notification !== "undefined" && Notification.permission === "granted";
|
|
4833
|
+
$("notify").onchange = async (e) => {
|
|
4834
|
+
const on = e.target.checked && await enableNotifications();
|
|
4835
|
+
e.target.checked = on;
|
|
4836
|
+
ui.notify = on;
|
|
4837
|
+
savePrefs();
|
|
4838
|
+
};
|
|
4270
4839
|
$("studio").hidden = !EDITABLE;
|
|
4271
4840
|
$("studio").onclick = openStudio;
|
|
4272
4841
|
var autoTimer = null;
|
|
@@ -4315,10 +4884,15 @@
|
|
|
4315
4884
|
if (ui.open) {
|
|
4316
4885
|
e.preventDefault();
|
|
4317
4886
|
closeItem();
|
|
4887
|
+
return;
|
|
4888
|
+
}
|
|
4889
|
+
if (ui.selecting) {
|
|
4890
|
+
e.preventDefault();
|
|
4891
|
+
setSelecting(false);
|
|
4318
4892
|
}
|
|
4319
4893
|
return;
|
|
4320
4894
|
}
|
|
4321
|
-
if (ui.open && !typing) {
|
|
4895
|
+
if (ui.open && !typing && !$("dialog-host").childNodes.length) {
|
|
4322
4896
|
if (e.key === "ArrowLeft") {
|
|
4323
4897
|
e.preventDefault();
|
|
4324
4898
|
step(-1);
|
|
@@ -4326,6 +4900,14 @@
|
|
|
4326
4900
|
e.preventDefault();
|
|
4327
4901
|
step(1);
|
|
4328
4902
|
}
|
|
4903
|
+
const frames = drawerFrames();
|
|
4904
|
+
if (frames && (e.key === "," || e.key === ".")) {
|
|
4905
|
+
e.preventDefault();
|
|
4906
|
+
frames.step(e.key === "," ? -1 : 1);
|
|
4907
|
+
} else if (frames && e.key === " " && !/^(BUTTON|A)$/.test(document.activeElement?.tagName || "")) {
|
|
4908
|
+
e.preventDefault();
|
|
4909
|
+
frames.toggle();
|
|
4910
|
+
}
|
|
4329
4911
|
}
|
|
4330
4912
|
});
|
|
4331
4913
|
document.addEventListener("visibilitychange", () => {
|