Skip to content

Commit

Permalink
Add compact player controls and support for custom wrapped components (
Browse files Browse the repository at this point in the history
…#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
  • Loading branch information
Abbondanzo committed Oct 7, 2019
1 parent 9cfb7fb commit ae660b9
Show file tree
Hide file tree
Showing 16 changed files with 317 additions and 21 deletions.
4 changes: 3 additions & 1 deletion docs/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -33,9 +33,11 @@ class App extends Component<any, State> {
</div>
<Tabs value={tab} onChange={this.handleChange}>
<Tab label="Viewer" value="viewer" />
<Tab label="Compact" value="compact" />
<Tab label="Other" value="other" />
</Tabs>
{tab === "viewer" && <Main />}
{tab === "compact" && <Main compact />}
{tab === "other" && <div>Other</div>}
</div>
)
Expand Down
55 changes: 55 additions & 0 deletions docs/src/components/CompactViewer.tsx
Original file line number Diff line number Diff line change
@@ -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<Props, State> {
constructor(props: Props) {
super(props)
this.state = {}
}

renderContent() {
const { gameManager } = this.state

if (!gameManager) {
return "Food machine broke..."
}

return (
<Grid container direction="column" justify="center" spacing={24}>
<Grid item style={{ minHeight: 0, maxWidth: 900, width: "100%" }}>
<ReplayViewer gameManager={gameManager}>
<CompactPlayControls />
</ReplayViewer>
</Grid>
</Grid>
)
}

render() {
const { options } = this.props
const onLoad = (gm: GameManager) => this.setState({ gameManager: gm })
return (
<GameManagerLoader options={options} onLoad={onLoad}>
{this.renderContent()}
</GameManagerLoader>
)
}
}

export default CompactViewer
26 changes: 20 additions & 6 deletions docs/src/components/Main.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
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<any, State> {
constructor(props: any) {
class Main extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {}
}

componentDidMount() {
const REPLAY_ID = "9944A36A11E987D3E286C1B524E68ECC"

loadReplay(REPLAY_ID).then(([replayData, replayMetadata]) => {
loadReplay(REPLAY_ID, true).then(([replayData, replayMetadata]) => {
this.setState({
options: {
replayData,
Expand All @@ -33,8 +44,11 @@ class Main extends Component<any, State> {
if (!options) {
return "Loading..."
}

return <Viewer options={options} />
return this.props.compact ? (
<CompactViewer options={options} />
) : (
<Viewer options={options} />
)
}
}

Expand Down
2 changes: 1 addition & 1 deletion docs/src/components/Viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class Viewer extends Component<Props, State> {
spacing={24}
>
<Grid item style={{ minHeight: 0, maxWidth: 900, width: "100%" }}>
<ReplayViewer gameManager={gameManager} />
<ReplayViewer gameManager={gameManager} autoplay />
</Grid>
<Grid item>
<Grid
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "replay-viewer",
"version": "0.5.1",
"version": "0.5.2",
"description": "Rocket League replay viewer React component and tooling",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export { default as ReplayViewer } from "./viewer/components/ReplayViewer"
export {
default as GameManagerLoader,
} from "./viewer/components/GameManagerLoader"
export {
default as CompactPlayControls,
} from "./viewer/components/CompactPlayControls"
export { default as PlayControls } from "./viewer/components/PlayControls"
export {
default as PlayerCameraControls,
Expand Down
3 changes: 3 additions & 0 deletions src/managers/GameManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ export class GameManager {
this.render = this.render.bind(this)
this.clock = clock

// Spawns the animation clips
AnimationManager.getInstance().playAnimationClips()
// Forces every animation to "take position"
AnimationManager.getInstance().updateAnimationClips(0)
addPlayPauseListener(this.onPlayPause)
addFrameListener(this.animate)
addCanvasResizeListener(this.updateSize)
Expand Down
26 changes: 20 additions & 6 deletions src/viewer/clients/loadReplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,25 @@ const fetchByURL = (url: string) =>
},
}).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()
}
135 changes: 135 additions & 0 deletions src/viewer/components/CompactPlayControls.tsx
Original file line number Diff line number Diff line change
@@ -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<Props, State> {
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 (
<ControlsWrapper>
<Grid container spacing={24} alignItems="center">
<Grid item>
<Button onClick={this.setPlayPause}>
{paused ? <PlayIcon /> : <PausedIcon />}
</Button>
</Grid>
<Grid item zeroMinWidth xs>
<Slider
classes={{
thumb,
track,
focused,
}}
/>
</Grid>
<Grid item>
<Button onClick={this.showCameraControls}>
<Camera />
</Button>
</Grid>
</Grid>
<Dialog open={cameraControlsShowing} onClose={this.hideCameraControls}>
<DialogTitle>Camera Controls</DialogTitle>
<DialogContent>
<Typography>Field Cameras</Typography>
<FieldCameraControls />
<Typography>Player Cameras</Typography>
<PlayerCameraControls />
</DialogContent>
</Dialog>
</ControlsWrapper>
)
}
}

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)
2 changes: 1 addition & 1 deletion src/viewer/components/GameManagerLoader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class GameManagerLoader extends Component<Props, State> {
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
Expand Down
10 changes: 8 additions & 2 deletions src/viewer/components/ReplayViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ 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"
import Scoreboard from "./ScoreBoard"

interface Props {
gameManager: GameManager
autoplay?: boolean
}

interface State {
Expand All @@ -34,10 +36,13 @@ class ReplayViewer extends PureComponent<Props, State> {
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)
}
Expand Down Expand Up @@ -77,6 +82,7 @@ class ReplayViewer extends PureComponent<Props, State> {
</Typography>
</Button>
</FullscreenToggle>
{this.props.children}
</FullscreenWrapper>
</ViewerContainer>
)
Expand Down
Loading

0 comments on commit ae660b9

Please sign in to comment.