2020-04-23 13:31:39 -07:00
|
|
|
import React from "react";
|
2020-05-03 00:15:03 -07:00
|
|
|
import { css, cx } from "emotion";
|
2020-04-24 00:28:00 -07:00
|
|
|
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
2020-04-23 13:31:39 -07:00
|
|
|
|
2020-07-20 21:32:42 -07:00
|
|
|
import { Box, Flex, Spinner, Text } from "@chakra-ui/core";
|
|
|
|
import { WarningIcon } from "@chakra-ui/icons";
|
2020-04-24 19:16:24 -07:00
|
|
|
|
2020-05-02 13:40:37 -07:00
|
|
|
import useOutfitAppearance from "./useOutfitAppearance";
|
2020-04-24 23:13:28 -07:00
|
|
|
|
2020-05-02 13:40:37 -07:00
|
|
|
/**
|
|
|
|
* OutfitPreview renders the actual image layers for the outfit we're viewing!
|
|
|
|
*/
|
|
|
|
function OutfitPreview({ outfitState }) {
|
2020-06-05 23:56:42 -07:00
|
|
|
const {
|
|
|
|
loading: loading1,
|
|
|
|
error: error1,
|
|
|
|
visibleLayers,
|
|
|
|
} = useOutfitAppearance(outfitState);
|
2020-04-25 07:22:03 -07:00
|
|
|
|
2020-06-05 23:56:42 -07:00
|
|
|
const { loading: loading2, error: error2, loadedLayers } = usePreloadLayers(
|
|
|
|
visibleLayers
|
|
|
|
);
|
|
|
|
|
|
|
|
if (error1 || error2) {
|
2020-04-23 13:31:39 -07:00
|
|
|
return (
|
|
|
|
<FullScreenCenter>
|
|
|
|
<Text color="gray.50" d="flex" alignItems="center">
|
2020-07-20 21:32:42 -07:00
|
|
|
<WarningIcon />
|
2020-04-23 13:31:39 -07:00
|
|
|
<Box width={2} />
|
|
|
|
Could not load preview. Try again?
|
|
|
|
</Text>
|
|
|
|
</FullScreenCenter>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-05-02 22:59:30 -07:00
|
|
|
return (
|
|
|
|
<OutfitLayers
|
2020-06-05 23:56:42 -07:00
|
|
|
loading={loading1 || loading2}
|
|
|
|
visibleLayers={loadedLayers}
|
2020-05-02 22:59:30 -07:00
|
|
|
doAnimations
|
|
|
|
/>
|
|
|
|
);
|
2020-05-02 21:04:54 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* OutfitLayers is the raw UI component for rendering outfit layers. It's
|
|
|
|
* used both in the main outfit preview, and in other minor UIs!
|
|
|
|
*/
|
2020-05-02 22:59:30 -07:00
|
|
|
export function OutfitLayers({ loading, visibleLayers, doAnimations = false }) {
|
2020-04-23 15:19:36 -07:00
|
|
|
return (
|
2020-05-02 13:40:37 -07:00
|
|
|
<Box pos="relative" height="100%" width="100%">
|
2020-05-02 23:16:26 -07:00
|
|
|
<TransitionGroup enter={false} exit={doAnimations}>
|
2020-04-24 23:13:28 -07:00
|
|
|
{visibleLayers.map((layer) => (
|
2020-04-24 00:28:00 -07:00
|
|
|
<CSSTransition
|
2020-05-02 23:32:45 -07:00
|
|
|
// We manage the fade-in and fade-out separately! The fade-out
|
|
|
|
// happens here, when the layer exits the DOM.
|
2020-04-24 00:28:00 -07:00
|
|
|
key={layer.id}
|
2020-04-25 23:17:59 -07:00
|
|
|
classNames={css`
|
|
|
|
&-exit {
|
|
|
|
opacity: 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
&-exit-active {
|
|
|
|
opacity: 0;
|
|
|
|
transition: opacity 0.2s;
|
|
|
|
}
|
|
|
|
`}
|
2020-04-24 00:28:00 -07:00
|
|
|
timeout={200}
|
|
|
|
>
|
|
|
|
<FullScreenCenter>
|
2020-06-05 23:56:42 -07:00
|
|
|
<img
|
2020-05-11 21:19:34 -07:00
|
|
|
src={getBestImageUrlForLayer(layer)}
|
2020-05-18 01:08:56 -07:00
|
|
|
alt=""
|
2020-05-02 13:40:37 -07:00
|
|
|
// We manage the fade-in and fade-out separately! The fade-in
|
|
|
|
// happens here, when the <Image> finishes preloading and
|
|
|
|
// applies the src to the underlying <img>.
|
2020-05-03 00:15:03 -07:00
|
|
|
className={cx(
|
2020-05-02 22:59:30 -07:00
|
|
|
css`
|
2020-05-03 00:15:03 -07:00
|
|
|
object-fit: contain;
|
|
|
|
max-width: 100%;
|
|
|
|
max-height: 100%;
|
|
|
|
|
|
|
|
&.do-animations {
|
2020-06-05 23:56:42 -07:00
|
|
|
animation: fade-in 0.2s;
|
2020-05-03 00:15:03 -07:00
|
|
|
}
|
2020-04-25 23:17:59 -07:00
|
|
|
|
2020-06-05 23:56:42 -07:00
|
|
|
@keyframes fade-in {
|
|
|
|
from {
|
|
|
|
opacity: 0;
|
|
|
|
}
|
|
|
|
to {
|
|
|
|
opacity: 1;
|
|
|
|
}
|
2020-05-02 22:59:30 -07:00
|
|
|
}
|
2020-05-03 00:15:03 -07:00
|
|
|
`,
|
|
|
|
doAnimations && "do-animations"
|
|
|
|
)}
|
2020-04-24 23:13:28 -07:00
|
|
|
// This sets up the cache to not need to reload images during
|
|
|
|
// download!
|
2020-04-25 03:02:11 -07:00
|
|
|
// TODO: Re-enable this once we get our change into Chakra
|
|
|
|
// main. For now, this will make Downloads a bit slower, which
|
|
|
|
// is fine!
|
|
|
|
// crossOrigin="Anonymous"
|
2020-04-24 00:28:00 -07:00
|
|
|
/>
|
|
|
|
</FullScreenCenter>
|
|
|
|
</CSSTransition>
|
|
|
|
))}
|
|
|
|
</TransitionGroup>
|
2020-06-05 23:56:42 -07:00
|
|
|
<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>
|
2020-05-02 13:40:37 -07:00
|
|
|
</Box>
|
2020-04-23 15:19:36 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-23 13:31:39 -07:00
|
|
|
function FullScreenCenter({ children }) {
|
|
|
|
return (
|
|
|
|
<Flex
|
2020-04-23 15:19:36 -07:00
|
|
|
pos="absolute"
|
|
|
|
top="0"
|
|
|
|
right="0"
|
|
|
|
bottom="0"
|
|
|
|
left="0"
|
2020-04-23 13:31:39 -07:00
|
|
|
alignItems="center"
|
|
|
|
justifyContent="center"
|
|
|
|
>
|
|
|
|
{children}
|
|
|
|
</Flex>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-05-11 21:19:34 -07:00
|
|
|
function getBestImageUrlForLayer(layer) {
|
|
|
|
if (layer.svgUrl) {
|
|
|
|
return `/api/assetProxy?url=${encodeURIComponent(layer.svgUrl)}`;
|
|
|
|
} else {
|
|
|
|
return layer.imageUrl;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-05 23:56:42 -07:00
|
|
|
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(() => {
|
|
|
|
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);
|
|
|
|
};
|
|
|
|
|
|
|
|
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 };
|
|
|
|
}
|
|
|
|
|
2020-04-23 13:31:39 -07:00
|
|
|
export default OutfitPreview;
|