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 computeAndSizeCanvasSize() { setCanvasSize( Math.min( containerRef.current.offsetWidth, containerRef.current.offsetHeight ) ); } window.addEventListener("resize", computeAndSizeCanvasSize); return () => window.removeEventListener("resize", computeAndSizeCanvasSize); }, [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;