Previously I tried to be clever and pre-optimize by putting all the layers onto one canvas… I think this probably helped by batching their paints, but it made fades less smooth by not taking advantage of native CSS transitions, and it made us dip into JS way more often than necessary. Here, I take the simpler approach: just layers of <img> and <canvas> tags, with each animated layer on its own canvas, and letting the browser handle transitions and compositing, and separate `setInterval` timers to manage their framerates. I have a suspicion that batching the paints could help performance more, but honestly, maybe that batching is already happening somehow, because things look pretty great on my big-screen stress test now; and so if it _is_ relevant, I want to wait and see after testing on low-power devices.
92 lines
3 KiB
JavaScript
92 lines
3 KiB
JavaScript
import React from "react";
|
|
import { useToast } from "@chakra-ui/core";
|
|
import loadable from "@loadable/component";
|
|
|
|
import ItemsAndSearchPanels from "./ItemsAndSearchPanels";
|
|
import OutfitPreview from "../components/OutfitPreview";
|
|
import SupportOnly from "./support/SupportOnly";
|
|
import useOutfitState, { OutfitStateContext } from "./useOutfitState";
|
|
import { usePageTitle } from "../util";
|
|
import WardrobePageLayout from "./WardrobePageLayout";
|
|
|
|
const OutfitControls = loadable(() =>
|
|
import(/* webpackPreload: true */ "./OutfitControls")
|
|
);
|
|
const WardrobeDevHacks = loadable(() => import("./WardrobeDevHacks"));
|
|
|
|
/**
|
|
* WardrobePage is the most fun page on the site - it's where you create
|
|
* outfits!
|
|
*
|
|
* This page has two sections: the OutfitPreview, where we show the outfit as a
|
|
* big image; and the ItemsAndSearchPanels, which let you manage which items
|
|
* are in the outfit and find new ones.
|
|
*
|
|
* This component manages shared outfit state, and the fullscreen responsive
|
|
* page layout.
|
|
*/
|
|
function WardrobePage() {
|
|
const toast = useToast();
|
|
const { loading, error, outfitState, dispatchToOutfit } = useOutfitState();
|
|
|
|
// Whether the current outfit preview has animations. Determines whether we
|
|
// show the play/pause button.
|
|
const [hasAnimations, setHasAnimations] = React.useState(false);
|
|
|
|
usePageTitle(outfitState.name || "Untitled outfit");
|
|
|
|
// TODO: I haven't found a great place for this error UI yet, and this case
|
|
// isn't very common, so this lil toast notification seems good enough!
|
|
React.useEffect(() => {
|
|
if (error) {
|
|
console.log(error);
|
|
toast({
|
|
title: "We couldn't load this outfit 😖",
|
|
description: "Please reload the page to try again. Sorry!",
|
|
status: "error",
|
|
isClosable: true,
|
|
duration: 999999999,
|
|
});
|
|
}
|
|
}, [error, toast]);
|
|
|
|
// NOTE: Most components pass around outfitState directly, to make the data
|
|
// relationships more explicit... but there are some deep components
|
|
// that need it, where it's more useful and more performant to access
|
|
// via context.
|
|
return (
|
|
<OutfitStateContext.Provider value={outfitState}>
|
|
<SupportOnly>
|
|
<WardrobeDevHacks />
|
|
</SupportOnly>
|
|
<WardrobePageLayout
|
|
preview={
|
|
<OutfitPreview
|
|
speciesId={outfitState.speciesId}
|
|
colorId={outfitState.colorId}
|
|
pose={outfitState.pose}
|
|
appearanceId={outfitState.appearanceId}
|
|
wornItemIds={outfitState.wornItemIds}
|
|
onChangeHasAnimations={setHasAnimations}
|
|
/>
|
|
}
|
|
controls={
|
|
<OutfitControls
|
|
outfitState={outfitState}
|
|
dispatchToOutfit={dispatchToOutfit}
|
|
showAnimationControls={hasAnimations}
|
|
/>
|
|
}
|
|
itemsAndSearch={
|
|
<ItemsAndSearchPanels
|
|
loading={loading}
|
|
outfitState={outfitState}
|
|
dispatchToOutfit={dispatchToOutfit}
|
|
/>
|
|
}
|
|
/>
|
|
</OutfitStateContext.Provider>
|
|
);
|
|
}
|
|
|
|
export default WardrobePage;
|