add svgUrl for image layers
This commit is contained in:
parent
072a95faba
commit
e18aba17c6
8 changed files with 116 additions and 9 deletions
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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[]")),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -15,6 +15,7 @@ describe("PetAppearance", () => {
|
||||||
layers {
|
layers {
|
||||||
id
|
id
|
||||||
imageUrl(size: SIZE_600)
|
imageUrl(size: SIZE_600)
|
||||||
|
svgUrl
|
||||||
zone {
|
zone {
|
||||||
depth
|
depth
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue