Add: global factor controlling fps base interval
This commit is contained in:
@@ -7,79 +7,79 @@
|
||||
"animations": [
|
||||
{
|
||||
"name": "idle",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [0, 1, 2, 3, 4, 5, 6, 7]
|
||||
},
|
||||
{
|
||||
"name": "active",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
||||
},
|
||||
{
|
||||
"name": "happy",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
||||
},
|
||||
{
|
||||
"name": "love",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
||||
},
|
||||
{
|
||||
"name": "excited",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [16, 17, 18, 19, 20, 21, 22, 23]
|
||||
},
|
||||
{
|
||||
"name": "celebrate",
|
||||
"fps": 10,
|
||||
"fps": 2,
|
||||
"frames": [16, 17, 18, 19, 20, 21, 22, 23],
|
||||
"one_shot": true
|
||||
},
|
||||
{
|
||||
"name": "success",
|
||||
"fps": 10,
|
||||
"fps": 2,
|
||||
"frames": [16, 17, 18, 19, 20, 21, 22, 23],
|
||||
"one_shot": true
|
||||
},
|
||||
{
|
||||
"name": "sleepy",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [24, 25, 26, 27, 28, 29, 30, 31]
|
||||
},
|
||||
{
|
||||
"name": "snoring",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [24, 25, 26, 27, 28, 29, 30, 31]
|
||||
},
|
||||
{
|
||||
"name": "working",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [32, 33, 34, 35, 36, 37, 38, 39]
|
||||
},
|
||||
{
|
||||
"name": "angry",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||
},
|
||||
{
|
||||
"name": "surprised",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||
},
|
||||
{
|
||||
"name": "shy",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||
},
|
||||
{
|
||||
"name": "dragging",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [48, 49, 50, 51, 52, 53, 54, 55]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -7,79 +7,79 @@
|
||||
"animations": [
|
||||
{
|
||||
"name": "idle",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [0, 1, 2, 3, 4, 5, 6, 7]
|
||||
},
|
||||
{
|
||||
"name": "active",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
||||
},
|
||||
{
|
||||
"name": "happy",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
||||
},
|
||||
{
|
||||
"name": "love",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
||||
},
|
||||
{
|
||||
"name": "excited",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [16, 17, 18, 19, 20, 21, 22, 23]
|
||||
},
|
||||
{
|
||||
"name": "celebrate",
|
||||
"fps": 10,
|
||||
"fps": 2,
|
||||
"frames": [16, 17, 18, 19, 20, 21, 22, 23],
|
||||
"one_shot": true
|
||||
},
|
||||
{
|
||||
"name": "success",
|
||||
"fps": 10,
|
||||
"fps": 2,
|
||||
"frames": [16, 17, 18, 19, 20, 21, 22, 23],
|
||||
"one_shot": true
|
||||
},
|
||||
{
|
||||
"name": "sleepy",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [24, 25, 26, 27, 28, 29, 30, 31]
|
||||
},
|
||||
{
|
||||
"name": "snoring",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [24, 25, 26, 27, 28, 29, 30, 31]
|
||||
},
|
||||
{
|
||||
"name": "working",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [32, 33, 34, 35, 36, 37, 38, 39]
|
||||
},
|
||||
{
|
||||
"name": "angry",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||
},
|
||||
{
|
||||
"name": "surprised",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||
},
|
||||
{
|
||||
"name": "shy",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||
},
|
||||
{
|
||||
"name": "dragging",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [48, 49, 50, 51, 52, 53, 54, 55]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -7,79 +7,79 @@
|
||||
"animations": [
|
||||
{
|
||||
"name": "idle",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [0, 1, 2, 3, 4, 5, 6, 7]
|
||||
},
|
||||
{
|
||||
"name": "active",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
||||
},
|
||||
{
|
||||
"name": "happy",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
||||
},
|
||||
{
|
||||
"name": "love",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
||||
},
|
||||
{
|
||||
"name": "excited",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [16, 17, 18, 19, 20, 21, 22, 23]
|
||||
},
|
||||
{
|
||||
"name": "celebrate",
|
||||
"fps": 10,
|
||||
"fps": 2,
|
||||
"frames": [16, 17, 18, 19, 20, 21, 22, 23],
|
||||
"one_shot": true
|
||||
},
|
||||
{
|
||||
"name": "success",
|
||||
"fps": 10,
|
||||
"fps": 2,
|
||||
"frames": [16, 17, 18, 19, 20, 21, 22, 23],
|
||||
"one_shot": true
|
||||
},
|
||||
{
|
||||
"name": "sleepy",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [24, 25, 26, 27, 28, 29, 30, 31]
|
||||
},
|
||||
{
|
||||
"name": "snoring",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [24, 25, 26, 27, 28, 29, 30, 31]
|
||||
},
|
||||
{
|
||||
"name": "working",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [32, 33, 34, 35, 36, 37, 38, 39]
|
||||
},
|
||||
{
|
||||
"name": "angry",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||
},
|
||||
{
|
||||
"name": "surprised",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||
},
|
||||
{
|
||||
"name": "shy",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||
},
|
||||
{
|
||||
"name": "dragging",
|
||||
"fps": 8,
|
||||
"fps": 1,
|
||||
"frames": [48, 49, 50, 51, 52, 53, 54, 55]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -159,6 +159,7 @@ pub enum FrontendBackend {
|
||||
pub struct FrontendConfig {
|
||||
pub backend: FrontendBackend,
|
||||
pub debug_overlay_visible: bool,
|
||||
pub tauri_animation_slowdown_factor: u8,
|
||||
}
|
||||
|
||||
impl Default for FrontendConfig {
|
||||
@@ -166,6 +167,7 @@ impl Default for FrontendConfig {
|
||||
Self {
|
||||
backend: FrontendBackend::Bevy,
|
||||
debug_overlay_visible: false,
|
||||
tauri_animation_slowdown_factor: 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,11 +227,13 @@ mod tests {
|
||||
config.window.x = 42.0;
|
||||
config.frontend.backend = super::FrontendBackend::Tauri;
|
||||
config.frontend.debug_overlay_visible = true;
|
||||
config.frontend.tauri_animation_slowdown_factor = 7;
|
||||
|
||||
save(&path, &config).expect("save");
|
||||
let (_, loaded) = load_or_create_at(&path).expect("reload");
|
||||
assert!((loaded.window.x - 42.0).abs() < f32::EPSILON);
|
||||
assert_eq!(loaded.frontend.backend, super::FrontendBackend::Tauri);
|
||||
assert!(loaded.frontend.debug_overlay_visible);
|
||||
assert_eq!(loaded.frontend.tauri_animation_slowdown_factor, 7);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ use tokio::runtime::Runtime;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tracing::warn;
|
||||
|
||||
const TAURI_ANIMATION_SLOWDOWN_FACTOR_MIN: u8 = 1;
|
||||
const TAURI_ANIMATION_SLOWDOWN_FACTOR_MAX: u8 = 20;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RuntimeCoreError {
|
||||
#[error("{0}")]
|
||||
@@ -104,6 +107,32 @@ impl RuntimeCore {
|
||||
self.persist_config()
|
||||
}
|
||||
|
||||
pub fn frontend_tauri_animation_slowdown_factor(&self) -> Result<u8, RuntimeCoreError> {
|
||||
let guard = self
|
||||
.config
|
||||
.read()
|
||||
.map_err(|_| RuntimeCoreError::ConfigPoisoned)?;
|
||||
Ok(clamp_tauri_animation_slowdown_factor(
|
||||
guard.frontend.tauri_animation_slowdown_factor,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn set_frontend_tauri_animation_slowdown_factor(
|
||||
&self,
|
||||
value: u8,
|
||||
) -> Result<u8, RuntimeCoreError> {
|
||||
let clamped = clamp_tauri_animation_slowdown_factor(value);
|
||||
{
|
||||
let mut guard = self
|
||||
.config
|
||||
.write()
|
||||
.map_err(|_| RuntimeCoreError::ConfigPoisoned)?;
|
||||
guard.frontend.tauri_animation_slowdown_factor = clamped;
|
||||
}
|
||||
self.persist_config()?;
|
||||
Ok(clamped)
|
||||
}
|
||||
|
||||
pub fn api_config(&self) -> ApiConfig {
|
||||
self.api_config.clone()
|
||||
}
|
||||
@@ -257,6 +286,13 @@ fn default_animation_for_state(state: sprimo_protocol::v1::FrontendState) -> &'s
|
||||
}
|
||||
}
|
||||
|
||||
fn clamp_tauri_animation_slowdown_factor(value: u8) -> u8 {
|
||||
value.clamp(
|
||||
TAURI_ANIMATION_SLOWDOWN_FACTOR_MIN,
|
||||
TAURI_ANIMATION_SLOWDOWN_FACTOR_MAX,
|
||||
)
|
||||
}
|
||||
|
||||
fn log_api_error(err: ApiServerError) {
|
||||
warn!(%err, "runtime core api server exited");
|
||||
}
|
||||
@@ -331,4 +367,36 @@ mod tests {
|
||||
core.set_frontend_debug_overlay_visible(true).expect("set");
|
||||
assert!(core.frontend_debug_overlay_visible().expect("get"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frontend_tauri_animation_slowdown_factor_roundtrips() {
|
||||
let temp = TempDir::new().expect("tempdir");
|
||||
let path = temp.path().join("config.toml");
|
||||
let core = RuntimeCore::new_with_config(path, AppConfig::default(), CapabilityFlags::default())
|
||||
.expect("core init");
|
||||
let persisted = core
|
||||
.set_frontend_tauri_animation_slowdown_factor(6)
|
||||
.expect("set");
|
||||
assert_eq!(persisted, 6);
|
||||
assert_eq!(
|
||||
core.frontend_tauri_animation_slowdown_factor().expect("get"),
|
||||
6
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frontend_tauri_animation_slowdown_factor_clamps() {
|
||||
let temp = TempDir::new().expect("tempdir");
|
||||
let path = temp.path().join("config.toml");
|
||||
let core = RuntimeCore::new_with_config(path, AppConfig::default(), CapabilityFlags::default())
|
||||
.expect("core init");
|
||||
let persisted = core
|
||||
.set_frontend_tauri_animation_slowdown_factor(0)
|
||||
.expect("set");
|
||||
assert_eq!(persisted, 1);
|
||||
assert_eq!(
|
||||
core.frontend_tauri_animation_slowdown_factor().expect("get"),
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ const MENU_ID_TOGGLE_DEBUG_OVERLAY: &str = "toggle_debug_overlay";
|
||||
const MENU_ID_QUIT: &str = "quit";
|
||||
const EVENT_RUNTIME_SNAPSHOT: &str = "runtime:snapshot";
|
||||
const EVENT_RUNTIME_DEBUG_OVERLAY_VISIBLE: &str = "runtime:debug-overlay-visible";
|
||||
const EVENT_RUNTIME_ANIMATION_SLOWDOWN_FACTOR: &str = "runtime:animation-slowdown-factor";
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
struct UiAnimationClip {
|
||||
@@ -80,6 +81,7 @@ struct UiSettingsSnapshot {
|
||||
scale: f32,
|
||||
visible: bool,
|
||||
always_on_top: bool,
|
||||
tauri_animation_slowdown_factor: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
@@ -183,9 +185,37 @@ fn settings_snapshot(state: tauri::State<'_, AppState>) -> Result<UiSettingsSnap
|
||||
scale: snapshot.scale,
|
||||
visible: snapshot.flags.visible,
|
||||
always_on_top: snapshot.flags.always_on_top,
|
||||
tauri_animation_slowdown_factor: state
|
||||
.runtime_core
|
||||
.frontend_tauri_animation_slowdown_factor()
|
||||
.map_err(|err| err.to_string())?,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn tauri_animation_slowdown_factor(state: tauri::State<'_, AppState>) -> Result<u8, String> {
|
||||
state
|
||||
.runtime_core
|
||||
.frontend_tauri_animation_slowdown_factor()
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_tauri_animation_slowdown_factor(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, AppState>,
|
||||
factor: u8,
|
||||
) -> Result<u8, String> {
|
||||
let persisted = state
|
||||
.runtime_core
|
||||
.set_frontend_tauri_animation_slowdown_factor(factor)
|
||||
.map_err(|err| err.to_string())?;
|
||||
app_handle
|
||||
.emit(EVENT_RUNTIME_ANIMATION_SLOWDOWN_FACTOR, persisted)
|
||||
.map_err(|err| err.to_string())?;
|
||||
Ok(persisted)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn list_sprite_packs(state: tauri::State<'_, AppState>) -> Result<Vec<UiSpritePackOption>, String> {
|
||||
let root = sprite_pack_root(state.runtime_core.as_ref())?;
|
||||
@@ -349,6 +379,8 @@ fn main() -> Result<(), AppError> {
|
||||
debug_overlay_visible,
|
||||
set_debug_overlay_visible,
|
||||
settings_snapshot,
|
||||
tauri_animation_slowdown_factor,
|
||||
set_tauri_animation_slowdown_factor,
|
||||
list_sprite_packs,
|
||||
set_sprite_pack,
|
||||
set_scale,
|
||||
|
||||
@@ -40,6 +40,7 @@ recovery_hotkey = "Ctrl+Alt+P"
|
||||
[frontend]
|
||||
backend = "bevy"
|
||||
debug_overlay_visible = false
|
||||
tauri_animation_slowdown_factor = 3
|
||||
```
|
||||
|
||||
## Notes
|
||||
@@ -50,3 +51,6 @@ debug_overlay_visible = false
|
||||
- On Windows, `recovery_hotkey` now forces `visible = true` and `always_on_top = true` for recovery.
|
||||
- `frontend.backend` selects runtime frontend implementation (`bevy` or `tauri`).
|
||||
- `frontend.debug_overlay_visible` controls whether tauri window debug diagnostics panel is shown.
|
||||
- `frontend.tauri_animation_slowdown_factor` controls tauri animation pacing multiplier.
|
||||
valid range: `1..20`
|
||||
effective frame interval: `(1000 / clip_fps) * factor`
|
||||
|
||||
@@ -92,3 +92,8 @@ Default one-shot policy:
|
||||
|
||||
- `celebrate` and `success` are one-shot.
|
||||
- other row animations loop by default.
|
||||
|
||||
Recommended FPS profile for 8-frame rows:
|
||||
|
||||
- looping rows: `1` fps
|
||||
- one-shot `celebrate`/`success`: `2` fps
|
||||
|
||||
@@ -115,7 +115,12 @@ An issue touching Tauri runtime behaviors must satisfy all requirements before `
|
||||
8. Chroma-key quality check:
|
||||
- verify no visible magenta background/fringe remains around sprite edges in normal runtime view,
|
||||
including non-exact gradient magenta atlas backgrounds (for example `ferris`/`demogorgon`)
|
||||
9. Scale anchor and bounds check:
|
||||
9. Animation tempo check for 8-frame rows:
|
||||
- looping row animations should feel very slow/readable (1 fps profile)
|
||||
- one-shot `celebrate`/`success` should run slightly faster than loops (2 fps profile)
|
||||
- tauri renderer applies an additional global slowdown factor (`x2`) over clip fps; verify perceived
|
||||
playback matches this expectation
|
||||
10. Scale anchor and bounds check:
|
||||
- repeated scale changes should keep window centered without directional drift
|
||||
- window must remain within current monitor bounds during scale adjustments
|
||||
- no runtime error is allowed from monitor lookup during scale operations (e.g. `currentMonitor`
|
||||
@@ -138,10 +143,14 @@ An issue touching Tauri runtime behaviors must satisfy all requirements before `
|
||||
- main overlay auto-fits without clipping
|
||||
- persisted/runtime scale value reflects the effective fitted scale after monitor/window bounds
|
||||
- value persists after restart
|
||||
5. Toggle `Visible` and verify:
|
||||
5. Change animation speed factor slider and verify:
|
||||
- runtime animation pace updates immediately in main overlay
|
||||
- value is clamped to `1..20`
|
||||
- value persists after restart via `frontend.tauri_animation_slowdown_factor`
|
||||
6. Toggle `Visible` and verify:
|
||||
- main overlay hide/show behavior
|
||||
- persisted value survives restart
|
||||
6. Toggle `Always on top` and verify:
|
||||
7. Toggle `Always on top` and verify:
|
||||
- main window z-order behavior updates
|
||||
- persisted value survives restart
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ type UiSettingsSnapshot = {
|
||||
scale: number;
|
||||
visible: boolean;
|
||||
always_on_top: boolean;
|
||||
tauri_animation_slowdown_factor: number;
|
||||
};
|
||||
|
||||
type UiSpritePackOption = {
|
||||
@@ -33,6 +34,9 @@ const SCALE_MIN = 0.5;
|
||||
const SCALE_MAX = 3.0;
|
||||
const LOGICAL_BASE_FRAME_WIDTH = 512;
|
||||
const LOGICAL_BASE_FRAME_HEIGHT = 512;
|
||||
const SLOWDOWN_FACTOR_MIN = 1;
|
||||
const SLOWDOWN_FACTOR_MAX = 20;
|
||||
const SLOWDOWN_FACTOR_DEFAULT = 3;
|
||||
|
||||
async function invokeSetSpritePack(packIdOrPath: string): Promise<UiSnapshot> {
|
||||
return invoke<UiSnapshot>("set_sprite_pack", { packIdOrPath });
|
||||
@@ -50,6 +54,14 @@ async function invokeSetAlwaysOnTop(alwaysOnTop: boolean): Promise<UiSnapshot> {
|
||||
return invoke<UiSnapshot>("set_always_on_top", { alwaysOnTop });
|
||||
}
|
||||
|
||||
async function invokeAnimationSlowdownFactor(): Promise<number> {
|
||||
return invoke<number>("tauri_animation_slowdown_factor");
|
||||
}
|
||||
|
||||
async function invokeSetAnimationSlowdownFactor(factor: number): Promise<number> {
|
||||
return invoke<number>("set_tauri_animation_slowdown_factor", { factor });
|
||||
}
|
||||
|
||||
function fittedWindowSize(
|
||||
scale: number
|
||||
): { width: number; height: number } {
|
||||
@@ -142,6 +154,7 @@ function MainOverlayWindow(): JSX.Element {
|
||||
const activePackRef = React.useRef<UiSpritePack | null>(null);
|
||||
const loadedPackKeyRef = React.useRef<string | null>(null);
|
||||
const effectiveScaleSyncRef = React.useRef<number | null>(null);
|
||||
const slowdownFactorRef = React.useRef<number>(SLOWDOWN_FACTOR_DEFAULT);
|
||||
const loadingPackRef = React.useRef(false);
|
||||
const mountedRef = React.useRef(false);
|
||||
|
||||
@@ -158,6 +171,7 @@ function MainOverlayWindow(): JSX.Element {
|
||||
}
|
||||
const previousRenderer = rendererRef.current;
|
||||
const nextRenderer = await PixiPetRenderer.create(hostRef.current, pack, nextSnapshot);
|
||||
nextRenderer.setAnimationSlowdownFactor(slowdownFactorRef.current);
|
||||
rendererRef.current = nextRenderer;
|
||||
activePackRef.current = pack;
|
||||
loadedPackKeyRef.current = nextSnapshot.active_sprite_pack;
|
||||
@@ -252,13 +266,18 @@ function MainOverlayWindow(): JSX.Element {
|
||||
Promise.all([
|
||||
invoke<UiSpritePack>("load_active_sprite_pack"),
|
||||
invoke<UiSnapshot>("current_state"),
|
||||
invoke<boolean>("debug_overlay_visible")
|
||||
invoke<boolean>("debug_overlay_visible"),
|
||||
invokeAnimationSlowdownFactor()
|
||||
])
|
||||
.then(async ([pack, initialSnapshot, showDebug]) => {
|
||||
.then(async ([pack, initialSnapshot, showDebug, slowdownFactor]) => {
|
||||
if (!mountedRef.current) {
|
||||
return;
|
||||
}
|
||||
setDebugOverlayVisible(showDebug);
|
||||
slowdownFactorRef.current = Math.min(
|
||||
Math.max(Math.round(slowdownFactor), SLOWDOWN_FACTOR_MIN),
|
||||
SLOWDOWN_FACTOR_MAX
|
||||
);
|
||||
await recreateRenderer(pack, initialSnapshot);
|
||||
await processSnapshot(initialSnapshot);
|
||||
|
||||
@@ -271,9 +290,23 @@ function MainOverlayWindow(): JSX.Element {
|
||||
}
|
||||
setDebugOverlayVisible(Boolean(event.payload));
|
||||
});
|
||||
const unlistenSlowdown = await listen<number>(
|
||||
"runtime:animation-slowdown-factor",
|
||||
(event) => {
|
||||
if (!mountedRef.current) {
|
||||
return;
|
||||
}
|
||||
slowdownFactorRef.current = Math.min(
|
||||
Math.max(Math.round(Number(event.payload)), SLOWDOWN_FACTOR_MIN),
|
||||
SLOWDOWN_FACTOR_MAX
|
||||
);
|
||||
rendererRef.current?.setAnimationSlowdownFactor(slowdownFactorRef.current);
|
||||
}
|
||||
);
|
||||
unlisten = () => {
|
||||
unlistenSnapshot();
|
||||
unlistenDebug();
|
||||
unlistenSlowdown();
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -406,10 +439,29 @@ function SettingsWindow(): JSX.Element {
|
||||
active_sprite_pack: payload.active_sprite_pack,
|
||||
scale: payload.scale,
|
||||
visible: payload.visible,
|
||||
always_on_top: payload.always_on_top
|
||||
always_on_top: payload.always_on_top,
|
||||
tauri_animation_slowdown_factor:
|
||||
prev.tauri_animation_slowdown_factor ?? SLOWDOWN_FACTOR_DEFAULT
|
||||
};
|
||||
});
|
||||
});
|
||||
const unlistenSlowdown = await listen<number>("runtime:animation-slowdown-factor", (event) => {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
const factor = Math.min(
|
||||
Math.max(Math.round(Number(event.payload)), SLOWDOWN_FACTOR_MIN),
|
||||
SLOWDOWN_FACTOR_MAX
|
||||
);
|
||||
setSettings((prev) =>
|
||||
prev === null ? prev : { ...prev, tauri_animation_slowdown_factor: factor }
|
||||
);
|
||||
});
|
||||
const previousUnlisten = unlisten;
|
||||
unlisten = () => {
|
||||
previousUnlisten();
|
||||
unlistenSlowdown();
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
if (mounted) {
|
||||
@@ -499,6 +551,29 @@ function SettingsWindow(): JSX.Element {
|
||||
[withPending]
|
||||
);
|
||||
|
||||
const onAnimationSlowdownFactorChange = React.useCallback(
|
||||
async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = Number(event.target.value);
|
||||
if (!Number.isFinite(value)) {
|
||||
return;
|
||||
}
|
||||
const clamped = Math.min(
|
||||
Math.max(Math.round(value), SLOWDOWN_FACTOR_MIN),
|
||||
SLOWDOWN_FACTOR_MAX
|
||||
);
|
||||
const persisted = await withPending(() => invokeSetAnimationSlowdownFactor(clamped));
|
||||
if (persisted === null) {
|
||||
return;
|
||||
}
|
||||
setSettings((prev) =>
|
||||
prev === null
|
||||
? prev
|
||||
: { ...prev, tauri_animation_slowdown_factor: Number(persisted) }
|
||||
);
|
||||
},
|
||||
[withPending]
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="settings-root">
|
||||
<section className="settings-card">
|
||||
@@ -535,6 +610,20 @@ function SettingsWindow(): JSX.Element {
|
||||
onChange={onScaleChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>
|
||||
Animation Speed Factor: x{settings.tauri_animation_slowdown_factor}
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min={SLOWDOWN_FACTOR_MIN}
|
||||
max={SLOWDOWN_FACTOR_MAX}
|
||||
step={1}
|
||||
value={settings.tauri_animation_slowdown_factor}
|
||||
disabled={pending}
|
||||
onChange={onAnimationSlowdownFactorChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
@@ -53,6 +53,9 @@ const HALO_SAT_MIN = 0.15;
|
||||
const HALO_VAL_MIN = 0.04;
|
||||
const RENDER_FIT_PADDING = 16;
|
||||
const MIN_RENDER_SCALE = 0.01;
|
||||
const ANIMATION_SLOWDOWN_FACTOR_MIN = 1;
|
||||
const ANIMATION_SLOWDOWN_FACTOR_MAX = 20;
|
||||
const ANIMATION_SLOWDOWN_FACTOR_DEFAULT = 3;
|
||||
|
||||
export class PixiPetRenderer {
|
||||
private app: Application;
|
||||
@@ -63,6 +66,7 @@ export class PixiPetRenderer {
|
||||
private frameCursor = 0;
|
||||
private frameElapsedMs = 0;
|
||||
private baseTexture: BaseTexture;
|
||||
private animationSlowdownFactor = ANIMATION_SLOWDOWN_FACTOR_DEFAULT;
|
||||
private disposed = false;
|
||||
|
||||
private constructor(
|
||||
@@ -431,13 +435,24 @@ export class PixiPetRenderer {
|
||||
this.layoutSprite();
|
||||
}
|
||||
|
||||
setAnimationSlowdownFactor(factor: number): void {
|
||||
if (!Number.isFinite(factor)) {
|
||||
return;
|
||||
}
|
||||
const rounded = Math.round(factor);
|
||||
this.animationSlowdownFactor = Math.min(
|
||||
Math.max(rounded, ANIMATION_SLOWDOWN_FACTOR_MIN),
|
||||
ANIMATION_SLOWDOWN_FACTOR_MAX
|
||||
);
|
||||
}
|
||||
|
||||
private startTicker(): void {
|
||||
this.app.ticker.add((ticker) => {
|
||||
if (this.disposed) {
|
||||
return;
|
||||
}
|
||||
this.layoutSprite();
|
||||
const frameMs = 1000 / Math.max(this.currentClip.fps, 1);
|
||||
const frameMs = (1000 / Math.max(this.currentClip.fps, 1)) * this.animationSlowdownFactor;
|
||||
this.frameElapsedMs += ticker.deltaMS;
|
||||
if (this.frameElapsedMs < frameMs) {
|
||||
return;
|
||||
|
||||
@@ -10,6 +10,7 @@ P2
|
||||
|
||||
Standardize `sprite.png` packs so each of the 7 rows maps to semantic animation groups, while
|
||||
keeping runtime compatibility with existing state names (`active`, `success`, `error`).
|
||||
Also retune 8-frame animation tempo to a slower profile for readability.
|
||||
|
||||
## Scope
|
||||
|
||||
@@ -42,6 +43,16 @@ One-shot defaults:
|
||||
2. All bundled sprite-pack manifests now expose row-based names and compatibility aliases.
|
||||
3. Added runtime-core unit test to confirm `SetState::Dragging` selects `"dragging"`.
|
||||
4. Updated schema/design docs to formalize the row convention.
|
||||
5. Retuned bundled pack fps profile:
|
||||
- loops: `1` fps
|
||||
- one-shot `celebrate` and `success`: `2` fps
|
||||
6. Tauri renderer-level pacing adjusted:
|
||||
- added global frame interval slowdown factor (`x2`) in Pixi ticker to further reduce perceived speed
|
||||
without changing manifest schema type (`fps` remains integer).
|
||||
7. Added persisted tauri slowdown factor control:
|
||||
- config key: `frontend.tauri_animation_slowdown_factor` (integer, range `1..20`, default `3`)
|
||||
- new tauri commands/events to read/update factor at runtime
|
||||
- settings window slider added for live tuning and persistence.
|
||||
|
||||
## Verification
|
||||
|
||||
|
||||
Reference in New Issue
Block a user