diff --git a/src/app/OutfitPreview.js b/src/app/OutfitPreview.js index 1c73ebb..394e9d9 100644 --- a/src/app/OutfitPreview.js +++ b/src/app/OutfitPreview.js @@ -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 ( @@ -25,10 +32,18 @@ function OutfitPreview({ outfitState }) { ); } + console.log( + "Loading?", + loading1 || loading2, + loading1, + loading2, + visibleLayers + ); + return ( ); @@ -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 ( @@ -64,7 +75,7 @@ export function OutfitLayers({ loading, visibleLayers, doAnimations = false }) { timeout={200} > - ))} - {loading && ( - - - - - - - - - )} + + + + + + + + ); } @@ -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; diff --git a/src/app/useOutfitAppearance.js b/src/app/useOutfitAppearance.js index 0e7aa3a..58b4401 100644 --- a/src/app/useOutfitAppearance.js +++ b/src/app/useOutfitAppearance.js @@ -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 }; }