diff --git a/app/components/Achievements/BottomScreen.vue b/app/components/Achievements/BottomScreen.vue
new file mode 100644
index 0000000..00ba1bd
--- /dev/null
+++ b/app/components/Achievements/BottomScreen.vue
@@ -0,0 +1,91 @@
+
diff --git a/app/components/Common/AchievementNotification.vue b/app/components/Achievements/Notification.vue
similarity index 94%
rename from app/components/Common/AchievementNotification.vue
rename to app/components/Achievements/Notification.vue
index 978abda..0e4e1f0 100644
--- a/app/components/Common/AchievementNotification.vue
+++ b/app/components/Achievements/Notification.vue
@@ -12,7 +12,7 @@ const x = ref(LOGICAL_WIDTH);
const isAnimating = ref(false);
const PADDING = 4;
-const LOGO_SIZE = assets.images.common.achievementNotificationLogo.rect.height;
+const LOGO_SIZE = assets.images.achievements.notificationLogo.rect.height;
const NOTIF_WIDTH = 120;
const NOTIF_HEIGHT = LOGO_SIZE + PADDING * 2;
const NOTIF_Y = 3;
@@ -66,7 +66,7 @@ const processQueue = () => {
onRender((ctx) => {
if (!currentAchievement.value) return;
- const logo = assets.images.common.achievementNotificationLogo;
+ const logo = assets.images.achievements.notificationLogo;
// shadow
ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
diff --git a/app/components/Achievements/TopScreen.vue b/app/components/Achievements/TopScreen.vue
new file mode 100644
index 0000000..a61e728
--- /dev/null
+++ b/app/components/Achievements/TopScreen.vue
@@ -0,0 +1,123 @@
+
diff --git a/app/components/Home/BottomScreen/Buttons.vue b/app/components/Home/BottomScreen/Buttons.vue
index 3ff28fd..161ea30 100644
--- a/app/components/Home/BottomScreen/Buttons.vue
+++ b/app/components/Home/BottomScreen/Buttons.vue
@@ -15,12 +15,16 @@ const { selected, selectorPosition } = useButtonNavigation({
theme: [0, 167, 31, 26],
settings: [112, 167, 31, 26],
- alarm: [225, 167, 31, 26],
+ achievements: [225, 167, 31, 26],
},
initialButton: "projects",
onButtonClick: (button) => {
- if (button === "theme" || button === "alarm")
- throw new Error(`Not implemented: ${button}`);
+ if (button === "theme") throw new Error(`Not implemented: ${button}`);
+
+ if (button === "achievements") {
+ store.animateOutro("achievements");
+ return;
+ }
store.animateOutro(button);
},
@@ -47,9 +51,9 @@ const { selected, selectorPosition } = useButtonNavigation({
settings: {
left: "theme",
up: "last",
- right: "alarm",
+ right: "achievements",
},
- alarm: {
+ achievements: {
left: "settings",
},
},
@@ -168,9 +172,9 @@ onRender((ctx) => {
/>
diff --git a/app/pages/index.vue b/app/pages/index.vue
index 59d8fc5..90dc47d 100644
--- a/app/pages/index.vue
+++ b/app/pages/index.vue
@@ -76,8 +76,9 @@ useKeyUp((key) => {
+
-
+
diff --git a/app/stores/achievementsScreen.ts b/app/stores/achievementsScreen.ts
new file mode 100644
index 0000000..5cae488
--- /dev/null
+++ b/app/stores/achievementsScreen.ts
@@ -0,0 +1,124 @@
+import gsap from "gsap";
+
+export const useAchievementsScreen = defineStore("achievementsScreen", {
+ state: () => ({
+ intro: {
+ stage1Opacity: 0,
+ stage2Opacity: 0,
+ itemOffsets: {} as Record,
+ progressBar: 0,
+ },
+
+ outro: {
+ stage1Opacity: 1,
+ stage2Opacity: 1,
+ stage3Opacity: 1,
+ },
+
+ isIntro: true,
+ isOutro: false,
+ }),
+
+ actions: {
+ animateIntro() {
+ this.isIntro = true;
+ this.isOutro = false;
+
+ const itemCount = ACHIEVEMENTS.length;
+
+ for (let i = 0; i < itemCount; i++) {
+ this.intro.itemOffsets[i] = -ACHIEVEMENTS_LINE_HEIGHT;
+ }
+
+ const tl = gsap.timeline({
+ onComplete: () => {
+ this.isIntro = false;
+ },
+ });
+
+ tl.fromTo(
+ this.intro,
+ { stage1Opacity: 0 },
+ {
+ stage1Opacity: 1,
+ duration: 0.3,
+ ease: "none",
+ },
+ )
+ .fromTo(
+ this.intro,
+ { stage2Opacity: 0 },
+ {
+ stage2Opacity: 1,
+ duration: 0.5,
+ ease: "none",
+ },
+ 0.5,
+ )
+ .fromTo(
+ this.intro,
+ { progressBar: 0 },
+ {
+ progressBar: 1,
+ duration: 1.5,
+ ease: "power2.out",
+ },
+ 0.25,
+ );
+
+ for (let i = 0; i < itemCount; i++) {
+ tl.to(
+ this.intro.itemOffsets,
+ {
+ [i]: 0,
+ duration: 0.4,
+ ease: "power2.out",
+ },
+ 0.75 + i * 0.05,
+ );
+ }
+ },
+
+ animateOutro() {
+ this.isIntro = false;
+ this.isOutro = true;
+
+ gsap
+ .timeline()
+ .fromTo(
+ this.outro,
+ { stage2Opacity: 1 },
+ {
+ stage2Opacity: 0,
+ duration: 0.3,
+ },
+ )
+ .fromTo(
+ this.outro,
+ { stage3Opacity: 1 },
+ {
+ stage3Opacity: 0,
+ duration: 0.3,
+ },
+ "-=0.15",
+ )
+ .fromTo(
+ this.outro,
+ { stage1Opacity: 1 },
+ {
+ stage1Opacity: 0,
+ duration: 0.3,
+ ease: "none",
+ },
+ )
+ .call(() => {
+ const app = useAppStore();
+ app.navigateTo("home");
+ });
+ },
+
+ getItemOffset(index: number): number {
+ return this.intro.itemOffsets[index] ?? 0;
+ },
+ },
+});
diff --git a/app/types/app.d.ts b/app/types/app.d.ts
index eb57fc5..9bacce1 100644
--- a/app/types/app.d.ts
+++ b/app/types/app.d.ts
@@ -1 +1,7 @@
-type AppScreen = "home" | "contact" | "projects" | "settings" | "gallery";
+type AppScreen =
+ | "home"
+ | "contact"
+ | "projects"
+ | "settings"
+ | "gallery"
+ | "achievements";
diff --git a/app/utils/achievements.ts b/app/utils/achievements.ts
new file mode 100644
index 0000000..3917e5a
--- /dev/null
+++ b/app/utils/achievements.ts
@@ -0,0 +1,30 @@
+export const ACHIEVEMENTS_LINE_HEIGHT = 14;
+export const ACHIEVEMENTS_HEADER_Y = 20;
+export const ACHIEVEMENTS_LIST_START_Y = ACHIEVEMENTS_HEADER_Y + 55;
+export const ACHIEVEMENTS_BOTTOM_START_Y = 10;
+export const ACHIEVEMENTS_TOP_SCREEN_COUNT = Math.floor(
+ (LOGICAL_HEIGHT - ACHIEVEMENTS_LIST_START_Y) / ACHIEVEMENTS_LINE_HEIGHT,
+);
+
+export const CHECKBOX_SIZE = 7;
+export const CHECKBOX_TEXT_GAP = 5;
+export const ACHIEVEMENTS_X = 55;
+
+export const drawCheckbox = (
+ ctx: CanvasRenderingContext2D,
+ x: number,
+ y: number,
+ checked: boolean,
+) => {
+ ctx.fillRect(x, y, CHECKBOX_SIZE, 1);
+ ctx.fillRect(x, y + CHECKBOX_SIZE - 1, CHECKBOX_SIZE, 1);
+ ctx.fillRect(x, y + 1, 1, CHECKBOX_SIZE - 2);
+ ctx.fillRect(x + CHECKBOX_SIZE - 1, y + 1, 1, CHECKBOX_SIZE - 2);
+
+ if (checked) {
+ for (let i = 2; i < CHECKBOX_SIZE - 2; i++) {
+ ctx.fillRect(x + i, y + i, 1, 1);
+ ctx.fillRect(x + CHECKBOX_SIZE - 1 - i, y + i, 1, 1);
+ }
+ }
+};
diff --git a/i18n/locales/en.json b/i18n/locales/en.json
index ecaf295..c78c02c 100644
--- a/i18n/locales/en.json
+++ b/i18n/locales/en.json
@@ -1,4 +1,7 @@
{
+ "achievementsScreen": {
+ "title": "Achievements"
+ },
"achievements": {
"boot": "Boot up the system",
"projects_visit": "Visit the projects\nsection",
diff --git a/public/nds/images/common/achievement-notification-logo.webp b/public/nds/images/achievements/notification-logo.webp
similarity index 100%
rename from public/nds/images/common/achievement-notification-logo.webp
rename to public/nds/images/achievements/notification-logo.webp
diff --git a/public/nds/images/achievements/quit.webp b/public/nds/images/achievements/quit.webp
new file mode 100644
index 0000000..0f2be67
Binary files /dev/null and b/public/nds/images/achievements/quit.webp differ
diff --git a/public/nds/images/home/bottom-screen/buttons/achievements.webp b/public/nds/images/home/bottom-screen/buttons/achievements.webp
new file mode 100644
index 0000000..1623aef
Binary files /dev/null and b/public/nds/images/home/bottom-screen/buttons/achievements.webp differ
diff --git a/public/nds/images/home/bottom-screen/buttons/alarm.webp b/public/nds/images/home/bottom-screen/buttons/alarm.webp
deleted file mode 100644
index cdea916..0000000
Binary files a/public/nds/images/home/bottom-screen/buttons/alarm.webp and /dev/null differ