chore: add nice avatar animation for donators

This commit is contained in:
HorizonCode 2025-07-04 16:09:44 +02:00
parent 804378e404
commit 66d0fd3dbc
2 changed files with 157 additions and 38 deletions

View File

@ -0,0 +1,106 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { Heart } from 'lucide-svelte';
interface Sparkle {
id: string;
x: string;
y: string;
color: string;
delay: number;
scale: number;
}
let {
className,
colors = {
first: '#9E7AFF',
second: '#FE8BBB',
},
count = 10,
}: {
className: string;
colors?: { first: string; second: string };
count?: number;
} = $props();
// Helper to check if two sparkles are too close
function isTooClose(x1: number, y1: number, x2: number, y2: number, minDist: number) {
const dx = x1 - x2;
const dy = y1 - y2;
return Math.sqrt(dx * dx + dy * dy) < minDist;
}
// Generate sparkles with minimum distance between them
let sparkles: Sparkle[] = [];
const minDistance = 25; // in percentage points (adjust as needed)
let attempts = 0;
while (sparkles.length < count && attempts < count * 20) {
attempts++;
let starX = Math.random() * 100;
let starY = Math.random() * 100;
let tooClose = sparkles.some((s) =>
isTooClose(Number.parseFloat(s.x), Number.parseFloat(s.y), starX, starY, minDistance)
);
if (tooClose) continue;
let color = Math.random() > 0.5 ? colors.first : colors.second;
let delay = Math.random() * 3;
let scale = Math.random() * 1 + 0.3;
let id = `${starX}-${starY}-${Date.now()}-${Math.random()}`;
sparkles.push({
id,
x: `${starX}%`,
y: `${starY}%`,
color,
delay,
scale,
});
}
</script>
<div class={cn('relative', className)}>
<span class={cn('relative inline-block', className)}>
{#each sparkles as item (item.id)}
<div
class="sparkle pointer-events-none absolute z-20 top-0 left-0"
id={item.id}
style="left: {item.x}; top: {item.y}; color: {item.color}; animation-delay: {item.delay}s; transform: scale({item.scale});"
>
<!-- <svg width="13" height="13" viewBox="0 0 21 21">
<path
d="M9.82531 0.843845C10.0553 0.215178 10.9446 0.215178 11.1746 0.843845L11.8618 2.72026C12.4006 4.19229 12.3916 6.39157 13.5 7.5C14.6084 8.60843 16.8077 8.59935 18.2797 9.13822L20.1561 9.82534C20.7858 10.0553 20.7858 10.9447 20.1561 11.1747L18.2797 11.8618C16.8077 12.4007 14.6084 12.3916 13.5 13.5C12.3916 14.6084 12.4006 16.8077 11.8618 18.2798L11.1746 20.1562C10.9446 20.7858 10.0553 20.7858 9.82531 20.1562L9.13819 18.2798C8.59932 16.8077 8.60843 14.6084 7.5 13.5C6.39157 12.3916 4.19225 12.4007 2.72023 11.8618L0.843814 11.1747C0.215148 10.9447 0.215148 10.0553 0.843814 9.82534L2.72023 9.13822C4.19225 8.59935 6.39157 8.60843 7.5 7.5C8.60843 6.39157 8.59932 4.19229 9.13819 2.72026L9.82531 0.843845Z"
fill={item.color}
/>
</svg> -->
<Heart fill={item.color} size={14} />
</div>
{/each}
</span>
</div>
<style lang="postcss">
.sparkle {
animation: sparkle-fade 1.2s linear infinite;
opacity: 0;
}
@keyframes sparkle-fade {
0% {
opacity: 0;
transform: scale(0) rotate(-50deg);
}
50% {
opacity: 1;
}
80% {
opacity: 1;
transform: scale(1) rotate(0deg);
}
100% {
opacity: 0;
transform: scale(0) rotate(50deg);
}
}
</style>

View File

@ -89,6 +89,8 @@
} from '@/osuUtil';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { Heart } from 'radix-icons-svelte';
import { ezppfarm } from '@/api/ezpp';
import Hearts from '@/components/ui/effects/Hearts.svelte';
let selectedTab = $state('home');
let progress = $state(-1);
@ -326,11 +328,12 @@
description: 'Failed to launch.',
});
launching.set(false);
}
setTimeout(() => {
launching.set(false);
}, 5000);
if ($currentUser) {
const userInfo = await ezppfarm.getUserInfo($currentUser.id);
if (userInfo) currentUserInfo.set(userInfo.player);
}
}
};
</script>
@ -352,30 +355,34 @@
<div class="grid grid-cols-[0.41fr_1fr] mt-[50px] h-[calc(100vh-50px)]">
<div class="w-full h-full border-r border-theme-800/90 flex flex-col gap-6 py-3">
<div class="flex flex-col items-center gap-2 border-b pb-6">
<div class="size-20 relative">
{#if $currentUser?.donor}
<Hearts className="top-0 left-0 h-[70px] w-[60px] absolute"></Hearts>
{/if}
<Avatar.Root class="w-20 h-20 border-2 border-theme-800">
<Avatar.Image src="https://a.ez-pp.farm/{$currentUser?.id ?? 0}" />
<Avatar.Fallback class="bg-theme-900">
<LoaderCircle class="animate-spin" size={32} />
</Avatar.Fallback>
</Avatar.Root>
</div>
<span class="font-semibold text-2xl text-theme-50">{$currentUser?.name ?? 'Guest'}</span>
<div class="flex flex-row gap-2">
<!-- <Badge variant="destructive">Owner</Badge> -->
{#if !$currentUser}
<Button
variant="outline"
size="sm"
class="bg-theme-900 hover:bg-theme-700/40 border-theme-800"
class="bg-theme-900 hover:bg-theme-700/40 border-theme-800 text-xs"
onclick={() => currentView.set(Login)}
>
<LogIn size={16} />
<LogIn class="!size-4" />
Login
</Button>
{:else}
<Button
variant="outline"
size="sm"
class="bg-theme-900 hover:bg-theme-700/40 border-theme-800"
class="bg-theme-900 hover:bg-theme-700/40 border-theme-800 text-xs"
onclick={async () => {
$userAuth.value('username').del();
$userAuth.value('password').del();
@ -387,7 +394,7 @@
currentUserInfo.set(undefined);
}}
>
<LogOut size={16} />
<LogOut class="!size-4" />
Logout
</Button>
{/if}
@ -494,9 +501,9 @@
</div>
</div>
<div class="grid grid-cols-[1fr_auto] border-t border-theme-800 pt-2">
<span class="text-sm text-muted-foreground font-semibold">Accuracy</span>
<span class="text-xs text-muted-foreground font-semibold">Accuracy</span>
<div
class="flex items-center flex-row-reverse h-full text-sm text-end font-semibold text-theme-50"
class="flex items-center flex-row-reverse h-full text-xs text-end font-semibold text-theme-50"
>
{#if $currentUserInfo}
<div in:fade>
@ -513,9 +520,9 @@
{/if}
</div>
<span class="text-sm text-muted-foreground font-semibold">Play Count</span>
<span class="text-xs text-muted-foreground font-semibold">Play Count</span>
<div
class="flex items-center flex-row-reverse h-full text-sm text-end font-semibold text-theme-50"
class="flex items-center flex-row-reverse h-full text-xs text-end font-semibold text-theme-50"
>
{#if $currentUserInfo}
<div in:fade>
@ -529,9 +536,9 @@
{/if}
</div>
<span class="text-sm text-muted-foreground font-semibold">Play Time</span>
<span class="text-xs text-muted-foreground font-semibold">Play Time</span>
<div
class="flex items-center flex-row-reverse h-full text-sm text-end font-semibold text-theme-50"
class="flex items-center flex-row-reverse h-full text-xs text-end font-semibold text-theme-50"
>
{#if $currentUserInfo}
<div in:fade>
@ -612,7 +619,7 @@
{/if}
</div>
</div>
<div class="text-muted-foreground text-sm">Beatmap Sets imported</div>
<div class="text-muted-foreground text-[12px] leading-4">Beatmap Sets imported</div>
</div>
<div
class="bg-theme-800/90 border border-theme-700/90 rounded-lg px-2 py-4 w-full flex flex-col gap-1 items-center justify-center"
@ -636,7 +643,7 @@
{/if}
</div>
</div>
<div class="text-muted-foreground text-sm">Skins</div>
<div class="text-muted-foreground text-[12px] leading-4">Skins</div>
</div>
<!-- <div
class="bg-theme-800/90 border border-theme-700/90 rounded-lg px-2 py-4 w-full flex flex-col gap-1 items-center justify-center"
@ -714,7 +721,7 @@
{/if}
</div>
</div>
<div class="text-muted-foreground text-sm">Ping to Server</div>
<div class="text-muted-foreground text-[12px] leading-4">Ping to Server</div>
</div>
</div>
<Button
@ -739,33 +746,33 @@
<span class="font-semibold text-muted-foreground text-sm">Client Info</span>
</div>
<div class="grid grid-cols-[1fr_auto] gap-1 mt-2 border-t border-theme-800 pt-2 px-2 pb-0">
<span class="text-sm text-muted-foreground font-semibold">osu! Release Stream</span>
<span class="text-sm font-semibold text-end text-theme-50">
<Badge>
<span class="text-xs text-muted-foreground font-semibold">osu! Release Stream</span>
<span class="text-xs font-semibold text-end text-theme-50">
<Badge class="text-[0.65rem] py-0.5 px-2 leading-none">
{#if $osuStream}
{releaseStreamToReadable($osuStream)}
{:else}
<LoaderCircle class="animate-spin" size={17} />
<LoaderCircle class="animate-spin" size={12} />
{/if}
</Badge>
</span>
<span class="text-sm text-muted-foreground font-semibold">osu! Version</span>
<span class="text-sm font-semibold text-end text-theme-50">
<Badge>
<span class="text-xs text-muted-foreground font-semibold">osu! Version</span>
<span class="text-xs font-semibold text-end text-theme-50">
<Badge class="text-[0.65rem] py-0.5 px-2 leading-none">
{#if $osuBuild}
{$osuBuild}
{:else}
<LoaderCircle class="animate-spin" size={17} />
<LoaderCircle class="animate-spin" size={12} />
{/if}
</Badge>
</span>
<span class="text-sm text-muted-foreground font-semibold">Skin</span>
<span class="text-sm font-semibold text-end text-theme-50">
<Badge>
<span class="text-xs text-muted-foreground font-semibold">Skin</span>
<span class="text-xs font-semibold text-end text-theme-50">
<Badge class="text-[0.65rem] py-0.5 px-2 leading-none">
{#if $currentSkin}
{$currentSkin}
{:else}
<LoaderCircle class="animate-spin" size={17} />
<LoaderCircle class="animate-spin" size={12} />
{/if}
</Badge>
</span>
@ -848,7 +855,6 @@
reduceAnimations.set(e);
$userSettings.save();
}}
disabled={!$customCursor}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
</div>
@ -876,7 +882,14 @@
</div>
</div>
</div>
<div class="mt-auto mx-auto flex flex-row items-center gap-2">
<div
class="mt-auto mx-auto flex flex-row items-center gap-2"
in:scale={{
duration: $reduceAnimations ? 0 : 400,
delay: $reduceAnimations ? 0 : 50,
start: 0.97,
}}
>
<Button
variant="link"
class="font-semibold font-mono text-sm text-theme-100/70"