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 { 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";
/**
* OutfitPreview renders the actual image layers for the outfit we're viewing!
*/
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 (
<FullScreenCenter>
<Text color="gray.50" d="flex" alignItems="center">
@ -25,10 +32,18 @@ function OutfitPreview({ outfitState }) {
);
}
console.log(
"Loading?",
loading1 || loading2,
loading1,
loading2,
visibleLayers
);
return (
<OutfitLayers
loading={loading}
visibleLayers={visibleLayers}
loading={loading1 || loading2}
visibleLayers={loadedLayers}
doAnimations
/>
);
@ -39,10 +54,6 @@ function OutfitPreview({ outfitState }) {
* used both in the main outfit preview, and in other minor UIs!
*/
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 (
<Box pos="relative" height="100%" width="100%">
<TransitionGroup enter={false} exit={doAnimations}>
@ -64,7 +75,7 @@ export function OutfitLayers({ loading, visibleLayers, doAnimations = false }) {
timeout={200}
>
<FullScreenCenter>
<ImageTag
<img
src={getBestImageUrlForLayer(layer)}
alt=""
// We manage the fade-in and fade-out separately! The fade-in
@ -76,13 +87,17 @@ export function OutfitLayers({ loading, visibleLayers, doAnimations = false }) {
max-width: 100%;
max-height: 100%;
transition: opacity 0.2s;
&.do-animations {
opacity: 0.01;
animation: fade-in 0.2s;
}
&.do-animations[src] {
opacity: 1;
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
`,
doAnimations && "do-animations"
@ -98,21 +113,24 @@ export function OutfitLayers({ loading, visibleLayers, doAnimations = false }) {
</CSSTransition>
))}
</TransitionGroup>
{loading && (
<Delay ms={0}>
<FullScreenCenter>
<Box
width="100%"
height="100%"
backgroundColor="gray.900"
opacity="0.8"
/>
</FullScreenCenter>
<FullScreenCenter>
<Spinner color="green.400" size="xl" />
</FullScreenCenter>
</Delay>
)}
<Box
// 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>
<Box
width="100%"
height="100%"
backgroundColor="gray.900"
opacity="0.8"
/>
</FullScreenCenter>
<FullScreenCenter>
<Spinner color="green.400" size="xl" />
</FullScreenCenter>
</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;

View file

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