diff --git a/src/app/WardrobePage/OutfitControls.js b/src/app/WardrobePage/OutfitControls.js index 0bccf4c..9f781a4 100644 --- a/src/app/WardrobePage/OutfitControls.js +++ b/src/app/WardrobePage/OutfitControls.js @@ -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 }) { + + + @@ -238,6 +243,23 @@ function CopyLinkButton({ outfitState }) { ); } +function PlayPauseButton() { + const [isPaused, setIsPaused] = useLocalStorage("DTIOutfitIsPaused", true); + + const label = isPaused ? "Start animations" : "Stop animations"; + return ( + + + : } + aria-label={label} + onClick={() => setIsPaused(!isPaused)} + /> + + + ); +} + /** * ControlButton is a UI helper to render the cute round buttons we use in * OutfitControls! diff --git a/src/app/components/OutfitCanvas.js b/src/app/components/OutfitCanvas.js index 4fdfc76..775109b 100644 --- a/src/app/components/OutfitCanvas.js +++ b/src/app/components/OutfitCanvas.js @@ -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; } diff --git a/src/app/components/OutfitPreview.js b/src/app/components/OutfitPreview.js index 656bb29..a22dd99 100644 --- a/src/app/components/OutfitPreview.js +++ b/src/app/components/OutfitPreview.js @@ -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 && ( - + {visibleLayers.map((layer) => layer.canvasMovieLibraryUrl ? ( { + 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]; } diff --git a/src/stories/OutfitCanvas.stories.js b/src/stories/OutfitCanvas.stories.js index 60aae55..7f8a2ad 100644 --- a/src/stories/OutfitCanvas.stories.js +++ b/src/stories/OutfitCanvas.stories.js @@ -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) => ( - + {args.pet === "Blue Acara" && ( <>