diff --git a/public/images/contact/bottom-screen/background.webp b/public/images/contact/bottom-screen/background.webp new file mode 100644 index 0000000..868887f Binary files /dev/null and b/public/images/contact/bottom-screen/background.webp differ diff --git a/public/images/contact/bottom-screen/bottom-bar.webp b/public/images/contact/bottom-screen/bottom-bar.webp new file mode 100644 index 0000000..386613f Binary files /dev/null and b/public/images/contact/bottom-screen/bottom-bar.webp differ diff --git a/public/images/contact/bottom-screen/buttons.webp b/public/images/contact/bottom-screen/buttons.webp new file mode 100644 index 0000000..b0e8024 Binary files /dev/null and b/public/images/contact/bottom-screen/buttons.webp differ diff --git a/public/images/contact/bottom-screen/notification.webp b/public/images/contact/bottom-screen/notification.webp new file mode 100644 index 0000000..c04bf59 Binary files /dev/null and b/public/images/contact/bottom-screen/notification.webp differ diff --git a/public/images/contact/bottom-screen/ok-button.webp b/public/images/contact/bottom-screen/ok-button.webp new file mode 100644 index 0000000..5f438af Binary files /dev/null and b/public/images/contact/bottom-screen/ok-button.webp differ diff --git a/public/images/contact/bottom-screen/top-bar.webp b/public/images/contact/bottom-screen/top-bar.webp new file mode 100644 index 0000000..7ceca75 Binary files /dev/null and b/public/images/contact/bottom-screen/top-bar.webp differ diff --git a/public/images/contact/top-screen/background.webp b/public/images/contact/top-screen/background.webp new file mode 100644 index 0000000..1158c6b Binary files /dev/null and b/public/images/contact/top-screen/background.webp differ diff --git a/public/images/contact/top-screen/left-bar-things.webp b/public/images/contact/top-screen/left-bar-things.webp new file mode 100644 index 0000000..55e5a51 Binary files /dev/null and b/public/images/contact/top-screen/left-bar-things.webp differ diff --git a/public/images/contact/top-screen/left-bar.webp b/public/images/contact/top-screen/left-bar.webp new file mode 100644 index 0000000..a1722e6 Binary files /dev/null and b/public/images/contact/top-screen/left-bar.webp differ diff --git a/public/images/contact/top-screen/title.webp b/public/images/contact/top-screen/title.webp new file mode 100644 index 0000000..43e24bb Binary files /dev/null and b/public/images/contact/top-screen/title.webp differ diff --git a/src/screens/contact/bottom/index.ts b/src/screens/contact/bottom/index.ts new file mode 100644 index 0000000..678144d --- /dev/null +++ b/src/screens/contact/bottom/index.ts @@ -0,0 +1,131 @@ +import { ImageLoader } from "../../../utils/loadImages"; +import { ButtonNavigation } from "../../../utils/buttonNavigation"; +import { ButtonSelector } from "../../../utils/buttonSelector"; +import { HomeScreen } from "../../home"; +import type { ContactScreenContext } from "../index"; + +type ContactButton = "github" | "email" | "website" | "cv"; + +const ACTIONS = { + github: ["Open", "Github profile", "https://github.com/pihkaal"], + email: ["Copy", "Email", "hello@pihkaal.me"], + website: ["Copy", "Website link", "https://pihkaal.me"], + cv: ["Open", "CV", "https://pihkaal.me/cv"], +} as const satisfies Record< + ContactButton, + [action: "Copy" | "Open", verb: string, content: string] +>; + +export class ContactBottomScreen { + private images = new ImageLoader({ + homeBackground: "/images/home/bottom-screen/background.webp", + contactBackground: "/images/contact/bottom-screen/background.webp", + buttons: "/images/contact/bottom-screen/buttons.webp", + topBar: "/images/contact/bottom-screen/top-bar.webp", + bottomBar: "/images/contact/bottom-screen/bottom-bar.webp", + okButton: "/images/contact/bottom-screen/ok-button.webp", + }); + + private navigation: ButtonNavigation; + private selector = new ButtonSelector([26, 27, 202, 42]); + private context: ContactScreenContext; + + constructor(context: ContactScreenContext) { + this.context = context; + this.navigation = new ButtonNavigation({ + buttons: { + github: [26, 27, 202, 42], + email: [26, 59, 202, 42], + website: [26, 91, 202, 42], + cv: [26, 123, 202, 42], + }, + navigation: { + github: { + down: "email", + }, + email: { + up: "github", + down: "website", + }, + website: { + up: "email", + down: "cv", + }, + cv: { + up: "website", + }, + }, + initialButton: "github", + onButtonClick: (button) => { + this.actionateButton(button); + }, + }); + } + + private async actionateButton(button: ContactButton): Promise { + const [action, verb, content] = ACTIONS[button]; + if (action === "Copy") { + try { + await navigator.clipboard.writeText(content); + this.context.pushNotification(`${verb} copied to clipboard`); + } catch (error) { + console.error("Failed to copy to clipboard:", error); + } + } else { + window.open(content, "_blank"); + this.context.pushNotification(`${verb} opened`); + } + } + + render(ctx: CanvasRenderingContext2D): void { + if (!this.images.isReady) return; + + // backgrounds + ctx.drawImage(this.images.require("homeBackground"), 0, 0); + ctx.drawImage(this.images.require("contactBackground"), 0, 0); + + // top bar + ctx.drawImage(this.images.require("topBar"), 0, 0); + + // buttons + ctx.drawImage(this.images.require("buttons"), 31, 32); + + // selector + this.selector.render(ctx, this.navigation.getSelectorPosition()); + + // bottom bar + ctx.save(); + ctx.translate(0, 192 - 24); + + ctx.drawImage(this.images.require("bottomBar"), 0, 0); + + ctx.drawImage(this.images.require("okButton"), 144, 4); + ctx.font = "10px NDS10"; + ctx.fillStyle = "#000000"; + const okLabel = ACTIONS[this.navigation.getSelectedButton()][0]; + ctx.fillText(okLabel, 144 + 35, 4 + 13); + + ctx.restore(); + } + + handleTouch(x: number, y: number): void { + // Handle Quit button + if (x >= 31 && x <= 111 && y >= 172 && y <= 190) { + this.context.navigate(new HomeScreen(this.context)); + return; + } + + // Handle OK button + if (x >= 144 && x <= 224 && y >= 172 && y <= 190) { + this.actionateButton(this.navigation.getSelectedButton()); + return; + } + + // Handle button navigation + this.navigation.handleTouch(x, y); + } + + destroy(): void { + this.navigation.destroy(); + } +} diff --git a/src/screens/contact/index.ts b/src/screens/contact/index.ts new file mode 100644 index 0000000..ed0ae61 --- /dev/null +++ b/src/screens/contact/index.ts @@ -0,0 +1,39 @@ +import type { Screen, ScreenContext } from "../../screen"; +import { ContactTopScreen } from "./top"; +import { ContactBottomScreen } from "./bottom"; + +export interface ContactScreenContext extends ScreenContext { + pushNotification: (message: string) => void; +} + +export class ContactScreen implements Screen { + private topScreen = new ContactTopScreen(); + private bottomScreen: ContactBottomScreen; + + constructor(context: ScreenContext) { + const contactContext: ContactScreenContext = { + ...context, + pushNotification: (message: string) => { + this.topScreen.pushNotification(message); + }, + }; + + this.bottomScreen = new ContactBottomScreen(contactContext); + } + + renderTop(ctx: CanvasRenderingContext2D) { + this.topScreen.render(ctx); + } + + renderBottom(ctx: CanvasRenderingContext2D) { + this.bottomScreen.render(ctx); + } + + handleTouch(x: number, y: number): void { + this.bottomScreen.handleTouch(x, y); + } + + destroy(): void { + this.bottomScreen.destroy(); + } +} diff --git a/src/screens/contact/top/index.ts b/src/screens/contact/top/index.ts new file mode 100644 index 0000000..666ce1f --- /dev/null +++ b/src/screens/contact/top/index.ts @@ -0,0 +1,88 @@ +import { ImageLoader } from "../../../utils/loadImages"; + +export class ContactTopScreen { + private images = new ImageLoader({ + homeBackground: "/images/home/top-screen/background.webp", + contactBackground: "/images/contact/top-screen/background.webp", + leftBar: "/images/contact/top-screen/left-bar.webp", + leftBarThings: "/images/contact/top-screen/left-bar-things.webp", + notification: "/images/contact/bottom-screen/notification.webp", + title: "/images/contact/top-screen/title.webp", + }); + + private notifications: string[] = []; + private notificationsYOffset = 0; + + public pushNotification(message: string): void { + this.notifications.push(message); + } + + render(ctx: CanvasRenderingContext2D): void { + if (!this.images.isReady) return; + + // backgrounds + // NOTE: animate home background + ctx.drawImage(this.images.require("homeBackground"), 0, 0); + ctx.drawImage(this.images.require("contactBackground"), 0, 0); + + // left bar + ctx.drawImage(this.images.require("leftBar"), 0, 0); + ctx.drawImage(this.images.require("leftBarThings"), 0, 0); + + ctx.font = "10px NDS10"; + + // notifications + for (let i = this.notifications.length - 1; i >= 0; i--) { + const index = this.notifications.length - 1 - i; + const y = 169 - 24 * index + this.notificationsYOffset; + if (y < -24) break; + + ctx.drawImage(this.images.require("notification"), 21, y); + + const content = this.notifications[i]!; + ctx.fillStyle = content.includes("opened") ? "#00fbba" : "#e3f300"; + ctx.fillText(this.notifications[i]!, 27, y + 15); + } + + // title + ctx.drawImage( + this.images.require("title"), + 21, + 169 - 24 * this.notifications.length + this.notificationsYOffset, + ); + + // notifications count (left bar) + const MAX = 36; + const MAX_VISIBLE = 8; + + let visibleNotifications = Math.min(this.notifications.length, MAX_VISIBLE); + const extraActive = + this.notificationsYOffset > 0 && this.notifications.length > MAX_VISIBLE; + + if (extraActive) { + visibleNotifications += 1; + } + + ctx.fillStyle = "#415969"; + for (let i = 0; i < visibleNotifications; i++) { + ctx.fillRect(3, 161 - i * 4, 12, 2); + } + + ctx.fillStyle = "#b2c3db"; + const startY = 161 - visibleNotifications * 4; + const top = MAX - MAX_VISIBLE - (extraActive ? 1 : 0); + for ( + let i = 0; + i < this.notifications.length - visibleNotifications && i < top; + i++ + ) { + if (i === top - 1) { + ctx.fillRect(7, startY - i * 4, 4, 2); + } else if (i === top - 2) { + ctx.fillRect(5, startY - i * 4, 8, 2); + } else { + ctx.fillRect(3, startY - i * 4, 12, 2); + } + } + } +}