Share appearance data via useOutfitPreview

Here, we offer a second syntax for `<OutfitPreview />`: a hook that offers the same UI as `preview`, but _also_ shares the `appearance` data.

This makes it easier to have UI that depends on the outfit appearance, without having to commit to all the `useOutfitAppearance` stuff in the parent. Same easy syntax! :3

I've refactored the item page to use this for compatibility testing, instead of using the Apollo cache (which was also cute and same perf impact, but more overhead!)
This commit is contained in:
Emi Matchu 2021-02-09 20:28:03 -08:00
parent 8694729b38
commit 193273f00f
3 changed files with 50 additions and 58 deletions

View file

@ -40,7 +40,7 @@ import {
itemAppearanceFragment, itemAppearanceFragment,
petAppearanceFragment, petAppearanceFragment,
} from "./components/useOutfitAppearance"; } from "./components/useOutfitAppearance";
import OutfitPreview from "./components/OutfitPreview"; import { useOutfitPreview } from "./components/OutfitPreview";
import SpeciesColorPicker, { import SpeciesColorPicker, {
useAllValidPetPoses, useAllValidPetPoses,
getValidPoses, getValidPoses,
@ -646,39 +646,28 @@ function ItemPageOutfitPreview({ itemId }) {
valids, valids,
} = useAllValidPetPoses(); } = useAllValidPetPoses();
// To check whether the item is compatible with this pet, query for the
// appearance, but only against the cache. That way, we don't send a
// redundant network request just for this (the OutfitPreview component will
// handle it!), but we'll get an update once it arrives in the cache.
const { data: cachedData } = useQuery(
gql`
query ItemPageOutfitPreview_CacheOnly(
$itemId: ID!
$speciesId: ID!
$colorId: ID!
) {
item(id: $itemId) {
appearanceOn(speciesId: $speciesId, colorId: $colorId) {
layers {
id
}
}
}
}
`,
{
variables: {
itemId,
speciesId: petState.speciesId,
colorId: petState.colorId,
},
fetchPolicy: "cache-only",
}
);
const [hasAnimations, setHasAnimations] = React.useState(false); const [hasAnimations, setHasAnimations] = React.useState(false);
const [isPaused, setIsPaused] = useLocalStorage("DTIOutfitIsPaused", true); const [isPaused, setIsPaused] = useLocalStorage("DTIOutfitIsPaused", true);
// This is like <OutfitPreview />, but we can use the appearance data, too!
const { appearance, preview } = useOutfitPreview({
speciesId: petState.speciesId,
colorId: petState.colorId,
pose: petState.pose,
appearanceId: petState.appearanceId,
wornItemIds: [itemId],
isLoading: loadingGQL || loadingValids,
spinnerVariant: "corner",
loadingDelayMs: 200,
engine: "canvas",
onChangeHasAnimations: setHasAnimations,
});
// If there's an appearance loaded for this item, but it's empty, then the
// item is incompatible. (There should only be one item appearance: this one!)
const itemAppearance = appearance?.itemAppearances?.[0];
const isIncompatible = itemAppearance && itemAppearance.layers.length === 0;
const borderColor = useColorModeValue("green.700", "green.400"); const borderColor = useColorModeValue("green.700", "green.400");
const errorColor = useColorModeValue("red.600", "red.400"); const errorColor = useColorModeValue("red.600", "red.400");
@ -687,12 +676,6 @@ function ItemPageOutfitPreview({ itemId }) {
return <Box color="red.400">{error.message}</Box>; return <Box color="red.400">{error.message}</Box>;
} }
// If the layers are null-y, then we're still loading. Otherwise, if the
// layers are an empty array, then we're incomaptible. Or, if they're a
// non-empty array, then we're compatible!
const layers = cachedData?.item?.appearanceOn?.layers;
const isIncompatible = Array.isArray(layers) && layers.length === 0;
return ( return (
<Stack <Stack
direction={{ base: "column", md: "row" }} direction={{ base: "column", md: "row" }}
@ -714,18 +697,7 @@ function ItemPageOutfitPreview({ itemId }) {
overflow="hidden" overflow="hidden"
> >
<Box> <Box>
<OutfitPreview {preview}
speciesId={petState.speciesId}
colorId={petState.colorId}
pose={petState.pose}
appearanceId={petState.appearanceId}
wornItemIds={[itemId]}
isLoading={loadingGQL || loadingValids}
spinnerVariant="corner"
loadingDelayMs={2000}
engine="canvas"
onChangeHasAnimations={setHasAnimations}
/>
<CustomizeMoreButton <CustomizeMoreButton
speciesId={petState.speciesId} speciesId={petState.speciesId}
colorId={petState.colorId} colorId={petState.colorId}

View file

@ -28,7 +28,19 @@ import useOutfitAppearance from "./useOutfitAppearance";
* TODO: There's some duplicate work happening in useOutfitAppearance and * TODO: There's some duplicate work happening in useOutfitAppearance and
* useOutfitState both getting appearance data on first load... * useOutfitState both getting appearance data on first load...
*/ */
function OutfitPreview({ function OutfitPreview(props) {
const { preview } = useOutfitPreview(props);
return preview;
}
/**
* useOutfitPreview is like `<OutfitPreview />`, but a bit more power!
*
* It takes the same props and returns a `preview` field, which is just like
* `<OutfitPreview />` - but it also returns `appearance` data too, in case you
* want to show some additional UI that uses the appearance data we loaded!
*/
export function useOutfitPreview({
speciesId, speciesId,
colorId, colorId,
pose, pose,
@ -41,13 +53,14 @@ function OutfitPreview({
spinnerVariant, spinnerVariant,
onChangeHasAnimations = null, onChangeHasAnimations = null,
}) { }) {
const { loading, error, visibleLayers } = useOutfitAppearance({ const appearance = useOutfitAppearance({
speciesId, speciesId,
colorId, colorId,
pose, pose,
appearanceId, appearanceId,
wornItemIds, wornItemIds,
}); });
const { loading, error, visibleLayers } = appearance;
const { const {
loading: loading2, loading: loading2,
@ -76,7 +89,7 @@ function OutfitPreview({
); );
} }
return ( const preview = (
<OutfitLayers <OutfitLayers
loading={isLoading || loading || loading2} loading={isLoading || loading || loading2}
visibleLayers={loadedLayers} visibleLayers={loadedLayers}
@ -89,6 +102,8 @@ function OutfitPreview({
isPaused={isPaused} isPaused={isPaused}
/> />
); );
return { appearance, preview };
} }
/** /**

View file

@ -69,7 +69,7 @@ export default function useOutfitAppearance(outfitState) {
) { ) {
items(ids: $wornItemIds) { items(ids: $wornItemIds) {
id id
appearanceOn(speciesId: $speciesId, colorId: $colorId) { appearance: appearanceOn(speciesId: $speciesId, colorId: $colorId) {
...ItemAppearanceForOutfitPreview ...ItemAppearanceForOutfitPreview
} }
} }
@ -86,20 +86,25 @@ export default function useOutfitAppearance(outfitState) {
} }
); );
const petAppearance = data1?.petAppearance;
const items = data2?.items;
const itemAppearances = React.useMemo( const itemAppearances = React.useMemo(
() => (data2?.items || []).map((i) => i.appearanceOn), () => (items || []).map((i) => i.appearance),
[data2] [items]
); );
const visibleLayers = React.useMemo( const visibleLayers = React.useMemo(
() => getVisibleLayers(data1?.petAppearance, itemAppearances), () => getVisibleLayers(petAppearance, itemAppearances),
[data1, itemAppearances] [petAppearance, itemAppearances]
); );
const bodyId = data1?.petAppearance?.bodyId; const bodyId = petAppearance?.bodyId;
return { return {
loading: loading1 || loading2, loading: loading1 || loading2,
error: error1 || error2, error: error1 || error2,
petAppearance,
items,
itemAppearances,
visibleLayers, visibleLayers,
bodyId, bodyId,
}; };