pixelkiln 0.72.0 → 0.74.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.
@@ -341,3 +341,19 @@ textarea.mono { font-family:var(--mono); font-size:11.5px; }
341
341
  .pose-strip svg.pose { width:64px; height:64px; }
342
342
  .pose-strip span { font:10.5px/1 var(--mono); color:var(--dim); }
343
343
  @media (max-width: 640px) { .pose-editor { grid-template-columns:1fr; } }
344
+
345
+ .sheet.studio { max-width:900px; overflow-y:auto; }
346
+ .studio-loop > button { justify-self:start; }
347
+ .studio-form { border:none; margin:0; padding:16px 18px 22px; }
348
+ .studio-part { border-top:1px solid var(--line); padding-top:12px; display:grid; gap:10px; }
349
+ .studio-part h3 { margin:0; font:650 12.5px/1 var(--mono); color:var(--accent-soft); }
350
+ .studio-new-style { display:grid; gap:10px; }
351
+ .studio-loops { display:grid; gap:10px; }
352
+ .studio-loop { border:1px solid var(--line); padding:10px; display:grid; gap:8px; }
353
+ .studio-dirs { display:flex; flex-wrap:wrap; gap:4px 12px; }
354
+ .studio-dirs .field.check { font-size:11.5px; }
355
+ .studio-ref { image-rendering:pixelated; width:96px; height:96px; object-fit:contain; background:var(--panel-deep); border:1px solid var(--line); }
356
+ .studio-price-rows { display:grid; gap:3px; font-size:12px; }
357
+ .studio-price-rows > div { display:flex; justify-content:space-between; gap:12px; border-bottom:1px dotted var(--line); padding:2px 0; }
358
+ .studio-price-rows .mono { font-family:var(--mono); color:var(--dim); }
359
+ .studio-total { font:650 12.5px/1.4 var(--mono); }
@@ -3331,8 +3331,460 @@
3331
3331
  }
3332
3332
  }
3333
3333
 
