pixelkiln 0.75.0 → 0.77.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 +143 -15
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.css +48 -15
- package/dist/client/gallery.js +374 -60
- package/dist/index.cjs +38 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +31 -1
- package/dist/index.d.ts +31 -1
- package/dist/index.js +37 -1
- package/dist/index.js.map +1 -1
- package/docs/CHARACTERS.md +5 -2
- package/docs/CLI.md +21 -0
- package/docs/GENERATORS.md +3 -0
- package/package.json +1 -1
package/dist/client/gallery.js
CHANGED
|
@@ -39,8 +39,72 @@
|
|
|
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
|
|
43
48
|
};
|
|
49
|
+
var PREFS_KEY = "pixelkiln.gallery.prefs";
|
|
50
|
+
var PREF_NAMES = ["playLoops", "backdrop", "notify", "onion"];
|
|
51
|
+
function loadPrefs() {
|
|
52
|
+
try {
|
|
53
|
+
const saved = JSON.parse(localStorage.getItem(PREFS_KEY) || "{}");
|
|
54
|
+
for (const name of PREF_NAMES) if (typeof saved[name] === typeof ui[name]) ui[name] = saved[name];
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
applyBackdrop();
|
|
58
|
+
}
|
|
59
|
+
function savePrefs() {
|
|
60
|
+
try {
|
|
61
|
+
localStorage.setItem(PREFS_KEY, JSON.stringify(Object.fromEntries(PREF_NAMES.map((n) => [n, ui[n]]))));
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
var HEX = /^#[0-9a-f]{6}$/i;
|
|
66
|
+
function applyBackdrop() {
|
|
67
|
+
const custom = HEX.test(ui.backdrop);
|
|
68
|
+
document.body.dataset.backdrop = custom ? "custom" : ui.backdrop;
|
|
69
|
+
document.body.style.setProperty("--stage-bg", custom ? ui.backdrop : "");
|
|
70
|
+
if (!custom) document.body.style.removeProperty("--stage-bg");
|
|
71
|
+
}
|
|
72
|
+
function backdropControl(onChange) {
|
|
73
|
+
const wrap = el("span", "backdrop");
|
|
74
|
+
wrap.title = "What sprites sit on: judge them against dark, light, or your game's own background";
|
|
75
|
+
const options = [["checker", "Transparency grid"], ["dark", "Dark"], ["light", "Light"]];
|
|
76
|
+
const buttons = [];
|
|
77
|
+
const picker = el("input");
|
|
78
|
+
picker.type = "color";
|
|
79
|
+
picker.setAttribute("aria-label", "Custom backdrop colour");
|
|
80
|
+
const sync = () => {
|
|
81
|
+
for (const b of buttons) b.classList.toggle("on", b.dataset.value === ui.backdrop);
|
|
82
|
+
picker.classList.toggle("on", HEX.test(ui.backdrop));
|
|
83
|
+
if (HEX.test(ui.backdrop)) picker.value = ui.backdrop;
|
|
84
|
+
};
|
|
85
|
+
const set = (value) => {
|
|
86
|
+
ui.backdrop = value;
|
|
87
|
+
applyBackdrop();
|
|
88
|
+
savePrefs();
|
|
89
|
+
for (const other of document.querySelectorAll(".backdrop")) other.__sync?.();
|
|
90
|
+
onChange?.();
|
|
91
|
+
};
|
|
92
|
+
for (const [value, label] of options) {
|
|
93
|
+
const b = el("button", "bd-" + value);
|
|
94
|
+
b.type = "button";
|
|
95
|
+
b.dataset.value = value;
|
|
96
|
+
b.title = label;
|
|
97
|
+
b.setAttribute("aria-label", "Backdrop: " + label);
|
|
98
|
+
b.onclick = () => set(value);
|
|
99
|
+
buttons.push(b);
|
|
100
|
+
wrap.append(b);
|
|
101
|
+
}
|
|
102
|
+
picker.oninput = () => set(picker.value.toLowerCase());
|
|
103
|
+
wrap.append(picker);
|
|
104
|
+
wrap.__sync = sync;
|
|
105
|
+
sync();
|
|
106
|
+
return wrap;
|
|
107
|
+
}
|
|
44
108
|
var formRegion = () => !ui.editing ? null : /^(new|style|cand):/.test(ui.editing) ? "main" : "drawer";
|
|
45
109
|
var $ = (id) => document.getElementById(id);
|
|
46
110
|
var el = (tag, cls, text) => {
|
|
@@ -190,7 +254,7 @@
|
|
|
190
254
|
};
|
|
191
255
|
zoomBar.append(b);
|
|
192
256
|
}
|
|
193
|
-
bar.append(zoomBar);
|
|
257
|
+
bar.append(zoomBar, backdropControl());
|
|
194
258
|
const zoom = ui.compareZoom === "auto" ? auto : ui.compareZoom === "fit" ? Math.min(auto, 1) : Number(ui.compareZoom);
|
|
195
259
|
const body = el("div", "compare-body");
|
|
196
260
|
const table = el("table");
|
|
@@ -856,6 +920,98 @@
|
|
|
856
920
|
return form;
|
|
857
921
|
}
|
|
858
922
|
|
|
923
|
+
// src/gallery/client/src/motion.ts
|
|
924
|
+
var loops = /* @__PURE__ */ new Map();
|
|
925
|
+
var raf = null;
|
|
926
|
+
var t0 = 0;
|
|
927
|
+
var seen = typeof IntersectionObserver === "function" ? new IntersectionObserver((entries) => {
|
|
928
|
+
for (const e of entries) {
|
|
929
|
+
const loop = loops.get(e.target);
|
|
930
|
+
if (loop) loop.visible = e.isIntersecting;
|
|
931
|
+
}
|
|
932
|
+
wake();
|
|
933
|
+
}) : null;
|
|
934
|
+
function registerLoop(img, urls, fps, hoverTarget) {
|
|
935
|
+
if (urls.length < 2) return;
|
|
936
|
+
const loop = { img, urls, fps: fps || 12, index: 0, visible: !seen, hovered: false, first: img.src };
|
|
937
|
+
loops.set(img, loop);
|
|
938
|
+
seen?.observe(img);
|
|
939
|
+
let warmed = false;
|
|
940
|
+
const warm = () => {
|
|
941
|
+
if (warmed) return;
|
|
942
|
+
warmed = true;
|
|
943
|
+
for (const u of urls) {
|
|
944
|
+
const p = new Image();
|
|
945
|
+
p.src = u;
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
hoverTarget.addEventListener("pointerenter", () => {
|
|
949
|
+
warm();
|
|
950
|
+
loop.hovered = true;
|
|
951
|
+
wake();
|
|
952
|
+
});
|
|
953
|
+
hoverTarget.addEventListener("pointerleave", () => {
|
|
954
|
+
loop.hovered = false;
|
|
955
|
+
rest(loop);
|
|
956
|
+
});
|
|
957
|
+
hoverTarget.addEventListener("focus", () => {
|
|
958
|
+
warm();
|
|
959
|
+
loop.hovered = true;
|
|
960
|
+
wake();
|
|
961
|
+
});
|
|
962
|
+
hoverTarget.addEventListener("blur", () => {
|
|
963
|
+
loop.hovered = false;
|
|
964
|
+
rest(loop);
|
|
965
|
+
});
|
|
966
|
+
if (ui.playLoops) warm();
|
|
967
|
+
}
|
|
968
|
+
function pruneLoops() {
|
|
969
|
+
for (const [img, loop] of loops) if (!img.isConnected) {
|
|
970
|
+
seen?.unobserve(img);
|
|
971
|
+
loops.delete(img);
|
|
972
|
+
void loop;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
function setPlayLoops(on) {
|
|
976
|
+
ui.playLoops = on;
|
|
977
|
+
if (on) for (const loop of loops.values()) for (const u of loop.urls) {
|
|
978
|
+
const p = new Image();
|
|
979
|
+
p.src = u;
|
|
980
|
+
}
|
|
981
|
+
else for (const loop of loops.values()) if (!loop.hovered) rest(loop);
|
|
982
|
+
wake();
|
|
983
|
+
}
|
|
984
|
+
function rest(loop) {
|
|
985
|
+
if (ui.playLoops) return;
|
|
986
|
+
loop.index = 0;
|
|
987
|
+
if (loop.img.src !== loop.first) loop.img.src = loop.first;
|
|
988
|
+
}
|
|
989
|
+
function wake() {
|
|
990
|
+
if (raf === null && typeof requestAnimationFrame === "function") {
|
|
991
|
+
t0 = performance.now();
|
|
992
|
+
raf = requestAnimationFrame(tick);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
function tick(now) {
|
|
996
|
+
raf = null;
|
|
997
|
+
let active = false;
|
|
998
|
+
const seconds = Math.max(0, (now - t0) / 1e3);
|
|
999
|
+
for (const [img, loop] of loops) {
|
|
1000
|
+
if (!img.isConnected) continue;
|
|
1001
|
+
if (!loop.hovered && !(ui.playLoops && loop.visible)) continue;
|
|
1002
|
+
active = true;
|
|
1003
|
+
const i = Math.floor(seconds * loop.fps) % loop.urls.length;
|
|
1004
|
+
if (i !== loop.index) {
|
|
1005
|
+
loop.index = i;
|
|
1006
|
+
img.src = loop.urls[i];
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (active && !document.hidden) raf = requestAnimationFrame(tick);
|
|
1010
|
+
}
|
|
1011
|
+
document.addEventListener("visibilitychange", () => {
|
|
1012
|
+
if (!document.hidden) wake();
|
|
1013
|
+
});
|
|
1014
|
+
|
|
859
1015
|
// src/gallery/client/src/grid.ts
|
|
860
1016
|
function visibleItems() {
|
|
861
1017
|
const q = ui.q.trim().toLowerCase();
|
|
@@ -1064,11 +1220,15 @@
|
|
|
1064
1220
|
if (e.shiftKey) toggleCompare(item.id);
|
|
1065
1221
|
else openItem(item.id);
|
|
1066
1222
|
};
|
|
1223
|
+
const still = cell.querySelector(":scope > img");
|
|
1224
|
+
const shown = item.edit && item.editStatus === "edited" && item.edits.every((e) => e.url) ? item.edits : item.outputs.filter((o) => o.url);
|
|
1225
|
+
if (still && isFrameSet(item) && shown.length > 1) registerLoop(still, shown.map((o) => o.url), item.fps || 12, c);
|
|
1067
1226
|
return c;
|
|
1068
1227
|
}
|
|
1069
1228
|
function renderMain(items) {
|
|
1070
1229
|
const root = $("root");
|
|
1071
1230
|
root.textContent = "";
|
|
1231
|
+
pruneLoops();
|
|
1072
1232
|
if (!S.snap.items.length) {
|
|
1073
1233
|
const e = el("div", "empty");
|
|
1074
1234
|
e.append(el("p", null, "Nothing has been generated for this project yet."));
|
|
@@ -1464,6 +1624,7 @@
|
|
|
1464
1624
|
if (!ACTIVE_PHASES.has(job.phase) && !ui.settled.has(job.id)) {
|
|
1465
1625
|
ui.settled.add(job.id);
|
|
1466
1626
|
changed = true;
|
|
1627
|
+
notifyJob(job);
|
|
1467
1628
|
const open = ui.open && S.snap.items.find((i) => i.id === ui.open);
|
|
1468
1629
|
if (open && job.project === open.project && job.keys.includes(open.key)) {
|
|
1469
1630
|
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 +1633,7 @@
|
|
|
1472
1633
|
if (job.phase === "review" && !ui.settled.has(job.id + ":review")) {
|
|
1473
1634
|
ui.settled.add(job.id + ":review");
|
|
1474
1635
|
changed = true;
|
|
1636
|
+
notifyJob(job);
|
|
1475
1637
|
}
|
|
1476
1638
|
}
|
|
1477
1639
|
if (changed) refresh();
|
|
@@ -1489,6 +1651,34 @@
|
|
|
1489
1651
|
clearTimeout(jobsTimer ?? void 0);
|
|
1490
1652
|
if (active) jobsTimer = setTimeout(pollJobs, 2e3);
|
|
1491
1653
|
}
|
|
1654
|
+
var DONE_VERB = { generate: "Generated", resume: "Resumed", refresh: "Pulled upstream changes for", restore: "Restored", revert: "Brought back a generation of" };
|
|
1655
|
+
async function enableNotifications() {
|
|
1656
|
+
if (typeof Notification === "undefined") return false;
|
|
1657
|
+
if (Notification.permission === "granted") return true;
|
|
1658
|
+
if (Notification.permission === "denied") {
|
|
1659
|
+
$("note").textContent = " Notifications are blocked for this page in the browser's site settings.";
|
|
1660
|
+
return false;
|
|
1661
|
+
}
|
|
1662
|
+
try {
|
|
1663
|
+
return await Notification.requestPermission() === "granted";
|
|
1664
|
+
} catch {
|
|
1665
|
+
return false;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
function notifyJob(job) {
|
|
1669
|
+
if (!ui.notify || !document.hidden || typeof Notification === "undefined" || Notification.permission !== "granted") return;
|
|
1670
|
+
const n = job.keys.length;
|
|
1671
|
+
const what = n === 1 ? job.keys[0] : n + " assets";
|
|
1672
|
+
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;
|
|
1673
|
+
try {
|
|
1674
|
+
const note = new Notification("pixelkiln" + (job.project ? " · " + job.project : ""), { body, tag: "pixelkiln-job-" + job.id });
|
|
1675
|
+
note.onclick = () => {
|
|
1676
|
+
window.focus();
|
|
1677
|
+
note.close();
|
|
1678
|
+
};
|
|
1679
|
+
} catch {
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1492
1682
|
function renderJobs() {
|
|
1493
1683
|
const host = $("jobs");
|
|
1494
1684
|
host.textContent = "";
|
|
@@ -2135,6 +2325,7 @@
|
|
|
2135
2325
|
var SPEEDS = [0.5, 1, 2];
|
|
2136
2326
|
var SPIN_MS = 420;
|
|
2137
2327
|
var DRAG_STEP_PX = 28;
|
|
2328
|
+
var CELL_PX = 68;
|
|
2138
2329
|
var F = null;
|
|
2139
2330
|
var reducedMotion = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
2140
2331
|
var byKey = (project, key) => S.snap.items.find((i) => i.key === key && i.project === project);
|
|
@@ -2243,7 +2434,7 @@
|
|
|
2243
2434
|
t.append(el("i", "dot " + STATE_TONE[state]), document.createTextNode(n + " " + state));
|
|
2244
2435
|
tally.append(t);
|
|
2245
2436
|
}
|
|
2246
|
-
bar.append(tally);
|
|
2437
|
+
bar.append(tally, backdropControl());
|
|
2247
2438
|
if (GENERATION) {
|
|
2248
2439
|
const todo = members.filter((m) => m.declared && m.currentSpecHash !== null && ["missing", "stale", "failed"].includes(m.state));
|
|
2249
2440
|
if (todo.length) {
|
|
@@ -2270,7 +2461,7 @@
|
|
|
2270
2461
|
host.append(scrim, panel);
|
|
2271
2462
|
panel.scrollTop = scroll;
|
|
2272
2463
|
highlight(F.direction);
|
|
2273
|
-
if (F.raf === null) F.raf = requestAnimationFrame(
|
|
2464
|
+
if (F.raf === null) F.raf = requestAnimationFrame(tick2);
|
|
2274
2465
|
}
|
|
2275
2466
|
function turntable(root, turnables, source, order) {
|
|
2276
2467
|
const wrap = el("section", "turntable");
|
|
@@ -2407,11 +2598,11 @@
|
|
|
2407
2598
|
return wrap;
|
|
2408
2599
|
}
|
|
2409
2600
|
function loopGrid(root, members, order) {
|
|
2410
|
-
const
|
|
2601
|
+
const loops2 = members.filter((m) => m.character.kind === "animation");
|
|
2411
2602
|
const wrap = el("section", "loop-grid");
|
|
2412
2603
|
const head = el("div", "lg-head");
|
|
2413
|
-
head.append(el("h3", null,
|
|
2414
|
-
if (
|
|
2604
|
+
head.append(el("h3", null, loops2.length ? "Loops" : "No loops yet"));
|
|
2605
|
+
if (loops2.length) {
|
|
2415
2606
|
const play = el("button", null, F.playing ? "Pause" : "Play");
|
|
2416
2607
|
play.type = "button";
|
|
2417
2608
|
play.onclick = () => {
|
|
@@ -2431,14 +2622,14 @@
|
|
|
2431
2622
|
head.append(speed);
|
|
2432
2623
|
}
|
|
2433
2624
|
wrap.append(head);
|
|
2434
|
-
if (!
|
|
2625
|
+
if (!loops2.length) {
|
|
2435
2626
|
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
2627
|
return wrap;
|
|
2437
2628
|
}
|
|
2438
|
-
const used = /* @__PURE__ */ new Set([...order, ...
|
|
2629
|
+
const used = /* @__PURE__ */ new Set([...order, ...loops2.map((l) => l.character.direction).filter(Boolean)]);
|
|
2439
2630
|
const columns = COMPASS.filter((d) => used.has(d));
|
|
2440
2631
|
const rows = /* @__PURE__ */ new Map();
|
|
2441
|
-
for (const l of
|
|
2632
|
+
for (const l of loops2) {
|
|
2442
2633
|
const k = (l.character.parentKey || "") + "|" + loopName(l);
|
|
2443
2634
|
if (!rows.has(k)) rows.set(k, []);
|
|
2444
2635
|
rows.get(k).push(l);
|
|
@@ -2505,6 +2696,7 @@
|
|
|
2505
2696
|
img.alt = item.assetId;
|
|
2506
2697
|
const urls = frameUrls(item);
|
|
2507
2698
|
if (urls.length) {
|
|
2699
|
+
fitCell(img, item.width, item.height);
|
|
2508
2700
|
img.src = urls[0];
|
|
2509
2701
|
for (const u of urls) {
|
|
2510
2702
|
const p = new Image();
|
|
@@ -2514,6 +2706,7 @@
|
|
|
2514
2706
|
} else {
|
|
2515
2707
|
const ghost = rotationUrl(parent, direction);
|
|
2516
2708
|
if (ghost) {
|
|
2709
|
+
fitCell(img, parent.width, parent.height);
|
|
2517
2710
|
img.src = ghost;
|
|
2518
2711
|
b.classList.add("ghost");
|
|
2519
2712
|
} else img.hidden = true;
|
|
@@ -2539,6 +2732,7 @@
|
|
|
2539
2732
|
img.alt = item.assetId;
|
|
2540
2733
|
const urls = frameUrls(item);
|
|
2541
2734
|
if (urls.length) {
|
|
2735
|
+
fitCell(img, item.width, item.height);
|
|
2542
2736
|
img.src = urls[0];
|
|
2543
2737
|
if (urls.length > 1) F.anims.push({ img, urls, fps: item.fps || 12, index: 0 });
|
|
2544
2738
|
} else img.hidden = true;
|
|
@@ -2551,11 +2745,16 @@
|
|
|
2551
2745
|
wrap.append(strip);
|
|
2552
2746
|
return wrap;
|
|
2553
2747
|
}
|
|
2748
|
+
function fitCell(img, width, height) {
|
|
2749
|
+
const s = displayScale(width, height, CELL_PX, CELL_PX);
|
|
2750
|
+
img.width = Math.max(1, Math.round(width * s));
|
|
2751
|
+
img.height = Math.max(1, Math.round(height * s));
|
|
2752
|
+
}
|
|
2554
2753
|
function highlight(direction) {
|
|
2555
2754
|
if (!F) return;
|
|
2556
2755
|
for (const [d, nodes] of F.columns) for (const n of nodes) n.classList.toggle("col-on", d === direction);
|
|
2557
2756
|
}
|
|
2558
|
-
function
|
|
2757
|
+
function tick2(now) {
|
|
2559
2758
|
if (!F) return;
|
|
2560
2759
|
if (!F.panel || !F.panel.isConnected) {
|
|
2561
2760
|
stopFamily();
|
|
@@ -2577,7 +2776,7 @@
|
|
|
2577
2776
|
const order = source ? turnOrder(source) : COMPASS;
|
|
2578
2777
|
F.turn(order[(order.indexOf(F.direction) + 1) % order.length]);
|
|
2579
2778
|
}
|
|
2580
|
-
F.raf = requestAnimationFrame(
|
|
2779
|
+
F.raf = requestAnimationFrame(tick2);
|
|
2581
2780
|
}
|
|
2582
2781
|
var familyState = () => F && { rootId: F.rootId, source: F.source, direction: F.direction, playing: F.playing, anims: F.anims.length };
|
|
2583
2782
|
|
|
@@ -2932,11 +3131,11 @@
|
|
|
2932
3131
|
for (const [i, pose] of [set.firstFrameKeypoints, ...set.frames].entries()) {
|
|
2933
3132
|
const where = i === 0 ? "the starting pose" : "frame " + i;
|
|
2934
3133
|
if (!Array.isArray(pose) || pose.length !== 18) return where + " needs all 18 joints";
|
|
2935
|
-
const
|
|
3134
|
+
const seen2 = /* @__PURE__ */ new Set();
|
|
2936
3135
|
for (const j of pose) {
|
|
2937
3136
|
if (!SKELETON_LABELS.includes(j && j.label)) return where + ': "' + (j && j.label) + `" is not one of PixelLab's 18 joints`;
|
|
2938
|
-
if (
|
|
2939
|
-
|
|
3137
|
+
if (seen2.has(j.label)) return where + ': "' + j.label + '" appears twice';
|
|
3138
|
+
seen2.add(j.label);
|
|
2940
3139
|
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
3140
|
}
|
|
2942
3141
|
}
|
|
@@ -3255,25 +3454,34 @@
|
|
|
3255
3454
|
host.append(preview);
|
|
3256
3455
|
return;
|
|
3257
3456
|
}
|
|
3258
|
-
|
|
3259
|
-
const frameSet = isFrameSet(item);
|
|
3457
|
+
let idx = Math.min(ui.member, shown.length - 1);
|
|
3458
|
+
const frameSet = isFrameSet(item) && shown.length > 1;
|
|
3260
3459
|
const w = item.width, h = item.height;
|
|
3261
3460
|
const box = Math.min(window.innerWidth, 540) - 62;
|
|
3262
3461
|
const auto = displayScale(w, h, box, Math.min(window.innerHeight * 0.55, 560));
|
|
3263
3462
|
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
|
-
|
|
3463
|
+
const sized = (img2) => {
|
|
3464
|
+
if (zoom >= 1) {
|
|
3465
|
+
img2.width = Math.round(w * zoom);
|
|
3466
|
+
img2.height = Math.round(h * zoom);
|
|
3467
|
+
} else {
|
|
3468
|
+
img2.style.maxWidth = "100%";
|
|
3469
|
+
img2.style.maxHeight = "55vh";
|
|
3470
|
+
img2.style.width = "auto";
|
|
3471
|
+
img2.style.height = "auto";
|
|
3472
|
+
}
|
|
3473
|
+
return img2;
|
|
3474
|
+
};
|
|
3475
|
+
const stack = el("div", "stack");
|
|
3476
|
+
const img = sized(el("img"));
|
|
3477
|
+
img.src = shown[idx].url;
|
|
3478
|
+
img.alt = shown[idx].role || item.assetId;
|
|
3479
|
+
const onion = sized(el("img", "onion"));
|
|
3480
|
+
onion.alt = "";
|
|
3481
|
+
onion.setAttribute("aria-hidden", "true");
|
|
3482
|
+
onion.hidden = !(frameSet && ui.onion);
|
|
3483
|
+
stack.append(onion, img);
|
|
3484
|
+
preview.append(stack);
|
|
3277
3485
|
const zoomBar = el("div", "zoom");
|
|
3278
3486
|
zoomBar.append(el("span", null, "zoom"));
|
|
3279
3487
|
for (const z of ["fit", 1, 2, 4, 8]) {
|
|
@@ -3286,34 +3494,98 @@
|
|
|
3286
3494
|
zoomBar.append(b);
|
|
3287
3495
|
}
|
|
3288
3496
|
const label = el("span", "grow", w + "×" + h + (zoom >= 1 ? " at " + zoom + "×" : " fitted"));
|
|
3289
|
-
zoomBar.append(label);
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3497
|
+
zoomBar.append(label, backdropControl());
|
|
3498
|
+
host.append(preview, zoomBar);
|
|
3499
|
+
let strip = null;
|
|
3500
|
+
const show = (i) => {
|
|
3501
|
+
idx = (i + shown.length) % shown.length;
|
|
3502
|
+
ui.member = idx;
|
|
3503
|
+
img.src = shown[idx].url;
|
|
3504
|
+
img.alt = shown[idx].role || item.assetId;
|
|
3505
|
+
if (frameSet) {
|
|
3506
|
+
onion.src = shown[(idx - 1 + shown.length) % shown.length].url;
|
|
3507
|
+
scrub.value = String(idx);
|
|
3508
|
+
count.textContent = idx + 1 + " / " + shown.length;
|
|
3509
|
+
}
|
|
3510
|
+
if (strip) for (const [j, b] of [...strip.querySelectorAll("button")].entries()) b.classList.toggle("on", j === idx);
|
|
3511
|
+
};
|
|
3512
|
+
const scrub = el("input");
|
|
3513
|
+
const count = el("span", "count");
|
|
3514
|
+
if (frameSet) {
|
|
3515
|
+
const bar = el("div", "frames-bar");
|
|
3516
|
+
const own = item.fps || 12;
|
|
3517
|
+
const fps = () => ui.fps || own;
|
|
3518
|
+
const prev = el("button", null, "◀");
|
|
3519
|
+
prev.type = "button";
|
|
3520
|
+
prev.title = "Previous frame (,)";
|
|
3521
|
+
const next = el("button", null, "▶");
|
|
3522
|
+
next.type = "button";
|
|
3523
|
+
next.title = "Next frame (.)";
|
|
3524
|
+
const play = el("button", "play", "Play");
|
|
3293
3525
|
play.type = "button";
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3526
|
+
play.title = "Play or pause (space)";
|
|
3527
|
+
const pause = () => {
|
|
3528
|
+
stopPlayback();
|
|
3529
|
+
play.textContent = "Play";
|
|
3530
|
+
};
|
|
3531
|
+
const start = () => {
|
|
3532
|
+
stopPlayback();
|
|
3301
3533
|
play.textContent = "Pause";
|
|
3302
3534
|
ui.playing = setInterval(() => {
|
|
3303
|
-
if (document.hidden)
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3535
|
+
if (!document.hidden) show(idx + 1);
|
|
3536
|
+
}, Math.max(16, Math.round(1e3 / fps())));
|
|
3537
|
+
};
|
|
3538
|
+
play.onclick = () => ui.playing ? pause() : start();
|
|
3539
|
+
prev.onclick = () => {
|
|
3540
|
+
pause();
|
|
3541
|
+
show(idx - 1);
|
|
3542
|
+
};
|
|
3543
|
+
next.onclick = () => {
|
|
3544
|
+
pause();
|
|
3545
|
+
show(idx + 1);
|
|
3546
|
+
};
|
|
3547
|
+
scrub.type = "range";
|
|
3548
|
+
scrub.min = "0";
|
|
3549
|
+
scrub.max = String(shown.length - 1);
|
|
3550
|
+
scrub.step = "1";
|
|
3551
|
+
scrub.setAttribute("aria-label", "Frame");
|
|
3552
|
+
scrub.oninput = () => {
|
|
3553
|
+
pause();
|
|
3554
|
+
show(Number(scrub.value));
|
|
3309
3555
|
};
|
|
3310
|
-
|
|
3556
|
+
const speed = el("select");
|
|
3557
|
+
speed.setAttribute("aria-label", "Frames per second");
|
|
3558
|
+
const rates = [.../* @__PURE__ */ new Set([own, 4, 6, 8, 10, 12, 15, 24])].sort((a, b) => a - b);
|
|
3559
|
+
for (const r of rates) speed.append(new Option(r + " fps" + (r === own ? " (its own)" : ""), String(r)));
|
|
3560
|
+
speed.value = String(fps());
|
|
3561
|
+
speed.onchange = () => {
|
|
3562
|
+
ui.fps = Number(speed.value) === own ? null : Number(speed.value);
|
|
3563
|
+
if (ui.playing) start();
|
|
3564
|
+
};
|
|
3565
|
+
const onionBox = el("label", "chip");
|
|
3566
|
+
const onionToggle = el("input");
|
|
3567
|
+
onionToggle.type = "checkbox";
|
|
3568
|
+
onionToggle.checked = ui.onion;
|
|
3569
|
+
onionToggle.onchange = () => {
|
|
3570
|
+
ui.onion = onionToggle.checked;
|
|
3571
|
+
onion.hidden = !ui.onion;
|
|
3572
|
+
savePrefs();
|
|
3573
|
+
};
|
|
3574
|
+
onionBox.title = "Show the previous frame faintly under this one";
|
|
3575
|
+
onionBox.append(onionToggle, document.createTextNode(" onion skin"));
|
|
3576
|
+
bar.append(prev, play, next, scrub, count, speed, onionBox);
|
|
3577
|
+
host.append(bar);
|
|
3578
|
+
host.__frames = { step: (n) => {
|
|
3579
|
+
pause();
|
|
3580
|
+
show(idx + n);
|
|
3581
|
+
}, toggle: () => play.click() };
|
|
3582
|
+
} else {
|
|
3583
|
+
host.__frames = null;
|
|
3311
3584
|
}
|
|
3312
|
-
host.append(preview, zoomBar);
|
|
3313
3585
|
if (shown.length > 1) {
|
|
3314
|
-
|
|
3586
|
+
strip = el("div", "members");
|
|
3315
3587
|
shown.forEach((o, i) => {
|
|
3316
|
-
const b = el("button",
|
|
3588
|
+
const b = el("button", i === idx ? "on" : null);
|
|
3317
3589
|
b.type = "button";
|
|
3318
3590
|
const t = el("img");
|
|
3319
3591
|
t.src = o.url;
|
|
@@ -3321,13 +3593,21 @@
|
|
|
3321
3593
|
t.loading = "lazy";
|
|
3322
3594
|
b.append(t, el("span", null, o.role || String(i + 1)));
|
|
3323
3595
|
b.onclick = () => {
|
|
3324
|
-
|
|
3325
|
-
|
|
3596
|
+
if (frameSet) {
|
|
3597
|
+
stopPlayback();
|
|
3598
|
+
}
|
|
3599
|
+
show(i);
|
|
3600
|
+
if (!frameSet) renderPreview(item, host);
|
|
3326
3601
|
};
|
|
3327
3602
|
strip.append(b);
|
|
3328
3603
|
});
|
|
3329
3604
|
host.append(strip);
|
|
3330
3605
|
}
|
|
3606
|
+
show(idx);
|
|
3607
|
+
}
|
|
3608
|
+
function drawerFrames() {
|
|
3609
|
+
const host = document.querySelector(".drawer .dbody > div");
|
|
3610
|
+
return host ? host.__frames ?? null : null;
|
|
3331
3611
|
}
|
|
3332
3612
|
var CHARACTER_KIND = {
|
|
3333
3613
|
base: "base",
|
|
@@ -3907,7 +4187,7 @@
|
|
|
3907
4187
|
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
4188
|
refField.append(refPreview);
|
|
3909
4189
|
baseSection.append(baseRow, field("description", prompt), refField);
|
|
3910
|
-
const
|
|
4190
|
+
const loops2 = [];
|
|
3911
4191
|
const loopsSection = el("section", "studio-part");
|
|
3912
4192
|
const loopList = el("div", "studio-loops");
|
|
3913
4193
|
const addLoop = el("button", "add", "+ Add a loop");
|
|
@@ -3970,7 +4250,7 @@
|
|
|
3970
4250
|
newStyle.hidden = !isNewStyle();
|
|
3971
4251
|
styleId.required = isNewStyle();
|
|
3972
4252
|
if (!outDirTouched) outDir.value = "art/" + (styleId.value.trim() || "characters");
|
|
3973
|
-
for (const row2 of
|
|
4253
|
+
for (const row2 of loops2) syncLoopDirections(row2);
|
|
3974
4254
|
}
|
|
3975
4255
|
function syncLoopDirections(row2) {
|
|
3976
4256
|
const allowed = styleDirections();
|
|
@@ -4015,7 +4295,7 @@
|
|
|
4015
4295
|
remove.type = "button";
|
|
4016
4296
|
const row2 = { id, mode, what, directions: boxes, wrap };
|
|
4017
4297
|
remove.onclick = () => {
|
|
4018
|
-
|
|
4298
|
+
loops2.splice(loops2.indexOf(row2), 1);
|
|
4019
4299
|
wrap.remove();
|
|
4020
4300
|
schedulePrice();
|
|
4021
4301
|
};
|
|
@@ -4027,7 +4307,7 @@
|
|
|
4027
4307
|
line.append(field("loop id", id), field("mode", mode), field("template or motion", what));
|
|
4028
4308
|
wrap.append(line, dirs, remove);
|
|
4029
4309
|
loopList.append(wrap);
|
|
4030
|
-
|
|
4310
|
+
loops2.push(row2);
|
|
4031
4311
|
syncLoopMode(row2);
|
|
4032
4312
|
syncLoopDirections(row2);
|
|
4033
4313
|
}
|
|
@@ -4044,7 +4324,7 @@
|
|
|
4044
4324
|
}
|
|
4045
4325
|
const base = baseId.value.trim();
|
|
4046
4326
|
out.push({ action: "add-asset", assetId: base, asset: { prompt: prompt.value.trim(), styles: [sid], ...reference ? { reference } : {} } });
|
|
4047
|
-
for (const row2 of
|
|
4327
|
+
for (const row2 of loops2) {
|
|
4048
4328
|
const chosen = [...row2.directions].filter(([, box]) => box.checked).map(([d]) => d);
|
|
4049
4329
|
if (!row2.id.value.trim() || !chosen.length) continue;
|
|
4050
4330
|
const animation = { of: base, directions: chosen };
|
|
@@ -4114,10 +4394,21 @@
|
|
|
4114
4394
|
const table = el("div", "studio-price-rows");
|
|
4115
4395
|
for (const i of mine) {
|
|
4116
4396
|
const row2 = el("div");
|
|
4117
|
-
|
|
4397
|
+
const cost = el("span", null, i.cost ? fmtCost(i.costUnit, i.cost) : i.quote === "live" ? "free" : "free (mirrored)");
|
|
4398
|
+
if (i.quote === "live") {
|
|
4399
|
+
const tag = el("span", "quote-live", "live");
|
|
4400
|
+
tag.title = "PixelLab's own Pro Flash quote; the offline estimate was " + fmtCost(i.costUnit, i.estimate);
|
|
4401
|
+
cost.append(" ", tag);
|
|
4402
|
+
}
|
|
4403
|
+
row2.append(el("span", "mono", i.key), cost);
|
|
4118
4404
|
table.append(row2);
|
|
4119
4405
|
}
|
|
4120
4406
|
priceBox.append(table);
|
|
4407
|
+
if (mine.some((i) => i.quote === "live")) {
|
|
4408
|
+
priceBox.append(el("small", "state-dim", "Rows marked live are PixelLab's current Pro Flash quote, which it calls provisional; the lock records what the finished job billed."));
|
|
4409
|
+
} else if (price.quoteError) {
|
|
4410
|
+
priceBox.append(el("small", "state-dim", "Pro Flash is priced offline: PixelLab's quote could not be had (" + price.quoteError + ")."));
|
|
4411
|
+
}
|
|
4121
4412
|
const totals = {};
|
|
4122
4413
|
for (const i of mine) totals[i.costUnit] = (totals[i.costUnit] || 0) + i.cost;
|
|
4123
4414
|
priceBox.append(el("div", "studio-total", "Total: " + (Object.entries(totals).map(([u, n]) => fmtCost(u, n)).join(" + ") || "free")));
|
|
@@ -4255,7 +4546,22 @@
|
|
|
4255
4546
|
}
|
|
4256
4547
|
|
|
4257
4548
|
// src/gallery/client/src/main.ts
|
|
4549
|
+
loadPrefs();
|
|
4258
4550
|
$("refresh").onclick = refresh;
|
|
4551
|
+
$("playloops").checked = ui.playLoops;
|
|
4552
|
+
$("playloops").onchange = (e) => {
|
|
4553
|
+
setPlayLoops(e.target.checked);
|
|
4554
|
+
savePrefs();
|
|
4555
|
+
};
|
|
4556
|
+
$("backdrop-slot").replaceWith(backdropControl());
|
|
4557
|
+
$("notify-chip").hidden = !GENERATION || typeof Notification === "undefined";
|
|
4558
|
+
$("notify").checked = ui.notify && typeof Notification !== "undefined" && Notification.permission === "granted";
|
|
4559
|
+
$("notify").onchange = async (e) => {
|
|
4560
|
+
const on = e.target.checked && await enableNotifications();
|
|
4561
|
+
e.target.checked = on;
|
|
4562
|
+
ui.notify = on;
|
|
4563
|
+
savePrefs();
|
|
4564
|
+
};
|
|
4259
4565
|
$("studio").hidden = !EDITABLE;
|
|
4260
4566
|
$("studio").onclick = openStudio;
|
|
4261
4567
|
var autoTimer = null;
|
|
@@ -4307,7 +4613,7 @@
|
|
|
4307
4613
|
}
|
|
4308
4614
|
return;
|
|
4309
4615
|
}
|
|
4310
|
-
if (ui.open && !typing) {
|
|
4616
|
+
if (ui.open && !typing && !$("dialog-host").childNodes.length) {
|
|
4311
4617
|
if (e.key === "ArrowLeft") {
|
|
4312
4618
|
e.preventDefault();
|
|
4313
4619
|
step(-1);
|
|
@@ -4315,6 +4621,14 @@
|
|
|
4315
4621
|
e.preventDefault();
|
|
4316
4622
|
step(1);
|
|
4317
4623
|
}
|
|
4624
|
+
const frames = drawerFrames();
|
|
4625
|
+
if (frames && (e.key === "," || e.key === ".")) {
|
|
4626
|
+
e.preventDefault();
|
|
4627
|
+
frames.step(e.key === "," ? -1 : 1);
|
|
4628
|
+
} else if (frames && e.key === " " && !/^(BUTTON|A)$/.test(document.activeElement?.tagName || "")) {
|
|
4629
|
+
e.preventDefault();
|
|
4630
|
+
frames.toggle();
|
|
4631
|
+
}
|
|
4318
4632
|
}
|
|
4319
4633
|
});
|
|
4320
4634
|
document.addEventListener("visibilitychange", () => {
|