From ae660b9b0494a9c17ef3213fbe51589c0409f53b Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Sun, 6 Oct 2019 21:40:40 -0400 Subject: [PATCH] Add compact player controls and support for custom wrapped components (#57) * Add play/pause icons * Support rendering children inside replay viewer * Update assets commit hash * Cache replay position requests * Allow prop passthrough for slider * Include compact controls * Show compact viewer in separate docs tab * Add Camera icon * Show camera dialog * Revert change to default App tab * Fixed bug where animated objects would spawn at the center of the scene * Added autoplay property to ReplayViewer * Statisfy unused parameter name linting error * Force play/pause dispatch from replay viewer * Bump patch version --- docs/src/App.tsx | 4 +- docs/src/components/CompactViewer.tsx | 55 +++++++ docs/src/components/Main.tsx | 26 +++- docs/src/components/Viewer.tsx | 2 +- package.json | 2 +- src/assets | 2 +- src/index.ts | 3 + src/managers/GameManager.ts | 3 + src/viewer/clients/loadReplay.ts | 26 +++- src/viewer/components/CompactPlayControls.tsx | 135 ++++++++++++++++++ src/viewer/components/GameManagerLoader.tsx | 2 +- src/viewer/components/ReplayViewer.tsx | 10 +- src/viewer/components/Slider.tsx | 5 +- src/viewer/components/icons/Camera.tsx | 23 +++ src/viewer/components/icons/PausedIcon.tsx | 20 +++ src/viewer/components/icons/PlayIcon.tsx | 20 +++ 16 files changed, 317 insertions(+), 21 deletions(-) create mode 100644 docs/src/components/CompactViewer.tsx create mode 100644 src/viewer/components/CompactPlayControls.tsx create mode 100644 src/viewer/components/icons/Camera.tsx create mode 100644 src/viewer/components/icons/PausedIcon.tsx create mode 100644 src/viewer/components/icons/PlayIcon.tsx diff --git a/docs/src/App.tsx b/docs/src/App.tsx index 204d9ac..39dcaac 100644 --- a/docs/src/App.tsx +++ b/docs/src/App.tsx @@ -4,7 +4,7 @@ import React, { Component } from "react" import Main from "./components/Main" -type ActiveTab = "viewer" | "other" +type ActiveTab = "viewer" | "compact" | "other" interface State { tab: ActiveTab @@ -33,9 +33,11 @@ class App extends Component { + {tab === "viewer" &&
} + {tab === "compact" &&
} {tab === "other" &&
Other
} ) diff --git a/docs/src/components/CompactViewer.tsx b/docs/src/components/CompactViewer.tsx new file mode 100644 index 0000000..9f8abb3 --- /dev/null +++ b/docs/src/components/CompactViewer.tsx @@ -0,0 +1,55 @@ +import Grid from "@material-ui/core/Grid" +import React, { Component } from "react" + +import { + CompactPlayControls, + GameBuilderOptions, + GameManager, + GameManagerLoader, + ReplayViewer, +} from "../../../src" + +interface Props { + options: GameBuilderOptions +} + +interface State { + gameManager?: GameManager +} + +class CompactViewer extends Component { + constructor(props: Props) { + super(props) + this.state = {} + } + + renderContent() { + const { gameManager } = this.state + + if (!gameManager) { + return "Food machine broke..." + } + + return ( + + + + + + + + ) + } + + render() { + const { options } = this.props + const onLoad = (gm: GameManager) => this.setState({ gameManager: gm }) + return ( + + {this.renderContent()} + + ) + } +} + +export default CompactViewer diff --git a/docs/src/components/Main.tsx b/docs/src/components/Main.tsx index aa217d9..4cf5528 100644 --- a/docs/src/components/Main.tsx +++ b/docs/src/components/Main.tsx @@ -1,14 +1,25 @@ import React, { Component } from "react" -import { FPSClock, GameBuilderOptions, loadReplay } from "../../../src" +import { + FPSClock, + GameBuilderOptions, + loadReplay, + ReplayData, + ReplayMetadata, +} from "../../../src" +import CompactViewer from "./CompactViewer" import Viewer from "./Viewer" +interface Props { + compact?: boolean +} + interface State { options?: GameBuilderOptions } -class Main extends Component { - constructor(props: any) { +class Main extends Component { + constructor(props: Props) { super(props) this.state = {} } @@ -16,7 +27,7 @@ class Main extends Component { componentDidMount() { const REPLAY_ID = "9944A36A11E987D3E286C1B524E68ECC" - loadReplay(REPLAY_ID).then(([replayData, replayMetadata]) => { + loadReplay(REPLAY_ID, true).then(([replayData, replayMetadata]) => { this.setState({ options: { replayData, @@ -33,8 +44,11 @@ class Main extends Component { if (!options) { return "Loading..." } - - return + return this.props.compact ? ( + + ) : ( + + ) } } diff --git a/docs/src/components/Viewer.tsx b/docs/src/components/Viewer.tsx index ce80840..e7d134a 100644 --- a/docs/src/components/Viewer.tsx +++ b/docs/src/components/Viewer.tsx @@ -44,7 +44,7 @@ class Viewer extends Component { spacing={24} > - + }, }).then(response => response.json()) -export const loadReplay = ( - replayId: string +const cache: { [key: string]: [ReplayData, ReplayMetadata] } = {} + +export const loadReplay = async ( + replayId: string, + cached?: boolean ): Promise<[ReplayData, ReplayMetadata]> => { - return Promise.all([ - fetchByURL(`https://calculated.gg/api/replay/${replayId}/positions`), - fetchByURL(`https://calculated.gg/api/v1/replay/${replayId}?key=1`), - ]) + const fetch = () => + Promise.all([ + fetchByURL(`https://calculated.gg/api/replay/${replayId}/positions`), + fetchByURL(`https://calculated.gg/api/v1/replay/${replayId}?key=1`), + ]) + if (cached) { + if (!cache[replayId]) { + return fetch().then(data => { + cache[replayId] = data + return data + }) + } + return Promise.resolve(cache[replayId]) + } + return fetch() } diff --git a/src/viewer/components/CompactPlayControls.tsx b/src/viewer/components/CompactPlayControls.tsx new file mode 100644 index 0000000..96564c0 --- /dev/null +++ b/src/viewer/components/CompactPlayControls.tsx @@ -0,0 +1,135 @@ +import Button from "@material-ui/core/Button" +import Dialog from "@material-ui/core/Dialog" +import DialogContent from "@material-ui/core/DialogContent" +import DialogTitle from "@material-ui/core/DialogTitle" +import Grid from "@material-ui/core/Grid" +import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles" +import Typography from "@material-ui/core/Typography" +import React, { Component } from "react" +import styled from "styled-components" + +import { + addCameraChangeListener, + removeCameraChangeListener, +} from "../../eventbus/events/cameraChange" +import { + addPlayPauseListener, + dispatchPlayPauseEvent, + PlayPauseEvent, + removePlayPauseListener, +} from "../../eventbus/events/playPause" +import FieldCameraControls from "./FieldCameraControls" +import Camera from "./icons/Camera" +import PausedIcon from "./icons/PausedIcon" +import PlayIcon from "./icons/PlayIcon" +import PlayerCameraControls from "./PlayerCameraControls" +import Slider from "./Slider" + +interface Props extends WithStyles {} + +interface State { + paused: boolean + cameraControlsShowing: boolean +} + +class CompactPlayControls extends Component { + constructor(props: Props) { + super(props) + this.state = { + paused: false, + cameraControlsShowing: false, + } + + addPlayPauseListener(this.onPlayPause) + addCameraChangeListener(this.hideCameraControls) + } + + componentWillUnmount() { + removePlayPauseListener(this.onPlayPause) + removeCameraChangeListener(this.hideCameraControls) + } + + setPlayPause = () => { + const isPaused = this.state.paused + dispatchPlayPauseEvent({ + paused: !isPaused, + }) + } + + onPlayPause = ({ paused }: PlayPauseEvent) => { + this.setState({ + paused, + }) + } + + showCameraControls = () => { + this.setState({ + cameraControlsShowing: true, + }) + } + + hideCameraControls = () => { + this.setState({ + cameraControlsShowing: false, + }) + } + + render() { + const { paused, cameraControlsShowing } = this.state + const { focused, thumb, track } = this.props.classes + return ( + + + + + + + + + + + + + + Camera Controls + + Field Cameras + + Player Cameras + + + + + ) + } +} + +const ControlsWrapper = styled.div` + position: absolute; + bottom: 6px; + left: 12px; + right: 60px; +` + +export default withStyles({ + focused: {}, + track: { + backgroundColor: "#fff", + }, + thumb: { + backgroundColor: "#fff", + "&:focus,&:hover,&$active": { + boxShadow: "inherit", + }, + }, +})(CompactPlayControls) diff --git a/src/viewer/components/GameManagerLoader.tsx b/src/viewer/components/GameManagerLoader.tsx index dc76b25..270e52e 100644 --- a/src/viewer/components/GameManagerLoader.tsx +++ b/src/viewer/components/GameManagerLoader.tsx @@ -45,7 +45,7 @@ class GameManagerLoader extends Component { GameManager.destruct() } - handleProgress = (item: any, loaded: number, total: number) => { + handleProgress = (_: any, loaded: number, total: number) => { const newPercent = Math.round((loaded / total) * 1000) / 10 const { percentLoaded } = this.state const stateValue = newPercent > percentLoaded ? newPercent : percentLoaded diff --git a/src/viewer/components/ReplayViewer.tsx b/src/viewer/components/ReplayViewer.tsx index 5eb5b0b..49bd59c 100644 --- a/src/viewer/components/ReplayViewer.tsx +++ b/src/viewer/components/ReplayViewer.tsx @@ -5,6 +5,7 @@ import FullScreen from "react-full-screen" import styled from "styled-components" import { dispatchCanvasResizeEvent } from "../../eventbus/events/canvasResize" +import { dispatchPlayPauseEvent } from "../../eventbus/events/playPause" import { GameManager } from "../../managers/GameManager" import FullscreenExitIcon from "./icons/FullscreenExitIcon" import FullscreenIcon from "./icons/FullscreenIcon" @@ -12,6 +13,7 @@ import Scoreboard from "./ScoreBoard" interface Props { gameManager: GameManager + autoplay?: boolean } interface State { @@ -34,10 +36,13 @@ class ReplayViewer extends PureComponent { if (!current) { throw new Error("Did not mount replay viewer correctly") } - const { gameManager } = this.props + const { gameManager, autoplay } = this.props + // Mount and resize canvas current.appendChild(gameManager.getDOMNode()) this.handleResize() - gameManager.clock.play() + + // Set the play/pause status to match autoplay property + dispatchPlayPauseEvent({ paused: !autoplay }) addEventListener("resize", this.handleResize) } @@ -77,6 +82,7 @@ class ReplayViewer extends PureComponent { + {this.props.children} ) diff --git a/src/viewer/components/Slider.tsx b/src/viewer/components/Slider.tsx index 3004ff8..11b689e 100644 --- a/src/viewer/components/Slider.tsx +++ b/src/viewer/components/Slider.tsx @@ -1,4 +1,4 @@ -import MUISlider from "@material-ui/lab/Slider" +import MUISlider, { SliderProps } from "@material-ui/lab/Slider" import debounce from "lodash.debounce" import React, { Component } from "react" @@ -10,7 +10,7 @@ import { import DataManager from "../../managers/DataManager" import { GameManager } from "../../managers/GameManager" -interface Props {} +interface Props extends Partial {} interface State { frame: number @@ -72,6 +72,7 @@ class Slider extends Component { }} > { + return ( +
+ + + +
+ ) +} + +export default Camera diff --git a/src/viewer/components/icons/PausedIcon.tsx b/src/viewer/components/icons/PausedIcon.tsx new file mode 100644 index 0000000..c51571f --- /dev/null +++ b/src/viewer/components/icons/PausedIcon.tsx @@ -0,0 +1,20 @@ +/* tslint:disable */ + +import React from "react" + +const PausedIcon = () => { + return ( +
+ + + +
+ ) +} + +export default PausedIcon diff --git a/src/viewer/components/icons/PlayIcon.tsx b/src/viewer/components/icons/PlayIcon.tsx new file mode 100644 index 0000000..3cd27f0 --- /dev/null +++ b/src/viewer/components/icons/PlayIcon.tsx @@ -0,0 +1,20 @@ +/* tslint:disable */ + +import React from "react" + +const PlayIcon = () => { + return ( +
+ + + +
+ ) +} + +export default PlayIcon