Skip to content

Commit

Permalink
Merge pull request #430 from charlielee/issue-380
Browse files Browse the repository at this point in the history
Reimplement short play
  • Loading branch information
charlielee committed Dec 16, 2022
2 parents 2aa6094 + 287e818 commit 0ec2598
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 33 deletions.
2 changes: 2 additions & 0 deletions src/common/UserPreferences.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
export interface UserPreferences {
playCaptureSound: boolean;
shortPlayLength: number;
workingDirectory: string | undefined;
}

export const defaultUserPreferences: UserPreferences = {
playCaptureSound: true,
shortPlayLength: 6,
workingDirectory: undefined,
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useState } from "react";
import { useSelector } from "react-redux";
import PlaybackContext, {
PlaybackFrameName,
} from "../../../context/PlaybackContext/PlaybackContext";
import { RootState } from "../../../redux/store";
import IconName from "../../common/Icon/IconName";
import IconButton from "../../common/IconButton/IconButton";
import InputRange from "../../common/Input/InputRange/InputRange";
Expand All @@ -15,15 +17,22 @@ interface AnimationToolbarProps {
startOrPausePlayback: () => void;
stopPlayback: () => void;
displayFrame: (name: PlaybackFrameName) => void;
shortPlay: () => void;
playing: boolean;
}

