draft of animated layers in storybook

Not running in the real app yet, but there's something a bit off where changes seem to pause animations and I don't understand why
This commit is contained in:
Emi Matchu 2020-09-22 03:03:01 -07:00
parent c806de4e83
commit 08bdf560a4
4 changed files with 276 additions and 53 deletions

View file

@ -96,19 +96,19 @@
(canvas.offsetHeight * window.devicePixelRatio) / (canvas.offsetHeight * window.devicePixelRatio) /
library.properties.height; library.properties.height;
movieClip.alpha = 0; // movieClip.alpha = 0;
const tween = createjs.Tween.get(movieClip, { paused: true }).to( // const tween = createjs.Tween.get(movieClip, { paused: true }).to(
{ alpha: 1 }, // { alpha: 1 },
200 // 200
); // );
stage.on( // stage.on(
"drawend", // "drawend",
() => { // () => {
tween.paused = false; // tween.paused = false;
}, // },
null, // null,
true // true
); // );
// TODO: I'm not 100% clear on why, but manually caching the movie and // TODO: I'm not 100% clear on why, but manually caching the movie and
// manually updating the cache at a 60FPS rate (that's how often // manually updating the cache at a 60FPS rate (that's how often
@ -119,13 +119,13 @@
// layers, so it looks better? Although hell, maybe applying // layers, so it looks better? Although hell, maybe applying
// alpha to a cached raster just _is_ faster than applying it to // alpha to a cached raster just _is_ faster than applying it to
// like 200 overlapping layers, that would just make sense... // like 200 overlapping layers, that would just make sense...
movieClip.cache( // movieClip.cache(
0, // 0,
0, // 0,
library.properties.width, // library.properties.width,
library.properties.height // library.properties.height
); // );
movieClip.on("tick", () => movieClip.updateCache()); // movieClip.on("tick", () => movieClip.updateCache());
stage.addChild(movieClip); stage.addChild(movieClip);

View file

