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, DownloadIcon,
LinkIcon, LinkIcon,
} from "@chakra-ui/icons"; } from "@chakra-ui/icons";
import { MdPause, MdPlayArrow } from "react-icons/md";
import { Link } from "react-router-dom";
import PosePicker from "./PosePicker"; import PosePicker from "./PosePicker";
import SpeciesColorPicker from "../components/SpeciesColorPicker"; import SpeciesColorPicker from "../components/SpeciesColorPicker";
import { useLocalStorage } from "../util";
import useOutfitAppearance from "../components/useOutfitAppearance"; import useOutfitAppearance from "../components/useOutfitAppearance";
import { Link } from "react-router-dom";
/** /**
* OutfitControls is the set of controls layered over the outfit preview, to * OutfitControls is the set of controls layered over the outfit preview, to
@ -150,6 +152,9 @@ function OutfitControls({ outfitState, dispatchToOutfit }) {
<Box> <Box>
<CopyLinkButton outfitState={outfitState} /> <CopyLinkButton outfitState={outfitState} />
</Box> </Box>
<Box>
<PlayPauseButton />
</Box>
</Stack> </Stack>
<Box gridArea="space" onClick={maybeUnlockFocus} /> <Box gridArea="space" onClick={maybeUnlockFocus} />
<Flex gridArea="picker" justify="center" 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 * ControlButton is a UI helper to render the cute round buttons we use in
* OutfitControls! * OutfitControls!

View file

@ -8,7 +8,7 @@ const EaselContext = React.createContext({
removeResizeListener: () => {}, removeResizeListener: () => {},
}); });
function OutfitCanvas({ children, width, height }) { function OutfitCanvas({ children, width, height, pauseMovieLayers }) {
const [stage, setStage] = React.useState(null); const [stage, setStage] = React.useState(null);
const resizeListenersRef = React.useRef([]); const resizeListenersRef = React.useRef([]);
const canvasRef = React.useRef(null); const canvasRef = React.useRef(null);
@ -78,6 +78,16 @@ function OutfitCanvas({ children, width, height }) {
// updating here actually paused all movies! So, don't!) // updating here actually paused all movies! So, don't!)
}, [stage, width, height]); }, [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) { if (loading) {
return null; return null;
} }

View file

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

View file

@ -180,8 +180,9 @@ export function useFetch(url, { responseType }) {
* *
* Adapted from https://usehooks.com/useLocalStorage/. * Adapted from https://usehooks.com/useLocalStorage/.
*/ */
let storageListeners = [];
export function useLocalStorage(key, initialValue) { export function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = React.useState(() => { const loadValue = React.useCallback(() => {
try { try {
const item = window.localStorage.getItem(key); const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue; return item ? JSON.parse(item) : initialValue;
@ -189,16 +190,38 @@ export function useLocalStorage(key, initialValue) {
console.log(error); console.log(error);
return initialValue; return initialValue;
} }
}); }, [key, initialValue]);
const [storedValue, setStoredValue] = React.useState(loadValue);
const setValue = (value) => { const setValue = (value) => {
try { try {
setStoredValue(value); setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value)); window.localStorage.setItem(key, JSON.stringify(value));
storageListeners.forEach((l) => l());
} catch (error) { } catch (error) {
console.log(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]; return [storedValue, setValue];
} }

View file

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