add svgUrl for image layers

This commit is contained in:
Matt Dunn-Rankin 2020-05-11 21:19:34 -07:00
parent 072a95faba
commit e18aba17c6
8 changed files with 116 additions and 9 deletions

View file

@ -6,7 +6,7 @@ const streamPipeline = util.promisify(stream.pipeline);
const VALID_URL_PATTERNS = [ const VALID_URL_PATTERNS = [
/^http:\/\/images\.neopets\.com\/items\/[a-zA-Z0-9_ -]+\.gif$/, /^http:\/\/images\.neopets\.com\/items\/[a-zA-Z0-9_ -]+\.gif$/,
/^http:\/\/pets\.neopets\.com\/cp\/[a-z0-9]+\/[0-9]+\/[0-9]+\.png$/, /^http:\/\/images\.neopets\.com\/cp\/(bio|items)\/data\/[0-9]{3}\/[0-9]{3}\/[0-9]{3}\/[a-f0-9_]+\/[0-9]+\.svg$/,
]; ];
export default async (req, res) => { export default async (req, res) => {
@ -34,6 +34,13 @@ export default async (req, res) => {
); );
res.status(proxyRes.status); res.status(proxyRes.status);
res.setHeader("Content-Length", proxyRes.headers.get("Content-Length"));
res.setHeader("Content-Type", proxyRes.headers.get("Content-Type"));
res.setHeader("Cache-Control", proxyRes.headers.get("Cache-Control")); res.setHeader("Cache-Control", proxyRes.headers.get("Cache-Control"));
res.setHeader("ETag", proxyRes.headers.get("ETag"));
res.setHeader("Last-Modified", proxyRes.headers.get("Last-Modified"));
streamPipeline(proxyRes.body, res); streamPipeline(proxyRes.body, res);
}; };

View file

@ -65,7 +65,7 @@ export function OutfitLayers({ loading, visibleLayers, doAnimations = false }) {
> >
<FullScreenCenter> <FullScreenCenter>
<ImageTag <ImageTag
src={layer.imageUrl} src={getBestImageUrlForLayer(layer)}
// We manage the fade-in and fade-out separately! The fade-in // We manage the fade-in and fade-out separately! The fade-in
// happens here, when the <Image> finishes preloading and // happens here, when the <Image> finishes preloading and
// applies the src to the underlying <img>. // applies the src to the underlying <img>.
@ -132,4 +132,12 @@ function FullScreenCenter({ children }) {
); );
} }
function getBestImageUrlForLayer(layer) {
if (layer.svgUrl) {
return `/api/assetProxy?url=${encodeURIComponent(layer.svgUrl)}`;
} else {
return layer.imageUrl;
}
}
export default OutfitPreview; export default OutfitPreview;

View file

