mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
feat: achievement animation
This commit is contained in:
parent
7cddcd8147
commit
7e2d9316f3
3 changed files with 105 additions and 30 deletions
|
@ -107,10 +107,10 @@ export class WindowManager {
|
||||||
focusable: false,
|
focusable: false,
|
||||||
skipTaskbar: true,
|
skipTaskbar: true,
|
||||||
frame: false,
|
frame: false,
|
||||||
width: 240,
|
width: 350,
|
||||||
height: 60,
|
height: 104,
|
||||||
x: 25,
|
x: 0,
|
||||||
y: 25,
|
y: 0,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, "../preload/index.mjs"),
|
preload: path.join(__dirname, "../preload/index.mjs"),
|
||||||
sandbox: false,
|
sandbox: false,
|
||||||
|
|
44
src/renderer/src/pages/achievement/achievement.css.ts
Normal file
44
src/renderer/src/pages/achievement/achievement.css.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { recipe } from "@vanilla-extract/recipes";
|
||||||
|
import { vars } from "../../theme.css";
|
||||||
|
import { keyframes, style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
|
const animationIn = keyframes({
|
||||||
|
"0%": { transform: `translateY(-240px)` },
|
||||||
|
"100%": { transform: "translateY(0)" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const animationOut = keyframes({
|
||||||
|
"0%": { transform: `translateY(0)` },
|
||||||
|
"100%": { transform: "translateY(-240px)" },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const container = recipe({
|
||||||
|
base: {
|
||||||
|
marginTop: "24px",
|
||||||
|
marginLeft: "24px",
|
||||||
|
animationDuration: "1.0s",
|
||||||
|
height: "60px",
|
||||||
|
display: "flex",
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
closing: {
|
||||||
|
true: {
|
||||||
|
animationName: animationOut,
|
||||||
|
transform: "translateY(-240px)",
|
||||||
|
},
|
||||||
|
false: {
|
||||||
|
animationName: animationIn,
|
||||||
|
transform: "translateY(0)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const content = style({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: "8px",
|
||||||
|
alignItems: "center",
|
||||||
|
background: vars.color.background,
|
||||||
|
paddingRight: "8px",
|
||||||
|
});
|
|
@ -1,7 +1,7 @@
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import achievementSound from "@renderer/assets/audio/achievement.wav";
|
import achievementSound from "@renderer/assets/audio/achievement.wav";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { vars } from "@renderer/theme.css";
|
import * as styles from "./achievement.css";
|
||||||
|
|
||||||
interface AchievementInfo {
|
interface AchievementInfo {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
@ -11,8 +11,16 @@ interface AchievementInfo {
|
||||||
export function Achievement() {
|
export function Achievement() {
|
||||||
const { t } = useTranslation("achievement");
|
const { t } = useTranslation("achievement");
|
||||||
|
|
||||||
|
const [isClosing, setIsClosing] = useState(false);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
const [achievements, setAchievements] = useState<AchievementInfo[]>([]);
|
const [achievements, setAchievements] = useState<AchievementInfo[]>([]);
|
||||||
|
const [currentAchievement, setCurrentAchievement] =
|
||||||
|
useState<AchievementInfo | null>(null);
|
||||||
|
|
||||||
const achievementAnimation = useRef(-1);
|
const achievementAnimation = useRef(-1);
|
||||||
|
const closingAnimation = useRef(-1);
|
||||||
|
const visibleAnimation = useRef(-1);
|
||||||
|
|
||||||
const audio = useMemo(() => {
|
const audio = useMemo(() => {
|
||||||
const audio = new Audio(achievementSound);
|
const audio = new Audio(achievementSound);
|
||||||
|
@ -24,11 +32,9 @@ export function Achievement() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = window.electron.onAchievementUnlocked(
|
const unsubscribe = window.electron.onAchievementUnlocked(
|
||||||
(_object, _shop, achievements) => {
|
(_object, _shop, achievements) => {
|
||||||
if (!achievements) return;
|
if (!achievements || !achievements.length) return;
|
||||||
|
|
||||||
if (achievements.length) {
|
setAchievements((ach) => ach.concat(achievements));
|
||||||
setAchievements((ach) => ach.concat(achievements));
|
|
||||||
}
|
|
||||||
|
|
||||||
audio.play();
|
audio.play();
|
||||||
}
|
}
|
||||||
|
@ -41,12 +47,37 @@ export function Achievement() {
|
||||||
|
|
||||||
const hasAchievementsPending = achievements.length > 0;
|
const hasAchievementsPending = achievements.length > 0;
|
||||||
|
|
||||||
|
const startAnimateClosing = useCallback(() => {
|
||||||
|
cancelAnimationFrame(closingAnimation.current);
|
||||||
|
cancelAnimationFrame(visibleAnimation.current);
|
||||||
|
cancelAnimationFrame(achievementAnimation.current);
|
||||||
|
|
||||||
|
setIsClosing(true);
|
||||||
|
|
||||||
|
const zero = performance.now();
|
||||||
|
closingAnimation.current = requestAnimationFrame(
|
||||||
|
function animateClosing(time) {
|
||||||
|
if (time - zero <= 1000) {
|
||||||
|
closingAnimation.current = requestAnimationFrame(animateClosing);
|
||||||
|
} else {
|
||||||
|
setIsVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasAchievementsPending) {
|
if (hasAchievementsPending) {
|
||||||
|
setIsClosing(false);
|
||||||
|
setIsVisible(true);
|
||||||
|
|
||||||
let zero = performance.now();
|
let zero = performance.now();
|
||||||
|
cancelAnimationFrame(closingAnimation.current);
|
||||||
|
cancelAnimationFrame(visibleAnimation.current);
|
||||||
|
cancelAnimationFrame(achievementAnimation.current);
|
||||||
achievementAnimation.current = requestAnimationFrame(
|
achievementAnimation.current = requestAnimationFrame(
|
||||||
function animateLock(time) {
|
function animateLock(time) {
|
||||||
if (time - zero > 3000) {
|
if (time - zero > 2500) {
|
||||||
zero = performance.now();
|
zero = performance.now();
|
||||||
setAchievements((ach) => ach.slice(1));
|
setAchievements((ach) => ach.slice(1));
|
||||||
}
|
}
|
||||||
|
@ -54,30 +85,30 @@ export function Achievement() {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
cancelAnimationFrame(achievementAnimation.current);
|
startAnimateClosing();
|
||||||
}
|
}
|
||||||
}, [hasAchievementsPending]);
|
}, [hasAchievementsPending]);
|
||||||
|
|
||||||
if (!hasAchievementsPending) return null;
|
useEffect(() => {
|
||||||
|
if (achievements.length) {
|
||||||
|
setCurrentAchievement(achievements[0]);
|
||||||
|
}
|
||||||
|
}, [achievements]);
|
||||||
|
|
||||||
|
if (!isVisible || !currentAchievement) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={styles.container({ closing: isClosing })}>
|
||||||
style={{
|
<div className={styles.content}>
|
||||||
display: "flex",
|
<img
|
||||||
flexDirection: "row",
|
src={currentAchievement.iconUrl}
|
||||||
gap: "8px",
|
alt={currentAchievement.displayName}
|
||||||
alignItems: "center",
|
style={{ flex: 1, width: "60px" }}
|
||||||
background: vars.color.background,
|
/>
|
||||||
}}
|
<div>
|
||||||
>
|
<p>{t("achievement_unlocked")}</p>
|
||||||
<img
|
<p>{currentAchievement.displayName}</p>
|
||||||
src={achievements[0].iconUrl}
|
</div>
|
||||||
alt={achievements[0].displayName}
|
|
||||||
style={{ width: 60, height: 60 }}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<p>{t("achievement_unlocked")}</p>
|
|
||||||
<p>{achievements[0].displayName}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue