Skip to content
This repository has been archived by the owner on Nov 21, 2023. It is now read-only.

Commit

Permalink
Add gba bios styled intro animation
Browse files Browse the repository at this point in the history
(using it to mock mGBA frame placement for now)
  • Loading branch information
ethanl21 committed Jul 15, 2023
1 parent 8b4f35f commit d76450d
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 110 deletions.
2 changes: 1 addition & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
!.storybook

# shadcn/ui components
# not linting for now since they are unmodified
# not linting for now since they are (mostly) unmodified
src/components/ui/**
Binary file added public/bios_animation.mp4
Binary file not shown.
42 changes: 0 additions & 42 deletions src/App.css

This file was deleted.

88 changes: 60 additions & 28 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
import { useEffect, useState } from "react";
import "./App.css";
import { useEffect, useRef, useState } from "react";
import TopMenuBar from "@/components/wasmgba/TopMenubar";
import QuickControls from "@/components/wasmgba/QuickControls";
import AboutDialog from "@/components/wasmgba/AboutDialog";
import UsageDialog from "@/components/wasmgba/UsageDialog";
import IntroAnimationPlayer from "@/components/wasmgba/IntroAnimationPlayer";
import { useTheme } from "@/lib/ThemeProviderUseTheme";
import ManageCheatDialog from "./components/wasmgba/ManageCheatsDialog";
import ManageCheatDialog from "@/components/wasmgba/ManageCheatsDialog";

function App() {
const [aboutDialogIsOpen, setAboutDialogIsOpen] = useState(false);
const [usageDialogIsOpen, setUsageDialogIsOpen] = useState(false);
const [cheatsDialogIsOpen, setCheatsDialogIsOpen] = useState(false);
const [introAnimationIsPlaying, setIntroAnimationIsPlaying] = useState(false);
const [introVideoIsVisible, setIntroVideoIsVisible] = useState(false);

const [paused, setPaused] = useState(false);
const [muted, setMuted] = useState(false);
const [volume, setVolume] = useState([100]);
const [fastForward, setFastForward] = useState(false);
const [pixelated, setPixelated] = useState(false);

const introAnimationPlayerRef = useRef<HTMLVideoElement | null>(null);

// light/dark mode
const [_mode, setMode] = useState("light");
const { setTheme } = useTheme();

const onSelectMode = (mode: string) => {
setMode(mode);
setTheme(mode);
Expand All @@ -46,6 +50,16 @@ function App() {
};
});

// event handlers
const onImportROM = () => {
setIntroAnimationIsPlaying(true);
setIntroVideoIsVisible(true);
};
const onIntroAnimationFinished = () => {
setIntroAnimationIsPlaying(false);
setIntroVideoIsVisible(false);
};

return (
<>
<AboutDialog
Expand All @@ -60,30 +74,48 @@ function App() {
open={cheatsDialogIsOpen}
setIsOpen={setCheatsDialogIsOpen}
/>
<main className="flex h-screen p-2">
<div className="flex flex-col justify-between flex-1 space-y-2">
<div className="flex">
<TopMenuBar
onOpenAboutDialog={() => setAboutDialogIsOpen(true)}
onOpenUsageDialog={() => setUsageDialogIsOpen(true)}
onOpenCheatsDialog={() => setCheatsDialogIsOpen(true)}
onImportROM={() => onImportROM()} // this just plays the intro for now, todo: import ROM
repo={WASMGBA_REPO_URL}
licenses={WASMGBA_OSS_LICENSES_URL}
volume={volume[0]}
onVolumeChange={setVolume}
muted={muted}
onMutedChange={setMuted}
paused={paused}
onPausedChange={setPaused}
fastForward={fastForward}
onFastForwardChange={setFastForward}
pixelated={pixelated}
onPixelatedChange={setPixelated}
/>
</div>

<div className="container">
<header className="fixed top-0 left-0 m-5">
<TopMenuBar
onOpenAboutDialog={() => setAboutDialogIsOpen(true)}
onOpenUsageDialog={() => setUsageDialogIsOpen(true)}
onOpenCheatsDialog={() => setCheatsDialogIsOpen(true)}
repo={WASMGBA_REPO_URL}
licenses={WASMGBA_OSS_LICENSES_URL}
volume={volume[0]}
onVolumeChange={setVolume}
muted={muted}
onMutedChange={setMuted}
paused={paused}
onPausedChange={setPaused}
fastForward={fastForward}
onFastForwardChange={setFastForward}
pixelated={pixelated}
onPixelatedChange={setPixelated}
/>
</header>
{/* this just hides the intro for now, todo crossfade between intro and mGBA frame */}
<div className="flex justify-center">
<div className="border-8 rounded-sm">
<div className={introVideoIsVisible ? "" : "hidden"}>
<IntroAnimationPlayer
isPlaying={introAnimationIsPlaying}
ref={introAnimationPlayerRef}
onVideoEnded={onIntroAnimationFinished}
volumePercentage={volume[0] / 100}
muted={muted}
/>
</div>
<div className={introVideoIsVisible ? "hidden" : "p-10"}>
<p>gba content goes here</p>
</div>
</div>
</div>

<main>
<div className="fixed bottom-0 left-0 m-5 w-1/3">
<div className="flex w-1/2">
<QuickControls
paused={paused}
onPausedChanged={setPaused}
Expand All @@ -93,8 +125,8 @@ function App() {
onVolumeChanged={setVolume}
/>
</div>
</main>
</div>
</div>
</main>
</>
);
}
Expand Down
Binary file removed src/assets/bios_animation.mp4
Binary file not shown.
58 changes: 34 additions & 24 deletions src/components/wasmgba/IntroAnimationPlayer.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,47 @@
import { forwardRef, useState } from "react";
import BiosAnimation from "../../assets/bios_animation.mp4";
import { forwardRef, useRef } from "react";
import biosAnimation from "/bios_animation.mp4";

