very hot looking puppet preview and move skin btn to scene view

This commit is contained in:
2026-08-30 01:48:48 +08:00
parent 64e5c363f2
commit 902093d75f
7 changed files with 508 additions and 232 deletions
@@ -12,8 +12,6 @@
let dirty = $state(false);
let busy = $state(false);
let error = $state("");
let skinFile = $state<File | null>(null);
let skinInput = $state<HTMLInputElement | null>(null);
$effect(() => {
if (!dirty) displayName = $profile?.displayName ?? "";
@@ -23,15 +21,12 @@
onMount(() => register("general", { apply, reset }, { dirty, busy }));
function updateDirty() {
dirty =
displayName.trim() !== ($profile?.displayName ?? "") || skinFile !== null;
dirty = displayName.trim() !== ($profile?.displayName ?? "");
error = "";
}
function reset() {
displayName = $profile?.displayName ?? "";
skinFile = null;
if (skinInput) skinInput.value = "";
dirty = false;
error = "";
}
@@ -47,13 +42,8 @@
busy = true;
error = "";
try {
const skinData = skinFile
? Array.from(new Uint8Array(await skinFile.arrayBuffer()))
: null;
await commands.updateProfile(nextName, skinData);
await commands.updateProfile(nextName, null);
displayName = nextName;
skinFile = null;
if (skinInput) skinInput.value = "";
dirty = false;
return true;
} catch (cause) {
@@ -88,34 +78,6 @@
This name can be changed at any time.
</p>
<label class="fieldset-label mt-2" for="skin-file">Character skin</label>
<div class="join w-full">
<input
class="input join-item min-w-0 flex-1"
value={skinFile?.name ?? ($profile?.skinHash ? "Custom skin" : "Default skin")}
readonly
aria-label="Selected character skin"
/>
<button
class="btn join-item"
type="button"
disabled={!$profile || busy}
onclick={() => skinInput?.click()}>Choose PNG</button
>
</div>
<input
id="skin-file"
class="hidden"
type="file"
accept="image/png"
bind:this={skinInput}
onchange={(event) => {
skinFile = event.currentTarget.files?.[0] ?? null;
updateDirty();
}}
/>
<p class="fieldset-label">64×64 Minecraft-format PNG, base layer only.</p>
<label class="fieldset-label mt-2" for="public-key"
>Identification Key
<div
@@ -4,7 +4,9 @@
sceneConfiguration,
sceneConfigurationListenerError,
} from "$lib/listeners/scene-configuration";
import { onMount } from "svelte";
import { profile, profileListenerError } from "$lib/listeners/profile";
import { onDestroy, onMount } from "svelte";
import PuppetPreview from "../../../routes/scene/components/renderer/puppet/preview.svelte";
import PanelMessage from "./panel-message.svelte";
import type { RegisterPanel } from "./types";
@@ -13,6 +15,9 @@
let puppetScale = $state(1);
let puppetOpacity = $state(1);
let puppetMovementMode = $state<PuppetMovementMode>("free");
let skinFile = $state<File | null>(null);
let skinPreviewUrl = $state<string | null>(null);
let skinInput = $state<HTMLInputElement | null>(null);
let dirty = $state(false);
let busy = $state(false);
let error = $state("");
@@ -27,19 +32,42 @@
});
onMount(() => register("scene", { apply, reset }, { dirty, busy }));
onDestroy(() => {
if (skinPreviewUrl) URL.revokeObjectURL(skinPreviewUrl);
});
function updateDirty() {
dirty =
function hasConfigurationChanges() {
return (
puppetScale !== $sceneConfiguration.puppetScale ||
puppetOpacity !== $sceneConfiguration.puppetOpacity ||
puppetMovementMode !== $sceneConfiguration.puppetMovementMode;
puppetMovementMode !== $sceneConfiguration.puppetMovementMode
);
}
function updateDirty() {
dirty = hasConfigurationChanges() || skinFile !== null;
error = "";
}
function clearSkinDraft() {
if (skinPreviewUrl) URL.revokeObjectURL(skinPreviewUrl);
skinFile = null;
skinPreviewUrl = null;
if (skinInput) skinInput.value = "";
}
function selectSkinFile(file: File | null) {
clearSkinDraft();
skinFile = file;
skinPreviewUrl = file ? URL.createObjectURL(file) : null;
updateDirty();
}
function reset() {
puppetScale = $sceneConfiguration.puppetScale;
puppetOpacity = $sceneConfiguration.puppetOpacity;
puppetMovementMode = $sceneConfiguration.puppetMovementMode;
clearSkinDraft();
dirty = false;
error = "";
}
@@ -50,17 +78,29 @@
busy = true;
error = "";
try {
const configuration = await commands.updateSceneConfiguration({
puppetScale,
puppetOpacity,
puppetMovementMode,
});
puppetScale = configuration.puppetScale;
puppetOpacity = configuration.puppetOpacity;
puppetMovementMode = configuration.puppetMovementMode;
if (skinFile) {
const currentProfile = await commands.getProfile();
const skinData = Array.from(
new Uint8Array(await skinFile.arrayBuffer()),
);
await commands.updateProfile(currentProfile.displayName, skinData);
clearSkinDraft();
}
if (hasConfigurationChanges()) {
const configuration = await commands.updateSceneConfiguration({
puppetScale,
puppetOpacity,
puppetMovementMode,
});
puppetScale = configuration.puppetScale;
puppetOpacity = configuration.puppetOpacity;
puppetMovementMode = configuration.puppetMovementMode;
}
dirty = false;
return true;
} catch (cause) {
dirty = hasConfigurationChanges() || skinFile !== null;
error = String(cause);
return false;
} finally {
@@ -70,52 +110,117 @@
</script>
<div class="space-y-3">
{#if error || $sceneConfigurationListenerError}
{#if error || $sceneConfigurationListenerError || $profileListenerError}
<PanelMessage kind="error">
{error || $sceneConfigurationListenerError}
{error || $sceneConfigurationListenerError || $profileListenerError}
</PanelMessage>
{/if}
<fieldset class="fieldset border border-base-300 bg-base-100 p-3">
<legend class="fieldset-legend px-1">Puppets</legend>
<div class="flex items-center justify-between gap-3">
<label class="fieldset-label" for="puppet-scale">Scale</label>
<output class="badge badge-outline tabular-nums" for="puppet-scale">
{puppetScale.toFixed(2)}×
</output>
</div>
<input
id="puppet-scale"
class="range range-sm"
type="range"
min="0.5"
max="2"
step="0.05"
bind:value={puppetScale}
oninput={updateDirty}
disabled={busy}
aria-describedby="puppet-scale-help"
/>
<div
class="grid grid-cols-[minmax(0,3fr)_minmax(0,4fr)] items-center gap-3"
>
<div class="flex flex-col gap-2">
<div class="aspect-square min-w-0">
{#if $profile}
<div
class="border border-primary relative shadow-[inset_0_0_10px] bg-primary/5 shadow-primary card"
>
<div
class="size-full absolute bg-gridded opacity-25 inset-0"
></div>
<div class="size-full absolute">
<div
class="flex flex-row size-full items-end justify-between text-[10px] text-primary p-1"
>
<div class="text-start flex flex-col">
<p>Scale</p>
<p>Opacity</p>
</div>
<div class="text-end flex flex-col">
<p>{(puppetScale * 100).toFixed(0)}%</p>
<p>{(puppetOpacity * 100).toFixed(0)}%</p>
</div>
</div>
</div>
<div class="size-full z-10">
<PuppetPreview
userId={$profile.id}
skinHash={$profile.skinHash}
skinSource={skinPreviewUrl}
scale={puppetScale}
opacity={puppetOpacity}
/>
</div>
</div>
{:else}
<div
class="skeleton aspect-square size-full"
aria-label="Loading puppet preview"
></div>
{/if}
</div>
</div>
<div class="mt-3 flex items-center justify-between gap-3">
<label class="fieldset-label" for="puppet-opacity">Opacity</label>
<output class="badge badge-outline tabular-nums" for="puppet-opacity">
{Math.round(puppetOpacity * 100)}%
</output>
<div class="min-w-0 space-y-2">
<div>
<label class="fieldset-label" for="puppet-scale">Scale</label>
<input
id="puppet-scale"
class="range range-sm"
type="range"
min="0.5"
max="2"
step="0.05"
bind:value={puppetScale}
oninput={updateDirty}
disabled={busy}
aria-describedby="puppet-scale-help"
/>
</div>
<div>
<label class="fieldset-label" for="puppet-opacity">Opacity</label>
<input
id="puppet-opacity"
class="range range-sm"
type="range"
min="0.25"
max="1"
step="0.05"
bind:value={puppetOpacity}
oninput={updateDirty}
disabled={busy}
aria-describedby="puppet-opacity-help"
/>
</div>
<div>
<label class="fieldset-label" for="puppet-skin">Custom Skin</label>
<div>
<button
class="btn w-full"
type="button"
disabled={!$profile || busy}
onclick={() => skinInput?.click()}
>
Choose file
</button>
<input
id="skin-file"
class="hidden"
type="file"
accept="image/png"
bind:this={skinInput}
onchange={(event) =>
selectSkinFile(event.currentTarget.files?.[0] ?? null)}
/>
</div>
</div>
</div>
</div>
<input
id="puppet-opacity"
class="range range-sm"
type="range"
min="0.25"
max="1"
step="0.05"
bind:value={puppetOpacity}
oninput={updateDirty}
disabled={busy}
aria-describedby="puppet-opacity-help"
/>
<fieldset class="mt-4">
<legend class="fieldset-label mb-2">Movement</legend>
@@ -185,3 +290,34 @@
</fieldset>
</fieldset>
</div>
<style>
.bg-gridded {
background-image:
linear-gradient(
0deg,
transparent 24%,
var(--color-primary) 25%,
var(--color-primary) 26%,
transparent 27%,
transparent 74%,
var(--color-primary) 75%,
var(--color-primary) 76%,
transparent 77%,
transparent
),
linear-gradient(
90deg,
transparent 24%,
var(--color-primary) 25%,
var(--color-primary) 26%,
transparent 27%,
transparent 74%,
var(--color-primary) 75%,
var(--color-primary) 76%,
transparent 77%,
transparent
);
background-size: 32px 32px;
}
</style>