@ -1,5 +1,7 @@
import React from "react"; import React from "react";
import { safeImageUrl } from "../util";
const EaselContext = React.createContext({ const EaselContext = React.createContext({
stage: null, stage: null,
addResizeListener: () => {}, addResizeListener: () => {},
@ -26,9 +28,9 @@ function OutfitCanvas({ children, width, height }) {
} }
window.createjs.Ticker.timingMode = window.createjs.Ticker.RAF; window.createjs.Ticker.timingMode = window.createjs.Ticker.RAF;
window.createjs.Ticker.on("tick", onTick); window.createjs.Ticker.addEventListener("tick", onTick);
return () => window.createjs.Ticker.off("tick", onTick); return () => window.createjs.Ticker.removeEventListener("tick", onTick);
}, [loading]); }, [loading]);
const addChild = React.useCallback( const addChild = React.useCallback(
@ -182,6 +184,116 @@ export function OutfitCanvasImage({ src, zIndex }) {
return null; return null;
} }
export function OutfitCanvasMovie({ librarySrc, zIndex }) {
const {
width,
height,
addChild,
removeChild,
addResizeListener,
removeResizeListener,
} = React.useContext(EaselContext);
React.useEffect(() => {
let library;
let movieClip;
let tween;
function updateSize() {
movieClip.scaleX = width / library.properties.width;
movieClip.scaleY = height / library.properties.height;
}
async function addMovieClip() {
library = await loadMovieLibrary(librarySrc);
let constructorName;
try {
const fileName = librarySrc.split("/").pop();
const fileNameWithoutExtension = fileName.split(".")[0];
constructorName = fileNameWithoutExtension.replace(/[ -]/g, "");
} catch (e) {
console.error(
`Movie librarySrc %s did not match expected format: %o`,
JSON.stringify(librarySrc),
e
);
return;
}
const LibraryMovieClipConstructor = library[constructorName];
if (!LibraryMovieClipConstructor) {
console.error(
`Expected JS movie library %s to contain a constructor named ` +
`%s, but it did not: %o`,
JSON.stringify(librarySrc),
JSON.stringify(constructorName),
library
);
return;
}
movieClip = new LibraryMovieClipConstructor();
movieClip.cache(
0,
0,
library.properties.width,
library.properties.height
);
movieClip.on("tick", () => {
console.log("clip tick", movieClip.framerate);
movieClip.updateCache();
});
// We're gonna fade in! Wait for the first frame to draw, to make the
// timing smooth, but yeah here we go!
movieClip.alpha = 0;
tween = window.createjs.Tween.get(movieClip, { paused: true }).to(
{ alpha: 1 },
200
);
const startFadeIn = () => {
console.log("first draw");
tween.paused = false;
};
// Get it actually running! We need to set framerate _after_ adding it
// to the stage, to overwrite the stage's defaults.
updateSize();
addChild(movieClip, zIndex, { afterFirstDraw: startFadeIn });
movieClip.framerate = library.properties.fps;
addResizeListener(updateSize);
}
function removeMovieClip() {
removeResizeListener(updateSize);
removeChild(movieClip);
}
addMovieClip();
return () => {
if (movieClip) {
// Reverse the fade-in into a fade-out, then remove the bitmap.
tween.reversed = true;
tween.setPosition(0);
tween.paused = false;
tween.on("complete", removeMovieClip, null, true);
}
};
}, [
librarySrc,
zIndex,
width,
height,
addChild,
removeChild,
addResizeListener,
removeResizeListener,
]);
return null;
}
/** /**
* useEaselDependenciesLoader loads the CreateJS scripts we use in OutfitCanvas. * useEaselDependenciesLoader loads the CreateJS scripts we use in OutfitCanvas.
* We load it as part of OutfitCanvas, but callers can also use this to preload * We load it as part of OutfitCanvas, but callers can also use this to preload
@ -215,14 +327,11 @@ function useScriptTag(src) {
} }
let canceled = false; let canceled = false;
const script = document.createElement("script"); loadScriptTag(src).then(() => {
script.onload = () => {
if (!canceled) { if (!canceled) {
setLoading(false); setLoading(false);
} }
}; });
script.src = src;
document.body.appendChild(script);
return () => { return () => {
canceled = true; canceled = true;
@ -246,4 +355,61 @@ export function loadImage(url) {
return promise; return promise;
} }
async function loadMovieLibrary(librarySrc) {
// These library JS files are interesting in their operation. It seems like
// the idea is, it pushes an object to a global array, and you need to snap
// it up and see it at the end of the array! And I don't really see a way to
// like, get by a name or ID that we know by this point. So, here we go, just
// try to grab it once it arrives!
//
// TODO: How reliable is the timing on this? My assumption is that, the
// scripts will trigger their onloads in order of arrival, and my
// _hope_ is that the onload will execute before the next script to
// arrive executes. Let's, ah, find out!
await loadScriptTag(librarySrc);
const composition = Object.values(window.AdobeAn.compositions).pop();
const library = composition.getLibrary();
// One more loading step as part of loading this library is loading the
// images it uses for sprites.
//
// TODO: I guess the manifest has these too, so if we could use our DB cache
// to get the manifest to us faster, then we could avoid a network RTT
// on the critical path by preloading these images before the JS file
// even gets to us?
const librarySrcDir = librarySrc.split("/").slice(0, -1).join("/");
const manifestImages = new Map(
library.properties.manifest.map(({ id, src }) => [
id,
loadImage(safeImageUrl(librarySrcDir + "/" + src)),
])
);
await Promise.all(manifestImages.values());
// Finally, once we have the images loaded, the library object expects us to
// mutate it (!) to give it the actual sprite sheet objects based on the
// loaded images. That's how the MovieClip's objects will access the loaded
// versions!
const spriteSheets = composition.getSpriteSheet();
for (const { name, frames } of library.ssMetadata) {
const image = await manifestImages.get(name);
spriteSheets[name] = new window.createjs.SpriteSheet({
images: [image],
frames,
});
}
return library;
}
function loadScriptTag(src) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.onload = () => resolve(script);
script.onerror = (e) => reject(e);
script.src = src;
document.body.appendChild(script);
});
}
export default OutfitCanvas; export default OutfitCanvas;

View file

@ -65,7 +65,15 @@ export function Heading2({ children, ...props }) {
* safeImageUrl returns an HTTPS-safe image URL for Neopets assets! * safeImageUrl returns an HTTPS-safe image URL for Neopets assets!
*/ */
export function safeImageUrl(url) { export function safeImageUrl(url) {
return `/api/assetProxy?url=${encodeURIComponent(url)}`; let safeUrl = `/api/assetProxy?url=${encodeURIComponent(url)}`;
// On our Storybook server, we need to request from the main dev server.
const { host } = document.location;
if (host === "localhost:6006") {
safeUrl = "http://localhost:3000" + safeUrl;
}
return safeUrl;
} }
/** /**

View file

@ -2,38 +2,87 @@ import React from "react";
import OutfitCanvas, { import OutfitCanvas, {
OutfitCanvasImage, OutfitCanvasImage,
OutfitCanvasMovie,
} from "../app/components/OutfitCanvas"; } from "../app/components/OutfitCanvas";
export default { export default {
title: "Dress to Impress/OutfitCanvas", title: "Dress to Impress/OutfitCanvas",
component: OutfitCanvas, component: OutfitCanvas,
argTypes: {
pet: {
name: "Pet",
control: {
type: "radio",
options: ["None", "Blue Acara"],
},
},
items: {
name: "Items",
control: {
type: "multi-select",
options: ["Bubbles In Water Foreground"],
},
},
},
}; };
export const BlueAcara = () => ( // NOTE: We don't bother with assetProxy here, because we only run Storybook
// locally, and localhost isn't subject to the same mixed content rules.
// So this is noticeably faster!
const Template = (args) => (
<OutfitCanvas width={300} height={300}> <OutfitCanvas width={300} height={300}>
<OutfitCanvasImage {args.pet === "Blue Acara" && (
src="http://images.neopets.com/cp/bio/data/000/000/002/2426_898928db88/2426.svg" <>
zIndex={10} <OutfitCanvasImage
/> src="http://images.neopets.com/cp/bio/data/000/000/002/2426_898928db88/2426.svg"
<OutfitCanvasImage zIndex={10}
src="http://images.neopets.com/cp/bio/data/000/000/002/2425_501f596cef/2425.svg" />
zIndex={20} <OutfitCanvasImage
/> src="http://images.neopets.com/cp/bio/data/000/000/002/2425_501f596cef/2425.svg"
<OutfitCanvasImage zIndex={20}
src="http://images.neopets.com/cp/bio/data/000/000/002/2427_f12853f18a/2427.svg" />
zIndex={30} <OutfitCanvasImage
/> src="http://images.neopets.com/cp/bio/data/000/000/002/2427_f12853f18a/2427.svg"
<OutfitCanvasImage zIndex={30}
src="http://images.neopets.com/cp/bio/data/000/000/032/32185_dc8f076ae3/32185.svg" />
zIndex={40} <OutfitCanvasImage
/> src="http://images.neopets.com/cp/bio/data/000/000/032/32185_dc8f076ae3/32185.svg"
<OutfitCanvasImage zIndex={40}
src="http://images.neopets.com/cp/bio/data/000/000/002/2428_991dcdedc7/2428.svg" />
zIndex={50} <OutfitCanvasImage
/> src="http://images.neopets.com/cp/bio/data/000/000/002/2428_991dcdedc7/2428.svg"
<OutfitCanvasImage zIndex={50}
src="http://images.neopets.com/cp/bio/data/000/000/002/2430_87edccba4c/2430.svg" />
zIndex={60} <OutfitCanvasImage
/> src="http://images.neopets.com/cp/bio/data/000/000/002/2430_87edccba4c/2430.svg"
zIndex={60}
/>
</>
)}
{args.items.includes("Bubbles In Water Foreground") && (
<OutfitCanvasMovie
librarySrc="http://images.neopets.com/cp/items/data/000/000/564/564507_fc3216b9b8/all-item_foreground_lower.js"
zIndex={100}
/>
)}
</OutfitCanvas> </OutfitCanvas>
); );
export const BlueAcara = Template.bind({});
BlueAcara.args = {
pet: "Blue Acara",
items: [],
};
export const BubblesOnWaterForeground = Template.bind({});
BubblesOnWaterForeground.args = {
pet: "None",
items: ["Bubbles In Water Foreground"],
};
export const BlueAcaraWithForeground = Template.bind({});
BlueAcaraWithForeground.args = {
pet: "Blue Acara",
items: ["Bubbles In Water Foreground"],
};