basic outfits page with pet-only thumbnails

This commit is contained in:
Emi Matchu 2021-01-04 08:10:35 +00:00
parent b8d919b247
commit 30e0a149be
4 changed files with 89 additions and 21 deletions

View file

@ -10,8 +10,8 @@ const beeline = require("honeycomb-beeline")({
const { renderOutfitImage } = require("../src/server/outfit-images"); const { renderOutfitImage } = require("../src/server/outfit-images");
const VALID_LAYER_URLS = [ const VALID_LAYER_URLS = [
/^https:\/\/impress-asset-images\.s3\.amazonaws\.com\/(biology|object)\/[0-9]{3}\/[0-9]{3}\/[0-9]{3}\/[0-9]+\/150x150\.png$/, /^https:\/\/impress-asset-images\.s3\.amazonaws\.com\/(biology|object)\/[0-9]{3}\/[0-9]{3}\/[0-9]{3}\/[0-9]+\/(150|300)x(150|300)\.png(\?[a-zA-Z0-9_-]+)?$/,
/^http:\/\/images\.neopets\.com\/cp\/(biology|object)\/data\/[0-9]{3}\/[0-9]{3}\/[0-9]{3}\/[a-zA-Z0-9_]+\/[a-zA-Z0-9_]+\.svg$/, /^http:\/\/images\.neopets\.com\/cp\/(bio|object)\/data\/[0-9]{3}\/[0-9]{3}\/[0-9]{3}\/[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+\.svg$/,
]; ];
async function handle(req, res) { async function handle(req, res) {
@ -19,9 +19,14 @@ async function handle(req, res) {
res.setHeader("Content-Type", "text/plain"); res.setHeader("Content-Type", "text/plain");
return res.status(400).send(`Missing required parameter: layerUrls`); return res.status(400).send(`Missing required parameter: layerUrls`);
} }
const layerUrls = req.query.layerUrls.split(","); const layerUrls = req.query.layerUrls.split(",");
const size = parseInt(req.query.size);
if (size !== 150 && size !== 300) {
res.setHeader("Content-Type", "text/plain");
return res.status(400).send(`Size must be 150 or 300`);
}
for (const layerUrl of layerUrls) { for (const layerUrl of layerUrls) {
if (!VALID_LAYER_URLS.some((pattern) => layerUrl.match(pattern))) { if (!VALID_LAYER_URLS.some((pattern) => layerUrl.match(pattern))) {
return res.status(400).send(`Unexpected layer URL format: ${layerUrl}`); return res.status(400).send(`Unexpected layer URL format: ${layerUrl}`);
@ -30,7 +35,7 @@ async function handle(req, res) {
let imageResult; let imageResult;
try { try {
imageResult = await renderOutfitImage(layerUrls, 150); imageResult = await renderOutfitImage(layerUrls, size);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
res.setHeader("Content-Type", "text/plain"); res.setHeader("Content-Type", "text/plain");

View file

@ -1,16 +1,20 @@
import React from "react"; import React from "react";
import { Box, Center } from "@chakra-ui/react"; import { Box, Center, Flex, Wrap, WrapItem } from "@chakra-ui/react";
import gql from "graphql-tag"; import gql from "graphql-tag";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import { ErrorMessage, Heading1 } from "./util"; import { ErrorMessage, Heading1 } from "./util";
import {
getVisibleLayers,
petAppearanceFragmentForGetVisibleLayers,
} from "./components/useOutfitAppearance";
import HangerSpinner from "./components/HangerSpinner"; import HangerSpinner from "./components/HangerSpinner";
import useRequireLogin from "./components/useRequireLogin"; import useRequireLogin from "./components/useRequireLogin";
function UserOutfitsPage() { function UserOutfitsPage() {
return ( return (
<Box> <Box>
<Heading1>Your outfits</Heading1> <Heading1 marginBottom="4">Your outfits</Heading1>
<UserOutfitsPageContent /> <UserOutfitsPageContent />
</Box> </Box>
); );
@ -21,22 +25,26 @@ function UserOutfitsPageContent() {
const { loading: queryLoading, error, data } = useQuery( const { loading: queryLoading, error, data } = useQuery(
gql` gql`
query UserOutfitsPageContent { query UserOutfitsPageContent($size: LayerImageSize) {
currentUser { currentUser {
outfits { outfits {
id id
name name
petAppearance { petAppearance {
id id
} layers {
wornItems { id
id svgUrl
imageUrl(size: $size)
}
...PetAppearanceForGetVisibleLayers
} }
} }
} }
} }
${petAppearanceFragmentForGetVisibleLayers}
`, `,
{ skip: userLoading } { variables: { size: "SIZE_" + getBestImageSize() }, skip: userLoading }
); );
if (userLoading || queryLoading) { if (userLoading || queryLoading) {
@ -51,11 +59,54 @@ function UserOutfitsPageContent() {
return <ErrorMessage>Error loading outfits: {error.message}</ErrorMessage>; return <ErrorMessage>Error loading outfits: {error.message}</ErrorMessage>;
} }
const outfits = data.currentUser.outfits;
return ( return (
<code> <Wrap spacing="4">
<pre>Data: {JSON.stringify(data, null, 4)}</pre> {outfits.map((outfit) => (
</code> <WrapItem key={outfit.id}>
<OutfitCard outfit={outfit} />
</WrapItem>
))}
</Wrap>
); );
} }
function OutfitCard({ outfit }) {
const thumbnailUrl = buildOutfitThumbnailUrl(outfit.petAppearance, []);
return (
<Flex
direction="column"
alignItems="center"
textAlign="center"
boxShadow="md"
borderRadius="md"
padding="3"
width="calc(150px + 2em)"
>
<Box as="img" src={thumbnailUrl} width={150} height={150} />
<Box>{outfit.name}</Box>
</Flex>
);
}
function buildOutfitThumbnailUrl(petAppearance, itemAppearances) {
const size = getBestImageSize();
const visibleLayers = getVisibleLayers(petAppearance, itemAppearances);
const layerUrls = visibleLayers.map(
(layer) => layer.svgUrl || layer.imageUrl
);
return `/api/outfitImage?size=${size}&layerUrls=${layerUrls.join(",")}`;
}
function getBestImageSize() {
if (window.devicePixelRatio > 1) {
return 300;
} else {
return 150;
}
}
export default UserOutfitsPage; export default UserOutfitsPage;

View file

@ -160,15 +160,11 @@ export const itemAppearanceFragment = gql`
} }
`; `;
export const petAppearanceFragment = gql` export const petAppearanceFragmentForGetVisibleLayers = gql`
fragment PetAppearanceForOutfitPreview on PetAppearance { fragment PetAppearanceForGetVisibleLayers on PetAppearance {
id id
bodyId
layers { layers {
id id
svgUrl
canvasMovieLibraryUrl
imageUrl(size: SIZE_600)
zone { zone {
id id
depth @client depth @client
@ -179,3 +175,19 @@ export const petAppearanceFragment = gql`
} }
} }
`; `;
export const petAppearanceFragment = gql`
fragment PetAppearanceForOutfitPreview on PetAppearance {
id
bodyId
layers {
id
svgUrl
canvasMovieLibraryUrl
imageUrl(size: SIZE_600)
}
...PetAppearanceForGetVisibleLayers
}
${petAppearanceFragmentForGetVisibleLayers}
`;

View file

@ -91,7 +91,7 @@ const resolvers = {
const layer = await swfAssetLoader.load(id); const layer = await swfAssetLoader.load(id);
return layer.url; return layer.url;
}, },
imageUrl: async ({ id }, { size }, { swfAssetLoader }) => { imageUrl: async ({ id }, { size = "SIZE_150" }, { swfAssetLoader }) => {
const layer = await swfAssetLoader.load(id); const layer = await swfAssetLoader.load(id);
// If there's no image, return null. (In the development db, which isn't // If there's no image, return null. (In the development db, which isn't