diff --git a/packages/app/public/decorative/minecraft/diamond-pickaxe.png b/packages/app/public/decorative/minecraft/diamond-pickaxe.png new file mode 100644 index 00000000..dae141fa Binary files /dev/null and b/packages/app/public/decorative/minecraft/diamond-pickaxe.png differ diff --git a/packages/app/public/decorative/minecraft/diamond.png b/packages/app/public/decorative/minecraft/diamond.png new file mode 100644 index 00000000..be4fda0d Binary files /dev/null and b/packages/app/public/decorative/minecraft/diamond.png differ diff --git a/packages/app/public/decorative/minecraft/ender-dragon.gif b/packages/app/public/decorative/minecraft/ender-dragon.gif new file mode 100644 index 00000000..11f7f808 Binary files /dev/null and b/packages/app/public/decorative/minecraft/ender-dragon.gif differ diff --git a/packages/app/public/decorative/minecraft/grass-block.png b/packages/app/public/decorative/minecraft/grass-block.png new file mode 100644 index 00000000..870cd88b Binary files /dev/null and b/packages/app/public/decorative/minecraft/grass-block.png differ diff --git a/packages/app/public/decorative/minecraft/tnt.png b/packages/app/public/decorative/minecraft/tnt.png new file mode 100644 index 00000000..c4dd2e4e Binary files /dev/null and b/packages/app/public/decorative/minecraft/tnt.png differ diff --git a/packages/app/public/decorative/minecraft/zombified-piglin.png b/packages/app/public/decorative/minecraft/zombified-piglin.png new file mode 100644 index 00000000..9df1c1c5 Binary files /dev/null and b/packages/app/public/decorative/minecraft/zombified-piglin.png differ diff --git a/packages/app/src/app/globals.css b/packages/app/src/app/globals.css index 50849070..ee7bc7fb 100644 --- a/packages/app/src/app/globals.css +++ b/packages/app/src/app/globals.css @@ -319,6 +319,41 @@ display: none; } +/* Ender-dragon fly-across — one-shot animation on minecraft theme activation, + * triggered from minecraft-decorations.tsx. Plays once (iteration-count: 1) + * and lands the dragon off-screen left so the GIF stops being rendered. */ +@keyframes mc-dragon-flyacross { + 0% { + transform: translate(110vw, 0) scale(1); + opacity: 0; + } + 8% { + opacity: 0.95; + } + 50% { + transform: translate(40vw, -3vh) scale(1.05); + } + 92% { + opacity: 0.95; + } + 100% { + transform: translate(-130vw, 1vh) scale(1); + opacity: 0; + } +} + +.mc-dragon-flyacross { + animation: mc-dragon-flyacross 9s cubic-bezier(0.4, 0, 0.6, 1) 1 forwards; + will-change: transform, opacity; +} + +@media (prefers-reduced-motion: reduce) { + .mc-dragon-flyacross { + animation: none; + display: none; + } +} + /* Splash text — yellow bouncing rotated text (Minecraft title screen style) */ @keyframes splash-bounce { 0%, diff --git a/packages/app/src/app/layout.tsx b/packages/app/src/app/layout.tsx index 22bda276..93088655 100644 --- a/packages/app/src/app/layout.tsx +++ b/packages/app/src/app/layout.tsx @@ -11,6 +11,7 @@ import { Footer } from '@/components/footer/footer'; import { Header } from '@/components/header/header'; import { CircuitBackground } from '@/components/circuit-background'; import { MinecraftBackgroundLazy } from '@/components/minecraft/minecraft-background-lazy'; +import { MinecraftDecorations } from '@/components/minecraft/minecraft-decorations'; import { ThemeProvider } from '@/components/ui/theme-provider'; import { AUTHOR_HANDLE, @@ -178,6 +179,7 @@ export default async function RootLayout({ + diff --git a/packages/app/src/components/minecraft/minecraft-decorations.tsx b/packages/app/src/components/minecraft/minecraft-decorations.tsx new file mode 100644 index 00000000..95aa36ca --- /dev/null +++ b/packages/app/src/components/minecraft/minecraft-decorations.tsx @@ -0,0 +1,164 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +/** + * Decorative Minecraft asset PNGs scattered around the viewport corners, + * plus a one-shot Ender Dragon fly-across at theme activation. Only renders + * while the minecraft theme is active. Watches `document.documentElement` + * class changes via a MutationObserver so it appears / disappears live with + * the mode-toggle (mirrors `minecraft-toggles.tsx`). + * + * All static images are `pointer-events: none` at low z-index. The dragon is + * absolutely positioned and flies right-to-left once per theme activation + * (animation-iteration-count: 1) — the GIF's wing-flap plays during the + * traversal so the entrance feels alive. + * + * Asset provenance: minecraft.wiki (CC BY-NC-SA 3.0) for blocks/items/dragon. + */ +const DECORATIONS = [ + // Top-left: Zombified Piglin, mirrored so its sword arm points toward the + // middle of the screen rather than off-screen left. Keep the original + // -6deg tilt — flipping over X also flips the rotate direction visually, + // which lands as a slight forward lean toward the chart area. + { + src: '/decorative/minecraft/zombified-piglin.png', + alt: 'Zombified Piglin', + style: { + top: '5rem', + left: '0.5rem', + width: 'min(110px, 9vw)', + transform: 'scaleX(-1) rotate(-6deg)', + }, + }, + // Top-right: Diamond pickaxe, mining vibe. + { + src: '/decorative/minecraft/diamond-pickaxe.png', + alt: 'Diamond pickaxe', + style: { + top: '5rem', + right: '0.5rem', + width: 'min(110px, 9vw)', + transform: 'rotate(35deg)', + }, + }, + // Bottom-left: Grass block, classic anchor. + { + src: '/decorative/minecraft/grass-block.png', + alt: 'Grass block', + style: { + bottom: '4rem', + left: '0.5rem', + width: 'min(110px, 9vw)', + transform: 'rotate(-4deg)', + }, + }, + // Bottom-mid-left: TNT, ready to go boom. + { + src: '/decorative/minecraft/tnt.png', + alt: 'TNT', + style: { + bottom: '4rem', + left: '20%', + width: 'min(95px, 8vw)', + transform: 'rotate(6deg)', + }, + }, + // Bottom-right: Diamond, the loot. + { + src: '/decorative/minecraft/diamond.png', + alt: 'Diamond', + style: { + bottom: '4rem', + right: '0.5rem', + width: 'min(95px, 8vw)', + transform: 'rotate(-6deg)', + }, + }, +] as const; + +export function MinecraftDecorations() { + const [active, setActive] = useState(false); + // Bumps each time the theme is (re)activated, used as the React key on the + // dragon `` to force-remount and re-trigger the CSS fly-across. + const [dragonNonce, setDragonNonce] = useState(0); + + useEffect(() => { + let wasMinecraft = document.documentElement.classList.contains('minecraft'); + setActive(wasMinecraft); + if (wasMinecraft) setDragonNonce((n) => n + 1); + + const check = () => { + const isMinecraft = document.documentElement.classList.contains('minecraft'); + setActive(isMinecraft); + // Re-trigger the dragon only on a transition off→on, not on every + // unrelated class change. + if (isMinecraft && !wasMinecraft) setDragonNonce((n) => n + 1); + wasMinecraft = isMinecraft; + }; + + const observer = new MutationObserver(check); + observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); + return () => observer.disconnect(); + }, []); + + if (!active) return null; + + return ( + <> + + + {/* One-shot Ender Dragon fly-across. Keyed on dragonNonce so it + * remounts (and the CSS animation re-plays) every theme activation. */} + + + + + ); +}