diff options
| author | 2026-01-14 18:15:31 +0800 | |
|---|---|---|
| committer | 2026-01-14 18:15:31 +0800 | |
| commit | eed52135e7d6ffbbbd64070cf567bcf08653c7d5 (patch) | |
| tree | c6fba957f507b2368125f7c2e1dfed6cef5aad53 | |
| parent | 802b8cf5c0723b606ba5936c060e01d4c83222dd (diff) | |
| download | DropOut-eed52135e7d6ffbbbd64070cf567bcf08653c7d5.tar.gz DropOut-eed52135e7d6ffbbbd64070cf567bcf08653c7d5.zip | |
feat: Enhance UI components and add visual effects
- Updated Sidebar component styles for improved aesthetics and usability.
- Refactored VersionsView component with a new layout and enhanced version filtering.
- Improved DownloadMonitor and GameConsole components for better performance and visual consistency.
- Added new settings for GPU acceleration and visual effects in settings store.
- Introduced ParticleBackground component with customizable effects (Constellation and Saturn).
- Implemented ConstellationEffect and SaturnEffect classes for dynamic background animations.
| -rw-r--r-- | src-tauri/Cargo.toml | 1 | ||||
| -rw-r--r-- | src-tauri/capabilities/default.json | 3 | ||||
| -rw-r--r-- | src-tauri/src/core/config.rs | 8 | ||||
| -rw-r--r-- | src-tauri/src/main.rs | 1 | ||||
| -rw-r--r-- | ui/package.json | 1 | ||||
| -rw-r--r-- | ui/pnpm-lock.yaml | 10 | ||||
| -rw-r--r-- | ui/src/App.svelte | 145 | ||||
| -rw-r--r-- | ui/src/components/BottomBar.svelte | 79 | ||||
| -rw-r--r-- | ui/src/components/HomeView.svelte | 57 | ||||
| -rw-r--r-- | ui/src/components/ModLoaderSelector.svelte | 214 | ||||
| -rw-r--r-- | ui/src/components/ParticleBackground.svelte | 57 | ||||
| -rw-r--r-- | ui/src/components/SettingsView.svelte | 282 | ||||
| -rw-r--r-- | ui/src/components/Sidebar.svelte | 76 | ||||
| -rw-r--r-- | ui/src/components/VersionsView.svelte | 184 | ||||
| -rw-r--r-- | ui/src/lib/DownloadMonitor.svelte | 2 | ||||
| -rw-r--r-- | ui/src/lib/GameConsole.svelte | 2 | ||||
| -rw-r--r-- | ui/src/lib/effects/ConstellationEffect.ts | 163 | ||||
| -rw-r--r-- | ui/src/lib/effects/SaturnEffect.ts | 194 | ||||
| -rw-r--r-- | ui/src/stores/settings.svelte.ts | 3 | ||||
| -rw-r--r-- | ui/src/types/index.ts | 4 |
20 files changed, 1080 insertions, 406 deletions
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d7319a7..1d4ccc5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -23,6 +23,7 @@ sha1 = "0.10" hex = "0.4" zip = "2.2.2" serde_urlencoded = "0.7.1" +tauri-plugin-dialog = "2.5.0" [build-dependencies] tauri-build = { version = "2.0", features = [] } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 894b905..4d8b907 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -12,6 +12,7 @@ "core:app:allow-version", "core:path:default", "core:window:default", - "shell:allow-open" + "shell:allow-open", + "dialog:default" ] } diff --git a/src-tauri/src/core/config.rs b/src-tauri/src/core/config.rs index d6d594f..27e0011 100644 --- a/src-tauri/src/core/config.rs +++ b/src-tauri/src/core/config.rs @@ -13,6 +13,10 @@ pub struct LauncherConfig { pub width: u32, pub height: u32, pub download_threads: u32, // concurrent download threads (1-128) + pub custom_background_path: Option<String>, + pub enable_gpu_acceleration: bool, + pub enable_visual_effects: bool, + pub active_effect: String, } impl Default for LauncherConfig { @@ -24,6 +28,10 @@ impl Default for LauncherConfig { width: 854, height: 480, download_threads: 32, + custom_background_path: None, + enable_gpu_acceleration: false, + enable_visual_effects: true, + active_effect: "constellation".to_string(), } } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ba16f7a..f7a391a 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1004,6 +1004,7 @@ async fn install_forge( fn main() { tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_shell::init()) .manage(core::auth::AccountState::new()) .manage(MsRefreshTokenState::new()) diff --git a/ui/package.json b/ui/package.json index 0806781..03cc405 100644 --- a/ui/package.json +++ b/ui/package.json @@ -29,6 +29,7 @@ }, "dependencies": { "@tauri-apps/api": "^2.9.1", + "@tauri-apps/plugin-dialog": "^2.5.0", "@tauri-apps/plugin-shell": "^2.3.4" } } diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index d48c01e..23d4ee2 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@tauri-apps/api': specifier: ^2.9.1 version: 2.9.1 + '@tauri-apps/plugin-dialog': + specifier: ^2.5.0 + version: 2.5.0 '@tauri-apps/plugin-shell': specifier: ^2.3.4 version: 2.3.4 @@ -296,6 +299,9 @@ packages: '@tauri-apps/api@2.9.1': resolution: {integrity: sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==} + '@tauri-apps/plugin-dialog@2.5.0': + resolution: {integrity: sha512-I0R0ygwRd9AN8Wj5GnzCogOlqu2+OWAtBd0zEC4+kQCI32fRowIyuhPCBoUv4h/lQt2bM39kHlxPHD5vDcFjiA==} + '@tauri-apps/plugin-shell@2.3.4': resolution: {integrity: sha512-ktsRWf8wHLD17aZEyqE8c5x98eNAuTizR1FSX475zQ4TxaiJnhwksLygQz+AGwckJL5bfEP13nWrlTNQJUpKpA==} @@ -808,6 +814,10 @@ snapshots: '@tauri-apps/api@2.9.1': {} + '@tauri-apps/plugin-dialog@2.5.0': + dependencies: + '@tauri-apps/api': 2.9.1 + '@tauri-apps/plugin-shell@2.3.4': dependencies: '@tauri-apps/api': 2.9.1 diff --git a/ui/src/App.svelte b/ui/src/App.svelte index 3750f11..d93374e 100644 --- a/ui/src/App.svelte +++ b/ui/src/App.svelte @@ -1,6 +1,7 @@ <script lang="ts"> import { getVersion } from "@tauri-apps/api/app"; - import { onMount } from "svelte"; + import { onMount, onDestroy } from "svelte"; + import { convertFileSrc } from "@tauri-apps/api/core"; import DownloadMonitor from "./lib/DownloadMonitor.svelte"; import GameConsole from "./lib/GameConsole.svelte"; @@ -12,6 +13,7 @@ import BottomBar from "./components/BottomBar.svelte"; import LoginModal from "./components/LoginModal.svelte"; import StatusToast from "./components/StatusToast.svelte"; + import ParticleBackground from "./components/ParticleBackground.svelte"; // Stores import { uiState } from "./stores/ui.svelte"; @@ -19,46 +21,139 @@ import { settingsState } from "./stores/settings.svelte"; import { gameState } from "./stores/game.svelte"; + let mouseX = $state(0); + let mouseY = $state(0); + + function handleMouseMove(e: MouseEvent) { + mouseX = (e.clientX / window.innerWidth) * 2 - 1; + mouseY = (e.clientY / window.innerHeight) * 2 - 1; + } + onMount(async () => { authState.checkAccount(); settingsState.loadSettings(); gameState.loadVersions(); getVersion().then((v) => (uiState.appVersion = v)); + window.addEventListener("mousemove", handleMouseMove); + }); + + onDestroy(() => { + if (typeof window !== 'undefined') + window.removeEventListener("mousemove", handleMouseMove); }); </script> <div - class="flex h-screen bg-zinc-900 text-white font-sans overflow-hidden select-none" + class="relative h-screen w-screen overflow-hidden text-white font-sans selection:bg-indigo-500/30" > - <Sidebar /> + <!-- Modern Animated Background --> + <div class="absolute inset-0 z-0 bg-[#09090b] overflow-hidden"> + {#if settingsState.settings.custom_background_path} + <img + src={convertFileSrc(settingsState.settings.custom_background_path)} + alt="Background" + class="absolute inset-0 w-full h-full object-cover transition-transform duration-[20s] ease-linear hover:scale-105" + /> + <!-- Dimming Overlay for readability --> + <div class="absolute inset-0 bg-black/50 "></div> + {:else if settingsState.settings.enable_visual_effects} + <!-- Original Gradient --> + <div + class="absolute inset-0 opacity-60 bg-gradient-to-br from-emerald-900 via-zinc-900 to-indigo-950" + ></div> - <!-- Main Content --> - <main class="flex-1 flex flex-col relative min-w-0"> - <DownloadMonitor /> - <!-- Top Bar (Window Controls Placeholder) --> - <div - class="h-8 w-full bg-zinc-900/50 absolute top-0 left-0 z-50 drag-region" - data-tauri-drag-region - > - <!-- Windows/macOS controls would go here or be handled by OS --> - </div> - - <!-- Background / Poster area --> - <div class="flex-1 relative overflow-hidden group"> {#if uiState.currentView === "home"} - <HomeView /> - {:else if uiState.currentView === "versions"} - <VersionsView /> - {:else if uiState.currentView === "settings"} - <SettingsView /> + <ParticleBackground /> {/if} + + <div + class="absolute inset-0 bg-gradient-to-t from-zinc-900 via-transparent to-black/50" + ></div> + {/if} + + <!-- Subtle Grid Overlay --> + <div class="absolute inset-0 z-0 opacity-10 pointer-events-none" + style="background-image: linear-gradient(#ffffff 1px, transparent 1px), linear-gradient(90deg, #ffffff 1px, transparent 1px); background-size: 40px 40px; mask-image: radial-gradient(circle at 50% 50%, black 30%, transparent 70%);"> </div> + </div> + + <!-- Content Wrapper --> + <div class="relative z-10 flex h-full p-4 gap-4"> + <!-- Floating Sidebar --> + <Sidebar /> + + <!-- Main Content Area - Transparent & Flat --> + <main class="flex-1 flex flex-col relative min-w-0 overflow-hidden transition-all duration-300"> + + <!-- Window Drag Region --> + <div + class="h-8 w-full absolute top-0 left-0 z-50 drag-region" + data-tauri-drag-region + ></div> - <BottomBar /> - </main> + <!-- App Content --> + <div class="flex-1 relative overflow-hidden flex flex-col"> + <!-- Views Container --> + <div class="flex-1 relative overflow-hidden"> + {#if uiState.currentView === "home"} + <HomeView mouseX={mouseX} mouseY={mouseY} /> + {:else if uiState.currentView === "versions"} + <VersionsView /> + {:else if uiState.currentView === "settings"} + <SettingsView /> + {/if} + </div> + + <!-- Download Monitor Overlay --> + <div class="absolute bottom-20 left-4 right-4 pointer-events-none z-20"> + <div class="pointer-events-auto"> + <DownloadMonitor /> + </div> + </div> + + <!-- Bottom Bar --> + <BottomBar /> + </div> + </main> + </div> <LoginModal /> <StatusToast /> - - <GameConsole visible={uiState.showConsole} /> + + {#if uiState.showConsole} + <!-- Assuming GameConsole handles its own display mode or overlay --> + <div class="fixed inset-0 z-[100] bg-black/80 flex items-center justify-center p-10"> + <div class="w-full h-full bg-[#1e1e1e] rounded-xl overflow-hidden border border-white/10 shadow-2xl relative"> + <button class="absolute top-4 right-4 text-white hover:text-red-400 z-10" onclick={() => uiState.toggleConsole()}>✕</button> + <GameConsole /> + </div> + </div> + {/if} </div> + +<style> + :global(body) { + margin: 0; + padding: 0; + background: #000; + } + + /* Modern Scrollbar */ + :global(*::-webkit-scrollbar) { + width: 6px; + height: 6px; + } + + :global(*::-webkit-scrollbar-track) { + background: transparent; + } + + :global(*::-webkit-scrollbar-thumb) { + background: rgba(255, 255, 255, 0.1); + border-radius: 999px; + } + + :global(*::-webkit-scrollbar-thumb:hover) { + background: rgba(255, 255, 255, 0.25); + } +</style> diff --git a/ui/src/components/BottomBar.svelte b/ui/src/components/BottomBar.svelte index dcad9e8..dd218f3 100644 --- a/ui/src/components/BottomBar.svelte +++ b/ui/src/components/BottomBar.svelte @@ -5,18 +5,19 @@ </script> <div - class="h-24 bg-zinc-900 border-t border-zinc-800 flex items-center px-8 justify-between z-20 shadow-2xl" + class="h-24 bg-gradient-to-t from-black/50 to-transparent border-t border-white/5 flex items-center px-8 justify-between z-20 backdrop-blur-md" > - <div class="flex items-center gap-4"> + <!-- Account Area --> + <div class="flex items-center gap-6"> <div - class="flex items-center gap-4 cursor-pointer hover:opacity-80 transition-opacity" + class="group flex items-center gap-4 cursor-pointer transition-all duration-300 hover:scale-105" onclick={() => authState.openLoginModal()} role="button" tabindex="0" onkeydown={(e) => e.key === "Enter" && authState.openLoginModal()} > <div - class="w-12 h-12 rounded bg-gradient-to-tr from-indigo-500 to-purple-500 shadow-lg flex items-center justify-center text-white font-bold text-xl overflow-hidden" + class="w-12 h-12 rounded-xl bg-gradient-to-tr from-indigo-500 to-purple-500 shadow-lg shadow-indigo-500/20 flex items-center justify-center text-white font-bold text-xl overflow-hidden ring-2 ring-transparent group-hover:ring-white/20 transition-all" > {#if authState.currentAccount} <img @@ -25,63 +26,73 @@ class="w-full h-full" /> {:else} - ? + <span class="text-white/50 text-2xl">?</span> {/if} </div> <div> - <div class="font-bold text-white text-lg"> - {authState.currentAccount ? authState.currentAccount.username : "Click to Login"} + <div class="font-bold text-white text-lg group-hover:text-indigo-300 transition-colors"> + {authState.currentAccount ? authState.currentAccount.username : "Login"} </div> - <div class="text-xs text-zinc-400 flex items-center gap-1"> + <div class="text-xs text-zinc-400 flex items-center gap-1.5"> <span - class="w-1.5 h-1.5 rounded-full {authState.currentAccount - ? 'bg-green-500' - : 'bg-zinc-500'}" + class="w-2 h-2 rounded-full {authState.currentAccount + ? 'bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]' + : 'bg-zinc-600'}" ></span> - {authState.currentAccount ? "Ready" : "Guest"} + {authState.currentAccount ? "Ready to play" : "Guest Mode"} </div> </div> </div> + + <div class="h-8 w-px bg-white/10"></div> + <!-- Console Toggle --> <button - class="ml-4 text-xs text-zinc-500 hover:text-zinc-300 transition" + class="text-xs font-mono text-zinc-500 hover:text-white transition-colors flex items-center gap-2" onclick={() => uiState.toggleConsole()} > + <span class="text-lg">_</span> {uiState.showConsole ? "Hide Logs" : "Show Logs"} </button> </div> - <div class="flex items-center gap-4"> + <!-- Action Area --> + <div class="flex items-center gap-6"> <div class="flex flex-col items-end mr-2"> <label for="version-select" - class="text-xs text-zinc-500 mb-1 uppercase font-bold tracking-wider" - >Version</label + class="text-[10px] text-white/40 mb-1 uppercase font-bold tracking-wider" + >Selected Version</label > - <select - id="version-select" - bind:value={gameState.selectedVersion} - class="bg-zinc-950 text-zinc-200 border border-zinc-700 rounded px-4 py-2 hover:border-zinc-500 transition-colors cursor-pointer outline-none focus:ring-1 focus:ring-indigo-500 w-48" - > - {#if gameState.versions.length === 0} - <option>Loading...</option> - {:else} - {#each gameState.versions as version} - <option value={version.id}>{version.id} ({version.type})</option - > - {/each} - {/if} - </select> + <div class="relative group"> + <select + id="version-select" + bind:value={gameState.selectedVersion} + class="appearance-none bg-black/40 text-white border border-white/10 rounded-xl pl-4 pr-10 py-2.5 hover:border-white/30 transition-all cursor-pointer outline-none focus:ring-2 focus:ring-indigo-500/50 w-56 text-sm font-mono backdrop-blur-sm shadow-inner" + > + {#if gameState.versions.length === 0} + <option>Loading...</option> + {:else} + {#each gameState.versions as version} + <option value={version.id}>{version.id} {version.type !== 'release' ? `(${version.type})` : ''}</option> + {/each} + {/if} + </select> + <div class="absolute right-3 top-1/2 -translate-y-1/2 text-white/20 pointer-events-none group-hover:text-white/50 transition-colors">▼</div> + </div> </div> <button onclick={() => gameState.startGame()} - class="bg-green-600 hover:bg-green-500 text-white font-bold h-14 px-12 rounded transition-all transform active:scale-95 shadow-[0_0_15px_rgba(22,163,74,0.4)] hover:shadow-[0_0_25px_rgba(22,163,74,0.6)] flex flex-col items-center justify-center uppercase tracking-wider text-lg" + class="bg-gradient-to-r from-emerald-600 to-green-600 hover:from-emerald-500 hover:to-green-500 text-white font-bold h-14 px-10 rounded-xl transition-all duration-300 transform hover:scale-105 active:scale-95 shadow-[0_0_20px_rgba(16,185,129,0.3)] hover:shadow-[0_0_40px_rgba(16,185,129,0.5)] flex flex-col items-center justify-center uppercase tracking-widest text-xl relative overflow-hidden group" > - Play + <div class="absolute inset-0 bg-white/20 translate-y-full group-hover:translate-y-0 transition-transform duration-300 skew-y-12"></div> + <span class="relative z-10 flex items-center gap-2"> + PLAY + </span> <span - class="text-[10px] font-normal opacity-80 normal-case tracking-normal" - >Click to launch</span + class="relative z-10 text-[9px] font-normal opacity-70 normal-case tracking-wide -mt-1" + >Launch Game</span > </button> </div> diff --git a/ui/src/components/HomeView.svelte b/ui/src/components/HomeView.svelte index e876c14..036c03a 100644 --- a/ui/src/components/HomeView.svelte +++ b/ui/src/components/HomeView.svelte @@ -1,26 +1,47 @@ <script lang="ts"> - // No script needed currently, just static markup mostly + type Props = { + mouseX: number; + mouseY: number; + }; + let { mouseX = 0, mouseY = 0 }: Props = $props(); </script> -<!-- Background Image - Using gradient fallback --> -<div - class="absolute inset-0 z-0 opacity-60 bg-gradient-to-br from-emerald-900 via-zinc-900 to-indigo-950 transition-transform duration-[10s] ease-linear group-hover:scale-105" -></div> -<div - class="absolute inset-0 z-0 bg-gradient-to-t from-zinc-900 via-transparent to-black/50" -></div> +<div class="absolute inset-0 z-0 overflow-hidden"> + <!-- Parallax Background Layers --> + + <div class="absolute inset-0 bg-gradient-to-t from-[#09090b] via-[#09090b]/40 to-transparent"></div> +</div> -<div class="absolute bottom-24 left-8 z-10 p-4"> - <h1 - class="text-6xl font-black mb-2 tracking-tight text-white drop-shadow-lg" +<div class="relative z-10 h-full flex flex-col justify-end p-12 pb-24"> + <!-- 3D Floating Hero Text --> + <div + class="transition-transform duration-200 ease-out origin-bottom-left" + style:transform={`perspective(1000px) rotateX(${mouseY * -2}deg) rotateY(${mouseX * 2}deg)`} > - MINECRAFT - </h1> - <div class="flex items-center gap-2 text-zinc-300"> - <span - class="bg-zinc-800 text-xs px-2 py-1 rounded border border-zinc-600" - >JAVA EDITION</span + <h1 + class="text-8xl font-black tracking-tighter text-white drop-shadow-2xl mb-4" + style="text-shadow: 0 10px 30px rgba(0,0,0,0.5);" > - <span class="text-lg">Release 1.20.4</span> + MINECRAFT + </h1> + + <div class="flex items-center gap-4"> + <div + class="bg-white/10 backdrop-blur-md border border-white/10 px-4 py-1.5 rounded-full text-sm font-bold uppercase tracking-widest text-emerald-400 shadow-xl" + > + Java Edition + </div> + <div class="text-2xl font-light text-zinc-300"> + Latest Release 1.21 + </div> + </div> + </div> + + <!-- Action Area --> + <div class="mt-8 flex gap-4"> + <!-- Quick Play Button (Visual only here, logic is in BottomBar usually) --> + <div class="text-zinc-400 text-sm italic"> + Ready to play. Select version below or hit Launch. + </div> </div> </div> diff --git a/ui/src/components/ModLoaderSelector.svelte b/ui/src/components/ModLoaderSelector.svelte index 06eb6ae..4a59916 100644 --- a/ui/src/components/ModLoaderSelector.svelte +++ b/ui/src/components/ModLoaderSelector.svelte @@ -112,134 +112,134 @@ } </script> -<div class="bg-zinc-800 rounded-lg p-4 border border-zinc-700"> - <h3 class="text-sm font-semibold text-zinc-400 mb-3">Mod Loader</h3> +<div class="space-y-4"> + <div class="flex items-center justify-between"> + <h3 class="text-xs font-bold uppercase tracking-widest text-white/40">Select Mod Loader</h3> + </div> - <!-- Loader Type Tabs --> - <div class="flex gap-1 mb-4 bg-zinc-900 rounded-lg p-1"> + <!-- Loader Type Tabs - Segmented Control --> + <div class="flex p-1 bg-black/40 rounded-xl border border-white/5 backdrop-blur-sm"> <button - class="flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors {selectedLoader === - 'vanilla' - ? 'bg-zinc-700 text-white' - : 'text-zinc-400 hover:text-white hover:bg-zinc-800'}" + class="flex-1 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 + {selectedLoader === 'vanilla' + ? 'bg-white/10 text-white shadow-lg border border-white/10' + : 'text-zinc-500 hover:text-white hover:bg-white/5'}" onclick={() => onLoaderChange("vanilla")} > Vanilla </button> <button - class="flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors {selectedLoader === - 'fabric' - ? 'bg-blue-600 text-white' - : 'text-zinc-400 hover:text-white hover:bg-zinc-800'}" + class="flex-1 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 + {selectedLoader === 'fabric' + ? 'bg-indigo-500/20 text-indigo-300 shadow-lg border border-indigo-500/20' + : 'text-zinc-500 hover:text-white hover:bg-white/5'}" onclick={() => onLoaderChange("fabric")} > Fabric </button> <button - class="flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors {selectedLoader === - 'forge' - ? 'bg-orange-600 text-white' - : 'text-zinc-400 hover:text-white hover:bg-zinc-800'}" + class="flex-1 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 + {selectedLoader === 'forge' + ? 'bg-orange-500/20 text-orange-300 shadow-lg border border-orange-500/20' + : 'text-zinc-500 hover:text-white hover:bg-white/5'}" onclick={() => onLoaderChange("forge")} > Forge </button> </div> - {#if selectedLoader === "vanilla"} - <p class="text-sm text-zinc-500"> - Launch the selected Minecraft version without any mod loaders. - </p> - {:else if !selectedGameVersion} - <p class="text-sm text-zinc-500"> - Select a Minecraft version first to see available {selectedLoader} versions. - </p> - {:else if isLoading} - <div class="flex items-center gap-2 text-sm text-zinc-400"> - <svg - class="animate-spin h-4 w-4" - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - > - <circle - class="opacity-25" - cx="12" - cy="12" - r="10" - stroke="currentColor" - stroke-width="4" - ></circle> - <path - class="opacity-75" - fill="currentColor" - d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" - ></path> - </svg> - Loading {selectedLoader} versions... - </div> - {:else if error} - <p class="text-sm text-red-400">{error}</p> - {:else if selectedLoader === "fabric"} - <div class="space-y-3"> - <div> - <label for="fabric-loader-select" class="block text-xs text-zinc-500 mb-1" - >Loader Version</label - > - <select - id="fabric-loader-select" - class="w-full bg-zinc-900 border border-zinc-700 rounded px-3 py-2 text-sm focus:outline-none focus:border-blue-500" - bind:value={selectedFabricLoader} - > - {#each fabricLoaders as loader} - <option value={loader.version}> - {loader.version} - {loader.stable ? "(stable)" : ""} - </option> - {/each} - </select> - </div> - <button - class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded font-medium text-sm transition-colors disabled:opacity-50 disabled:cursor-not-allowed" - onclick={installModLoader} - disabled={isLoading || !selectedFabricLoader} - > - Install Fabric {selectedFabricLoader} - </button> - </div> - {:else if selectedLoader === "forge"} - <div class="space-y-3"> - {#if forgeVersions.length === 0} - <p class="text-sm text-zinc-500"> - No Forge versions available for Minecraft {selectedGameVersion} - </p> - {:else} + <!-- Content Area --> + <div class="min-h-[100px] flex flex-col justify-center"> + {#if selectedLoader === "vanilla"} + <div class="text-center p-4 rounded-xl bg-white/5 border border-dashed border-white/10 text-white/40 text-sm"> + No mod loader selected. <br> Pure vanilla experience. + </div> + + {:else if !selectedGameVersion} + <div class="text-center p-4 rounded-xl bg-red-500/10 border border-red-500/20 text-red-300 text-sm"> + ⚠️ Please select a base Minecraft version first. + </div> + + {:else if isLoading} + <div class="flex flex-col items-center gap-2 text-sm text-white/50 py-4"> + <div class="w-6 h-6 border-2 border-white/20 border-t-white rounded-full animate-spin"></div> + Loading {selectedLoader} versions... + </div> + + {:else if error} + <div class="p-4 bg-red-500/10 border border-red-500/20 rounded-xl text-red-300 text-sm break-words"> + {error} + </div> + + {:else if selectedLoader === "fabric"} + <div class="space-y-4 animate-in fade-in slide-in-from-bottom-2 duration-300"> <div> - <label for="forge-version-select" class="block text-xs text-zinc-500 mb-1" - >Forge Version</label - > - <select - id="forge-version-select" - class="w-full bg-zinc-900 border border-zinc-700 rounded px-3 py-2 text-sm focus:outline-none focus:border-orange-500" - bind:value={selectedForgeVersion} - > - {#each forgeVersions as version} - <option value={version.version}> - {version.version} - {version.recommended ? "⭐ recommended" : ""} - {version.latest ? "(latest)" : ""} - </option> - {/each} - </select> + <label for="fabric-loader-select" class="block text-xs text-white/40 mb-2 pl-1" + >Loader Version</label + > + <div class="relative"> + <select + id="fabric-loader-select" + class="w-full appearance-none bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-sm focus:outline-none focus:border-indigo-500/50 text-white transition-colors" + bind:value={selectedFabricLoader} + > + {#each fabricLoaders as loader} + <option value={loader.version}> + {loader.version} {loader.stable ? "(stable)" : ""} + </option> + {/each} + </select> + <div class="absolute right-4 top-1/2 -translate-y-1/2 text-white/20 pointer-events-none">▼</div> + </div> </div> + <button - class="w-full bg-orange-600 hover:bg-orange-700 text-white py-2 px-4 rounded font-medium text-sm transition-colors disabled:opacity-50 disabled:cursor-not-allowed" - onclick={installModLoader} - disabled={isLoading || !selectedForgeVersion} + class="w-full bg-indigo-600 hover:bg-indigo-500 text-white py-3 px-4 rounded-xl font-bold text-sm transition-all shadow-lg shadow-indigo-500/20 disabled:opacity-50 disabled:shadow-none hover:scale-[1.02] active:scale-[0.98]" + onclick={installModLoader} + disabled={isLoading || !selectedFabricLoader} > - Install Forge {selectedForgeVersion} + Install Fabric {selectedFabricLoader} </button> - {/if} - </div> - {/if} + </div> + + {:else if selectedLoader === "forge"} + <div class="space-y-4 animate-in fade-in slide-in-from-bottom-2 duration-300"> + {#if forgeVersions.length === 0} + <div class="text-center p-4 text-sm text-white/40 italic"> + No Forge versions available for {selectedGameVersion} + </div> + {:else} + <div> + <label for="forge-version-select" class="block text-xs text-white/40 mb-2 pl-1" + >Forge Version</label + > + <div class="relative"> + <select + id="forge-version-select" + class="w-full appearance-none bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-sm focus:outline-none focus:border-orange-500/50 text-white transition-colors" + bind:value={selectedForgeVersion} + > + {#each forgeVersions as version} + <option value={version.version}> + {version.version} + {version.recommended ? "⭐ recommended" : ""} + {version.latest ? "(latest)" : ""} + </option> + {/each} + </select> + <div class="absolute right-4 top-1/2 -translate-y-1/2 text-white/20 pointer-events-none">▼</div> + </div> + </div> + + <button + class="w-full bg-orange-600 hover:bg-orange-500 text-white py-3 px-4 rounded-xl font-bold text-sm transition-all shadow-lg shadow-orange-500/20 disabled:opacity-50 disabled:shadow-none hover:scale-[1.02] active:scale-[0.98]" + onclick={installModLoader} + disabled={isLoading || !selectedForgeVersion} + > + Install Forge {selectedForgeVersion} + </button> + {/if} + </div> + {/if} + </div> </div> diff --git a/ui/src/components/ParticleBackground.svelte b/ui/src/components/ParticleBackground.svelte new file mode 100644 index 0000000..080f1f2 --- /dev/null +++ b/ui/src/components/ParticleBackground.svelte @@ -0,0 +1,57 @@ +<script lang="ts"> + import { onMount, onDestroy } from "svelte"; + import { ConstellationEffect } from "../lib/effects/ConstellationEffect"; + import { SaturnEffect } from "../lib/effects/SaturnEffect"; + import { settingsState } from "../stores/settings.svelte"; + + let canvas: HTMLCanvasElement; + let activeEffect: any; + + function loadEffect() { + if (activeEffect) { + activeEffect.destroy(); + } + + if (!canvas) return; + + if (settingsState.settings.active_effect === "saturn") { + activeEffect = new SaturnEffect(canvas); + } else { + activeEffect = new ConstellationEffect(canvas); + } + + // Ensure correct size immediately + activeEffect.resize(window.innerWidth, window.innerHeight); + } + + $effect(() => { + const _ = settingsState.settings.active_effect; + if (canvas) { + loadEffect(); + } + }); + + onMount(() => { + const resizeObserver = new ResizeObserver(() => { + if (canvas && activeEffect) { + activeEffect.resize(window.innerWidth, window.innerHeight); + } + }); + + resizeObserver.observe(document.body); + + return () => { + resizeObserver.disconnect(); + if (activeEffect) activeEffect.destroy(); + }; + }); + + onDestroy(() => { + if (activeEffect) activeEffect.destroy(); + }); +</script> + +<canvas + bind:this={canvas} + class="absolute inset-0 z-0 pointer-events-none" +></canvas> diff --git a/ui/src/components/SettingsView.svelte b/ui/src/components/SettingsView.svelte index 801970b..4c92220 100644 --- a/ui/src/components/SettingsView.svelte +++ b/ui/src/components/SettingsView.svelte @@ -1,150 +1,274 @@ <script lang="ts"> import { settingsState } from "../stores/settings.svelte"; + import { open } from "@tauri-apps/plugin-dialog"; + import { convertFileSrc } from "@tauri-apps/api/core"; + + async function selectBackground() { + try { + const selected = await open({ + multiple: false, + filters: [ + { + name: "Images", + extensions: ["png", "jpg", "jpeg", "webp", "gif"], + }, + ], + }); + + if (selected && typeof selected === "string") { + settingsState.settings.custom_background_path = selected; + settingsState.saveSettings(); + } + } catch (e) { + console.error("Failed to select background:", e); + } + } + + function clearBackground() { + settingsState.settings.custom_background_path = undefined; + settingsState.saveSettings(); + } </script> -<div class="p-8 bg-zinc-900 h-full overflow-y-auto"> - <h2 class="text-3xl font-bold mb-8">Settings</h2> +<div class="h-full flex flex-col p-6 overflow-hidden"> + <div class="flex items-center justify-between mb-6"> + <h2 class="text-3xl font-black bg-clip-text text-transparent bg-gradient-to-r from-white to-white/60">Settings</h2> + </div> + + <div class="flex-1 overflow-y-auto pr-2 space-y-6 custom-scrollbar pb-10"> - <div class="space-y-6 max-w-2xl"> - <!-- Java Path --> - <div class="bg-zinc-800/50 p-6 rounded-lg border border-zinc-700"> - <label - for="java-path" - class="block text-sm font-bold text-zinc-400 mb-2 uppercase tracking-wide" - >Java Executable Path</label - > - <div class="flex gap-2"> - <input - id="java-path" - bind:value={settingsState.settings.java_path} - type="text" - class="bg-zinc-950 text-white flex-1 p-3 rounded border border-zinc-700 focus:border-indigo-500 outline-none font-mono text-sm" - placeholder="e.g. java, /usr/bin/java" - /> - <button - onclick={() => settingsState.detectJava()} - disabled={settingsState.isDetectingJava} - class="bg-zinc-700 hover:bg-zinc-600 disabled:opacity-50 text-white px-4 py-2 rounded transition-colors whitespace-nowrap" - > - {settingsState.isDetectingJava ? "Detecting..." : "Auto Detect"} - </button> + <!-- Appearance / Background --> + <div class="bg-black/20 p-6 rounded-2xl border border-white/5 "> + <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2"> + Appearance + </h3> + + <div class="space-y-4"> + <div> + <label class="block text-sm font-medium text-white/70 mb-3">Custom Background Image</label> + + <div class="flex items-center gap-6"> + <!-- Preview --> + <div class="w-40 h-24 rounded-xl overflow-hidden bg-black/50 border border-white/10 relative group shadow-lg"> + {#if settingsState.settings.custom_background_path} + <img + src={convertFileSrc(settingsState.settings.custom_background_path)} + alt="Background Preview" + class="w-full h-full object-cover" + /> + {:else} + <div class="w-full h-full bg-gradient-to-br from-emerald-900 via-zinc-900 to-indigo-950 opacity-100"></div> + <div class="absolute inset-0 flex items-center justify-center text-[10px] text-white/50 bg-black/20 ">Default Gradient</div> + {/if} + </div> + + <!-- Actions --> + <div class="flex flex-col gap-2"> + <button + onclick={selectBackground} + class="bg-white/10 hover:bg-white/20 text-white px-4 py-2 rounded-lg text-sm transition-colors border border-white/5" + > + Select Image + </button> + + {#if settingsState.settings.custom_background_path} + <button + onclick={clearBackground} + class="text-red-400 hover:text-red-300 text-sm px-4 py-1 text-left transition-colors" + > + Reset to Default + </button> + {/if} + </div> + </div> + <p class="text-xs text-white/30 mt-3"> + Select an image from your computer to replace the default gradient background. + Supported formats: PNG, JPG, WEBP, GIF. + </p> + </div> + + <!-- Visual Settings --> + <div class="pt-4 border-t border-white/5 space-y-4"> + <div class="flex items-center justify-between"> + <div> + <h4 class="text-sm font-medium text-white/90" id="visual-effects-label">Visual Effects</h4> + <p class="text-xs text-white/40 mt-1">Enable particle effects and animated gradients. (Default: On)</p> + </div> + <button + aria-labelledby="visual-effects-label" + onclick={() => { settingsState.settings.enable_visual_effects = !settingsState.settings.enable_visual_effects; settingsState.saveSettings(); }} + class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.enable_visual_effects ? 'bg-indigo-500' : 'bg-white/10'}" + > + <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.enable_visual_effects ? 'translate-x-5' : 'translate-x-0'}"></div> + </button> + </div> + + {#if settingsState.settings.enable_visual_effects} + <div class="flex items-center justify-between pl-2 border-l-2 border-white/5 ml-1"> + <div> + <h4 class="text-sm font-medium text-white/90" id="theme-effect-label">Theme Effect</h4> + <p class="text-xs text-white/40 mt-1">Select the active visual theme.</p> + </div> + <select + aria-labelledby="theme-effect-label" + bind:value={settingsState.settings.active_effect} + onchange={() => settingsState.saveSettings()} + class="bg-black/40 text-white text-xs px-3 py-2 rounded-lg border border-white/10 outline-none focus:border-indigo-500/50 appearance-none cursor-pointer hover:bg-white/5 transition-colors" + > + <option value="saturn">Saturn (Saturn)</option> + <option value="constellation">Network (Constellation)</option> + </select> + </div> + {/if} + + <div class="flex items-center justify-between"> + <div> + <h4 class="text-sm font-medium text-white/90" id="gpu-acceleration-label">GPU Acceleration</h4> + <p class="text-xs text-white/40 mt-1">Enable GPU acceleration for the interface. (Default: Off, Requires Restart)</p> + </div> + <button + aria-labelledby="gpu-acceleration-label" + onclick={() => { settingsState.settings.enable_gpu_acceleration = !settingsState.settings.enable_gpu_acceleration; settingsState.saveSettings(); }} + class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.enable_gpu_acceleration ? 'bg-indigo-500' : 'bg-white/10'}" + > + <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.enable_gpu_acceleration ? 'translate-x-5' : 'translate-x-0'}"></div> + </button> + </div> + </div> </div> + </div> + + <!-- Java Path --> + <div class="bg-black/20 p-6 rounded-2xl border border-white/5 "> + <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2"> + Java Environment + </h3> + <div class="space-y-4"> + <div> + <label for="java-path" class="block text-sm font-medium text-white/70 mb-2">Java Executable Path</label> + <div class="flex gap-2"> + <input + id="java-path" + bind:value={settingsState.settings.java_path} + type="text" + class="bg-black/40 text-white flex-1 px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none font-mono text-xs transition-colors" + placeholder="e.g. java, /usr/bin/java" + /> + <button + onclick={() => settingsState.detectJava()} + disabled={settingsState.isDetectingJava} + class="bg-white/10 hover:bg-white/20 disabled:opacity-50 text-white px-4 py-2 rounded-xl border border-white/5 transition-colors whitespace-nowrap text-sm font-medium" + > + {settingsState.isDetectingJava ? "Detecting..." : "Auto Detect"} + </button> + </div> + </div> {#if settingsState.javaInstallations.length > 0} <div class="mt-4 space-y-2"> - <p class="text-xs text-zinc-400 uppercase font-bold">Detected Java Installations:</p> + <p class="text-[10px] text-white/30 uppercase font-bold tracking-wider">Detected Installations</p> {#each settingsState.javaInstallations as java} <button onclick={() => settingsState.selectJava(java.path)} - class="w-full text-left p-3 bg-zinc-950 rounded border transition-colors {settingsState.settings.java_path === java.path ? 'border-indigo-500 bg-indigo-950/30' : 'border-zinc-700 hover:border-zinc-500'}" + class="w-full text-left p-3 rounded-lg border transition-all duration-200 group + {settingsState.settings.java_path === java.path + ? 'bg-indigo-500/20 border-indigo-500/30' + : 'bg-black/20 border-white/5 hover:bg-white/5 hover:border-white/10'}" > <div class="flex justify-between items-center"> <div> - <span class="text-white font-mono text-sm">{java.version}</span> - <span class="text-zinc-500 text-xs ml-2">{java.is_64bit ? "64-bit" : "32-bit"}</span> + <span class="text-white font-mono text-xs font-bold">{java.version}</span> + <span class="text-white/40 text-[10px] ml-2">{java.is_64bit ? "64-bit" : "32-bit"}</span> </div> {#if settingsState.settings.java_path === java.path} - <span class="text-indigo-400 text-xs">Selected</span> + <span class="text-indigo-300 text-[10px] font-bold uppercase tracking-wider">Selected</span> {/if} </div> - <div class="text-zinc-500 text-xs font-mono truncate mt-1">{java.path}</div> + <div class="text-white/30 text-[10px] font-mono truncate mt-1 group-hover:text-white/50">{java.path}</div> </button> {/each} </div> {/if} - - <p class="text-xs text-zinc-500 mt-2"> - The command or path to the Java Runtime Environment. Click "Auto Detect" to find installed Java versions. - </p> + </div> </div> <!-- Memory --> - <div class="bg-zinc-800/50 p-6 rounded-lg border border-zinc-700"> - <h3 - class="block text-sm font-bold text-zinc-400 mb-4 uppercase tracking-wide" - >Memory Allocation (RAM)</h3> - + <div class="bg-black/20 p-6 rounded-2xl border border-white/5 "> + <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2"> + Memory Allocation (RAM) + </h3> <div class="grid grid-cols-2 gap-6"> <div> - <label for="min-memory" class="block text-xs text-zinc-500 mb-1" - >Minimum (MB)</label - > + <label for="min-memory" class="block text-sm font-medium text-white/70 mb-2">Minimum (MB)</label> <input id="min-memory" bind:value={settingsState.settings.min_memory} type="number" - class="bg-zinc-950 text-white w-full p-3 rounded border border-zinc-700 focus:border-indigo-500 outline-none" + class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none transition-colors" /> </div> <div> - <label for="max-memory" class="block text-xs text-zinc-500 mb-1" - >Maximum (MB)</label - > + <label for="max-memory" class="block text-sm font-medium text-white/70 mb-2">Maximum (MB)</label> <input id="max-memory" bind:value={settingsState.settings.max_memory} type="number" - class="bg-zinc-950 text-white w-full p-3 rounded border border-zinc-700 focus:border-indigo-500 outline-none" + class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none transition-colors" /> </div> </div> </div> <!-- Resolution --> - <div class="bg-zinc-800/50 p-6 rounded-lg border border-zinc-700"> - <h3 - class="block text-sm font-bold text-zinc-400 mb-4 uppercase tracking-wide" - >Game Window Size</h3> + <div class="bg-black/20 p-6 rounded-2xl border border-white/5 "> + <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2"> + Game Window Size + </h3> <div class="grid grid-cols-2 gap-6"> <div> - <label for="window-width" class="block text-xs text-zinc-500 mb-1">Width</label> + <label for="window-width" class="block text-sm font-medium text-white/70 mb-2">Width</label> <input id="window-width" bind:value={settingsState.settings.width} type="number" - class="bg-zinc-950 text-white w-full p-3 rounded border border-zinc-700 focus:border-indigo-500 outline-none" + class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none transition-colors" /> </div> <div> - <label for="window-height" class="block text-xs text-zinc-500 mb-1">Height</label> + <label for="window-height" class="block text-sm font-medium text-white/70 mb-2">Height</label> <input id="window-height" bind:value={settingsState.settings.height} type="number" - class="bg-zinc-950 text-white w-full p-3 rounded border border-zinc-700 focus:border-indigo-500 outline-none" + class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none transition-colors" /> </div> </div> </div> <!-- Download Settings --> - <div class="bg-zinc-800/50 p-6 rounded-lg border border-zinc-700"> - <h3 - class="block text-sm font-bold text-zinc-400 mb-4 uppercase tracking-wide" - >Download Settings</h3> - <div> - <label for="download-threads" class="block text-xs text-zinc-500 mb-1" - >Concurrent Download Threads</label - > - <input - id="download-threads" - bind:value={settingsState.settings.download_threads} - type="number" - min="1" - max="128" - step="1" - class="bg-zinc-950 text-white w-full p-3 rounded border border-zinc-700 focus:border-indigo-500 outline-none" - /> - <p class="text-xs text-zinc-500 mt-2"> - Number of concurrent download threads (1-128). Higher values increase download speed but use more bandwidth and system resources. Default: 32 - </p> - </div> + <div class="bg-black/20 p-6 rounded-2xl border border-white/5 "> + <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2"> + Network + </h3> + <div> + <label for="download-threads" class="block text-sm font-medium text-white/70 mb-2">Concurrent Download Threads</label> + <input + id="download-threads" + bind:value={settingsState.settings.download_threads} + type="number" + min="1" + max="128" + class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none transition-colors" + /> + <p class="text-xs text-white/30 mt-2">Higher values usually mean faster downloads but use more CPU/Network.</p> + </div> </div> - <div class="pt-4"> + <div class="pt-4 flex justify-end"> <button onclick={() => settingsState.saveSettings()} - class="bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-3 px-8 rounded shadow-lg transition-transform active:scale-95" + class="bg-gradient-to-r from-emerald-600 to-green-600 hover:from-emerald-500 hover:to-green-500 text-white font-bold py-3 px-8 rounded-xl shadow-lg shadow-emerald-500/20 transition-all hover:scale-105 active:scale-95" > Save Settings </button> diff --git a/ui/src/components/Sidebar.svelte b/ui/src/components/Sidebar.svelte index a4f4e35..7976f6a 100644 --- a/ui/src/components/Sidebar.svelte +++ b/ui/src/components/Sidebar.svelte @@ -3,64 +3,56 @@ </script> <aside - class="w-20 lg:w-64 bg-zinc-950 flex flex-col items-center lg:items-start transition-all duration-300 border-r border-zinc-800 shrink-0" + class="w-20 lg:w-64 bg-black flex flex-col items-center lg:items-start transition-all duration-300 shrink-0 py-6" > + <!-- Logo Area --> <div - class="h-20 w-full flex items-center justify-center lg:justify-start lg:px-6 border-b border-zinc-800/50" + class="h-16 w-full flex items-center justify-center lg:justify-start lg:px-8 mb-6" > - <!-- Icon Logo (Visible on small) --> + <!-- Icon Logo (Small) --> <div - class="lg:hidden text-2xl font-black bg-clip-text text-transparent bg-gradient-to-tr from-indigo-400 to-purple-400" + class="lg:hidden text-3xl font-black bg-clip-text text-transparent bg-gradient-to-tr from-indigo-400 to-fuchsia-400 drop-shadow-lg" > D </div> - <!-- Full Logo (Visible on large) --> + <!-- Full Logo (Large) --> <div - class="hidden lg:block font-bold text-xl tracking-wider text-indigo-400" + class="hidden lg:block font-bold text-2xl tracking-wider text-white" > - DROP<span class="text-white">OUT</span> + <span class="bg-clip-text text-transparent bg-gradient-to-r from-indigo-400 to-fuchsia-400">DROP</span>OUT </div> </div> - <nav class="flex-1 w-full flex flex-col gap-2 p-3"> - <button - class="group flex items-center lg:gap-4 justify-center lg:justify-start w-full px-0 lg:px-4 py-3 rounded-lg hover:bg-zinc-800 {uiState.currentView === - 'home' - ? 'bg-zinc-800/80 text-white' - : 'text-zinc-400'} transition-all relative" - onclick={() => uiState.setView("home")} - > - <span class="text-xl relative z-10">🏠</span> - <span - class="hidden lg:block font-medium relative z-10 transition-opacity" - >Home</span + <!-- Navigation --> + <nav class="flex-1 w-full flex flex-col gap-3 px-3"> + <!-- Nav Item Helper --> + {#snippet navItem(view, icon, label)} + <button + class="group flex items-center lg:gap-4 justify-center lg:justify-start w-full px-0 lg:px-5 py-3.5 rounded-xl transition-all duration-200 relative overflow-hidden + {uiState.currentView === view + ? 'bg-gradient-to-r from-indigo-500/20 to-purple-500/20 text-white shadow-lg shadow-indigo-500/10 border border-white/10' + : 'text-zinc-400 hover:text-white hover:bg-white/5'}" + onclick={() => uiState.setView(view)} > - </button> - <button - class="group flex items-center lg:gap-4 justify-center lg:justify-start w-full px-0 lg:px-4 py-3 rounded-lg hover:bg-zinc-800 {uiState.currentView === - 'versions' - ? 'bg-zinc-800/80 text-white' - : 'text-zinc-400'} transition-all" - onclick={() => uiState.setView("versions")} - > - <span class="text-xl">📦</span> - <span class="hidden lg:block font-medium">Versions</span> - </button> - <button - class="group flex items-center lg:gap-4 justify-center lg:justify-start w-full px-0 lg:px-4 py-3 rounded-lg hover:bg-zinc-800 {uiState.currentView === - 'settings' - ? 'bg-zinc-800/80 text-white' - : 'text-zinc-400'} transition-all" - onclick={() => uiState.setView("settings")} - > - <span class="text-xl">⚙️</span> - <span class="hidden lg:block font-medium">Settings</span> - </button> + <span class="text-xl relative z-10 transition-transform group-hover:scale-110 duration-200">{icon}</span> + <span class="hidden lg:block font-medium relative z-10">{label}</span> + + <!-- Active Indicator Line --> + {#if uiState.currentView === view} + <div class="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-indigo-500 rounded-r-full lg:hidden"></div> + {/if} + </button> + {/snippet} + + {@render navItem('home', '🏠', 'Home')} + {@render navItem('versions', '📦', 'Versions')} + {@render navItem('settings', '⚙️', 'Settings')} </nav> + <!-- Footer Info --> <div - class="p-4 w-full border-t border-zinc-800 flex justify-center lg:justify-start" + class="p-4 w-full flex justify-center lg:justify-start lg:px-8 opacity-50 hover:opacity-100 transition-opacity" > - <div class="text-xs text-zinc-600 font-mono">v{uiState.appVersion}</div> + <div class="text-xs font-mono tracking-widest text-zinc-500">v{uiState.appVersion}</div> </div> </aside> diff --git a/ui/src/components/VersionsView.svelte b/ui/src/components/VersionsView.svelte index 1ea4878..00ac281 100644 --- a/ui/src/components/VersionsView.svelte +++ b/ui/src/components/VersionsView.svelte @@ -73,15 +73,15 @@ function getVersionBadge(type: string) { switch (type) { case "release": - return { text: "Release", class: "bg-green-600" }; + return { text: "Release", class: "bg-emerald-500/20 text-emerald-300 border-emerald-500/30" }; case "snapshot": - return { text: "Snapshot", class: "bg-yellow-600" }; + return { text: "Snapshot", class: "bg-amber-500/20 text-amber-300 border-amber-500/30" }; case "fabric": - return { text: "Fabric", class: "bg-blue-600" }; + return { text: "Fabric", class: "bg-indigo-500/20 text-indigo-300 border-indigo-500/30" }; case "forge": - return { text: "Forge", class: "bg-orange-600" }; + return { text: "Forge", class: "bg-orange-500/20 text-orange-300 border-orange-500/30" }; default: - return { text: type, class: "bg-zinc-600" }; + return { text: type, class: "bg-zinc-500/20 text-zinc-300 border-zinc-500/30" }; } } @@ -114,101 +114,92 @@ }); </script> -<div class="p-8 h-full overflow-y-auto bg-zinc-900"> - <h2 class="text-3xl font-bold mb-6">Versions</h2> +<div class="h-full flex flex-col p-6 overflow-hidden"> + <div class="flex items-center justify-between mb-6"> + <h2 class="text-3xl font-black bg-clip-text text-transparent bg-gradient-to-r from-white to-white/60">Version Manager</h2> + <div class="text-sm text-white/40">Select a version to play or modify</div> + </div> - <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> + <div class="flex-1 grid grid-cols-1 lg:grid-cols-3 gap-6 overflow-hidden"> <!-- Left: Version List --> - <div class="lg:col-span-2 space-y-4"> - <!-- Search and Filters --> + <div class="lg:col-span-2 flex flex-col gap-4 overflow-hidden"> + <!-- Search and Filters (Glass Bar) --> <div class="flex gap-3"> - <input - type="text" - placeholder="Search versions..." - class="flex-1 p-3 bg-zinc-800 border border-zinc-700 rounded text-white focus:outline-none focus:border-green-500 transition-colors" - bind:value={searchQuery} - /> + <div class="relative flex-1"> + <span class="absolute left-3 top-1/2 -translate-y-1/2 text-white/30">🔍</span> + <input + type="text" + placeholder="Search versions..." + class="w-full pl-9 pr-4 py-3 bg-black/20 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:border-indigo-500/50 focus:bg-black/40 transition-all backdrop-blur-sm" + bind:value={searchQuery} + /> + </div> </div> - <!-- Type Filter Tabs --> - <div class="flex gap-1 bg-zinc-800 rounded-lg p-1"> - <button - class="flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors {typeFilter === - 'all' - ? 'bg-zinc-700 text-white' - : 'text-zinc-400 hover:text-white'}" - onclick={() => (typeFilter = "all")} - > - All - </button> - <button - class="flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors {typeFilter === - 'release' - ? 'bg-green-600 text-white' - : 'text-zinc-400 hover:text-white'}" - onclick={() => (typeFilter = "release")} - > - Releases - </button> - <button - class="flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors {typeFilter === - 'snapshot' - ? 'bg-yellow-600 text-white' - : 'text-zinc-400 hover:text-white'}" - onclick={() => (typeFilter = "snapshot")} - > - Snapshots - </button> - <button - class="flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors {typeFilter === - 'modded' - ? 'bg-purple-600 text-white' - : 'text-zinc-400 hover:text-white'}" - onclick={() => (typeFilter = "modded")} - > - Modded - </button> + <!-- Type Filter Tabs (Glass Caps) --> + <div class="flex p-1 bg-black/20 rounded-xl border border-white/5"> + {#each ['all', 'release', 'snapshot', 'modded'] as filter} + <button + class="flex-1 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 capitalize + {typeFilter === filter + ? 'bg-white/10 text-white shadow-lg border border-white/10' + : 'text-white/40 hover:text-white hover:bg-white/5'}" + onclick={() => (typeFilter = filter as any)} + > + {filter} + </button> + {/each} </div> - <!-- Version List --> - <div class="grid gap-2 max-h-[calc(100vh-320px)] overflow-y-auto pr-2"> + <!-- Version List SCROLL --> + <div class="flex-1 overflow-y-auto pr-2 space-y-2 custom-scrollbar"> {#if gameState.versions.length === 0} - <div class="text-zinc-500">Loading versions...</div> + <div class="flex items-center justify-center h-40 text-white/30 italic animate-pulse"> + Fetching manifest... + </div> {:else if filteredVersions().length === 0} - <div class="text-zinc-500"> - {#if normalizedQuery.length > 0} - No versions found matching "{searchQuery}" - {:else} - No versions in this category - {/if} + <div class="flex flex-col items-center justify-center -40 text-white/30 gap-2"> + <span class="text-2xl">👻</span> + <span>No matching versions found</span> </div> {:else} {#each filteredVersions() as version} {@const badge = getVersionBadge(version.type)} + {@const isSelected = gameState.selectedVersion === version.id} <button - class="flex items-center justify-between p-4 bg-zinc-800 rounded hover:bg-zinc-700 transition text-left border border-zinc-700 {gameState.selectedVersion === - version.id - ? 'border-green-500 bg-zinc-800/80 ring-1 ring-green-500' - : ''}" + class="w-full group flex items-center justify-between p-4 rounded-xl text-left border transition-all duration-200 relative overflow-hidden + {isSelected + ? 'bg-indigo-600/20 border-indigo-500/50 shadow-[0_0_20px_rgba(99,102,241,0.2)]' + : 'bg-white/5 border-white/5 hover:bg-white/10 hover:border-white/10 hover:translate-x-1'}" onclick={() => (gameState.selectedVersion = version.id)} > - <div class="flex items-center gap-3"> + <!-- Selection Glow --> + {#if isSelected} + <div class="absolute inset-0 bg-gradient-to-r from-indigo-500/10 to-transparent pointer-events-none"></div> + {/if} + + <div class="relative z-10 flex items-center gap-4"> <span - class="px-2 py-0.5 rounded text-xs font-medium {badge.class}" + class="px-2.5 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wide border {badge.class}" > {badge.text} </span> <div> - <div class="font-bold font-mono">{version.id}</div> + <div class="font-bold font-mono text-lg tracking-tight {isSelected ? 'text-white' : 'text-zinc-300 group-hover:text-white'}"> + {version.id} + </div> {#if version.releaseTime && version.type !== "fabric" && version.type !== "forge"} - <div class="text-xs text-zinc-400"> + <div class="text-xs text-white/30"> {new Date(version.releaseTime).toLocaleDateString()} </div> {/if} </div> </div> - {#if gameState.selectedVersion === version.id} - <div class="text-green-500 font-bold text-sm">SELECTED</div> + + {#if isSelected} + <div class="relative z-10 text-indigo-400"> + <span class="text-lg">Selected</span> + </div> {/if} </button> {/each} @@ -217,32 +208,29 @@ </div> <!-- Right: Mod Loader Panel --> - <div class="space-y-4"> - <!-- Selected Version Info --> - {#if gameState.selectedVersion} - <div class="bg-zinc-800 rounded-lg p-4 border border-zinc-700"> - <h3 class="text-sm font-semibold text-zinc-400 mb-2">Selected</h3> - <p class="font-mono text-lg text-green-400"> - {gameState.selectedVersion} - </p> - </div> - {/if} - - <!-- Mod Loader Selector --> - <ModLoaderSelector - selectedGameVersion={selectedBaseVersion()} - onInstall={handleModLoaderInstall} - /> - - <!-- Help Text --> - <div class="bg-zinc-800/50 rounded-lg p-4 border border-zinc-700/50"> - <h4 class="text-sm font-semibold text-zinc-400 mb-2">💡 Tip</h4> - <p class="text-xs text-zinc-500"> - Select a vanilla Minecraft version, then use the Mod Loader panel to - install Fabric or Forge. Installed modded versions will appear in the - list with colored badges. - </p> + <div class="flex flex-col gap-4"> + <!-- Selected Version Info Card --> + <div class="bg-gradient-to-br from-white/10 to-white/5 p-6 rounded-2xl border border-white/10 backdrop-blur-md relative overflow-hidden group"> + <div class="absolute top-0 right-0 p-8 bg-indigo-500/20 blur-[60px] rounded-full group-hover:bg-indigo-500/30 transition-colors"></div> + + <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-2 relative z-10">Current Selection</h3> + {#if gameState.selectedVersion} + <p class="font-mono text-3xl font-black text-transparent bg-clip-text bg-gradient-to-r from-white to-white/70 relative z-10 truncate"> + {gameState.selectedVersion} + </p> + {:else} + <p class="text-white/20 italic relative z-10">None selected</p> + {/if} </div> + + <!-- Mod Loader Selector Card --> + <div class="bg-black/20 p-4 rounded-2xl border border-white/5 backdrop-blur-sm flex-1 flex flex-col"> + <ModLoaderSelector + selectedGameVersion={selectedBaseVersion()} + onInstall={handleModLoaderInstall} + /> + </div> + </div> </div> </div> diff --git a/ui/src/lib/DownloadMonitor.svelte b/ui/src/lib/DownloadMonitor.svelte index 52c935c..860952c 100644 --- a/ui/src/lib/DownloadMonitor.svelte +++ b/ui/src/lib/DownloadMonitor.svelte @@ -156,7 +156,7 @@ {#if visible} <div - class="fixed bottom-28 right-8 z-50 w-80 bg-zinc-900/90 backdrop-blur-md border border-zinc-700 rounded-lg shadow-2xl p-4 animate-in slide-in-from-right-10 fade-in duration-300" + class="fixed bottom-28 right-8 z-50 w-80 bg-zinc-900/90 border border-zinc-700 rounded-lg shadow-2xl p-4 animate-in slide-in-from-right-10 fade-in duration-300" > <div class="flex items-center justify-between mb-2"> <h3 class="text-white font-bold text-sm">Downloads</h3> diff --git a/ui/src/lib/GameConsole.svelte b/ui/src/lib/GameConsole.svelte index 281dc85..8d5e0ce 100644 --- a/ui/src/lib/GameConsole.svelte +++ b/ui/src/lib/GameConsole.svelte @@ -75,7 +75,7 @@ </script> {#if visible} -<div class="fixed bottom-0 left-0 right-0 h-64 bg-zinc-950/95 border-t border-zinc-700 backdrop-blur flex flex-col z-50 transition-transform duration-300 transform translate-y-0"> +<div class="fixed bottom-0 left-0 right-0 h-64 bg-zinc-950/95 border-t border-zinc-700 flex flex-col z-50 transition-transform duration-300 transform translate-y-0"> <div class="flex items-center justify-between px-4 py-2 border-b border-zinc-800 bg-zinc-900/50"> <div class="flex items-center gap-4"> <span class="text-xs font-bold text-zinc-400 uppercase tracking-widest">Logs</span> diff --git a/ui/src/lib/effects/ConstellationEffect.ts b/ui/src/lib/effects/ConstellationEffect.ts new file mode 100644 index 0000000..2cc702e --- /dev/null +++ b/ui/src/lib/effects/ConstellationEffect.ts @@ -0,0 +1,163 @@ + +export class ConstellationEffect { + private canvas: HTMLCanvasElement; + private ctx: CanvasRenderingContext2D; + private width: number = 0; + private height: number = 0; + private particles: Particle[] = []; + private animationId: number = 0; + private mouseX: number = -1000; + private mouseY: number = -1000; + + // Configuration + private readonly particleCount = 100; + private readonly connectionDistance = 150; + private readonly particleSpeed = 0.5; + + constructor(canvas: HTMLCanvasElement) { + this.canvas = canvas; + this.ctx = canvas.getContext("2d", { alpha: true })!; + + // Bind methods + this.animate = this.animate.bind(this); + this.handleMouseMove = this.handleMouseMove.bind(this); + + // Initial setup + this.resize(window.innerWidth, window.innerHeight); + this.initParticles(); + + // Mouse interaction + window.addEventListener("mousemove", this.handleMouseMove); + + // Start animation + this.animate(); + } + + resize(width: number, height: number) { + const dpr = window.devicePixelRatio || 1; + this.width = width; + this.height = height; + + this.canvas.width = width * dpr; + this.canvas.height = height * dpr; + this.canvas.style.width = `${width}px`; + this.canvas.style.height = `${height}px`; + + this.ctx.scale(dpr, dpr); + + // Re-initialize if screen size changes significantly to maintain density + if (this.particles.length === 0) { + this.initParticles(); + } + } + + private initParticles() { + this.particles = []; + // Adjust density based on screen area + const area = this.width * this.height; + const density = Math.floor(area / 15000); // 1 particle per 15000px² + const count = Math.min(Math.max(density, 50), 200); // Clamp between 50 and 200 + + for (let i = 0; i < count; i++) { + this.particles.push(new Particle(this.width, this.height, this.particleSpeed)); + } + } + + private handleMouseMove(e: MouseEvent) { + const rect = this.canvas.getBoundingClientRect(); + this.mouseX = e.clientX - rect.left; + this.mouseY = e.clientY - rect.top; + } + + animate() { + this.ctx.clearRect(0, 0, this.width, this.height); + + // Update and draw particles + this.particles.forEach(p => { + p.update(this.width, this.height); + p.draw(this.ctx); + }); + + // Draw lines + this.drawConnections(); + + this.animationId = requestAnimationFrame(this.animate); + } + + private drawConnections() { + this.ctx.lineWidth = 1; + + for (let i = 0; i < this.particles.length; i++) { + const p1 = this.particles[i]; + + // Connect to mouse if close + const distMouse = Math.hypot(p1.x - this.mouseX, p1.y - this.mouseY); + if (distMouse < this.connectionDistance + 50) { + const alpha = 1 - (distMouse / (this.connectionDistance + 50)); + this.ctx.strokeStyle = `rgba(255, 255, 255, ${alpha * 0.4})`; // Brighter near mouse + this.ctx.beginPath(); + this.ctx.moveTo(p1.x, p1.y); + this.ctx.lineTo(this.mouseX, this.mouseY); + this.ctx.stroke(); + + // Gently attract to mouse + if (distMouse > 10) { + p1.x += (this.mouseX - p1.x) * 0.005; + p1.y += (this.mouseY - p1.y) * 0.005; + } + } + + // Connect to other particles + for (let j = i + 1; j < this.particles.length; j++) { + const p2 = this.particles[j]; + const dist = Math.hypot(p1.x - p2.x, p1.y - p2.y); + + if (dist < this.connectionDistance) { + const alpha = 1 - (dist / this.connectionDistance); + this.ctx.strokeStyle = `rgba(255, 255, 255, ${alpha * 0.15})`; + this.ctx.beginPath(); + this.ctx.moveTo(p1.x, p1.y); + this.ctx.lineTo(p2.x, p2.y); + this.ctx.stroke(); + } + } + } + } + + destroy() { + cancelAnimationFrame(this.animationId); + window.removeEventListener("mousemove", this.handleMouseMove); + } +} + +class Particle { + x: number; + y: number; + vx: number; + vy: number; + size: number; + + constructor(w: number, h: number, speed: number) { + this.x = Math.random() * w; + this.y = Math.random() * h; + this.vx = (Math.random() - 0.5) * speed; + this.vy = (Math.random() - 0.5) * speed; + this.size = Math.random() * 2 + 1; + } + + update(w: number, h: number) { + this.x += this.vx; + this.y += this.vy; + + // Bounce off walls + if (this.x < 0 || this.x > w) this.vx *= -1; + if (this.y < 0 || this.y > h) this.vy *= -1; + } + + draw(ctx: CanvasRenderingContext2D) { + ctx.fillStyle = "rgba(255, 255, 255, 0.4)"; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); + ctx.fill(); + } +} diff --git a/ui/src/lib/effects/SaturnEffect.ts b/ui/src/lib/effects/SaturnEffect.ts new file mode 100644 index 0000000..8a1c11f --- /dev/null +++ b/ui/src/lib/effects/SaturnEffect.ts @@ -0,0 +1,194 @@ +// Optimized Saturn Effect for low-end hardware +// Uses TypedArrays for memory efficiency and reduced particle density + +export class SaturnEffect { + private canvas: HTMLCanvasElement; + private ctx: CanvasRenderingContext2D; + private width: number = 0; + private height: number = 0; + + // Data-oriented design for performance + // xyz: Float32Array where [i*3, i*3+1, i*3+2] corresponds to x, y, z + private xyz: Float32Array | null = null; + // types: Uint8Array where 0 = planet, 1 = ring + private types: Uint8Array | null = null; + private count: number = 0; + + private animationId: number = 0; + private angle: number = 0; + private scaleFactor: number = 1; + + constructor(canvas: HTMLCanvasElement) { + this.canvas = canvas; + this.ctx = canvas.getContext('2d', { + alpha: true, + desynchronized: false // default is usually fine, 'desynchronized' can help latency but might flicker + })!; + + // Initial resize will set up everything + this.resize(window.innerWidth, window.innerHeight); + this.initParticles(); + + this.animate = this.animate.bind(this); + this.animate(); + } + + resize(width: number, height: number) { + const dpr = window.devicePixelRatio || 1; + this.width = width; + this.height = height; + + this.canvas.width = width * dpr; + this.canvas.height = height * dpr; + this.canvas.style.width = `${width}px`; + this.canvas.style.height = `${height}px`; + + this.ctx.scale(dpr, dpr); + + // Dynamic scaling based on screen size + const minDim = Math.min(width, height); + this.scaleFactor = minDim * 0.45; + } + + initParticles() { + // Significantly reduced particle count for CPU optimization + // Planet: 1800 -> 1000 + // Rings: 5000 -> 2500 + // Total approx 3500 vs 6800 previously (approx 50% reduction) + const planetCount = 1000; + const ringCount = 2500; + this.count = planetCount + ringCount; + + // Use TypedArrays for better memory locality + this.xyz = new Float32Array(this.count * 3); + this.types = new Uint8Array(this.count); + + let idx = 0; + + // 1. Planet + for (let i = 0; i < planetCount; i++) { + const theta = Math.random() * Math.PI * 2; + const phi = Math.acos((Math.random() * 2) - 1); + const r = 1.0; + + // x, y, z + this.xyz[idx * 3] = r * Math.sin(phi) * Math.cos(theta); + this.xyz[idx * 3 + 1] = r * Math.sin(phi) * Math.sin(theta); + this.xyz[idx * 3 + 2] = r * Math.cos(phi); + + this.types[idx] = 0; // 0 for planet + idx++; + } + + // 2. Rings + const ringInner = 1.4; + const ringOuter = 2.3; + + for (let i = 0; i < ringCount; i++) { + const angle = Math.random() * Math.PI * 2; + const dist = Math.sqrt(Math.random() * (ringOuter*ringOuter - ringInner*ringInner) + ringInner*ringInner); + + // x, y, z + this.xyz[idx * 3] = dist * Math.cos(angle); + this.xyz[idx * 3 + 1] = (Math.random() - 0.5) * 0.05; + this.xyz[idx * 3 + 2] = dist * Math.sin(angle); + + this.types[idx] = 1; // 1 for ring + idx++; + } + } + + animate() { + this.ctx.clearRect(0, 0, this.width, this.height); + + // Normal blending + this.ctx.globalCompositeOperation = 'source-over'; + + // Slower rotation (from 0.0015 to 0.0005) + this.angle += 0.0005; + + const cx = this.width * 0.6; + const cy = this.height * 0.5; + + // Pre-calculate rotation matrices + const rotationY = this.angle; + const rotationX = 0.4; + const rotationZ = 0.15; + + const sinY = Math.sin(rotationY); + const cosY = Math.cos(rotationY); + const sinX = Math.sin(rotationX); + const cosX = Math.cos(rotationX); + const sinZ = Math.sin(rotationZ); + const cosZ = Math.cos(rotationZ); + + const fov = 1500; + const scaleFactor = this.scaleFactor; + + if (!this.xyz || !this.types) return; + + for (let i = 0; i < this.count; i++) { + const x = this.xyz[i * 3]; + const y = this.xyz[i * 3 + 1]; + const z = this.xyz[i * 3 + 2]; + + // Apply Scale + const px = x * scaleFactor; + const py = y * scaleFactor; + const pz = z * scaleFactor; + + // 1. Rotate Y + const x1 = px * cosY - pz * sinY; + const z1 = pz * cosY + px * sinY; + // y1 = py + + // 2. Rotate X + const y2 = py * cosX - z1 * sinX; + const z2 = z1 * cosX + py * sinX; + // x2 = x1 + + // 3. Rotate Z + const x3 = x1 * cosZ - y2 * sinZ; + const y3 = y2 * cosZ + x1 * sinZ; + const z3 = z2; + + const scale = fov / (fov + z3); + + if (z3 > -fov) { + const x2d = cx + x3 * scale; + const y2d = cy + y3 * scale; + + // Size calculation - slightly larger dots to compensate for lower count + // Previously Planet 2.0 -> 2.4, Ring 1.3 -> 1.5 + const type = this.types[i]; + const sizeBase = type === 0 ? 2.4 : 1.5; + const size = sizeBase * scale; + + // Opacity + let alpha = (scale * scale * scale); + if (alpha > 1) alpha = 1; + if (alpha < 0.15) continue; // Skip very faint particles for performance + + // Optimization: Planet color vs Ring color + if (type === 0) { + // Planet: Warn White + this.ctx.fillStyle = `rgba(255, 240, 220, ${alpha})`; + } else { + // Ring: Cool White + this.ctx.fillStyle = `rgba(220, 240, 255, ${alpha})`; + } + + // Render as squares (fillRect) instead of circles (arc) + // This is significantly faster for software rendering and reduces GPU usage. + this.ctx.fillRect(x2d, y2d, size, size); + } + } + + this.animationId = requestAnimationFrame(this.animate); + } + + destroy() { + cancelAnimationFrame(this.animationId); + } +} + diff --git a/ui/src/stores/settings.svelte.ts b/ui/src/stores/settings.svelte.ts index 397b9a6..c59bf3c 100644 --- a/ui/src/stores/settings.svelte.ts +++ b/ui/src/stores/settings.svelte.ts @@ -10,6 +10,9 @@ export class SettingsState { width: 854, height: 480, download_threads: 32, + enable_gpu_acceleration: false, + enable_visual_effects: true, + active_effect: "constellation", }); javaInstallations = $state<JavaInstallation[]>([]); isDetectingJava = $state(false); diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts index 933aab5..fa48075 100644 --- a/ui/src/types/index.ts +++ b/ui/src/types/index.ts @@ -30,6 +30,10 @@ export interface LauncherConfig { width: number; height: number; download_threads: number; + custom_background_path?: string; + enable_gpu_acceleration: boolean; + enable_visual_effects: boolean; + active_effect: string; } export interface JavaInstallation { |