OutfitPreview waits for all new layers to load

This commit is contained in:
Matt Dunn-Rankin 2020-06-05 23:56:42 -07:00
parent 82078d20bb
commit 462488a8f8
2 changed files with 116 additions and 32 deletions

View file

@ -2,18 +2,25 @@ import React from "react";
import { css, cx } from "emotion"; import { css, cx } from "emotion";
import { CSSTransition, TransitionGroup } from "react-transition-group"; import { CSSTransition, TransitionGroup } from "react-transition-group";
import { Box, Flex, Icon, Image, Spinner, Text } from "@chakra-ui/core"; import { Box, Flex, Icon, Spinner, Text } from "@chakra-ui/core";
import { Delay } from "./util";
import useOutfitAppearance from "./useOutfitAppearance"; import useOutfitAppearance from "./useOutfitAppearance";
/** /**
* OutfitPreview renders the actual image layers for the outfit we're viewing! * OutfitPreview renders the actual image layers for the outfit we're viewing!
*/ */
function OutfitPreview({ outfitState }) { function OutfitPreview({ outfitState }) {
const { loading, error, visibleLayers } = useOutfitAppearance(outfitState); const {
loading: loading1,
error: error1,
visibleLayers,
} = useOutfitAppearance(outfitState);
if (error) { const { loading: loading2, error: error2, loadedLayers } = usePreloadLayers(
visibleLayers
);
if (error1 || error2) {
return ( return (
<FullScreenCenter> <FullScreenCenter>
<Text color="gray.50" d="flex" alignItems="center"> <Text color="gray.50" d="flex" alignItems="center">
@ -25,10 +32,18 @@ function OutfitPreview({ outfitState }) {
); );
} }
console.log(
"Loading?",
loading1 || loading2,
loading1,
loading2,
visibleLayers
);
return ( return (
<OutfitLayers <OutfitLayers
loading={loading} loading={loading1 || loading2}
visibleLayers={visibleLayers} visibleLayers={loadedLayers}
doAnimations doAnimations
/> />
); );
@ -39,10 +54,6 @@ function OutfitPreview({ outfitState }) {
* used both in the main outfit preview, and in other minor UIs! * used both in the main outfit preview, and in other minor UIs!
*/ */
export function OutfitLayers({ loading, visibleLayers, doAnimations = false }) { export function OutfitLayers({ loading, visibleLayers, doAnimations = false }) {
// If we're fading in, we should use Image, to detect the load success. But
// if not, just use a plain img, so that we load instantly without a flicker!
const ImageTag = doAnimations ? Image : "img";
return ( return (
<Box pos="relative" height="100%" width="100%"> <Box pos="relative" height="100%" width="100%">
<TransitionGroup enter={false} exit={doAnimations}> <TransitionGroup enter={false} exit={doAnimations}>
@ -64,7 +75,7 @@ export function OutfitLayers({ loading, visibleLayers, doAnimations = false }) {
timeout={200} timeout={200}
> >
<FullScreenCenter> <FullScreenCenter>
<ImageTag <img
src={getBestImageUrlForLayer(layer)} src={getBestImageUrlForLayer(layer)}
alt="" alt=""
// We manage the fade-in and fade-out separately! The fade-in // We manage the fade-in and fade-out separately! The fade-in
@ -76,14 +87,18 @@ export function OutfitLayers({ loading, visibleLayers, doAnimations = false }) {
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
transition: opacity 0.2s;
&.do-animations { &.do-animations {
opacity: 0.01; animation: fade-in 0.2s;
} }
&.do-animations[src] { @keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1; opacity: 1;
} }
}
`, `,
doAnimations && "do-animations" doAnimations && "do-animations"
)} )}
@ -98,8 +113,12 @@ export function OutfitLayers({ loading, visibleLayers, doAnimations = false }) {
</CSSTransition> </CSSTransition>
))} ))}
</TransitionGroup> </TransitionGroup>
{loading && ( <Box
<Delay ms={0}> // This is similar to our Delay util component, but Delay disappears
// immediately on load, whereas we want this to fade out smoothly.
opacity={loading ? 1 : 0}
transition={`opacity 0.2s ${loading ? "0.5s" : "0s"}`}
>
<FullScreenCenter> <FullScreenCenter>
<Box <Box
width="100%" width="100%"
@ -111,8 +130,7 @@ export function OutfitLayers({ loading, visibleLayers, doAnimations = false }) {
<FullScreenCenter> <FullScreenCenter>
<Spinner color="green.400" size="xl" /> <Spinner color="green.400" size="xl" />
</FullScreenCenter> </FullScreenCenter>
</Delay> </Box>
)}
</Box> </Box>
); );
} }
@ -141,4 +159,63 @@ function getBestImageUrlForLayer(layer) {
} }
} }
function loadImage(url) {
const image = new Image();
const promise = new Promise((resolve, reject) => {
image.onload = () => resolve();
image.onerror = () => reject();
image.src = url;
});
promise.cancel = () => {
image.src = "";
};
return promise;
}
/**
* usePreloadLayers preloads the images for the given layers, and yields them
* when done. This enables us to keep the old outfit preview on screen until
* all the new layers are ready, then show them all at once!
*/
function usePreloadLayers(layers) {
const [error, setError] = React.useState(null);
const [loadedLayers, setLoadedLayers] = React.useState([]);
React.useEffect(() => {
console.log("layers changed!", layers);
let canceled = false;
setError(null);
const loadImages = async () => {
const imagePromises = layers.map(getBestImageUrlForLayer).map(loadImage);
try {
// TODO: Load in one at a time, under a loading spinner & delay?
await Promise.all(imagePromises);
} catch (e) {
if (canceled) return;
console.error("Error preloading outfit layers", e);
imagePromises.forEach((p) => p.cancel());
setError(e);
return;
}
if (canceled) return;
setLoadedLayers(layers);
console.log("Loaded layers", layers);
};
loadImages();
return () => {
canceled = true;
};
}, [layers]);
// NOTE: This condition would need to change if we started loading one at a
// time, or if the error case would need to show a partial state!
const loading = loadedLayers !== layers;
return { loading, error, loadedLayers };
}
export default OutfitPreview; export default OutfitPreview;

View file

@ -1,3 +1,4 @@
import React from "react";
import gql from "graphql-tag"; import gql from "graphql-tag";
import { useQuery } from "@apollo/react-hooks"; import { useQuery } from "@apollo/react-hooks";
@ -40,8 +41,14 @@ export default function useOutfitAppearance(outfitState) {
} }
); );
const itemAppearances = (data?.items || []).map((i) => i.appearanceOn); const itemAppearances = React.useMemo(
const visibleLayers = getVisibleLayers(data?.petAppearance, itemAppearances); () => (data?.items || []).map((i) => i.appearanceOn),
[data]
);
const visibleLayers = React.useMemo(
() => getVisibleLayers(data?.petAppearance, itemAppearances),
[data, itemAppearances]
);
return { loading, error, visibleLayers }; return { loading, error, visibleLayers };
} }