import React from "react";
import { Box, DarkMode, Flex, Text } from "@chakra-ui/core";
import { WarningIcon } from "@chakra-ui/icons";
import { css, cx } from "emotion";
import { CSSTransition, TransitionGroup } from "react-transition-group";
import OutfitCanvas, {
OutfitCanvasImage,
OutfitCanvasMovie,
loadImage,
loadCanvasMovieLibrary,
useEaselDependenciesLoader,
} from "./OutfitCanvas";
import HangerSpinner from "./HangerSpinner";
import { useLocalStorage } from "../util";
import useOutfitAppearance from "./useOutfitAppearance";
/**
* OutfitPreview is for rendering a full outfit! It accepts outfit data,
* fetches the appearance data for it, and preloads and renders the layers
* together.
*
* If the species/color/pose fields are null and a `placeholder` node is
* provided instead, we'll render the placeholder. And then, once those props
* become non-null, we'll keep showing the placeholder below the loading
* overlay until loading completes. (We use this on the homepage to show the
* beach splash until outfit data arrives!)
*
* TODO: There's some duplicate work happening in useOutfitAppearance and
* useOutfitState both getting appearance data on first load...
*/
function OutfitPreview({
speciesId,
colorId,
pose,
wornItemIds,
appearanceId = null,
isLoading = false,
placeholder,
loadingDelayMs,
spinnerVariant,
engine = "images",
onChangeHasAnimations = null,
}) {
const { loading, error, visibleLayers } = useOutfitAppearance({
speciesId,
colorId,
pose,
appearanceId,
wornItemIds,
});
const { loading: loading2, error: error2, loadedLayers } = usePreloadLayers(
visibleLayers
);
if (error || error2) {
return (
Could not load preview. Try again?
);
}
return (
);
}
/**
* OutfitLayers is the raw UI component for rendering outfit layers. It's
* used both in the main outfit preview, and in other minor UIs!
*/
export function OutfitLayers({
loading,
visibleLayers,
placeholder,
loadingDelayMs = 500,
spinnerVariant = "overlay",
doTransitions = false,
engine = "images",
onChangeHasAnimations = null,
}) {
const containerRef = React.useRef(null);
const [canvasSize, setCanvasSize] = React.useState(0);
const [loadingDelayHasPassed, setLoadingDelayHasPassed] = React.useState(
false
);
const { loading: loadingEasel } = useEaselDependenciesLoader();
const loadingAnything = loading || loadingEasel;
const [isPaused] = useLocalStorage("DTIOutfitIsPaused", true);
// When we start in a loading state, or re-enter a loading state, start the
// loading delay timer.
React.useEffect(() => {
if (loadingAnything) {
setLoadingDelayHasPassed(false);
const t = setTimeout(
() => setLoadingDelayHasPassed(true),
loadingDelayMs
);
return () => clearTimeout(t);
}
}, [loadingDelayMs, loadingAnything]);
React.useLayoutEffect(() => {
function computeAndSaveCanvasSize() {
setCanvasSize(
Math.min(
containerRef.current.offsetWidth,
containerRef.current.offsetHeight
)
);
}
computeAndSaveCanvasSize();
window.addEventListener("resize", computeAndSaveCanvasSize);
return () => window.removeEventListener("resize", computeAndSaveCanvasSize);
}, [setCanvasSize]);
return (
{placeholder && (
{placeholder}
)}
{
// TODO: A bit of a mess in here! Extract these out?
engine === "canvas" ? (
!loadingEasel && (
{visibleLayers.map((layer) =>
layer.canvasMovieLibraryUrl ? (
) : (
)
)}
)
) : (
{visibleLayers.map((layer) => (
finishes preloading and
// applies the src to the underlying .
className={cx(
css`
object-fit: contain;
max-width: 100%;
max-height: 100%;
&.do-animations {
animation: fade-in 0.2s;
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
`,
doTransitions && "do-animations"
)}
// This sets up the cache to not need to reload images during
// download!
// 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"
/>
))}
)
}
{spinnerVariant === "overlay" && (
<>
{/* Against the dark overlay, use the Dark Mode spinner. */}
>
)}
{spinnerVariant === "corner" && (
)}
);
}
export function FullScreenCenter({ children, ...otherProps }) {
return (
{children}
);
}
function getBestImageUrlForLayer(layer) {
if (layer.svgUrl) {
return `/api/assetProxy?url=${encodeURIComponent(layer.svgUrl)}`;
} else {
return layer.imageUrl;
}
}
/**
* 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!
*/
export function usePreloadLayers(layers) {
const [error, setError] = React.useState(null);
const [loadedLayers, setLoadedLayers] = React.useState([]);
// 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;
React.useEffect(() => {
// HACK: Don't clear the preview when we have zero layers, because it
// usually means the parent is still loading data. I feel like this isn't
// the right abstraction, though...
if (loadedLayers.length > 0 && layers.length === 0) {
return;
}
// If the layers already match, we can ignore extra effect triggers.
if (!loading) {
return;
}
let canceled = false;
setError(null);
const loadAssets = async () => {
const assetPromises = layers.map((layer) => {
if (layer.canvasMovieLibraryUrl) {
return loadCanvasMovieLibrary(layer.canvasMovieLibraryUrl);
} else {
return loadImage(getBestImageUrlForLayer(layer));
}
});
try {
// TODO: Load in one at a time, under a loading spinner & delay?
await Promise.all(assetPromises);
} catch (e) {
if (canceled) return;
console.error("Error preloading outfit layers", e);
assetPromises.forEach((p) => p.cancel());
setError(e);
return;
}
if (canceled) return;
setLoadedLayers(layers);
};
loadAssets();
return () => {
canceled = true;
};
}, [layers, loadedLayers.length, loading]);
return { loading, error, loadedLayers };
}
export default OutfitPreview;