const AnimationToolbar = ({
startOrPausePlayback,
stopPlayback,
displayFrame,
shortPlay,
playing,
}: AnimationToolbarProps): JSX.Element => {
const shortPlayLength = useSelector(
(state: RootState) => state.app.userPreferences.shortPlayLength
);
const shortPlayFrameText = shortPlayLength === 1 ? "frame" : "frames";

const [onionSkinAmount, setOnionSkinAmount] = useState(0);
const [loopPlayback, setLoopPlayback] = useState(false);

Expand All @@ -36,9 +45,9 @@ const AnimationToolbar = ({
onClick={() => undefined}
/>
<IconButton
title="Short Play"
title={`Short Play (${shortPlayLength} ${shortPlayFrameText})`}
icon={IconName.PLAY_SHORT}
onClick={() => undefined}
onClick={shortPlay}
/>
<InputRange
id="animation-toolbar__onion-skin-range"
Expand Down Expand Up @@ -70,7 +79,7 @@ const AnimationToolbar = ({
<IconButton
title="Stop Playback"
icon={IconName.PLAY_STOP}
onClick={() => stopPlayback()}
onClick={stopPlayback}
/>
<IconButton
title="Next Frame"
Expand Down
8 changes: 7 additions & 1 deletion src/renderer/components/animator/Animator/Animator.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useSelector } from "react-redux";
import { Take } from "../../../../common/project/Take";
import PlaybackContextProvider from "../../../context/PlaybackContext/PlaybackContextProvider";
import { RootState } from "../../../redux/store";
import Content from "../../common/Content/Content";
import IconName from "../../common/Icon/IconName";
import Page from "../../common/Page/Page";
Expand Down Expand Up @@ -62,8 +64,12 @@ const Animator = ({ take }: AnimatorWithProviderProps): JSX.Element => {
const AnimatorWithProvider = ({
take,
}: AnimatorWithProviderProps): JSX.Element => {
const shortPlayLength = useSelector(
(state: RootState) => state.app.userPreferences.shortPlayLength
);

return (
<PlaybackContextProvider take={take}>
<PlaybackContextProvider take={take} shortPlayLength={shortPlayLength}>
<Animator take={take} />
</PlaybackContextProvider>
);
Expand Down
23 changes: 20 additions & 3 deletions src/renderer/components/common/Input/InputNumber/InputNumber.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { useRef, useState } from "react";

interface InputNumberProps {
id?: string;
min: number;
max: number;
value: number;
onChange(newValue: number): void;
validateOnChange?: boolean;
}

const InputNumber = ({
Expand All @@ -12,18 +15,32 @@ const InputNumber = ({
max,
value,
onChange,
validateOnChange = false,
}: InputNumberProps): JSX.Element => {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) =>
onChange(parseInt(event.target.value, 10));
const [rawValue, setRawValue] = useState(value.toString(10));
const inputRef = useRef<HTMLInputElement>(null);

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (!inputRef.current) {
return;
}

setRawValue(event.target.value);
if (!validateOnChange || inputRef.current.reportValidity()) {
onChange(parseInt(event.target.value, 10));
}
};

return (
<input
ref={inputRef}
id={id}
type="number"
onChange={handleChange}
min={min}
max={max}
value={value}
value={rawValue}
required
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ContentBlock from "../../common/ContentBlock/ContentBlock";
import IconName from "../../common/Icon/IconName";
import InputGroup from "../../common/Input/InputGroup/InputGroup";
import InputLabel from "../../common/Input/InputLabel/InputLabel";
import InputNumber from "../../common/Input/InputNumber/InputNumber";
import InputSwitch from "../../common/Input/InputSwitch/InputSwitch";
import Modal from "../../common/Modal/Modal";
import ModalBody from "../../common/ModalBody/ModalBody";
Expand Down Expand Up @@ -58,6 +59,26 @@ const PreferencesModal = (): JSX.Element => {
Play capture sound
</InputLabel>
</InputGroup>

<InputGroup row>
<InputLabel inputId="preferencesShortPlayLength">
Short play length
</InputLabel>
<InputNumber
id="preferencesShortPlayLength"
min={1}
max={99}
value={userPreferences.shortPlayLength}
onChange={(newValue) =>
dispatch(
editUserPreferences({
shortPlayLength: newValue,
})
)
}
validateOnChange
/>
</InputGroup>
</ContentBlock>
</Content>
</PageBody>
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/context/PlaybackContext/PlaybackContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface PlaybackContextProps {
startOrPausePlayback: () => void;
stopPlayback: (i?: TimelineIndex | undefined, pause?: boolean) => void;
displayFrame: (name: PlaybackFrameName) => void;
shortPlay: () => void;
timelineIndex: TimelineIndex | undefined;
liveViewVisible: boolean;
playing: boolean;
Expand All @@ -21,6 +22,7 @@ const defaultValue: PlaybackContextProps = {
startOrPausePlayback: () => undefined,
stopPlayback: () => undefined,
displayFrame: () => undefined,
shortPlay: () => undefined,
timelineIndex: undefined,
liveViewVisible: true,
playing: false,
Expand Down
66 changes: 43 additions & 23 deletions src/renderer/context/PlaybackContext/PlaybackContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ReactNode, useRef, useState } from "react";
import { TimelineIndex } from "../../../common/Flavors";
import { FrameCount, TimelineIndex } from "../../../common/Flavors";
import { Take } from "../../../common/project/Take";
import useLinkedRefAndState from "../../hooks/useLinkedRefAndState";
import useRequestAnimationFrame from "../../hooks/useRequestAnimationFrame";
import { getTrackLength } from "../../services/project/projectCalculator";
import * as rLogger from "../../services/rLogger/rLogger";
Expand All @@ -10,44 +11,50 @@ import PlaybackContext, {
} from "./PlaybackContext";

interface PlaybackContextProviderProps {
shortPlayLength: FrameCount;
take: Take;
children: ReactNode;
}

const PlaybackContextProvider = ({
shortPlayLength,
take,
children,
}: PlaybackContextProviderProps) => {
const playForDuration = getTrackLength(take.frameTrack);

// Note: an `undefined` timeline index indicates the application is showing the live view
const [timelineIndex, setTimelineIndex] = useState<TimelineIndex | undefined>(
undefined
);
// An `undefined` timeline index indicates the application is showing the live view
const [timelineIndex, timelineIndexRef, setTimelineIndex] =
useLinkedRefAndState<TimelineIndex | undefined>(undefined);
const [playing, playingRef, setPlaying] = useLinkedRefAndState(false);

const [liveViewVisible, setLiveViewVisible] = useState(true);
const [playing, setPlaying] = useState(false);

const delay = 1000 / take.frameRate;
const previousTime = useRef<number>(0);
const animationFrameIndex = useRef<TimelineIndex | undefined>(undefined);
const lastFrameIndex = useRef<TimelineIndex>(0);

const [start, stop] = useRequestAnimationFrame((newTime) => {
const [startRAF, stopRAF] = useRequestAnimationFrame((newTime) => {
if (!playingRef.current) {
previousTime.current = newTime;
setPlaying(true);
}

if (
animationFrameIndex.current === undefined ||
timelineIndexRef.current === undefined ||
newTime >= previousTime.current + delay
) {
previousTime.current = newTime;

switch (animationFrameIndex.current) {
switch (timelineIndexRef.current) {
case undefined:
_updateFrameIndex(0);
setTimelineIndex(0);
break;
case lastFrameIndex.current:
stopPlayback();
break;
default:
_updateFrameIndex(animationFrameIndex.current + 1);
setTimelineIndex(timelineIndexRef.current + 1);
break;
}
}
Expand All @@ -58,10 +65,16 @@ const PlaybackContextProvider = ({

const stopPlayback = (i?: TimelineIndex | undefined) => {
_logPlayback("playback.stopPlayback");
stop();
_updateFrameIndex(i === undefined ? undefined : i);
setLiveViewVisible(i === undefined);
stopRAF();
setPlaying(false);

if (i === undefined || playForDuration === 0) {
setTimelineIndex(undefined);
setLiveViewVisible(true);
} else {
setTimelineIndex(i);
setLiveViewVisible(false);
}
};

const displayFrame = (name: PlaybackFrameName) => {
Expand All @@ -77,13 +90,24 @@ const PlaybackContextProvider = ({
}
};

const shortPlay = () => {
_logPlayback("playback.shortPlay");
const playFromFrame = playForDuration - shortPlayLength;

if (playFromFrame > 0) {
stopPlayback(playFromFrame);
} else {
stopPlayback(0);
}
_startPlayback();
};

const _startPlayback = () => {
_logPlayback("playback.startPlayback");
if (playForDuration > 0) {
lastFrameIndex.current = playForDuration - 1;
start();
startRAF();
setLiveViewVisible(false);
setPlaying(true);
}
};

Expand Down Expand Up @@ -130,22 +154,18 @@ const PlaybackContextProvider = ({
}
};

const _updateFrameIndex = (i: TimelineIndex | undefined) => {
animationFrameIndex.current = i;
setTimelineIndex(i);
};

const _logPlayback = (loggingCode: string) =>
rLogger.info(loggingCode, {
playForDuration,
frameRate: take.frameRate,
timelineIndex: animationFrameIndex.current ?? "(showing live view)",
timelineIndex: timelineIndexRef.current ?? "(showing live view)",
});

const value: PlaybackContextProps = {
startOrPausePlayback,
stopPlayback,
displayFrame,
shortPlay,
timelineIndex,
liveViewVisible,
playing,
Expand Down
17 changes: 17 additions & 0 deletions src/renderer/hooks/useLinkedRefAndState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { MutableRefObject, useRef, useState } from "react";

const useLinkedRefAndState = <T>(
initialValue: T
): [T, MutableRefObject<T>, (newValue: T) => void] => {
const [state, setState] = useState<T>(initialValue);
const ref = useRef<T>(initialValue);

const setRefAndState = (newValue: T) => {
setState(newValue);
ref.current = newValue;
};

return [state, ref, setRefAndState];
};

export default useLinkedRefAndState;
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ const useRequestAnimationFrame = (
callback(time);
};

const start = () => {
const startRAF = () => {
requestId.current = requestAnimationFrame(animate);
};

const stop = () => {
const stopRAF = () => {
cancelAnimationFrame(requestId.current ?? 0);
};

return [start, stop];
return [startRAF, stopRAF];
};

export default useRequestAnimationFrame;

0 comments on commit 0ec2598

Please sign in to comment.