interface IntroAnimationPlayerProps {
onVideoEnded?: () => void;
volumePercentage?: number;
isPlaying?: boolean;
muted?: boolean;
}
const IntroAnimationPlayerPlayer = forwardRef(function _introAnimationPlayer(
{ onVideoEnded = () => {} }: IntroAnimationPlayerProps,
ref: React.Ref<HTMLVideoElement>
const IntroAnimationPlayer = forwardRef(function _introAnimationPlayer(
{
onVideoEnded = () => {},
volumePercentage = 1,
isPlaying = false,
muted = false,
}: IntroAnimationPlayerProps,
parentRef
) {
const [isVideoEnded, setIsVideoEnded] = useState(false);
const handleVideoEnded = () => {
setIsVideoEnded(true);
onVideoEnded();
};
const localRef = useRef<HTMLVideoElement | null>(null);

if (localRef.current) {
localRef.current.volume = volumePercentage;
if (isPlaying) {
void localRef.current.play();
} else {
localRef.current.pause();
}
}

return (
<>
<video
className={
isVideoEnded
? "transition-opacity ease-out duration-700 opacity-0"
: "transition-opacity ease-in duration-100 opacity-100"
}
preload="auto"
ref={ref}
onEnded={handleVideoEnded}
onPlay={() => setIsVideoEnded(false)}
>
<source src={BiosAnimation} type="video/mp4" />
Your browser does not support HTML5 video.
</video>
ref={(ref) => {
if (typeof parentRef === "function") parentRef(ref);
else if (parentRef !== null) parentRef.current = ref;
if (localRef !== null) localRef.current = ref;
}}
src={biosAnimation}
onEnded={onVideoEnded}
playsInline
muted={muted}
/>
</>
);
});

export default IntroAnimationPlayerPlayer;
export default IntroAnimationPlayer;
2 changes: 2 additions & 0 deletions src/components/wasmgba/QuickControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ export default function QuickControls({
>
{muted ? <VolumeX /> : <Volume2 />}
</Toggle>
<div className="flex w-52">
<Slider
aria-label="Volume"
value={[volume]}
onValueChange={(e) => onVolumeChanged(e)}
disabled={muted}
/>
<p className="text-center content-center">{volume}%</p>
</div>
</Card>
</>
);
Expand Down
4 changes: 3 additions & 1 deletion src/components/wasmgba/TopMenubar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ interface TopMenuBarProps {
onOpenAboutDialog?: () => void;
onOpenUsageDialog?: () => void;
onOpenCheatsDialog?: () => void;
onImportROM?: () => void;
repo?: string;
licenses?: string;
volume?: number;
Expand All @@ -75,6 +76,7 @@ export default function TopMenuBar({
onOpenAboutDialog = () => {},
onOpenUsageDialog = () => {},
onOpenCheatsDialog = () => {},
onImportROM = () => {},
repo = "",
licenses = "",
volume = 100,
Expand All @@ -94,7 +96,7 @@ export default function TopMenuBar({
<MenubarMenu>
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent>
<MenubarItem>
<MenubarItem onClick={onImportROM}>
Import ROM <MenubarShortcut>⌘O</MenubarShortcut>
</MenubarItem>
<MenubarItem>Import Save</MenubarItem>
Expand Down
38 changes: 24 additions & 14 deletions src/stories/Components/IntroAnimationPlayer.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";

import IntroAnimationPlayer from "@/components/wasmgba/IntroAnimationPlayer";
import { useRef, useState } from "react";
import { Button } from "@/components/ui/button";

const meta: Meta<typeof IntroAnimationPlayer> = {
component: IntroAnimationPlayer,
Expand All @@ -13,15 +10,32 @@ const meta: Meta<typeof IntroAnimationPlayer> = {
disable: true,
},
},
volumePercentage: {
control: {
type: "range",
min: 0,
max: 1,
step: 0.01,
},
},
},
args: {
onVideoEnded: () => {},
volumePercentage: 1,
isPlaying: false,
muted: navigator.userAgent.includes("Safari"),
},
decorators: [
(Story) => (
<div className="flex flex-col justify-center items-center space-y-2">
<>
<div className="flex justify-center items-center flex-col">
<span className="flex border-2">
<Story />
</span>

{navigator.userAgent.includes("Safari") && <p>videos with audio will not autoplay on Safari</p>}
</div>
</>
),
],
};
Expand All @@ -31,18 +45,14 @@ export default meta;
type Story = StoryObj<typeof IntroAnimationPlayer>;
export const Default: Story = {
render: ({ ...args }) => {
const playerRef = useRef<HTMLVideoElement | null>(null);

return (
<>
<span onClick={() => playerRef.current?.play()}
className="border-2">
<IntroAnimationPlayer
ref={playerRef}
onVideoEnded={args.onVideoEnded}
/>
</span>
{/* <Button onClick={() => playerRef.current?.play()}>Play</Button> */}
<IntroAnimationPlayer
isPlaying={args.isPlaying}
onVideoEnded={args.onVideoEnded}
volumePercentage={args.volumePercentage}
muted={args.muted}
/>
</>
);
},
Expand Down

0 comments on commit d76450d

Please sign in to comment.