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,
|
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!
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue