play/pause button for animations
This commit is contained in:
parent
88f5c1f1aa
commit
5879324ebb
5 changed files with 74 additions and 7 deletions
|
@ -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!
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue