scene configuration, neko opacity & scale

This commit is contained in:
2026-03-24 22:52:49 +08:00
parent 4093b0eb0c
commit 75ab799a7f
19 changed files with 427 additions and 34 deletions

View File

@@ -1,8 +1,9 @@
use crate::{ use crate::{
lock_r, lock_r,
models::app_data::UserData, models::{app_data::UserData, app_state::{AppState, NekoPosition}},
services::{ services::{
app_data::{init_app_data_scoped, AppDataRefreshScope}, app_data::{init_app_data_scoped, AppDataRefreshScope},
app_state,
friends, friends,
presence_modules::models::ModuleMetadata, presence_modules::models::ModuleMetadata,
sprite, sprite,
@@ -45,3 +46,27 @@ pub fn get_friend_active_doll_sprites_base64() -> Result<friends::FriendActiveDo
friends::sync_active_doll_sprites_from_app_data(); friends::sync_active_doll_sprites_from_app_data();
Ok(friends::get_active_doll_sprites_snapshot()) Ok(friends::get_active_doll_sprites_snapshot())
} }
#[tauri::command]
#[specta::specta]
pub fn get_app_state() -> Result<AppState, String> {
Ok(app_state::get_snapshot())
}
#[tauri::command]
#[specta::specta]
pub fn set_scene_setup_nekos_position(nekos_position: Option<NekoPosition>) {
app_state::set_scene_setup_nekos_position(nekos_position);
}
#[tauri::command]
#[specta::specta]
pub fn set_scene_setup_nekos_opacity(nekos_opacity: f32) {
app_state::set_scene_setup_nekos_opacity(nekos_opacity);
}
#[tauri::command]
#[specta::specta]
pub fn set_scene_setup_nekos_scale(nekos_scale: f32) {
app_state::set_scene_setup_nekos_scale(nekos_scale);
}

View File

@@ -1,14 +1,12 @@
use crate::{ use crate::services::{
commands::app_state::get_modules, doll_editor::open_doll_editor_window,
services::{ scene::{get_scene_interactive, set_pet_menu_state, set_scene_interactive},
doll_editor::open_doll_editor_window,
scene::{get_scene_interactive, set_pet_menu_state, set_scene_interactive},
},
}; };
use commands::app::{quit_app, restart_app, retry_connection}; use commands::app::{quit_app, restart_app, retry_connection};
use commands::app_state::{ use commands::app_state::{
get_active_doll_sprite_base64, get_app_data, get_friend_active_doll_sprites_base64, get_active_doll_sprite_base64, get_app_data, get_app_state,
refresh_app_data, get_friend_active_doll_sprites_base64, get_modules, refresh_app_data,
set_scene_setup_nekos_opacity, set_scene_setup_nekos_position, set_scene_setup_nekos_scale,
}; };
use commands::auth::{logout_and_restart, start_discord_auth, start_google_auth}; use commands::auth::{logout_and_restart, start_discord_auth, start_google_auth};
use commands::config::{get_client_config, open_client_config, save_client_config}; use commands::config::{get_client_config, open_client_config, save_client_config};
@@ -27,11 +25,12 @@ use tauri::async_runtime;
use tauri_specta::{collect_commands, collect_events, Builder as SpectaBuilder, ErrorHandlingMode}; use tauri_specta::{collect_commands, collect_events, Builder as SpectaBuilder, ErrorHandlingMode};
use crate::services::app_events::{ use crate::services::app_events::{
ActiveDollSpriteChanged, AppDataRefreshed, AuthFlowUpdated, CreateDoll, CursorMoved, EditDoll, ActiveDollSpriteChanged, AppDataRefreshed, AppStateChanged, AuthFlowUpdated, CreateDoll,
FriendActiveDollChanged, FriendActiveDollSpritesUpdated, FriendCursorPositionsUpdated, CursorMoved, EditDoll, FriendActiveDollChanged, FriendActiveDollSpritesUpdated,
FriendDisconnected, FriendRequestAccepted, FriendRequestDenied, FriendRequestReceived, FriendCursorPositionsUpdated, FriendDisconnected, FriendRequestAccepted,
FriendUserStatusChanged, InteractionDeliveryFailed, InteractionReceived, FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged,
SceneInteractiveChanged, SetInteractionOverlay, Unfriended, UserStatusChanged, InteractionDeliveryFailed, InteractionReceived, SceneInteractiveChanged,
SetInteractionOverlay, Unfriended, UserStatusChanged,
}; };
static APP_HANDLE: std::sync::OnceLock<tauri::AppHandle<tauri::Wry>> = std::sync::OnceLock::new(); static APP_HANDLE: std::sync::OnceLock<tauri::AppHandle<tauri::Wry>> = std::sync::OnceLock::new();
@@ -68,6 +67,7 @@ pub fn run() {
.error_handling(ErrorHandlingMode::Throw) .error_handling(ErrorHandlingMode::Throw)
.commands(collect_commands![ .commands(collect_commands![
get_app_data, get_app_data,
get_app_state,
get_active_doll_sprite_base64, get_active_doll_sprite_base64,
get_friend_active_doll_sprites_base64, get_friend_active_doll_sprites_base64,
refresh_app_data, refresh_app_data,
@@ -102,12 +102,16 @@ pub fn run() {
start_discord_auth, start_discord_auth,
logout_and_restart, logout_and_restart,
send_interaction_cmd, send_interaction_cmd,
get_modules get_modules,
set_scene_setup_nekos_position,
set_scene_setup_nekos_opacity,
set_scene_setup_nekos_scale
]) ])
.events(collect_events![ .events(collect_events![
CursorMoved, CursorMoved,
SceneInteractiveChanged, SceneInteractiveChanged,
AppDataRefreshed, AppDataRefreshed,
AppStateChanged,
ActiveDollSpriteChanged, ActiveDollSpriteChanged,
SetInteractionOverlay, SetInteractionOverlay,
EditDoll, EditDoll,

View File

@@ -0,0 +1,39 @@
use serde::{Deserialize, Serialize};
use specta::Type;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)]
#[serde(rename_all = "kebab-case")]
pub enum NekoPosition {
TopLeft,
Top,
TopRight,
Left,
Right,
BottomLeft,
Bottom,
BottomRight,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct SceneSetup {
pub nekos_position: Option<NekoPosition>,
pub nekos_opacity: f32,
pub nekos_scale: f32,
}
impl Default for SceneSetup {
fn default() -> Self {
Self {
nekos_position: None,
nekos_opacity: 1.0,
nekos_scale: 1.0,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type, Default)]
#[serde(rename_all = "camelCase")]
pub struct AppState {
pub scene_setup: SceneSetup,
}

View File

@@ -1,4 +1,5 @@
pub mod app_data; pub mod app_data;
pub mod app_state;
pub mod dolls; pub mod dolls;
pub mod event_payloads; pub mod event_payloads;
pub mod friends; pub mod friends;

View File

@@ -5,6 +5,7 @@ use tauri_specta::Event;
use crate::{ use crate::{
models::{ models::{
app_data::UserData, app_data::UserData,
app_state::AppState,
event_payloads::{ event_payloads::{
FriendActiveDollChangedPayload, FriendDisconnectedPayload, FriendActiveDollChangedPayload, FriendDisconnectedPayload,
FriendRequestAcceptedPayload, FriendRequestDeniedPayload, FriendRequestReceivedPayload, FriendRequestAcceptedPayload, FriendRequestDeniedPayload, FriendRequestReceivedPayload,
@@ -47,6 +48,10 @@ pub struct SceneInteractiveChanged(pub bool);
#[tauri_specta(event_name = "app-data-refreshed")] #[tauri_specta(event_name = "app-data-refreshed")]
pub struct AppDataRefreshed(pub UserData); pub struct AppDataRefreshed(pub UserData);
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
#[tauri_specta(event_name = "app-state-changed")]
pub struct AppStateChanged(pub AppState);
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
#[tauri_specta(event_name = "active-doll-sprite-changed")] #[tauri_specta(event_name = "active-doll-sprite-changed")]
pub struct ActiveDollSpriteChanged(pub Option<String>); pub struct ActiveDollSpriteChanged(pub Option<String>);

View File

@@ -0,0 +1,42 @@
use std::sync::{Arc, LazyLock, RwLock};
use tauri_specta::Event as _;
use tracing::warn;
use crate::{
get_app_handle,
models::app_state::{AppState, NekoPosition},
services::app_events::AppStateChanged,
};
static APP_STATE: LazyLock<Arc<RwLock<AppState>>> =
LazyLock::new(|| Arc::new(RwLock::new(AppState::default())));
pub fn get_snapshot() -> AppState {
let guard = APP_STATE.read().expect("app state lock poisoned");
guard.clone()
}
pub fn set_scene_setup_nekos_position(nekos_position: Option<NekoPosition>) {
let mut guard = APP_STATE.write().expect("app state lock poisoned");
guard.scene_setup.nekos_position = nekos_position;
emit_snapshot(&guard);
}
pub fn set_scene_setup_nekos_opacity(nekos_opacity: f32) {
let mut guard = APP_STATE.write().expect("app state lock poisoned");
guard.scene_setup.nekos_opacity = nekos_opacity.clamp(0.1, 1.0);
emit_snapshot(&guard);
}
pub fn set_scene_setup_nekos_scale(nekos_scale: f32) {
let mut guard = APP_STATE.write().expect("app state lock poisoned");
guard.scene_setup.nekos_scale = nekos_scale.clamp(0.5, 2.0);
emit_snapshot(&guard);
}
fn emit_snapshot(app_state: &AppState) {
if let Err(error) = AppStateChanged(app_state.clone()).emit(get_app_handle()) {
warn!("Failed to emit app-state-changed event: {}", error);
}
}

View File

@@ -1,5 +1,6 @@
pub mod app_data; pub mod app_data;
pub mod app_events; pub mod app_events;
pub mod app_state;
pub mod app_menu; pub mod app_menu;
pub mod app_update; pub mod app_update;
pub mod accelerators; pub mod accelerators;

View File

@@ -0,0 +1,17 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-image-icon lucide-image"
><rect width="18" height="18" x="3" y="3" rx="2" ry="2" /><circle
cx="9"
cy="9"
r="2"
/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" /></svg
>

After

Width:  |  Height:  |  Size: 413 B

32
src/events/app-state.ts Normal file
View File

@@ -0,0 +1,32 @@
import { writable } from "svelte/store";
import {
commands,
events,
type AppState,
type NekoPosition,
} from "$lib/bindings";
import { createEventSource } from "./listener-utils";
export type NeksPosition = NekoPosition;
export type { AppState };
const initialState: AppState = {
sceneSetup: {
nekosPosition: null,
nekosOpacity: 1,
nekosScale: 1,
},
};
export const appState = writable<AppState>(initialState);
export const { start: startAppState, stop: stopAppState } = createEventSource(
async (addEventListener) => {
appState.set(await commands.getAppState());
addEventListener(
await events.appStateChanged.listen((event) => {
appState.set(event.payload);
}),
);
},
);

View File

@@ -8,6 +8,9 @@ export const commands = {
async getAppData() : Promise<UserData> { async getAppData() : Promise<UserData> {
return await TAURI_INVOKE("get_app_data"); return await TAURI_INVOKE("get_app_data");
}, },
async getAppState() : Promise<AppState> {
return await TAURI_INVOKE("get_app_state");
},
async getActiveDollSpriteBase64() : Promise<string | null> { async getActiveDollSpriteBase64() : Promise<string | null> {
return await TAURI_INVOKE("get_active_doll_sprite_base64"); return await TAURI_INVOKE("get_active_doll_sprite_base64");
}, },
@@ -118,6 +121,15 @@ async sendInteractionCmd(dto: SendInteractionDto) : Promise<null> {
}, },
async getModules() : Promise<ModuleMetadata[]> { async getModules() : Promise<ModuleMetadata[]> {
return await TAURI_INVOKE("get_modules"); return await TAURI_INVOKE("get_modules");
},
async setSceneSetupNekosPosition(nekosPosition: NekoPosition | null) : Promise<void> {
await TAURI_INVOKE("set_scene_setup_nekos_position", { nekosPosition });
},
async setSceneSetupNekosOpacity(nekosOpacity: number) : Promise<void> {
await TAURI_INVOKE("set_scene_setup_nekos_opacity", { nekosOpacity });
},
async setSceneSetupNekosScale(nekosScale: number) : Promise<void> {
await TAURI_INVOKE("set_scene_setup_nekos_scale", { nekosScale });
} }
} }
@@ -127,6 +139,7 @@ async getModules() : Promise<ModuleMetadata[]> {
export const events = __makeEvents__<{ export const events = __makeEvents__<{
activeDollSpriteChanged: ActiveDollSpriteChanged, activeDollSpriteChanged: ActiveDollSpriteChanged,
appDataRefreshed: AppDataRefreshed, appDataRefreshed: AppDataRefreshed,
appStateChanged: AppStateChanged,
authFlowUpdated: AuthFlowUpdated, authFlowUpdated: AuthFlowUpdated,
createDoll: CreateDoll, createDoll: CreateDoll,
cursorMoved: CursorMoved, cursorMoved: CursorMoved,
@@ -148,6 +161,7 @@ userStatusChanged: UserStatusChanged
}>({ }>({
activeDollSpriteChanged: "active-doll-sprite-changed", activeDollSpriteChanged: "active-doll-sprite-changed",
appDataRefreshed: "app-data-refreshed", appDataRefreshed: "app-data-refreshed",
appStateChanged: "app-state-changed",
authFlowUpdated: "auth-flow-updated", authFlowUpdated: "auth-flow-updated",
createDoll: "create-doll", createDoll: "create-doll",
cursorMoved: "cursor-moved", cursorMoved: "cursor-moved",
@@ -180,6 +194,8 @@ export type AcceleratorModifier = "cmd" | "alt" | "ctrl" | "shift"
export type ActiveDollSpriteChanged = string | null export type ActiveDollSpriteChanged = string | null
export type AppConfig = { api_base_url: string | null; debug_mode: boolean; accelerators?: Partial<{ [key in AcceleratorAction]: KeyboardAccelerator }> } export type AppConfig = { api_base_url: string | null; debug_mode: boolean; accelerators?: Partial<{ [key in AcceleratorAction]: KeyboardAccelerator }> }
export type AppDataRefreshed = UserData export type AppDataRefreshed = UserData
export type AppState = { sceneSetup: SceneSetup }
export type AppStateChanged = AppState
export type AuthFlowStatus = "started" | "succeeded" | "failed" | "cancelled" export type AuthFlowStatus = "started" | "succeeded" | "failed" | "cancelled"
export type AuthFlowUpdated = AuthFlowUpdatedPayload export type AuthFlowUpdated = AuthFlowUpdatedPayload
export type AuthFlowUpdatedPayload = { provider: string; status: AuthFlowStatus; message: string | null } export type AuthFlowUpdatedPayload = { provider: string; status: AuthFlowStatus; message: string | null }
@@ -217,9 +233,11 @@ export type InteractionPayloadDto = { senderUserId: string; senderName: string;
export type InteractionReceived = InteractionPayloadDto export type InteractionReceived = InteractionPayloadDto
export type KeyboardAccelerator = { modifiers?: AcceleratorModifier[]; key?: AcceleratorKey | null } export type KeyboardAccelerator = { modifiers?: AcceleratorModifier[]; key?: AcceleratorKey | null }
export type ModuleMetadata = { id: string; name: string; version: string; description: string | null } export type ModuleMetadata = { id: string; name: string; version: string; description: string | null }
export type NekoPosition = "top-left" | "top" | "top-right" | "left" | "right" | "bottom-left" | "bottom" | "bottom-right"
export type PresenceStatus = { title: string | null; subtitle: string | null; graphicsB64: string | null } export type PresenceStatus = { title: string | null; subtitle: string | null; graphicsB64: string | null }
export type SceneData = { display: DisplayData; grid_size: number } export type SceneData = { display: DisplayData; grid_size: number }
export type SceneInteractiveChanged = boolean export type SceneInteractiveChanged = boolean
export type SceneSetup = { nekosPosition: NekoPosition | null; nekosOpacity: number; nekosScale: number }
export type SendFriendRequestDto = { receiverId: string } export type SendFriendRequestDto = { receiverId: string }
export type SendInteractionDto = { recipientUserId: string; content: string; type: string } export type SendInteractionDto = { recipientUserId: string; content: string; type: string }
export type SetInteractionOverlay = boolean export type SetInteractionOverlay = boolean

View File

@@ -15,6 +15,7 @@
stopFriendActiveDollSprite, stopFriendActiveDollSprite,
} from "../events/friend-active-doll-sprite"; } from "../events/friend-active-doll-sprite";
import { startAppData } from "../events/app-data"; import { startAppData } from "../events/app-data";
import { startAppState, stopAppState } from "../events/app-state";
import { startInteraction, stopInteraction } from "../events/interaction"; import { startInteraction, stopInteraction } from "../events/interaction";
import { import {
startSceneInteractive, startSceneInteractive,
@@ -27,6 +28,7 @@
onMount(async () => { onMount(async () => {
try { try {
await startAppData(); await startAppData();
await startAppState();
await startActiveDollSprite(); await startActiveDollSprite();
await startFriendActiveDollSprite(); await startFriendActiveDollSprite();
await startCursorTracking(); await startCursorTracking();
@@ -47,6 +49,7 @@
stopSceneInteractive(); stopSceneInteractive();
stopInteraction(); stopInteraction();
stopUserStatus(); stopUserStatus();
stopAppState();
}); });
} }
</script> </script>

View File

@@ -9,6 +9,8 @@
import Users from "../../assets/icons/users.svelte"; import Users from "../../assets/icons/users.svelte";
import Settings from "../../assets/icons/settings.svelte"; import Settings from "../../assets/icons/settings.svelte";
import Blocks from "../../assets/icons/blocks.svelte"; import Blocks from "../../assets/icons/blocks.svelte";
import Image from "../../assets/icons/image.svelte";
import Scene from "./tabs/scene/scene.svelte";
let showInteractionOverlay = false; let showInteractionOverlay = false;
@@ -46,9 +48,19 @@
<input <input
type="radio" type="radio"
name="app_menu_tabs" name="app_menu_tabs"
aria-label="Your Nekos" aria-label="Scene Configuration"
checked checked
/> />
<div class="*:size-4">
<Image />
</div>
</label>
<div class="tab-content bg-base-100 border-base-300 p-4">
<Scene />
</div>
<label class="tab">
<input type="radio" name="app_menu_tabs" aria-label="Your Nekos" />
<div class="*:size-4"> <div class="*:size-4">
<PawPrint /> <PawPrint />
</div> </div>

View File

@@ -6,6 +6,8 @@
import type { DollColorSchemeDto } from "$lib/bindings"; import type { DollColorSchemeDto } from "$lib/bindings";
export let dollColorScheme: DollColorSchemeDto; export let dollColorScheme: DollColorSchemeDto;
export let spriteScale = 2;
export let spriteOpacity = 1;
let previewBase64: string | null = null; let previewBase64: string | null = null;
let error: string | null = null; let error: string | null = null;
@@ -80,7 +82,10 @@
}); });
</script> </script>
<div class="scale-200 p-4"> <div
style="transform: scale({spriteScale}); padding: {spriteScale *
10}px; opacity: {spriteOpacity};"
>
<div class="size-8"> <div class="size-8">
{#if error} {#if error}
<div <div

View File

@@ -0,0 +1,74 @@
<script lang="ts">
import { commands } from "$lib/bindings";
import { appState, type NeksPosition } from "../../../../events/app-state";
const positions: { value: NeksPosition | null; label: string }[] = [
{ value: "top-left", label: "Top Left" },
{ value: "top", label: "Top" },
{ value: "top-right", label: "Top Right" },
{ value: "left", label: "Left" },
{ value: null, label: "" },
{ value: "right", label: "Right" },
{ value: "bottom-left", label: "Bottom Left" },
{ value: "bottom", label: "Bottom" },
{ value: "bottom-right", label: "Bottom Right" },
];
async function selectPosition(position: NeksPosition | null) {
await commands.setSceneSetupNekosPosition(
$appState.sceneSetup.nekosPosition === position ? null : position,
);
}
let selectedLabel = $derived(
positions.find((p) => p.value === $appState.sceneSetup.nekosPosition)
?.label ?? "",
);
</script>
<div class="collapse bg-base-100 border-base-300 border">
<input type="checkbox" checked />
<div class="collapse-title py-2 text-sm opacity-70">Neko Reposition</div>
<div class="collapse-content">
<div class="flex flex-row gap-4 h-full pt-4 border-t border-base-300">
<div class="h-full flex flex-col justify-between">
<div>
<p class="text-sm opacity-50">
Choose a corner to gather nekos into a cluster
</p>
</div>
<div>
<input
type="checkbox"
checked={$appState.sceneSetup.nekosPosition !== null}
onclick={() =>
selectPosition(
$appState.sceneSetup.nekosPosition ? null : "bottom-left",
)}
class="toggle toggle-xl {$appState.sceneSetup.nekosPosition
? 'bg-primary/10 toggle-primary'
: 'bg-base-200'}"
/>
</div>
</div>
<div class="card bg-base-200/50 p-1 w-max border border-base-300">
<div class="grid grid-cols-3 gap-6 items-center w-max">
{#each positions as pos}
{#if pos.value === null}
<div></div>
{:else}
<button
class={"btn-xs btn btn-square " +
($appState.sceneSetup.nekosPosition === pos.value
? "btn-primary"
: "")}
aria-label={pos.label}
onclick={() => selectPosition(pos.value)}
></button>
{/if}
{/each}
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,85 @@
<script lang="ts">
import { commands } from "$lib/bindings";
import { appState } from "../../../../events/app-state";
import DollPreview from "../../components/doll-preview.svelte";
async function updateOpacity(value: number) {
await commands.setSceneSetupNekosOpacity(value);
}
async function updateScale(value: number) {
await commands.setSceneSetupNekosScale(value);
}
</script>
<div class="collapse bg-base-100 border-base-300 border">
<input type="checkbox" checked />
<div class="collapse-title py-2 text-sm opacity-70">Neko View</div>
<div class="collapse-content">
<div class="pt-4 border-t border-base-300">
<div class="flex flex-row gap-4">
<div class="border border-primary bg-base-200/50 w-40 card relative">
<div class="size-full absolute">
<div
class="flex flex-row size-full items-end justify-between text-[8px] opacity-50 p-1 font-mono"
>
<div class="text-start flex flex-col">
<p>Scale</p>
<p>Opacity</p>
</div>
<div class="text-end flex flex-col">
<p>{($appState.sceneSetup.nekosScale * 100).toFixed(0)}%</p>
<p>{($appState.sceneSetup.nekosOpacity * 100).toFixed(0)}%</p>
</div>
</div>
</div>
<div
class="size-full flex flex-row -translate-y-2 justify-center items-center"
>
<DollPreview
dollColorScheme={{ body: "b7f2ff", outline: "496065" }}
spriteScale={$appState.sceneSetup.nekosScale}
spriteOpacity={$appState.sceneSetup.nekosOpacity}
/>
</div>
</div>
<div class="flex flex-col gap-4 w-full">
<div class="flex flex-col gap-2">
<p class="text-xs opacity-70">Opacity</p>
<div class="flex flex-row gap-2 items-center">
<input
type="range"
class="range flex-1"
min="0.1"
max="1"
step="0.01"
value={$appState.sceneSetup.nekosOpacity}
oninput={(event) =>
updateOpacity(
Number((event.currentTarget as HTMLInputElement).value),
)}
/>
</div>
</div>
<div class="flex flex-col gap-2">
<p class="text-xs opacity-70">Scale</p>
<div class="flex flex-row gap-2 items-center">
<input
type="range"
class="range flex-1"
min="0.5"
max="2"
step="0.25"
value={$appState.sceneSetup.nekosScale}
oninput={(event) =>
updateScale(
Number((event.currentTarget as HTMLInputElement).value),
)}
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,10 @@
<script lang="ts">
import NekoView from "./neko-view.svelte";
import NekoReposition from "./neko-reposition.svelte";
</script>
<div class="flex flex-col gap-4 w-full h-full">
<p class="text-lg font-bold">Scene Configuration</p>
<NekoView />
<NekoReposition />
</div>

View File

@@ -1,9 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { import { commands, type AppConfig } from "$lib/bindings";
commands,
type AppConfig,
} from "$lib/bindings";
let form: AppConfig = { let form: AppConfig = {
api_base_url: "", api_base_url: "",
@@ -109,7 +106,6 @@
bind:checked={form.debug_mode} bind:checked={form.debug_mode}
/> />
</label> </label>
</div> </div>
{#if errorMessage} {#if errorMessage}

View File

@@ -17,6 +17,7 @@
import PetMessagePop from "./components/pet-message-pop.svelte"; import PetMessagePop from "./components/pet-message-pop.svelte";
import PetMessageSend from "./components/pet-message-send.svelte"; import PetMessageSend from "./components/pet-message-send.svelte";
import type { UserBasicDto } from "$lib/bindings"; import type { UserBasicDto } from "$lib/bindings";
import { appState } from "../../events/app-state";
let debugMode = false; let debugMode = false;
@@ -46,6 +47,8 @@
targetX={$cursorPositionOnScreen.raw.x} targetX={$cursorPositionOnScreen.raw.x}
targetY={$cursorPositionOnScreen.raw.y} targetY={$cursorPositionOnScreen.raw.y}
spriteUrl={$activeDollSpriteUrl} spriteUrl={$activeDollSpriteUrl}
scale={$appState.sceneSetup.nekosScale}
opacity={$appState.sceneSetup.nekosOpacity}
/> />
{/if} {/if}
{#each Object.entries($friendsCursorPositions) as [friendId, position] (friendId)} {#each Object.entries($friendsCursorPositions) as [friendId, position] (friendId)}
@@ -57,6 +60,8 @@
spriteUrl={$friendActiveDollSpriteUrls[friendId]} spriteUrl={$friendActiveDollSpriteUrls[friendId]}
initialX={position.raw.x} initialX={position.raw.x}
initialY={position.raw.y} initialY={position.raw.y}
scale={$appState.sceneSetup.nekosScale}
opacity={$appState.sceneSetup.nekosOpacity}
> >
<PetMenu user={friend!} ariaLabel={`Open ${friend?.name} actions`} /> <PetMenu user={friend!} ariaLabel={`Open ${friend?.name} actions`} />
<PetMessagePop userId={friendId} /> <PetMessagePop userId={friendId} />
@@ -67,12 +72,12 @@
{#if debugMode} {#if debugMode}
<div id="debug-bar"> <div id="debug-bar">
<DebugBar <DebugBar
isInteractive={$sceneInteractive} isInteractive={$sceneInteractive}
cursorPosition={$cursorPositionOnScreen} cursorPosition={$cursorPositionOnScreen}
presenceStatus={$currentPresenceState?.presenceStatus ?? null} presenceStatus={$currentPresenceState?.presenceStatus ?? null}
friendsCursorPositions={$friendsCursorPositions} friendsCursorPositions={$friendsCursorPositions}
friends={$appData?.friends ?? []} friends={$appData?.friends ?? []}
friendsPresenceStates={$friendsPresenceStates} friendsPresenceStates={$friendsPresenceStates}
/> />
</div> </div>
{/if} {/if}

View File

@@ -4,6 +4,7 @@
import { setSprite } from "./sprites"; import { setSprite } from "./sprites";
import { calculateDirection, moveTowards, clampPosition } from "./physics"; import { calculateDirection, moveTowards, clampPosition } from "./physics";
import { updateIdle } from "./idle"; import { updateIdle } from "./idle";
import { appState } from "../../../../events/app-state";
interface Props { interface Props {
targetX: number; targetX: number;
@@ -11,6 +12,8 @@
spriteUrl: string; spriteUrl: string;
initialX?: number; initialX?: number;
initialY?: number; initialY?: number;
scale?: number;
opacity?: number;
children?: Snippet; children?: Snippet;
} }
@@ -20,10 +23,13 @@
spriteUrl, spriteUrl,
initialX = 32, initialX = 32,
initialY = 32, initialY = 32,
scale = 1.0,
opacity = 1.0,
children, children,
}: Props = $props(); }: Props = $props();
let nekoEl: HTMLDivElement; let nekoEl: HTMLDivElement;
let wrapperEl: HTMLDivElement;
let animationFrameId: number; let animationFrameId: number;
let nekoPos = $state({ x: initialX, y: initialY }); let nekoPos = $state({ x: initialX, y: initialY });
@@ -86,11 +92,15 @@
const newPos = moveTowards(nekoPos.x, nekoPos.y, targetPos.x, targetPos.y); const newPos = moveTowards(nekoPos.x, nekoPos.y, targetPos.x, targetPos.y);
nekoPos = newPos; nekoPos = newPos;
nekoEl.style.transform = `translate(${nekoPos.x - 16}px, ${nekoPos.y - 16}px)`; nekoEl.style.transform = `scale(${scale ?? 1.0})`;
nekoEl.style.opacity = `${opacity ?? 1.0}`;
wrapperEl.style.transform = `translate(${nekoPos.x - 16}px, ${nekoPos.y - 16}px)`;
} }
onMount(() => { onMount(() => {
nekoEl.style.backgroundImage = `url(${spriteUrl})`; nekoEl.style.backgroundImage = `url(${spriteUrl})`;
nekoEl.style.opacity = `${opacity ?? 1.0}`;
wrapperEl.style.transform = `translate(${nekoPos.x - 16}px, ${nekoPos.y - 16}px)`;
animationFrameId = requestAnimationFrame(frame); animationFrameId = requestAnimationFrame(frame);
}); });
@@ -101,18 +111,27 @@
}); });
$effect(() => { $effect(() => {
if (nekoEl && spriteUrl) { if (nekoEl && spriteUrl && $appState) {
nekoEl.style.transform = `scale(${scale ?? 1.0})`;
nekoEl.style.backgroundImage = `url(${spriteUrl})`; nekoEl.style.backgroundImage = `url(${spriteUrl})`;
nekoEl.style.opacity = `${opacity ?? 1.0}`;
} }
}); });
</script> </script>
<div <div
bind:this={nekoEl} bind:this={wrapperEl}
class="pointer-events-none fixed z-999 size-8 select-none" class="pointer-events-none fixed z-999 size-8 select-none"
style="width: 32px; height: 32px; position: fixed; image-rendering: pixelated;" style="position: fixed; width: 32px; height: 32px;"
> >
<div class="relative size-full"> <div class="relative">
{@render children?.()} <div
bind:this={nekoEl}
class="size-8"
style="position: absolute; image-rendering: pixelated;"
></div>
<div class="absolute size-8">
{@render children?.()}
</div>
</div> </div>
</div> </div>