diff --git a/README.md b/README.md index ffa9d0a..31254dd 100644 --- a/README.md +++ b/README.md @@ -124,4 +124,4 @@ Built with Next.js 16, React 19, TypeScript, Tailwind CSS 4, dnd-kit and Rechart ## Status -Personal project, used daily by its author. Not affiliated with any company or product of a similar name. There is no open-source license yet, so all rights are reserved; ask before reusing the code. +Personal project, used daily by its author. The coffee-cup animation frames in `public/animations/coffee-cup/` are third-party artwork kept for private, personal use only; do not redistribute them. Not affiliated with any company or product of a similar name. There is no open-source license yet, so all rights are reserved; ask before reusing the code. diff --git a/docs/screenshots/animations.png b/docs/screenshots/animations.png index 181ea11..7fe3381 100644 Binary files a/docs/screenshots/animations.png and b/docs/screenshots/animations.png differ diff --git a/public/animations/coffee-cup/01.png b/public/animations/coffee-cup/01.png new file mode 100644 index 0000000..41fb5e6 Binary files /dev/null and b/public/animations/coffee-cup/01.png differ diff --git a/public/animations/coffee-cup/02.png b/public/animations/coffee-cup/02.png new file mode 100644 index 0000000..08fe9db Binary files /dev/null and b/public/animations/coffee-cup/02.png differ diff --git a/public/animations/coffee-cup/03.png b/public/animations/coffee-cup/03.png new file mode 100644 index 0000000..2521d3b Binary files /dev/null and b/public/animations/coffee-cup/03.png differ diff --git a/public/animations/coffee-cup/04.png b/public/animations/coffee-cup/04.png new file mode 100644 index 0000000..64eeeb4 Binary files /dev/null and b/public/animations/coffee-cup/04.png differ diff --git a/public/animations/coffee-cup/05.png b/public/animations/coffee-cup/05.png new file mode 100644 index 0000000..78a99ec Binary files /dev/null and b/public/animations/coffee-cup/05.png differ diff --git a/public/animations/coffee-cup/06.png b/public/animations/coffee-cup/06.png new file mode 100644 index 0000000..1d19d43 Binary files /dev/null and b/public/animations/coffee-cup/06.png differ diff --git a/public/animations/coffee-cup/07.png b/public/animations/coffee-cup/07.png new file mode 100644 index 0000000..9bcee7d Binary files /dev/null and b/public/animations/coffee-cup/07.png differ diff --git a/public/animations/coffee-cup/08.png b/public/animations/coffee-cup/08.png new file mode 100644 index 0000000..3764821 Binary files /dev/null and b/public/animations/coffee-cup/08.png differ diff --git a/public/animations/coffee-cup/09.png b/public/animations/coffee-cup/09.png new file mode 100644 index 0000000..37f34a2 Binary files /dev/null and b/public/animations/coffee-cup/09.png differ diff --git a/public/animations/coffee-cup/10.png b/public/animations/coffee-cup/10.png new file mode 100644 index 0000000..af50e5c Binary files /dev/null and b/public/animations/coffee-cup/10.png differ diff --git a/public/animations/coffee-cup/11.png b/public/animations/coffee-cup/11.png new file mode 100644 index 0000000..ec2ae88 Binary files /dev/null and b/public/animations/coffee-cup/11.png differ diff --git a/public/animations/coffee-cup/12.png b/public/animations/coffee-cup/12.png new file mode 100644 index 0000000..9261886 Binary files /dev/null and b/public/animations/coffee-cup/12.png differ diff --git a/public/animations/coffee-cup/13.png b/public/animations/coffee-cup/13.png new file mode 100644 index 0000000..ddf2717 Binary files /dev/null and b/public/animations/coffee-cup/13.png differ diff --git a/public/animations/coffee-cup/14.png b/public/animations/coffee-cup/14.png new file mode 100644 index 0000000..2f23e4f Binary files /dev/null and b/public/animations/coffee-cup/14.png differ diff --git a/public/animations/coffee-cup/15.png b/public/animations/coffee-cup/15.png new file mode 100644 index 0000000..6c52688 Binary files /dev/null and b/public/animations/coffee-cup/15.png differ diff --git a/public/animations/coffee-cup/16.png b/public/animations/coffee-cup/16.png new file mode 100644 index 0000000..e858568 Binary files /dev/null and b/public/animations/coffee-cup/16.png differ diff --git a/public/animations/coffee-cup/17.png b/public/animations/coffee-cup/17.png new file mode 100644 index 0000000..644a6a1 Binary files /dev/null and b/public/animations/coffee-cup/17.png differ diff --git a/public/animations/coffee-cup/18.png b/public/animations/coffee-cup/18.png new file mode 100644 index 0000000..016e475 Binary files /dev/null and b/public/animations/coffee-cup/18.png differ diff --git a/public/animations/coffee-cup/19.png b/public/animations/coffee-cup/19.png new file mode 100644 index 0000000..743ee24 Binary files /dev/null and b/public/animations/coffee-cup/19.png differ diff --git a/public/animations/coffee-cup/20.png b/public/animations/coffee-cup/20.png new file mode 100644 index 0000000..3352222 Binary files /dev/null and b/public/animations/coffee-cup/20.png differ diff --git a/public/animations/coffee-cup/21.png b/public/animations/coffee-cup/21.png new file mode 100644 index 0000000..f41fb6f Binary files /dev/null and b/public/animations/coffee-cup/21.png differ diff --git a/public/animations/coffee-cup/22.png b/public/animations/coffee-cup/22.png new file mode 100644 index 0000000..e18f0c9 Binary files /dev/null and b/public/animations/coffee-cup/22.png differ diff --git a/public/animations/coffee-cup/23.png b/public/animations/coffee-cup/23.png new file mode 100644 index 0000000..3a0add6 Binary files /dev/null and b/public/animations/coffee-cup/23.png differ diff --git a/public/animations/coffee-cup/24.png b/public/animations/coffee-cup/24.png new file mode 100644 index 0000000..a8bac5d Binary files /dev/null and b/public/animations/coffee-cup/24.png differ diff --git a/public/animations/coffee-cup/25.png b/public/animations/coffee-cup/25.png new file mode 100644 index 0000000..2a87016 Binary files /dev/null and b/public/animations/coffee-cup/25.png differ diff --git a/public/animations/coffee-cup/26.png b/public/animations/coffee-cup/26.png new file mode 100644 index 0000000..7188315 Binary files /dev/null and b/public/animations/coffee-cup/26.png differ diff --git a/src/components/focus/PixelArt.tsx b/src/components/focus/PixelArt.tsx index f92520b..6d87c41 100644 --- a/src/components/focus/PixelArt.tsx +++ b/src/components/focus/PixelArt.tsx @@ -1,5 +1,5 @@ -import type { ReactNode } from "react"; -import { metaOf, type AnimationType } from "./animations"; +import { useEffect, type ReactNode } from "react"; +import { BATTERY_LEVELS, metaOf, type AnimationType } from "./animations"; type Pixel = readonly [x: number, y: number, width: number, height: number, color: string]; @@ -11,64 +11,30 @@ const FLICKER_CSS = ` @media (prefers-reduced-motion: reduce) { .${FLICKER_CLASS} { animation: none; } } `; -const GLASS = "#cbd5e1"; -const COFFEE = "#6b3e26"; -const CREMA = "#c08552"; -const STEAM = "#64748b"; -const CUP_ROWS = 16; - -function coffeeCup(step: number): Pixel[] { - const level = Math.min(CUP_ROWS, step); - const coffee: Pixel[] = - level > 0 - ? [ - [9, 27 - level, 13, level, COFFEE], - [9, 27 - level, 13, 1, CREMA], - ] - : []; - const steam: Pixel[] = - level >= CUP_ROWS - 4 - ? [ - [12, 7, 1, 1, STEAM], - [13, 6, 1, 1, STEAM], - [12, 5, 1, 1, STEAM], - [17, 8, 1, 1, STEAM], - [18, 7, 1, 1, STEAM], - [17, 6, 1, 1, STEAM], - ] - : []; - return [ - ...steam, - ...coffee, - [8, 10, 1, 17, GLASS], - [22, 10, 1, 17, GLASS], - [8, 27, 15, 1, GLASS], - [10, 12, 1, 5, "#f8fafc"], - [23, 13, 3, 1, GLASS], - [25, 14, 1, 7, GLASS], - [23, 21, 3, 1, GLASS], - [5, 28, 22, 1, "#e2e8f0"], - [7, 29, 18, 1, "#94a3b8"], - ]; -} - -const CELLS = 5; const SHELL = "#e5e7eb"; -const EMPTY_CELL = "#1f2937"; +const CHARGE_LEFT = 12; +const CHARGE_WIDTH = 8; +const CHARGE_BOTTOM = 26; +const SEGMENT_ROWS = 4; +const SEGMENT_GAP = "#111827"; -const chargeColor = (charged: number) => (charged <= 1 ? "#ef4444" : charged === 2 ? "#f59e0b" : "#22c55e"); +const chargeColor = (fraction: number) => (fraction > 0.6 ? "#22c55e" : fraction > 0.3 ? "#f59e0b" : "#ef4444"); +/** Drains one row per frame: frame 1 is fully charged, the last frame is empty. */ function battery(step: number): Pixel[] { - const charged = Math.min(CELLS, step); - const cells = Array.from({ length: CELLS }, (_, index): Pixel => [ - 12, - 24 - index * 4, - 8, - 3, - index < charged ? chargeColor(charged) : EMPTY_CELL, - ]); + const level = Math.max(0, BATTERY_LEVELS - step); + const color = chargeColor(level / BATTERY_LEVELS); + const charge: Pixel[] = level > 0 ? [[CHARGE_LEFT, CHARGE_BOTTOM - level + 1, CHARGE_WIDTH, level, color]] : []; + // Thin gaps split the charge into the familiar battery segments. + const gaps = Array.from({ length: Math.floor(BATTERY_LEVELS / SEGMENT_ROWS) }, (_, index): Pixel => [ + CHARGE_LEFT, + CHARGE_BOTTOM - (index + 1) * SEGMENT_ROWS + 1, + CHARGE_WIDTH, + 1, + SEGMENT_GAP, + ]).filter(([, y]) => y > CHARGE_BOTTOM - level); const bolt: Pixel[] = - charged === CELLS + level === BATTERY_LEVELS ? [ [25, 12, 2, 1, "#facc15"], [24, 13, 2, 1, "#facc15"], @@ -83,7 +49,8 @@ function battery(step: number): Pixel[] { [10, 28, 12, 1, SHELL], [10, 6, 1, 23, SHELL], [21, 6, 1, 23, SHELL], - ...cells, + ...charge, + ...gaps, ...bolt, ]; } @@ -135,8 +102,7 @@ function rocket(step: number, frames: number): { pixels: Pixel[]; flame: Pixel[] }; } -function scene(type: AnimationType, step: number, frames: number): { pixels: Pixel[]; flame: Pixel[] } { - if (type === "coffee-cup") return { pixels: coffeeCup(step), flame: [] }; +function scene(type: Exclude, step: number, frames: number): { pixels: Pixel[]; flame: Pixel[] } { if (type === "battery") return { pixels: battery(step), flame: [] }; return rocket(step, frames); } @@ -146,10 +112,29 @@ const toRects = (pixels: readonly Pixel[]): ReactNode[] => )); +const coffeeFrameUrl = (frame: number) => `/animations/coffee-cup/${String(frame).padStart(2, "0")}.png`; + +let coffeeFramesRequested = false; + +/** Fetches every frame once up front so the first pass through the animation never blinks. */ +function preloadCoffeeFrames(frames: number): void { + if (coffeeFramesRequested) return; + coffeeFramesRequested = true; + for (let frame = 1; frame <= frames; frame++) new Image().src = coffeeFrameUrl(frame); +} + /** Draws `frame` (1-based) of a pixel-art animation on a 32×32 grid. */ export function PixelArt({ type, frame, className, label }: { type: AnimationType; frame: number; className: string; label: string }) { const { frames } = metaOf(type); - const { pixels, flame: flamePixels } = scene(type, Math.min(frames, Math.max(1, frame)) - 1, frames); + const clamped = Math.min(frames, Math.max(1, frame)); + useEffect(() => { + if (type === "coffee-cup") preloadCoffeeFrames(frames); + }, [type, frames]); + if (type === "coffee-cup") { + // eslint-disable-next-line @next/next/no-img-element -- frame-by-frame animation; next/image would re-optimize every frame + return {label}; + } + const { pixels, flame: flamePixels } = scene(type, clamped - 1, frames); return ( {flamePixels.length > 0 ? ( diff --git a/src/components/focus/animations.ts b/src/components/focus/animations.ts index 55846c0..1b93f6a 100644 --- a/src/components/focus/animations.ts +++ b/src/components/focus/animations.ts @@ -1,5 +1,8 @@ export type AnimationType = "coffee-cup" | "battery" | "rocket"; +/** Rows of charge the battery drains through; the extra frame is the empty battery. */ +export const BATTERY_LEVELS = 19; + export interface AnimationMeta { readonly type: AnimationType; readonly title: string; @@ -7,8 +10,8 @@ export interface AnimationMeta { } export const ANIMATIONS: readonly AnimationMeta[] = [ - { type: "coffee-cup", title: "Coffee Cup", frames: 17 }, - { type: "battery", title: "Battery", frames: 6 }, + { type: "coffee-cup", title: "Coffee Cup", frames: 26 }, + { type: "battery", title: "Battery", frames: BATTERY_LEVELS + 1 }, { type: "rocket", title: "Rocket", frames: 30 }, ];