3334
+ // src/gallery/client/src/studio.ts
3335
+ var ENGINES = [
3336
+ { id: "v3", label: "v3: PixelLab's recommended base engine (2 to 9 by size; 1 from a sprite)", min: 32, max: 256, directions: [8] },
3337
+ { id: "pro-flash", label: "pro-flash: the newest image model draws the south sprite (6 to 17 by size)", min: 16, max: 256, directions: [8] },
3338
+ { id: "pro", label: "pro: reference-based, 20 to 40 by size", min: 32, max: 168, directions: [8] },
3339
+ { id: "standard", label: "standard: skeleton template, 1 generation", min: 16, max: 256, directions: [8, 4] }
3340
+ ];
3341
+ var BODIES = ["mannequin", "bear", "cat", "dog", "horse", "lion"];
3342
+ var VIEWS = ["low top-down", "high top-down", "side"];
3343
+ var COMPASS = ["south", "south-east", "east", "north-east", "north", "north-west", "west", "south-west"];
3344
+ var FOUR = ["south", "east", "north", "west"];
3345
+ var PORTRAIT_SIZES = [16, 32, 48, 64, 128, 160];
3346
+ var LOOP_MODES = [
3347
+ { id: "skeleton-v3", label: "skeleton-v3: a template, posed by the skeleton model (steadiest; 2-4 per direction)" },
3348
+ { id: "template", label: "template: a template, redrawn (1 per direction)" },
3349
+ { id: "v3", label: "v3: described in words" },
3350
+ { id: "pro", label: "pro: described in words, sequential (20-40 per direction)" }
3351
+ ];
3352
+ var templated = (mode) => mode === "skeleton-v3" || mode === "template";
3353
+ var idPattern = "[^\\/\\\\:]+";
3354
+ function openStudio() {
3355
+ const host = $("dialog-host");
3356
+ host.textContent = "";
3357
+ const projects = S.snap.workspace ? S.snap.workspace.projects.filter((pr) => pr.manifestSha256) : S.snap.project && S.snap.project.manifestSha256 ? [S.snap.project] : [];
3358
+ if (!projects.length) return;
3359
+ const workspace = Boolean(S.snap.workspace);
3360
+ let priceTimer = null;
3361
+ let priceSeq = 0;
3362
+ const scrim = el("div", "sheet-scrim");
3363
+ const panel = el("div", "sheet studio");
3364
+ let closed = false;
3365
+ const close = () => {
3366
+ closed = true;
3367
+ if (priceTimer) clearTimeout(priceTimer);
3368
+ priceSeq++;
3369
+ host.textContent = "";
3370
+ };
3371
+ const bar = el("div", "rbar");
3372
+ const back = el("button", null, "Back to gallery");
3373
+ back.type = "button";
3374
+ back.onclick = close;
3375
+ bar.append(back, el("span", null, "A new character, priced as you draft it. Nothing is written until you create it."));
3376
+ scrim.onclick = close;
3377
+ const form = el("form", "edit studio-form");
3378
+ panel.append(bar, form);
3379
+ host.append(scrim, panel);
3380
+ const project = el("select");
3381
+ for (const pr of projects) project.append(new Option(pr.name, pr.id));
3382
+ const style = el("select");
3383
+ const styleId = el("input");
3384
+ styleId.type = "text";
3385
+ styleId.value = "characters";
3386
+ styleId.pattern = idPattern;
3387
+ const engine = el("select");
3388
+ for (const e of ENGINES) engine.append(new Option(e.label, e.id));
3389
+ const size = el("input");
3390
+ size.type = "number";
3391
+ size.value = "64";
3392
+ size.step = "1";
3393
+ const directions = el("select");
3394
+ const body = el("select");
3395
+ const view = el("select");
3396
+ for (const v of VIEWS) view.append(new Option(v, v));
3397
+ const outDir = el("input");
3398
+ outDir.type = "text";
3399
+ let outDirTouched = false;
3400
+ outDir.oninput = () => {
3401
+ outDirTouched = true;
3402
+ };
3403
+ const styleSection = el("section", "studio-part");
3404
+ styleSection.append(el("h3", null, "Style"));
3405
+ const styleRow = el("div", "row");
3406
+ if (workspace) styleRow.append(field("project", project));
3407
+ styleRow.append(field("style", style, "A character style sets the engine, size, and directions every base in it shares."));
3408
+ const newStyle = el("div", "studio-new-style");
3409
+ const newRow1 = el("div", "row");
3410
+ newRow1.append(field("new style id", styleId), field("size (px)", size), field("directions", directions));
3411
+ const newRow2 = el("div", "row");
3412
+ newRow2.append(field("body", body, "The skeleton template loops move: a biped, or a quadruped."), field("view", view), field("art folder", outDir));
3413
+ newStyle.append(field("engine", engine), newRow1, newRow2);
3414
+ styleSection.append(styleRow, newStyle);
3415
+ const baseId = el("input");
3416
+ baseId.type = "text";
3417
+ baseId.required = true;
3418
+ baseId.placeholder = "e.g. mira";
3419
+ baseId.pattern = idPattern;
3420
+ const prompt = el("textarea");
3421
+ prompt.required = true;
3422
+ prompt.placeholder = 'Who they are: "a young knight in dented silver armour, red scarf".';
3423
+ const refInput = el("input");
3424
+ refInput.type = "file";
3425
+ refInput.accept = "image/png,image/jpeg";
3426
+ const refPreview = el("img", "studio-ref");
3427
+ refPreview.hidden = true;
3428
+ refPreview.alt = "reference sprite";
3429
+ let reference = null;
3430
+ const baseSection = el("section", "studio-part");
3431
+ baseSection.append(el("h3", null, "Character"));
3432
+ const baseRow = el("div", "row");
3433
+ baseRow.append(field("asset id", baseId));
3434
+ 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.");
3435
+ refField.append(refPreview);
3436
+ baseSection.append(baseRow, field("description", prompt), refField);
3437
+ const loops = [];
3438
+ const loopsSection = el("section", "studio-part");
3439
+ const loopList = el("div", "studio-loops");
3440
+ const addLoop = el("button", "add", "+ Add a loop");
3441
+ addLoop.type = "button";
3442
+ 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);
3443
+ const portrait = el("input");
3444
+ portrait.type = "checkbox";
3445
+ const portraitSize = el("select");
3446
+ for (const s of PORTRAIT_SIZES) portraitSize.append(new Option(s + " px", String(s)));
3447
+ portraitSize.value = "64";
3448
+ const portraitSection = el("section", "studio-part");
3449
+ const portraitCheck = el("label", "field check");
3450
+ portraitCheck.append(portrait, el("span", null, "add a bust portrait (20 to 40 generations)"));
3451
+ portraitSection.append(el("h3", null, "Portrait"), portraitCheck, field("portrait size", portraitSize));
3452
+ const priceBox = el("section", "studio-part studio-price");
3453
+ const actions = el("div", "actions");
3454
+ const create = el("button", null, "Create");
3455
+ create.type = "submit";
3456
+ const createGen = el("button", "primary", "Create & generate");
3457
+ createGen.type = "button";
3458
+ const msg = el("span", "msg");
3459
+ actions.append(create);
3460
+ if (GENERATION) actions.append(createGen);
3461
+ actions.append(msg);
3462
+ form.append(styleSection, baseSection, loopsSection, portraitSection, priceBox, actions);
3463
+ const currentProject = () => {
3464
+ const live = S.snap.workspace ? S.snap.workspace.projects : S.snap.project ? [S.snap.project] : [];
3465
+ return live.find((pr) => pr.id === project.value) || live.find((pr) => pr.manifestSha256) || projects[0];
3466
+ };
3467
+ const projectId = () => workspace ? project.value : null;
3468
+ const characterStyles = () => S.snap.styles.filter((s) => s.generator === "character" && s.project === projectId());
3469
+ const chosenEngine = () => ENGINES.find((e) => e.id === engine.value);
3470
+ const isNewStyle = () => style.value === "";
3471
+ const styleDirections = () => {
3472
+ if (isNewStyle()) return Number(directions.value) === 4 ? FOUR : COMPASS;
3473
+ return COMPASS;
3474
+ };
3475
+ function fillStyles() {
3476
+ const keep = style.value;
3477
+ style.textContent = "";
3478
+ for (const s of characterStyles()) style.append(new Option(s.id + " (" + s.outDir + ")", s.id));
3479
+ style.append(new Option("a new character style…", ""));
3480
+ style.value = [...style.options].some((o) => o.value === keep) ? keep : style.options[0].value;
3481
+ }
3482
+ function syncEngine() {
3483
+ const e = chosenEngine();
3484
+ size.min = String(e.min);
3485
+ size.max = String(e.max);
3486
+ const keep = directions.value;
3487
+ directions.textContent = "";
3488
+ for (const d of e.directions) directions.append(new Option(d + " directions", String(d)));
3489
+ directions.value = e.directions.map(String).includes(keep) ? keep : String(e.directions[0]);
3490
+ const bodies = e.id === "pro-flash" ? [...BODIES, "custom"] : BODIES;
3491
+ const keepBody = body.value;
3492
+ body.textContent = "";
3493
+ for (const b of bodies) body.append(new Option(b, b));
3494
+ body.value = bodies.includes(keepBody) ? keepBody : "mannequin";
3495
+ }
3496
+ function syncStyle() {
3497
+ newStyle.hidden = !isNewStyle();
3498
+ styleId.required = isNewStyle();
3499
+ if (!outDirTouched) outDir.value = "art/" + (styleId.value.trim() || "characters");
3500
+ for (const row2 of loops) syncLoopDirections(row2);
3501
+ }
3502
+ function syncLoopDirections(row2) {
3503
+ const allowed = styleDirections();
3504
+ for (const [d, box] of row2.directions) {
3505
+ const wrap = box.parentElement;
3506
+ wrap.hidden = !allowed.includes(d);
3507
+ if (wrap.hidden) box.checked = false;
3508
+ }
3509
+ }
3510
+ function syncLoopMode(row2) {
3511
+ const t = templated(row2.mode.value);
3512
+ row2.what.placeholder = t ? "template, e.g. walk, breathing-idle, running-8-frames" : 'the motion, e.g. "swings a sword overhead"';
3513
+ row2.what.title = t ? "A PixelLab template id" : "What the loop does";
3514
+ }
3515
+ function addLoopRow(defaults = {}) {
3516
+ const wrap = el("div", "studio-loop");
3517
+ const id = el("input");
3518
+ id.type = "text";
3519
+ id.placeholder = "walk";
3520
+ id.required = true;
3521
+ id.pattern = idPattern;
3522
+ id.value = defaults.id ?? "";
3523
+ const mode = el("select");
3524
+ for (const m of LOOP_MODES) mode.append(new Option(m.label, m.id));
3525
+ mode.value = defaults.mode ?? "skeleton-v3";
3526
+ const what = el("input");
3527
+ what.type = "text";
3528
+ what.required = true;
3529
+ what.value = defaults.what ?? "";
3530
+ const dirs = el("div", "studio-dirs");
3531
+ const boxes = /* @__PURE__ */ new Map();
3532
+ for (const d of COMPASS) {
3533
+ const box = el("input");
3534
+ box.type = "checkbox";
3535
+ box.checked = (defaults.directions ?? ["south", "west", "north"]).includes(d);
3536
+ const label = el("label", "field check");
3537
+ label.append(box, el("span", null, d));
3538
+ boxes.set(d, box);
3539
+ dirs.append(label);
3540
+ }
3541
+ const remove = el("button", null, "Remove");
3542
+ remove.type = "button";
3543
+ const row2 = { id, mode, what, directions: boxes, wrap };
3544
+ remove.onclick = () => {
3545
+ loops.splice(loops.indexOf(row2), 1);
3546
+ wrap.remove();
3547
+ schedulePrice();
3548
+ };
3549
+ mode.onchange = () => {
3550
+ syncLoopMode(row2);
3551
+ schedulePrice();
3552
+ };
3553
+ const line = el("div", "row");
3554
+ line.append(field("loop id", id), field("mode", mode), field("template or motion", what));
3555
+ wrap.append(line, dirs, remove);
3556
+ loopList.append(wrap);
3557
+ loops.push(row2);
3558
+ syncLoopMode(row2);
3559
+ syncLoopDirections(row2);
3560
+ }
3561
+ function edits() {
3562
+ const out = [];
3563
+ const sid = isNewStyle() ? styleId.value.trim() : style.value;
3564
+ if (isNewStyle()) {
3565
+ const e = chosenEngine();
3566
+ out.push({
3567
+ action: "add-style",
3568
+ styleId: sid,
3569
+ style: { generator: "character", outDir: outDir.value.trim(), size: Number(size.value), mode: e.id, directions: Number(directions.value), template: body.value, view: view.value }
3570
+ });
3571
+ }
3572
+ const base = baseId.value.trim();
3573
+ out.push({ action: "add-asset", assetId: base, asset: { prompt: prompt.value.trim(), styles: [sid], ...reference ? { reference } : {} } });
3574
+ for (const row2 of loops) {
3575
+ const chosen = [...row2.directions].filter(([, box]) => box.checked).map(([d]) => d);
3576
+ if (!row2.id.value.trim() || !chosen.length) continue;
3577
+ const animation = { of: base, directions: chosen };
3578
+ const asset = { styles: [sid], animation };
3579
+ if (templated(row2.mode.value)) {
3580
+ animation.template = row2.what.value.trim();
3581
+ if (row2.mode.value === "skeleton-v3") animation.mode = "skeleton-v3";
3582
+ } else {
3583
+ animation.mode = row2.mode.value;
3584
+ asset.prompt = row2.what.value.trim();
3585
+ }
3586
+ out.push({ action: "add-asset", assetId: base + "." + row2.id.value.trim(), asset });
3587
+ }
3588
+ if (portrait.checked) out.push({ action: "add-asset", assetId: base + ".portrait", asset: { styles: [sid], portrait: { of: base, size: Number(portraitSize.value) } } });
3589
+ return out;
3590
+ }
3591
+ const request = () => {
3592
+ const body2 = { action: "batch", expectedSha256: currentProject().manifestSha256, edits: edits() };
3593
+ if (projectId()) body2.project = projectId();
3594
+ return body2;
3595
+ };
3596
+ const ours = (key) => {
3597
+ const base = baseId.value.trim();
3598
+ const assetId = key.slice(key.indexOf("/") + 1);
3599
+ return assetId === base || assetId.startsWith(base + ".");
3600
+ };
3601
+ let price = null;
3602
+ function schedulePrice() {
3603
+ if (closed) return;
3604
+ if (priceTimer) clearTimeout(priceTimer);
3605
+ priceTimer = setTimeout(updatePrice, 350);
3606
+ }
3607
+ async function updatePrice() {
3608
+ const seq = ++priceSeq;
3609
+ priceBox.textContent = "";
3610
+ priceBox.append(el("h3", null, "Estimated cost"));
3611
+ if (!baseId.value.trim() || !prompt.value.trim() || isNewStyle() && !styleId.value.trim()) {
3612
+ price = null;
3613
+ priceBox.append(el("div", "state-dim", "Name the character and describe it to see what it costs."));
3614
+ syncButtons();
3615
+ return;
3616
+ }
3617
+ const status = el("div", "state-dim", "pricing…");
3618
+ priceBox.append(status);
3619
+ try {
3620
+ const res = await fetch("/api/price", {
3621
+ method: "POST",
3622
+ headers: { "Content-Type": "application/json", "X-Pixelkiln-Session": SESSION ?? "" },
3623
+ body: JSON.stringify(request())
3624
+ });
3625
+ if (seq !== priceSeq) return;
3626
+ if (!res.ok) throw new Error(await res.text());
3627
+ price = await res.json();
3628
+ status.remove();
3629
+ renderPrice();
3630
+ } catch (err) {
3631
+ if (seq !== priceSeq) return;
3632
+ price = null;
3633
+ status.className = "msg bad";
3634
+ status.textContent = err.message;
3635
+ }
3636
+ syncButtons();
3637
+ }
3638
+ function renderPrice() {
3639
+ const mine = price.items.filter((i) => ours(i.key));
3640
+ const stray = price.items.filter((i) => !ours(i.key));
3641
+ const table = el("div", "studio-price-rows");
3642
+ for (const i of mine) {
3643
+ const row2 = el("div");
3644
+ row2.append(el("span", "mono", i.key), el("span", null, i.cost ? fmtCost(i.costUnit, i.cost) : "free (mirrored)"));
3645
+ table.append(row2);
3646
+ }
3647
+ priceBox.append(table);
3648
+ const totals = {};
3649
+ for (const i of mine) totals[i.costUnit] = (totals[i.costUnit] || 0) + i.cost;
3650
+ priceBox.append(el("div", "studio-total", "Total: " + (Object.entries(totals).map(([u, n]) => fmtCost(u, n)).join(" + ") || "free")));
3651
+ if (stray.length) {
3652
+ priceBox.append(el("div", "state-warn", "A new style also takes in every existing asset that names no styles, so " + stray.length + " more would be drawn in it: " + stray.slice(0, 6).map((i) => i.assetId).join(", ") + (stray.length > 6 ? ", …" : "") + ". Give those assets a styles list first, or use an existing style."));
3653
+ }
3654
+ }
3655
+ function mineTotal() {
3656
+ const mine = price ? price.items.filter((i) => ours(i.key)) : [];
3657
+ return { provider: mine[0]?.provider ?? null, unit: mine[0]?.costUnit ?? null, amount: mine.reduce((n, i) => n + i.cost, 0) };
3658
+ }
3659
+ function syncButtons() {
3660
+ const t = mineTotal();
3661
+ create.disabled = !price;
3662
+ if (!GENERATION) return;
3663
+ const left = t.provider ? remainingBudget(t.provider) : null;
3664
+ const over = left !== null && t.amount > left;
3665
+ createGen.disabled = !price || over;
3666
+ createGen.textContent = "Create & generate" + (price && t.unit ? " · " + fmtCost(t.unit, t.amount) : "");
3667
+ createGen.title = over ? "More than the " + fmtCost(t.unit, left) + " left in this session's budget" : "";
3668
+ }
3669
+ async function save(generate) {
3670
+ if (priceTimer) clearTimeout(priceTimer);
3671
+ priceSeq++;
3672
+ create.disabled = true;
3673
+ createGen.disabled = true;
3674
+ msg.className = "msg";
3675
+ msg.textContent = "saving…";
3676
+ const pid = projectId();
3677
+ const base = baseId.value.trim();
3678
+ const sid = isNewStyle() ? styleId.value.trim() : style.value;
3679
+ const keys = price ? price.items.filter((i) => ours(i.key)).map((i) => i.key) : [];
3680
+ try {
3681
+ S.snap = await postEdit(request());
3682
+ const newId = (pid ? pid + ":" : "") + sid + "/" + base;
3683
+ if (generate && keys.length) {
3684
+ await postGenerate({ keys, ...pid ? { project: pid } : {} });
3685
+ ui.notice = { id: newId, text: "Created, and generating " + keys.length + " asset(s): the base first, then everything drawn from it, as each lands." };
3686
+ pollJobs();
3687
+ } else {
3688
+ ui.notice = { id: newId, text: "Created " + base + " with " + (keys.length - 1) + " more asset(s). " + nextStep() };
3689
+ }
3690
+ close();
3691
+ render();
3692
+ if (S.snap.items.some((i) => i.id === newId)) openItem(newId);
3693
+ } catch (err) {
3694
+ showSaveError(msg, create, err);
3695
+ syncButtons();
3696
+ }
3697
+ }
3698
+ form.onsubmit = (e) => {
3699
+ e.preventDefault();
3700
+ void save(false);
3701
+ };
3702
+ createGen.onclick = () => {
3703
+ if (form.reportValidity()) void save(true);
3704
+ };
3705
+ refInput.onchange = async () => {
3706
+ const file = refInput.files && refInput.files[0];
3707
+ reference = null;
3708
+ refPreview.hidden = true;
3709
+ if (!file) {
3710
+ schedulePrice();
3711
+ return;
3712
+ }
3713
+ if (!baseId.value.trim()) {
3714
+ msg.className = "msg bad";
3715
+ msg.textContent = "Name the character first; the sprite is saved as refs/<id>.";
3716
+ refInput.value = "";
3717
+ return;
3718
+ }
3719
+ const ext = file.type === "image/jpeg" ? ".jpg" : ".png";
3720
+ const path = "refs/" + baseId.value.trim() + ext;
3721
+ const base64 = await new Promise((resolve, reject) => {
3722
+ const reader = new FileReader();
3723
+ reader.onload = () => resolve(String(reader.result).split(",")[1] || "");
3724
+ reader.onerror = () => reject(reader.error);
3725
+ reader.readAsDataURL(file);
3726
+ });
3727
+ const upload = async (overwrite) => postEdit({ action: "upload-image", path, base64, ...overwrite ? { overwrite: true } : {}, ...projectId() ? { project: projectId() } : {} });
3728
+ try {
3729
+ try {
3730
+ S.snap = await upload(false);
3731
+ } catch (err) {
3732
+ if (!/already exists/.test(err.message) || !confirm(path + " already exists. Replace it?")) throw err;
3733
+ S.snap = await upload(true);
3734
+ }
3735
+ reference = path;
3736
+ refPreview.src = URL.createObjectURL(file);
3737
+ refPreview.hidden = false;
3738
+ msg.className = "msg";
3739
+ msg.textContent = "Sprite saved as " + path + ".";
3740
+ } catch (err) {
3741
+ msg.className = "msg bad";
3742
+ msg.textContent = err.message;
3743
+ refInput.value = "";
3744
+ }
3745
+ schedulePrice();
3746
+ };
3747
+ project.onchange = () => {
3748
+ fillStyles();
3749
+ syncStyle();
3750
+ schedulePrice();
3751
+ };
3752
+ style.onchange = () => {
3753
+ syncStyle();
3754
+ schedulePrice();
3755
+ };
3756
+ engine.onchange = () => {
3757
+ syncEngine();
3758
+ syncStyle();
3759
+ schedulePrice();
3760
+ };
3761
+ styleId.oninput = () => {
3762
+ syncStyle();
3763
+ schedulePrice();
3764
+ };
3765
+ directions.onchange = () => {
3766
+ syncStyle();
3767
+ schedulePrice();
3768
+ };
3769
+ addLoop.onclick = () => {
3770
+ addLoopRow({});
3771
+ schedulePrice();
3772
+ };
3773
+ form.addEventListener("input", schedulePrice);
3774
+ form.addEventListener("change", schedulePrice);
3775
+ fillStyles();
3776
+ syncEngine();
3777
+ syncStyle();
3778
+ addLoopRow({ id: "walk", mode: "skeleton-v3", what: "walk", directions: ["south", "west", "north"] });
3779
+ addLoopRow({ id: "idle", mode: "skeleton-v3", what: "breathing-idle", directions: ["south"] });
3780
+ updatePrice();
3781
+ setTimeout(() => baseId.focus(), 0);
3782
+ }
3783
+
3334
3784
  // src/gallery/client/src/main.ts
3335
3785
  $("refresh").onclick = refresh;
3786
+ $("studio").hidden = !EDITABLE;
3787
+ $("studio").onclick = openStudio;
3336
3788
  var autoTimer = null;
3337
3789
  $("auto").onchange = (e) => {
3338
3790
  if (autoTimer) clearInterval(autoTimer ?? void 0);