@ -93,6 +93,7 @@ export const itemAppearanceFragment = gql`
fragment ItemAppearanceForOutfitPreview on ItemAppearance { fragment ItemAppearanceForOutfitPreview on ItemAppearance {
layers { layers {
id id
svgUrl
imageUrl(size: SIZE_600) imageUrl(size: SIZE_600)
zone { zone {
id id
@ -111,6 +112,7 @@ export const petAppearanceFragment = gql`
id id
layers { layers {
id id
svgUrl
imageUrl(size: SIZE_600) imageUrl(size: SIZE_600)
zone { zone {
id id

View file

@ -188,8 +188,8 @@ function parseOutfitUrl() {
colorId: urlParams.get("color"), colorId: urlParams.get("color"),
emotion: urlParams.get("emotion") || "HAPPY", emotion: urlParams.get("emotion") || "HAPPY",
genderPresentation: urlParams.get("genderPresentation") || "FEMININE", genderPresentation: urlParams.get("genderPresentation") || "FEMININE",
wornItemIds: urlParams.getAll("objects[]"), wornItemIds: new Set(urlParams.getAll("objects[]")),
closetedItemIds: urlParams.getAll("closet[]"), closetedItemIds: new Set(urlParams.getAll("closet[]")),
}; };
} }

View file

@ -65,6 +65,14 @@ const typeDefs = gql`
id: ID! id: ID!
zone: Zone! zone: Zone!
imageUrl(size: LayerImageSize): String imageUrl(size: LayerImageSize): String
"""
This layer as a single SVG, if available.
This might not be available if the asset isn't converted yet by Neopets,
or if it's not as simple as a single SVG (e.g. animated).
"""
svgUrl: String
} }
type Zone { type Zone {
@ -210,6 +218,42 @@ const resolvers = {
`/${rid1}/${rid2}/${rid3}/${rid}/${sizeNum}x${sizeNum}.png?v2-${time}` `/${rid1}/${rid2}/${rid3}/${rid}/${sizeNum}x${sizeNum}.png?v2-${time}`
); );
}, },
svgUrl: async (layer) => {
const manifest = await neopets.loadAssetManifest(layer.url);
if (!manifest) {
console.debug("expected manifest to exist, but it did not");
return null;
}
if (manifest.assets.length !== 1) {
console.debug(
"expected 1 asset in manifest, but found %d",
manifest.assets.length
);
return null;
}
const asset = manifest.assets[0];
if (asset.format !== "vector") {
console.debug(
'expected asset format "vector", but found %s',
asset.format
);
return null;
}
if (asset.assetData.length !== 1) {
console.debug(
"expected 1 datum in asset, but found %d",
asset.assetData.length
);
return null;
}
const assetDatum = asset.assetData[0];
const url = new URL(assetDatum.path, "http://images.neopets.com");
return url.toString();
},
}, },
Zone: { Zone: {
label: async (zone, _, { zoneTranslationLoader }) => { label: async (zone, _, { zoneTranslationLoader }) => {

View file

@ -1,12 +1,15 @@
const fetch = require("node-fetch"); const fetch = require("node-fetch");
async function loadPetData(petName) { async function loadPetData(petName) {
const res = await fetch( const url =
`http://www.neopets.com/amfphp/json.php/CustomPetService.getViewerData` + `http://www.neopets.com/amfphp/json.php/CustomPetService.getViewerData` +
`/${petName}` `/${petName}`;
); const res = await fetch(url);
if (!res.ok) { if (!res.ok) {
throw new Error(`neopets.com returned: ${res.statusText}`); throw new Error(
`for pet data, neopets.com returned: ` +
`${res.status} ${res.statusText}. (${url})`
);
} }
const json = await res.json(); const json = await res.json();
@ -17,4 +20,40 @@ async function loadPetData(petName) {
return json; return json;
} }
module.exports = { loadPetData }; async function loadAssetManifest(swfUrl) {
const manifestUrl = convertSwfUrlToManifestUrl(swfUrl);
const res = await fetch(manifestUrl);
if (res.status === 404) {
return null;
} else if (!res.ok) {
throw new Error(
`for asset manifest, images.neopets.com returned: ` +
`${res.status} ${res.statusText}. (${manifestUrl})`
);
}
const json = await res.json();
return {
assets: json["cpmanifest"]["assets"].map((asset) => ({
format: asset["format"],
assetData: asset["asset_data"].map((assetDatum) => ({
path: assetDatum["url"],
})),
})),
};
}
const SWF_URL_PATTERN = /^http:\/\/images\.neopets\.com\/cp\/(.+?)\/swf\/(.+?)\.swf$/;
function convertSwfUrlToManifestUrl(swfUrl) {
const match = swfUrl.match(SWF_URL_PATTERN);
if (!match) {
throw new Error(`unexpected SWF URL format: ${JSON.stringify(swfUrl)}`);
}
const [_, type, folders] = match;
return `http://images.neopets.com/cp/${type}/data/${folders}/manifest.json`;
}
module.exports = { loadPetData, loadAssetManifest };

View file

@ -15,6 +15,7 @@ describe("PetAppearance", () => {
layers { layers {
id id
imageUrl(size: SIZE_600) imageUrl(size: SIZE_600)
svgUrl
zone { zone {
depth depth
} }

View file

@ -7,6 +7,7 @@ Object {
Object { Object {
"id": "5995", "id": "5995",
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7941/600x600.png?v2-0", "imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7941/600x600.png?v2-0",
"svgUrl": "http://images.neopets.com/cp/bio/data/000/000/007/7941_2c4cc4b846/7941.svg",
"zone": Object { "zone": Object {
"depth": 18, "depth": 18,
}, },
@ -14,6 +15,7 @@ Object {
Object { Object {
"id": "5996", "id": "5996",
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7942/600x600.png?v2-0", "imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7942/600x600.png?v2-0",
"svgUrl": "http://images.neopets.com/cp/bio/data/000/000/007/7942_2eab06fd7b/7942.svg",
"zone": Object { "zone": Object {
"depth": 7, "depth": 7,
}, },
@ -21,6 +23,7 @@ Object {
Object { Object {
"id": "6000", "id": "6000",
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7946/600x600.png?v2-0", "imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7946/600x600.png?v2-0",
"svgUrl": "http://images.neopets.com/cp/bio/data/000/000/007/7946_0348dad587/7946.svg",
"zone": Object { "zone": Object {
"depth": 40, "depth": 40,
}, },
@ -28,6 +31,7 @@ Object {
Object { Object {
"id": "16467", "id": "16467",
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/024/24008/600x600.png?v2-0", "imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/024/24008/600x600.png?v2-0",
"svgUrl": "http://images.neopets.com/cp/bio/data/000/000/024/24008_a05fe9876a/24008.svg",
"zone": Object { "zone": Object {
"depth": 34, "depth": 34,
}, },
@ -35,6 +39,7 @@ Object {
Object { Object {
"id": "19784", "id": "19784",
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/028/28892/600x600.png?v2-1313418652000", "imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/028/28892/600x600.png?v2-1313418652000",
"svgUrl": "http://images.neopets.com/cp/bio/data/000/000/028/28892_a8e3a8b430/28892.svg",
"zone": Object { "zone": Object {
"depth": 37, "depth": 37,
}, },
@ -42,6 +47,7 @@ Object {
Object { Object {
"id": "178150", "id": "178150",
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/036/36887/600x600.png?v2-1354240708000", "imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/036/36887/600x600.png?v2-1354240708000",
"svgUrl": "http://images.neopets.com/cp/bio/data/000/000/036/36887_a335fbba09/36887.svg",
"zone": Object { "zone": Object {
"depth": 38, "depth": 38,
}, },