diff --git a/app/bootPreload.ts b/app/bootPreload.ts new file mode 100644 index 00000000..2cdf0278 --- /dev/null +++ b/app/bootPreload.ts @@ -0,0 +1,45 @@ +import environment from 'app/environment'; +import { preloadImages } from 'app/helpers/preloadImages'; + +// +// The icon styles shown on the top bar (app/components/menu/topBar.tsx) and bottom bar +// (app/components/menu/bottomBar.tsx / bottomBar/bottomBarSection.tsx), all rendered at +// size='large'. There's no shared asset manifest to derive this from, so it's hand-maintained - +// if a new icon is added to either bar, add its style name here too. +// +const STAGE_ONE_ICON_STYLES = [ + 'star_map', + 'planet_side', + 'inbox', + 'essentia', + 'stats', + 'logout', + 'food', + 'ore', + 'water', + 'energy', + 'waste', + 'happiness', + 'plots', + 'build', + 'build-no', + 'dashboard', +]; + +const stageOneAssetUrls = (): string[] => { + const assetsUrl = environment.getAssetsUrl(); + + return [ + `${assetsUrl}star_system/field.png`, + `${assetsUrl}ui/logo.png`, + ...STAGE_ONE_ICON_STYLES.map((style) => `${assetsUrl}ui/l/${style}.png`), + ]; +}; + +// +// Preloads everything the login screen and the main UI chrome need to paint without popping in, +// driving the boot progress bar the whole way. Awaited by app/load.ts before the app is mounted. +// +export const runBootPreload = async (): Promise => { + await preloadImages(stageOneAssetUrls()); +}; diff --git a/app/components/menu.test.tsx b/app/components/menu.test.tsx new file mode 100644 index 00000000..84723ceb --- /dev/null +++ b/app/components/menu.test.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import TopBar from 'app/components/menu/topBar'; +import BottomBar from 'app/components/menu/bottomBar'; +import LeftSidebarButton from 'app/components/menu/leftSidebar/button'; +import RightSidebarButton from 'app/components/menu/rightSidebar/button'; +import MenuStore from 'app/stores/menu'; + +// app/queries/empire transitively requires app/lacuna -> app/game -> ...-> react-markdown (an +// ESM-only package Jest can't parse) via the wider GameWindow tree. TopBar only calls +// useLogoutMutation()'s `mutate` from a click handler, never during render, so a bare stub is +// enough here - mocking the module's full shape would need jest.requireActual, which would pull +// that chain right back in. +jest.mock('app/queries/empire', () => ({ + useLogoutMutation: () => ({ mutate: jest.fn() }), +})); + +const outer = (container: HTMLElement) => container.firstChild as HTMLElement; + +afterEach(() => { + MenuStore.showMenu(); +}); + +test('top bar slides up off screen when the menu is hidden, and in when shown', () => { + MenuStore.hideMenu(); + const { container: hidden } = render(); + expect(outer(hidden).style.transform).toBe('translateY(-150%)'); + + MenuStore.showMenu(); + const { container: shown } = render(); + expect(outer(shown).style.transform).toBe('translateY(0)'); +}); + +test('bottom bar slides down off screen when the menu is hidden, and in when shown', () => { + MenuStore.hideMenu(); + const { container: hidden } = render(); + expect(outer(hidden).style.transform).toBe('translateY(150%)'); + + MenuStore.showMenu(); + const { container: shown } = render(); + expect(outer(shown).style.transform).toBe('translateY(0)'); +}); + +test('left sidebar button slides off/on screen to the left', () => { + MenuStore.hideMenu(); + const { container: hidden } = render(); + expect(outer(hidden).style.transform).toBe('translateX(-150%)'); + + MenuStore.showMenu(); + const { container: shown } = render(); + expect(outer(shown).style.transform).toBe('translateX(0)'); +}); + +test('right sidebar button slides off/on screen to the right', () => { + MenuStore.hideMenu(); + const { container: hidden } = render(); + expect(outer(hidden).style.transform).toBe('translateX(150%)'); + + MenuStore.showMenu(); + const { container: shown } = render(); + expect(outer(shown).style.transform).toBe('translateX(0)'); +}); diff --git a/app/components/menu.tsx b/app/components/menu.tsx index b405c502..d61ddc0b 100644 --- a/app/components/menu.tsx +++ b/app/components/menu.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { observer } from 'mobx-react'; import TopBar from 'app/components/menu/topBar'; import BottomBar from 'app/components/menu/bottomBar'; @@ -10,27 +9,23 @@ import RightSidebarButton from 'app/components/menu/rightSidebar/button'; import Loader from 'app/components/menu/loader'; import IncomingShips from 'app/components/incomingShips'; -import MenuStore from 'app/stores/menu'; - class Menu extends React.Component { render() { - if (MenuStore.menuShown) { - return ( -
- - - - -
- - - - -
- ); - } - return
; + // Always mounted (rather than conditionally rendered on MenuStore.menuShown) so TopBar, + // BottomBar and the sidebar toggle buttons can slide themselves on/off screen via a CSS + // transform instead of blinking in and out of existence. + return ( +
+ + + + + + + +
+ ); } } -export default observer(Menu); +export default Menu; diff --git a/app/components/menu/bottomBar.tsx b/app/components/menu/bottomBar.tsx index 607541c4..43ec585b 100644 --- a/app/components/menu/bottomBar.tsx +++ b/app/components/menu/bottomBar.tsx @@ -5,6 +5,7 @@ import BodyRPCStore from 'app/stores/rpc/body'; import ServerRPCStore from 'app/stores/rpc/server'; import EmpireRPCStore from 'app/stores/rpc/empire'; import WindowsStore from 'app/stores/windows'; +import MenuStore from 'app/stores/menu'; import BottomBarSection from 'app/components/menu/bottomBar/bottomBarSection'; @@ -20,6 +21,8 @@ class BottomBar extends React.Component { top: 'calc(100vh - 160px)', display: 'flex', justifyContent: 'center', + transform: MenuStore.menuShown ? 'translateY(0)' : 'translateY(150%)', + transition: 'transform 450ms ease', }} >
diff --git a/app/components/menu/gameWindow.tsx b/app/components/menu/gameWindow.tsx index 1a2b6fd5..c1eb6056 100644 --- a/app/components/menu/gameWindow.tsx +++ b/app/components/menu/gameWindow.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import environment from 'app/environment'; import LeftSidebar from 'app/components/menu/leftSidebar'; @@ -8,12 +8,23 @@ import Menu from 'app/components/menu'; import WindowManager from 'app/components/menu/windowManager'; import ErrorBoundary from 'app/components/errorBoundary'; import WindowsStore from 'app/stores/windows'; +import { prefersReducedMotion, STARFIELD_WAKE_MS } from 'app/stores/window/reveal'; // // This React component will be the main container of everything that appears on the screen. // const GameWindow: React.FunctionComponent = () => { + const [starfieldRevealed, setStarfieldRevealed] = useState(false); + + useEffect(() => { + // Painted at opacity 0 first, then flipped on the next frame so the transition actually + // animates instead of the image just appearing already-visible on the first paint. By now + // the image is already browser-cached from the boot preload (see app/bootPreload.ts). + const frame = requestAnimationFrame(() => setStarfieldRevealed(true)); + return () => cancelAnimationFrame(frame); + }, []); + useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if (event.key !== 'Escape') return; @@ -50,14 +61,25 @@ const GameWindow: React.FunctionComponent = () => { {/* The starfield backdrop. This used to come from the (now removed) legacy-styles.css `html { background: ... }` rule; Bulma 1.0's base styles now paint the body, so we - render our own full-viewport layer behind everything instead. + render our own full-viewport layer behind everything instead. Split into an opaque base + and a separately-fading image layer so it can slowly "wake up" from black on boot. */}
+
diff --git a/app/components/menu/leftSidebar/button.tsx b/app/components/menu/leftSidebar/button.tsx index a0955bea..f08d09fb 100644 --- a/app/components/menu/leftSidebar/button.tsx +++ b/app/components/menu/leftSidebar/button.tsx @@ -17,6 +17,8 @@ class LeftSidebarButton extends React.Component { zIndex: 2500, left: 15, top: 15, + transform: MenuStore.menuShown ? 'translateX(0)' : 'translateX(-150%)', + transition: 'transform 450ms ease', }} >