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" && (
<>