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 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$/,
/^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$/,
/^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\/(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) {
@ -19,9 +19,14 @@ async function handle(req, res) {
res.setHeader("Content-Type", "text/plain");
return res.status(400).send(`Missing required parameter: layerUrls`);
}
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) {
if (!VALID_LAYER_URLS.some((pattern) => layerUrl.match(pattern))) {
return res.status(400).send(`Unexpected layer URL format: ${layerUrl}`);
@ -30,7 +35,7 @@ async function handle(req, res) {
let imageResult;
try {
imageResult = await renderOutfitImage(layerUrls, 150);
imageResult = await renderOutfitImage(layerUrls, size);
} catch (e) {
console.error(e);
res.setHeader("Content-Type", "text/plain");

View file

@ -1,16 +1,20 @@
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 { useQuery } from "@apollo/client";
import { ErrorMessage, Heading1 } from "./util";
import {
getVisibleLayers,
petAppearanceFragmentForGetVisibleLayers,
} from "./components/useOutfitAppearance";
import HangerSpinner from "./components/HangerSpinner";
import useRequireLogin from "./components/useRequireLogin";
function UserOutfitsPage() {
return (
<Box>
<Heading1>Your outfits</Heading1>
<Heading1 marginBottom="4">Your outfits</Heading1>
<UserOutfitsPageContent />
</Box>
);
@ -21,22 +25,26 @@ function UserOutfitsPageContent() {
const { loading: queryLoading, error, data } = useQuery(
gql`
query UserOutfitsPageContent {
query UserOutfitsPageContent($size: LayerImageSize) {
currentUser {
outfits {
id
name
petAppearance {
id
}
wornItems {
id
layers {
id
svgUrl
imageUrl(size: $size)
}
...PetAppearanceForGetVisibleLayers
}
}
}
}
${petAppearanceFragmentForGetVisibleLayers}
`,
{ skip: userLoading }
{ variables: { size: "SIZE_" + getBestImageSize() }, skip: userLoading }
);
if (userLoading || queryLoading) {
@ -51,11 +59,54 @@ function UserOutfitsPageContent() {
return <ErrorMessage>Error loading outfits: {error.message}</ErrorMessage>;
}
const outfits = data.currentUser.outfits;
return (
<code>
<pre>Data: {JSON.stringify(data, null, 4)}</pre>
</code>
<Wrap spacing="4">
{outfits.map((outfit) => (
<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;

View file

@ -160,15 +160,11 @@ export const itemAppearanceFragment = gql`
}
`;
export const petAppearanceFragment = gql`
fragment PetAppearanceForOutfitPreview on PetAppearance {
export const petAppearanceFragmentForGetVisibleLayers = gql`
fragment PetAppearanceForGetVisibleLayers on PetAppearance {
id
bodyId
layers {
id
svgUrl
canvasMovieLibraryUrl
imageUrl(size: SIZE_600)
zone {
id
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);
return layer.url;
},
imageUrl: async ({ id }, { size }, { swfAssetLoader }) => {
imageUrl: async ({ id }, { size = "SIZE_150" }, { swfAssetLoader }) => {
const layer = await swfAssetLoader.load(id);
// If there's no image, return null. (In the development db, which isn't