feat: add game optimization mode, 3d cursor parallax, and embers particle effect
This commit is contained in:
1 parent
a75e723416
commit
62b31169cc
14 files changed
+301
-20
No files matched your search
@@ -21,6 +21,7 @@ It enhances the osu! experience with quality-of-life features, advanced launcher
|
|||||||
- [x] **Live Player Stats**: Displays rank, PP, accuracy, playcount, and server latency directly on the launcher dashboard.
|
- [x] **Live Player Stats**: Displays rank, PP, accuracy, playcount, and server latency directly on the launcher dashboard.
|
||||||
- [x] **Library Indexing**: Automatically counts imported beatmapsets and skins.
|
- [x] **Library Indexing**: Automatically counts imported beatmapsets and skins.
|
||||||
- [x] **OpenTabletDriver Integration**: Automatically starts and stops the OpenTabletDriver daemon on launch/exit (Windows only).
|
- [x] **OpenTabletDriver Integration**: Automatically starts and stops the OpenTabletDriver daemon on launch/exit (Windows only).
|
||||||
|
- [x] **Competitive Game Optimization Mode**: Optional low-latency engine setting that boosts osu! process priority to High and requests sub-millisecond Windows timer resolution (1ms) during gameplay.
|
||||||
- [x] **Performance Display Overlay**: In-game HUD overlay (available in the experimental patcher stream).
|
- [x] **Performance Display Overlay**: In-game HUD overlay (available in the experimental patcher stream).
|
||||||
|
|
||||||
### Visual Theme Engine v2 & In-App Studio
|
### Visual Theme Engine v2 & In-App Studio
|
||||||
@@ -32,6 +33,7 @@ It enhances the osu! experience with quality-of-life features, advanced launcher
|
|||||||
- [x] **Logic & Scripting System**: Visual WHEN/IF/THEN rule builder and sandboxed rule scripting (expressions, branches, loops, visibility toggles) without `eval`.
|
- [x] **Logic & Scripting System**: Visual WHEN/IF/THEN rule builder and sandboxed rule scripting (expressions, branches, loops, visibility toggles) without `eval`.
|
||||||
- [x] **Dynamic Placeholders & Clock Expressions**: Real-time variables (`{username}`, `{rank}`, `{pp}`) and second-accurate clock variables (`{time}`, `{hour}`, `{minute}`).
|
- [x] **Dynamic Placeholders & Clock Expressions**: Real-time variables (`{username}`, `{rank}`, `{pp}`) and second-accurate clock variables (`{time}`, `{hour}`, `{minute}`).
|
||||||
- [x] **Typography & Icon Families**: Gradient fills, outlines, drop shadows, custom `@fontsource` webfonts, and bundled Lucide, Phosphor, and Tabler icon catalogs.
|
- [x] **Typography & Icon Families**: Gradient fills, outlines, drop shadows, custom `@fontsource` webfonts, and bundled Lucide, Phosphor, and Tabler icon catalogs.
|
||||||
|
- [x] **Dynamic Particle Emitters & 3D Cursor Parallax**: Configurable background particles (snow, stars, petals, bubbles, and floating embers) plus smooth cursor-driven 3D parallax depth for layers and backgrounds.
|
||||||
- [x] **Backup & Rollback System**: Automatic versioned theme snapshots with one-click restoration, plus security trust prompts for executable components.
|
- [x] **Backup & Rollback System**: Automatic versioned theme snapshots with one-click restoration, plus security trust prompts for executable components.
|
||||||
|
|
||||||
### Discord Rich Presence Studio
|
### Discord Rich Presence Studio
|
||||||
|
|||||||
+13
-1
@@ -196,12 +196,24 @@ clamped to `0`–`1`.
|
|||||||
{ "type": "stars", "density": 40, "speed": 1, "color": "#ffd6f2" }
|
{ "type": "stars", "density": 40, "speed": 1, "color": "#ffd6f2" }
|
||||||
```
|
```
|
||||||
|
|
||||||
Supported types: `none`, `stars`, `snow`, `petals`, `bubbles`.
|
Supported types: `none`, `stars`, `snow`, `petals`, `bubbles`, `embers`.
|
||||||
|
|
||||||
- `density` is clamped to `0`–`120` particles.
|
- `density` is clamped to `0`–`120` particles.
|
||||||
- `speed` is clamped to `0.2`–`3`.
|
- `speed` is clamped to `0.2`–`3`.
|
||||||
- Effects are disabled when the user turns on **Reduce animations**.
|
- Effects are disabled when the user turns on **Reduce animations**.
|
||||||
|
|
||||||
|
### Parallax
|
||||||
|
|
||||||
|
Themes can enable subtle 3D cursor parallax on the background and freeform layers:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"parallax": { "enabled": true, "intensity": 0.25 }
|
||||||
|
```
|
||||||
|
|
||||||
|
- `intensity` is clamped to `0.05`–`1.0` (defaults to `0.25`).
|
||||||
|
- Individual layers and groups can define their own `parallax` depth factor from `-2.0` to `+2.0` in the inspector (where negative shifts with background and positive shifts with foreground).
|
||||||
|
- Parallax is automatically disabled when the user turns on **Reduce animations**.
|
||||||
|
|
||||||
### Audio
|
### Audio
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -559,12 +559,17 @@ pub async fn run_osu_updater(folder: String) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn run_osu(folder: String, patch: bool) -> Result<(), String> {
|
pub async fn run_osu(
|
||||||
log::info!(target: "osu", "Launching osu! from {folder} (patch={patch})");
|
folder: String,
|
||||||
|
patch: bool,
|
||||||
|
game_optimization: Option<bool>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let optimize = game_optimization.unwrap_or(false);
|
||||||
|
log::info!(target: "osu", "Launching osu! from {folder} (patch={patch}, optimize={optimize})");
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
return crate::macos::run_osu(&folder, patch).await;
|
return crate::macos::run_osu(&folder, patch, optimize).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
@@ -574,6 +579,18 @@ pub async fn run_osu(folder: String, patch: bool) -> Result<(), String> {
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
if optimize {
|
||||||
|
#[link(name = "winmm")]
|
||||||
|
extern "system" {
|
||||||
|
fn timeBeginPeriod(uPeriod: u32) -> u32;
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
timeBeginPeriod(1);
|
||||||
|
}
|
||||||
|
log::info!(target: "osu", "Game optimization: requested 1ms timer resolution");
|
||||||
|
}
|
||||||
|
|
||||||
let mut game_process = {
|
let mut game_process = {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
@@ -611,6 +628,22 @@ pub async fn run_osu(folder: String, patch: bool) -> Result<(), String> {
|
|||||||
|
|
||||||
if let Some(pid) = game_process.id() {
|
if let Some(pid) = game_process.id() {
|
||||||
log::info!(target: "osu", "osu! started (pid {pid})");
|
log::info!(target: "osu", "osu! started (pid {pid})");
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
if optimize {
|
||||||
|
unsafe {
|
||||||
|
use winapi::um::processthreadsapi::{OpenProcess, SetPriorityClass};
|
||||||
|
use winapi::um::winbase::HIGH_PRIORITY_CLASS;
|
||||||
|
use winapi::um::winnt::PROCESS_SET_INFORMATION;
|
||||||
|
let handle = OpenProcess(PROCESS_SET_INFORMATION, 0, pid);
|
||||||
|
if !handle.is_null() {
|
||||||
|
if SetPriorityClass(handle, HIGH_PRIORITY_CLASS) != 0 {
|
||||||
|
log::info!(target: "osu", "Game optimization: boosted osu! process priority to High (pid {pid})");
|
||||||
|
}
|
||||||
|
winapi::um::handleapi::CloseHandle(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
crate::logging::capture_osu_output(&mut game_process);
|
crate::logging::capture_osu_output(&mut game_process);
|
||||||
|
|
||||||
@@ -691,7 +724,21 @@ pub async fn run_osu(folder: String, patch: bool) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match game_process.wait().await {
|
let wait_res = game_process.wait().await;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
if optimize {
|
||||||
|
#[link(name = "winmm")]
|
||||||
|
extern "system" {
|
||||||
|
fn timeEndPeriod(uPeriod: u32) -> u32;
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
timeEndPeriod(1);
|
||||||
|
}
|
||||||
|
log::info!(target: "osu", "Game optimization: restored timer resolution");
|
||||||
|
}
|
||||||
|
|
||||||
|
match wait_res {
|
||||||
Ok(status) => log::info!(target: "osu", "osu! exited with {status}"),
|
Ok(status) => log::info!(target: "osu", "osu! exited with {status}"),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
log::warn!(target: "osu", "Could not wait for osu!: {error}");
|
log::warn!(target: "osu", "Could not wait for osu!: {error}");
|
||||||
|
|||||||
@@ -582,7 +582,14 @@ async fn clean_wine_font_registry(runtime: &WineRuntime) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Run osu! (`-devserver ez-pp.farm`) and wait until the game exits.
|
/// Run osu! (`-devserver ez-pp.farm`) and wait until the game exits.
|
||||||
pub async fn run_osu(osu_folder: &str, patch: bool) -> Result<(), String> {
|
pub async fn run_osu(
|
||||||
|
osu_folder: &str,
|
||||||
|
patch: bool,
|
||||||
|
game_optimization: bool,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if game_optimization {
|
||||||
|
log::info!(target: "osu", "Game optimization active on macOS");
|
||||||
|
}
|
||||||
let folder = Path::new(osu_folder);
|
let folder = Path::new(osu_folder);
|
||||||
let runtime = WineRuntime::for_osu_folder(folder).map_err(|error| {
|
let runtime = WineRuntime::for_osu_folder(folder).map_err(|error| {
|
||||||
log::error!(target: "osu", "Could not prepare the Wine runtime: {error}");
|
log::error!(target: "osu", "Could not prepare the Wine runtime: {error}");
|
||||||
|
|||||||
@@ -700,6 +700,7 @@
|
|||||||
{ value: 'snow', label: 'Snow' },
|
{ value: 'snow', label: 'Snow' },
|
||||||
{ value: 'petals', label: 'Petals' },
|
{ value: 'petals', label: 'Petals' },
|
||||||
{ value: 'bubbles', label: 'Bubbles' },
|
{ value: 'bubbles', label: 'Bubbles' },
|
||||||
|
{ value: 'embers', label: 'Floating Embers' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const notifyError = (description: string) => sileo.error({ title: 'Theme editor', description });
|
const notifyError = (description: string) => sileo.error({ title: 'Theme editor', description });
|
||||||
@@ -5390,6 +5391,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<div class="space-y-2 border-t border-theme-800/60 pt-2.5">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Label class={labelClass}>Mouse parallax</Label>
|
||||||
|
<p class="text-[10px] text-muted-foreground">
|
||||||
|
3D depth perspective on mouse move
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
checked={definition.parallax?.enabled ?? false}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (!definition.parallax) {
|
||||||
|
definition.parallax = { enabled: false, intensity: 0.25 };
|
||||||
|
}
|
||||||
|
definition.parallax.enabled = checked;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if definition.parallax?.enabled}
|
||||||
|
<div class="space-y-1.5 pt-1">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Label class={labelClass}>Parallax intensity</Label>
|
||||||
|
<span class="font-mono text-xs text-muted-foreground">
|
||||||
|
{Math.round((definition.parallax.intensity ?? 0.25) * 100)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
min={0.05}
|
||||||
|
max={1}
|
||||||
|
step={0.05}
|
||||||
|
value={definition.parallax.intensity ?? 0.25}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
if (!definition.parallax) {
|
||||||
|
definition.parallax = { enabled: true, intensity: 0.25 };
|
||||||
|
}
|
||||||
|
definition.parallax.intensity = val;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</ThemeSection>
|
</ThemeSection>
|
||||||
|
|
||||||
<ThemeSection title="Typography" icon={Type} open={false} contentClass="space-y-3 pt-2.5">
|
<ThemeSection title="Typography" icon={Type} open={false} contentClass="space-y-3 pt-2.5">
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
(transform.skewY ?? 0) === 0 &&
|
(transform.skewY ?? 0) === 0 &&
|
||||||
!transform.flipX &&
|
!transform.flipX &&
|
||||||
!transform.flipY &&
|
!transform.flipY &&
|
||||||
|
(transform.parallax ?? 0) === 0 &&
|
||||||
(!includeTranslate || ((transform.offsetX ?? 0) === 0 && (transform.offsetY ?? 0) === 0))
|
(!includeTranslate || ((transform.offsetX ?? 0) === 0 && (transform.offsetY ?? 0) === 0))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -87,6 +88,7 @@
|
|||||||
skewY: 0,
|
skewY: 0,
|
||||||
flipX: false,
|
flipX: false,
|
||||||
flipY: false,
|
flipY: false,
|
||||||
|
parallax: 0,
|
||||||
...(includeTranslate ? { offsetX: 0, offsetY: 0 } : {}),
|
...(includeTranslate ? { offsetX: 0, offsetY: 0 } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -257,4 +259,25 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2.5 flex items-center justify-between border-t border-theme-800/40 pt-2">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class={label}>Parallax depth</span>
|
||||||
|
<span class="text-[9px] text-muted-foreground">3D cursor perspective</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="-1"
|
||||||
|
max="1"
|
||||||
|
step="0.05"
|
||||||
|
value={transform.parallax ?? 0}
|
||||||
|
oninput={(e) => onchange({ parallax: Number((e.target as HTMLInputElement).value) })}
|
||||||
|
class="h-1.5 w-24 cursor-pointer appearance-none rounded-lg bg-theme-800 accent-primary"
|
||||||
|
/>
|
||||||
|
<span class="w-8 font-mono text-[10px] text-right text-muted-foreground">
|
||||||
|
{Math.round((transform.parallax ?? 0) * 100)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ThemeSection>
|
</ThemeSection>
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
import { themeAssetUrl } from '$lib/themeV2';
|
import { themeAssetUrl } from '$lib/themeV2';
|
||||||
import { reduceAnimations } from '$lib/userSettings';
|
import { reduceAnimations } from '$lib/userSettings';
|
||||||
import { trackMediaContainer } from '$lib/utils';
|
import { trackMediaContainer } from '$lib/utils';
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
definition,
|
definition,
|
||||||
@@ -19,6 +20,51 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let audioElement = $state<HTMLAudioElement | undefined>(undefined);
|
let audioElement = $state<HTMLAudioElement | undefined>(undefined);
|
||||||
|
let mousePx = $state(0);
|
||||||
|
let mousePy = $state(0);
|
||||||
|
let targetPx = 0;
|
||||||
|
let targetPy = 0;
|
||||||
|
let animFrame: number | undefined;
|
||||||
|
|
||||||
|
function onWindowMouseMove(e: MouseEvent) {
|
||||||
|
if ($reduceAnimations || !definition.parallax?.enabled) return;
|
||||||
|
const intensity = definition.parallax.intensity ?? 0.25;
|
||||||
|
const w = window.innerWidth || 1000;
|
||||||
|
const h = window.innerHeight || 650;
|
||||||
|
targetPx = (e.clientX / w - 0.5) * 2 * intensity;
|
||||||
|
targetPy = (e.clientY / h - 0.5) * 2 * intensity;
|
||||||
|
if (animFrame === undefined) {
|
||||||
|
animFrame = requestAnimationFrame(updateParallax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateParallax() {
|
||||||
|
animFrame = undefined;
|
||||||
|
mousePx += (targetPx - mousePx) * 0.12;
|
||||||
|
mousePy += (targetPy - mousePy) * 0.12;
|
||||||
|
if (Math.abs(targetPx - mousePx) > 0.001 || Math.abs(targetPy - mousePy) > 0.001) {
|
||||||
|
animFrame = requestAnimationFrame(updateParallax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!definition.parallax?.enabled) {
|
||||||
|
mousePx = 0;
|
||||||
|
mousePy = 0;
|
||||||
|
targetPx = 0;
|
||||||
|
targetPy = 0;
|
||||||
|
if (animFrame !== undefined) {
|
||||||
|
cancelAnimationFrame(animFrame);
|
||||||
|
animFrame = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (animFrame !== undefined) {
|
||||||
|
cancelAnimationFrame(animFrame);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const assetUrl = (path: string | undefined) => {
|
const assetUrl = (path: string | undefined) => {
|
||||||
if (!path) return '';
|
if (!path) return '';
|
||||||
@@ -43,17 +89,19 @@
|
|||||||
return Array.from({ length: count }, (_, index) => {
|
return Array.from({ length: count }, (_, index) => {
|
||||||
const value = (offset: number) => random(index + 1 + offset);
|
const value = (offset: number) => random(index + 1 + offset);
|
||||||
const isStar = effects.type === 'stars';
|
const isStar = effects.type === 'stars';
|
||||||
const size = isStar ? 1 + value(1) * 2.5 : 4 + value(1) * 7;
|
const isEmber = effects.type === 'embers';
|
||||||
const duration = (isStar ? 2 + value(2) * 3 : 9 + value(2) * 11) / speed;
|
const size = isStar ? 1 + value(1) * 2.5 : isEmber ? 2.5 + value(1) * 4.5 : 4 + value(1) * 7;
|
||||||
|
const duration =
|
||||||
|
(isStar ? 2 + value(2) * 3 : isEmber ? 5 + value(2) * 6 : 9 + value(2) * 11) / speed;
|
||||||
return {
|
return {
|
||||||
id: index,
|
id: index,
|
||||||
left: value(3) * 100,
|
left: value(3) * 100,
|
||||||
top: isStar ? value(4) * 100 : value(4) * 15,
|
top: isStar ? value(4) * 100 : isEmber ? 88 + value(4) * 16 : value(4) * 15,
|
||||||
size,
|
size,
|
||||||
duration,
|
duration,
|
||||||
delay: value(5) * duration,
|
delay: value(5) * duration,
|
||||||
drift: -90 + value(6) * 180,
|
drift: isEmber ? -50 + value(6) * 100 : -90 + value(6) * 180,
|
||||||
opacity: 0.25 + value(7) * 0.65,
|
opacity: isEmber ? 0.35 + value(7) * 0.65 : 0.25 + value(7) * 0.65,
|
||||||
rotate: value(8) * 360,
|
rotate: value(8) * 360,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -86,7 +134,13 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="theme-v2 absolute inset-0 overflow-hidden" use:trackMedia>
|
<svelte:window onmousemove={onWindowMouseMove} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="theme-v2 absolute inset-0 overflow-hidden"
|
||||||
|
use:trackMedia
|
||||||
|
style="--mouse-px:{mousePx};--mouse-py:{mousePy};"
|
||||||
|
>
|
||||||
{#if background.type === 'color'}
|
{#if background.type === 'color'}
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0"
|
class="absolute inset-0"
|
||||||
@@ -108,7 +162,11 @@
|
|||||||
style="object-fit:{background.fit ?? 'cover'};opacity:{background.opacity ??
|
style="object-fit:{background.fit ?? 'cover'};opacity:{background.opacity ??
|
||||||
1};filter:{background.blur
|
1};filter:{background.blur
|
||||||
? `blur(${background.blur}px)`
|
? `blur(${background.blur}px)`
|
||||||
: 'none'};transform:{background.blur ? 'scale(1.05)' : 'none'};"
|
: 'none'};transform:{background.blur
|
||||||
|
? 'scale(1.08)'
|
||||||
|
: definition.parallax?.enabled
|
||||||
|
? 'scale(1.06)'
|
||||||
|
: 'none'} translate3d({mousePx * -18}px, {mousePy * -18}px, 0);"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if background.type === 'video'}
|
{:else if background.type === 'video'}
|
||||||
@@ -126,7 +184,11 @@
|
|||||||
style="object-fit:{background.fit ?? 'cover'};opacity:{background.opacity ??
|
style="object-fit:{background.fit ?? 'cover'};opacity:{background.opacity ??
|
||||||
1};filter:{background.blur
|
1};filter:{background.blur
|
||||||
? `blur(${background.blur}px)`
|
? `blur(${background.blur}px)`
|
||||||
: 'none'};transform:{background.blur ? 'scale(1.05)' : 'none'};"
|
: 'none'};transform:{background.blur
|
||||||
|
? 'scale(1.08)'
|
||||||
|
: definition.parallax?.enabled
|
||||||
|
? 'scale(1.06)'
|
||||||
|
: 'none'} translate3d({mousePx * -18}px, {mousePy * -18}px, 0);"
|
||||||
></video>
|
></video>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -204,6 +266,33 @@
|
|||||||
animation-name: v2-stars;
|
animation-name: v2-stars;
|
||||||
animation-timing-function: ease-in-out;
|
animation-timing-function: ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.embers {
|
||||||
|
border-radius: 9999px;
|
||||||
|
box-shadow:
|
||||||
|
0 0 6px var(--particle-color),
|
||||||
|
0 0 12px var(--particle-color);
|
||||||
|
animation-name: v2-embers;
|
||||||
|
animation-timing-function: ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes v2-embers {
|
||||||
|
0% {
|
||||||
|
transform: translate3d(0, 0, 0) scale(0.6);
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 0.95;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: translate3d(calc(var(--particle-drift) * 0.65), -85vh, 0) scale(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate3d(var(--particle-drift), -115vh, 0) scale(0.3);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes v2-snow {
|
@keyframes v2-snow {
|
||||||
|
|||||||
@@ -1227,7 +1227,9 @@
|
|||||||
element,
|
element,
|
||||||
elements,
|
elements,
|
||||||
groups
|
groups
|
||||||
)};transform-origin:0 0;touch-action:none;outline:{selected
|
)}{element.parallax
|
||||||
|
? ` translate3d(calc(var(--mouse-px, 0) * ${element.parallax * 30}px), calc(var(--mouse-py, 0) * ${element.parallax * 30}px), 0)`
|
||||||
|
: ''};transform-origin:0 0;touch-action:none;outline:{selected
|
||||||
? `${selectedIds.length > 1 ? 1 : 2}px solid #ff006e`
|
? `${selectedIds.length > 1 ? 1 : 2}px solid #ff006e`
|
||||||
: 'none'};outline-offset:2px;"
|
: 'none'};outline-offset:2px;"
|
||||||
role={interactive || element.interactive ? 'button' : undefined}
|
role={interactive || element.interactive ? 'button' : undefined}
|
||||||
|
|||||||
+2
-2
@@ -88,8 +88,8 @@ export const getSkin = async (folder: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const runUpdater = async (folder: string) => await invoke('run_osu_updater', { folder });
|
export const runUpdater = async (folder: string) => await invoke('run_osu_updater', { folder });
|
||||||
export const runOsu = async (folder: string, patch: boolean) =>
|
export const runOsu = async (folder: string, patch: boolean, gameOptimization: boolean = false) =>
|
||||||
await invoke('run_osu', { folder, patch });
|
await invoke('run_osu', { folder, patch, gameOptimization });
|
||||||
|
|
||||||
export const getEZPPLauncherStreams = async () => {
|
export const getEZPPLauncherStreams = async () => {
|
||||||
const resp = await betterFetch<{ streams: string[] }>(updateUrl, {
|
const resp = await betterFetch<{ streams: string[] }>(updateUrl, {
|
||||||
|
|||||||
@@ -699,6 +699,12 @@ const drawParticles = (context: PreviewContext) => {
|
|||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, size / 2, 0, Math.PI * 2);
|
ctx.arc(x, y, size / 2, 0, Math.PI * 2);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
} else if (effects.type === 'embers') {
|
||||||
|
ctx.shadowColor = color;
|
||||||
|
ctx.shadowBlur = 8;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, size / 2.2, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
} else if (effects.type === 'petals') {
|
} else if (effects.type === 'petals') {
|
||||||
ctx.translate(x, y);
|
ctx.translate(x, y);
|
||||||
ctx.rotate(value(8) * Math.PI * 2);
|
ctx.rotate(value(8) * Math.PI * 2);
|
||||||
|
|||||||
+25
-2
@@ -48,7 +48,13 @@ export type ThemeV2Background =
|
|||||||
muted?: boolean;
|
muted?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ThemeV2EffectType = 'none' | 'snow' | 'stars' | 'petals' | 'bubbles';
|
export type ThemeV2EffectType = 'none' | 'snow' | 'stars' | 'petals' | 'bubbles' | 'embers';
|
||||||
|
|
||||||
|
export type ThemeV2Parallax = {
|
||||||
|
enabled?: boolean;
|
||||||
|
/** Intensity multiplier from 0.05 to 1.0, defaults to 0.25 */
|
||||||
|
intensity?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type ThemeV2Effects = {
|
export type ThemeV2Effects = {
|
||||||
type: ThemeV2EffectType;
|
type: ThemeV2EffectType;
|
||||||
@@ -121,6 +127,8 @@ export type ThemeV2Transform = {
|
|||||||
skewY?: number;
|
skewY?: number;
|
||||||
flipX?: boolean;
|
flipX?: boolean;
|
||||||
flipY?: boolean;
|
flipY?: boolean;
|
||||||
|
/** Parallax depth multiplier (-2 to 2); when parallax is enabled, offsets layer based on cursor. */
|
||||||
|
parallax?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A group in the layer list. Groups may be nested through `parentId`. */
|
/** A group in the layer list. Groups may be nested through `parentId`. */
|
||||||
@@ -644,6 +652,7 @@ export type ThemeV2Definition = {
|
|||||||
background: ThemeV2Background;
|
background: ThemeV2Background;
|
||||||
overlay: ThemeV2Overlay;
|
overlay: ThemeV2Overlay;
|
||||||
effects: ThemeV2Effects;
|
effects: ThemeV2Effects;
|
||||||
|
parallax?: ThemeV2Parallax;
|
||||||
audio?: ThemeV2Audio;
|
audio?: ThemeV2Audio;
|
||||||
/** Theme-wide fonts and freeform layers rendered over the launcher UI. */
|
/** Theme-wide fonts and freeform layers rendered over the launcher UI. */
|
||||||
typography: ThemeV2Typography;
|
typography: ThemeV2Typography;
|
||||||
@@ -789,6 +798,9 @@ const normalizeTransform = (source: Record<string, unknown>): ThemeV2Transform =
|
|||||||
skewY: clamp(source.skewY, 0, -89, 89),
|
skewY: clamp(source.skewY, 0, -89, 89),
|
||||||
flipX: source.flipX === true,
|
flipX: source.flipX === true,
|
||||||
flipY: source.flipY === true,
|
flipY: source.flipY === true,
|
||||||
|
...(typeof source.parallax === 'number' && Number.isFinite(source.parallax)
|
||||||
|
? { parallax: clamp(source.parallax, 0, -2, 2) }
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1756,6 +1768,10 @@ export const createDefaultThemeV2 = (): ThemeV2Definition => ({
|
|||||||
speed: 1,
|
speed: 1,
|
||||||
color: '#ffffff',
|
color: '#ffffff',
|
||||||
},
|
},
|
||||||
|
parallax: {
|
||||||
|
enabled: false,
|
||||||
|
intensity: 0.25,
|
||||||
|
},
|
||||||
typography: {
|
typography: {
|
||||||
body: 'Sora Variable',
|
body: 'Sora Variable',
|
||||||
heading: 'Sora Variable',
|
heading: 'Sora Variable',
|
||||||
@@ -1830,12 +1846,18 @@ export const normalizeThemeV2 = (raw: Partial<ThemeV2Definition>): ThemeV2Defini
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sourceEffects = (raw.effects ?? {}) as Record<string, unknown>;
|
const sourceEffects = (raw.effects ?? {}) as Record<string, unknown>;
|
||||||
const effectType = ['none', 'snow', 'stars', 'petals', 'bubbles'].includes(
|
const effectType = ['none', 'snow', 'stars', 'petals', 'bubbles', 'embers'].includes(
|
||||||
String(sourceEffects.type)
|
String(sourceEffects.type)
|
||||||
)
|
)
|
||||||
? (String(sourceEffects.type) as ThemeV2EffectType)
|
? (String(sourceEffects.type) as ThemeV2EffectType)
|
||||||
: 'none';
|
: 'none';
|
||||||
|
|
||||||
|
const sourceParallax = (raw.parallax ?? {}) as Record<string, unknown>;
|
||||||
|
const parallax: ThemeV2Parallax = {
|
||||||
|
enabled: sourceParallax.enabled === true,
|
||||||
|
intensity: clamp(sourceParallax.intensity, 0.25, 0.05, 1),
|
||||||
|
};
|
||||||
|
|
||||||
const sourceOverlay = (raw.overlay ?? {}) as Record<string, unknown>;
|
const sourceOverlay = (raw.overlay ?? {}) as Record<string, unknown>;
|
||||||
const sourceAudio = raw.audio as Record<string, unknown> | undefined;
|
const sourceAudio = raw.audio as Record<string, unknown> | undefined;
|
||||||
const sourceTypography = (raw.typography ?? {}) as Record<string, unknown>;
|
const sourceTypography = (raw.typography ?? {}) as Record<string, unknown>;
|
||||||
@@ -1868,6 +1890,7 @@ export const normalizeThemeV2 = (raw: Partial<ThemeV2Definition>): ThemeV2Defini
|
|||||||
speed: clamp(sourceEffects.speed, 1, 0.2, 3),
|
speed: clamp(sourceEffects.speed, 1, 0.2, 3),
|
||||||
color: color(sourceEffects.color, '#ffffff'),
|
color: color(sourceEffects.color, '#ffffff'),
|
||||||
},
|
},
|
||||||
|
parallax,
|
||||||
// A video background owns the soundtrack; combining it with theme music
|
// A video background owns the soundtrack; combining it with theme music
|
||||||
// would play two sources at once.
|
// would play two sources at once.
|
||||||
audio:
|
audio:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export const customCursor = writable<boolean>(false);
|
|||||||
export const cursorSmoothening = writable<boolean>(false);
|
export const cursorSmoothening = writable<boolean>(false);
|
||||||
export const cursorSmoothness = writable<number>(14);
|
export const cursorSmoothness = writable<number>(14);
|
||||||
export const reduceAnimations = writable<boolean>(false);
|
export const reduceAnimations = writable<boolean>(false);
|
||||||
|
export const gameOptimization = writable<boolean>(false);
|
||||||
export const patch = writable<boolean>(true);
|
export const patch = writable<boolean>(true);
|
||||||
export const loggingEnabled = writable<boolean>(true);
|
export const loggingEnabled = writable<boolean>(true);
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
import {
|
import {
|
||||||
cursorSmoothening,
|
cursorSmoothening,
|
||||||
customCursor,
|
customCursor,
|
||||||
|
gameOptimization,
|
||||||
loggingEnabled,
|
loggingEnabled,
|
||||||
patch,
|
patch,
|
||||||
reduceAnimations,
|
reduceAnimations,
|
||||||
@@ -260,6 +261,7 @@
|
|||||||
const config_custom_cursor = config.value<boolean>('custom_cursor');
|
const config_custom_cursor = config.value<boolean>('custom_cursor');
|
||||||
const config_cursor_smoothening = config.value<boolean>('cursor_smoothening');
|
const config_cursor_smoothening = config.value<boolean>('cursor_smoothening');
|
||||||
const config_reduce_animations = config.value<boolean>('reduce_animations');
|
const config_reduce_animations = config.value<boolean>('reduce_animations');
|
||||||
|
const config_game_optimization = config.value<boolean>('game_optimization');
|
||||||
const config_logging_enabled = config.value<boolean>('logging_enabled');
|
const config_logging_enabled = config.value<boolean>('logging_enabled');
|
||||||
const config_discord_presence = config.value<boolean>('discord_presence');
|
const config_discord_presence = config.value<boolean>('discord_presence');
|
||||||
const config_tracking_enabled = config.value<boolean>('tracking_consent');
|
const config_tracking_enabled = config.value<boolean>('tracking_consent');
|
||||||
@@ -291,6 +293,7 @@
|
|||||||
customCursor.set(await config_custom_cursor.get(true));
|
customCursor.set(await config_custom_cursor.get(true));
|
||||||
cursorSmoothening.set(await config_cursor_smoothening.get(true));
|
cursorSmoothening.set(await config_cursor_smoothening.get(true));
|
||||||
reduceAnimations.set(await config_reduce_animations.get(false));
|
reduceAnimations.set(await config_reduce_animations.get(false));
|
||||||
|
gameOptimization.set(await config_game_optimization.get(false));
|
||||||
loggingEnabled.set(await config_logging_enabled.get(true));
|
loggingEnabled.set(await config_logging_enabled.get(true));
|
||||||
discordPresence.set(await config_discord_presence.get(true));
|
discordPresence.set(await config_discord_presence.get(true));
|
||||||
openTabletDriverEnabled.set(await config_otd_enabled.get(false));
|
openTabletDriverEnabled.set(await config_otd_enabled.get(false));
|
||||||
@@ -303,6 +306,7 @@
|
|||||||
customCursor.subscribe(async (val) => await config_custom_cursor.set(val));
|
customCursor.subscribe(async (val) => await config_custom_cursor.set(val));
|
||||||
cursorSmoothening.subscribe(async (val) => await config_cursor_smoothening.set(val));
|
cursorSmoothening.subscribe(async (val) => await config_cursor_smoothening.set(val));
|
||||||
reduceAnimations.subscribe(async (val) => await config_reduce_animations.set(val));
|
reduceAnimations.subscribe(async (val) => await config_reduce_animations.set(val));
|
||||||
|
gameOptimization.subscribe(async (val) => await config_game_optimization.set(val));
|
||||||
loggingEnabled.subscribe(async (val) => {
|
loggingEnabled.subscribe(async (val) => {
|
||||||
await config_logging_enabled.set(val);
|
await config_logging_enabled.set(val);
|
||||||
await setLoggingEnabled(val);
|
await setLoggingEnabled(val);
|
||||||
|
|||||||
@@ -131,6 +131,7 @@
|
|||||||
import {
|
import {
|
||||||
cursorSmoothening,
|
cursorSmoothening,
|
||||||
customCursor,
|
customCursor,
|
||||||
|
gameOptimization,
|
||||||
loggingEnabled,
|
loggingEnabled,
|
||||||
osuInstallationPath,
|
osuInstallationPath,
|
||||||
patch,
|
patch,
|
||||||
@@ -1201,7 +1202,7 @@
|
|||||||
}, 1000 * 2);
|
}, 1000 * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
await runOsu(osuPath, $patch);
|
await runOsu(osuPath, $patch, $gameOptimization);
|
||||||
if ($trackingEnabled) umami.track('app_exit_osu');
|
if ($trackingEnabled) umami.track('app_exit_osu');
|
||||||
cleanup = true;
|
cleanup = true;
|
||||||
launchInfo = 'Cleaning up...';
|
launchInfo = 'Cleaning up...';
|
||||||
@@ -3192,6 +3193,27 @@
|
|||||||
class="mt-0.5"
|
class="mt-0.5"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start justify-between gap-8 py-3">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<Label class="text-sm" for="setting-game-optimization"
|
||||||
|
>Game optimization & low latency</Label
|
||||||
|
>
|
||||||
|
<p class="mt-0.5 text-xs leading-relaxed text-muted-foreground">
|
||||||
|
Boosts osu! process priority to High and requests maximum
|
||||||
|
sub-millisecond Windows timer resolution (1ms) during gameplay
|
||||||
|
to reduce input latency.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
id="setting-game-optimization"
|
||||||
|
checked={$gameOptimization}
|
||||||
|
onCheckedChange={async (e) => {
|
||||||
|
gameOptimization.set(e);
|
||||||
|
}}
|
||||||
|
class="mt-0.5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{:else if activeSettingsTab === 'integrations'}
|
{:else if activeSettingsTab === 'integrations'}
|
||||||
|
|||||||
Reference in new issue
Block a user