play/pause button for animations

This commit is contained in:
Emi Matchu 2020-09-22 05:39:48 -07:00
parent 88f5c1f1aa
commit 5879324ebb
5 changed files with 74 additions and 7 deletions

View file

@ -16,11 +16,13 @@ import {
DownloadIcon,
LinkIcon,
} from "@chakra-ui/icons";
import { MdPause, MdPlayArrow } from "react-icons/md";
import { Link } from "react-router-dom";
import PosePicker from "./PosePicker";
import SpeciesColorPicker from "../components/SpeciesColorPicker";
import { useLocalStorage } from "../util";
import useOutfitAppearance from "../components/useOutfitAppearance";
import { Link } from "react-router-dom";
/**
* OutfitControls is the set of controls layered over the outfit preview, to
@ -150,6 +152,9 @@ function OutfitControls({ outfitState, dispatchToOutfit }) {
<Box>
<CopyLinkButton outfitState={outfitState} />
</Box>
<Box>
<PlayPauseButton />
</Box>
</Stack>
<Box gridArea="space" onClick={maybeUnlockFocus} />
<Flex gridArea="picker" justify="center" onClick={maybeUnlockFocus}>
@ -238,6 +243,23 @@ function CopyLinkButton({ outfitState }) {
);
}
function PlayPauseButton() {
const [isPaused, setIsPaused] = useLocalStorage("DTIOutfitIsPaused", true);
const label = isPaused ? "Start animations" : "Stop animations";
return (
<Tooltip label={label} placement="left">
<Box>
<ControlButton
icon={isPaused ? <MdPlayArrow /> : <MdPause />}
aria-label={label}
onClick={() => setIsPaused(!isPaused)}
/>
</Box>
</Tooltip>
);
}
/**
* ControlButton is a UI helper to render the cute round buttons we use in
* OutfitControls!

View file

@ -8,7 +8,7 @@ const EaselContext = React.createContext({
removeResizeListener: () => {},
});
function OutfitCanvas({ children, width, height }) {
function OutfitCanvas({ children, width, height, pauseMovieLayers }) {
const [stage, setStage] = React.useState(null);
const resizeListenersRef = React.useRef([]);
const canvasRef = React.useRef(null);
@ -78,6 +78,16 @@ function OutfitCanvas({ children, width, height }) {
// updating here actually paused all movies! So, don't!)
}, [stage, width, height]);
// When it's time to pause/unpause the movie layers, we implement this by
// disabling/enabling passing ticks along to the children. We don't stop
// playing the ticks altogether though, because we do want our fade-in/out
// transitions to keep playing!
React.useEffect(() => {
if (stage) {
stage.tickOnUpdate = !pauseMovieLayers;
}
}, [stage, pauseMovieLayers]);
if (loading) {
return null;
}

View file

@ -11,6 +11,7 @@ import OutfitCanvas, {
useEaselDependenciesLoader,
} from "./OutfitCanvas";
import HangerSpinner from "./HangerSpinner";
import { useLocalStorage } from "../util";
import useOutfitAppearance from "./useOutfitAppearance";
/**
@ -96,9 +97,10 @@ export function OutfitLayers({
);
const { loading: loadingEasel } = useEaselDependenciesLoader();
const loadingAnything = loading || loadingEasel;
const [isPaused] = useLocalStorage("DTIOutfitIsPaused", true);
// When we start in a loading state, or re-enter a loading state, start the
// loading delay timer.
React.useEffect(() => {
@ -152,7 +154,11 @@ export function OutfitLayers({
engine === "canvas" ? (
!loadingEasel && (
<FullScreenCenter>
<OutfitCanvas width={canvasSize} height={canvasSize}>
<OutfitCanvas
width={canvasSize}
height={canvasSize}
pauseMovieLayers={isPaused}
>
{visibleLayers.map((layer) =>
layer.canvasMovieLibraryUrl ? (
<OutfitCanvasMovie

View file

@ -180,8 +180,9 @@ export function useFetch(url, { responseType }) {
*
* Adapted from https://usehooks.com/useLocalStorage/.
*/
let storageListeners = [];
export function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = React.useState(() => {
const loadValue = React.useCallback(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
@ -189,16 +190,38 @@ export function useLocalStorage(key, initialValue) {
console.log(error);
return initialValue;
}
});
}, [key, initialValue]);
const [storedValue, setStoredValue] = React.useState(loadValue);
const setValue = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
storageListeners.forEach((l) => l());
} catch (error) {
console.log(error);
}
};
const reloadValue = React.useCallback(() => {
setStoredValue(loadValue());
}, [loadValue, setStoredValue]);
// Listen for changes elsewhere on the page, and update here too!
React.useEffect(() => {
storageListeners.push(reloadValue);
return () => {
storageListeners = storageListeners.filter((l) => l !== reloadValue);
};
}, [reloadValue]);
// Listen for changes in other tabs, and update here too! (This does not
// catch same-page updates!)
React.useEffect(() => {
window.addEventListener("storage", reloadValue);
return () => window.removeEventListener("storage", reloadValue);
}, [reloadValue]);
return [storedValue, setValue];
}

View file

@ -9,6 +9,9 @@ export default {
title: "Dress to Impress/OutfitCanvas",
component: OutfitCanvas,
argTypes: {
paused: {
name: "Paused",
},
pet: {
name: "Pet",
control: {
@ -31,7 +34,7 @@ export default {
// So this is noticeably faster!
const Template = (args) => (
<OutfitCanvas width={300} height={300}>
<OutfitCanvas width={300} height={300} pauseMovieLayers={args.paused}>
{args.pet === "Blue Acara" && (
<>
<OutfitCanvasImage
@ -73,16 +76,19 @@ export const BlueAcara = Template.bind({});
BlueAcara.args = {
pet: "Blue Acara",
items: [],
paused: false,
};
export const BubblesOnWaterForeground = Template.bind({});
BubblesOnWaterForeground.args = {
pet: "None",
items: ["Bubbles In Water Foreground"],
paused: false,
};
export const BlueAcaraWithForeground = Template.bind({});
BlueAcaraWithForeground.args = {
pet: "Blue Acara",
items: ["Bubbles In Water Foreground"],
paused: false,
};