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:
parent
c806de4e83
commit
08bdf560a4
4 changed files with 276 additions and 53 deletions
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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"],
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue