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) /
|
||||
library.properties.height;
|
||||
|
||||
movieClip.alpha = 0;
|
||||
const tween = createjs.Tween.get(movieClip, { paused: true }).to(
|
||||
{ alpha: 1 },
|
||||
200
|
||||
);
|
||||
stage.on(
|
||||
"drawend",
|
||||
() => {
|
||||
tween.paused = false;
|
||||
},
|
||||
null,
|
||||
true
|
||||
);
|
||||
// movieClip.alpha = 0;
|
||||
// const tween = createjs.Tween.get(movieClip, { paused: true }).to(
|
||||
// { alpha: 1 },
|
||||
// 200
|
||||
// );
|
||||
// stage.on(
|
||||
// "drawend",
|
||||
// () => {
|
||||
// tween.paused = false;
|
||||
// },
|
||||
// null,
|
||||
// true
|
||||
// );
|
||||
|
||||
// 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
|
||||
|
@ -119,13 +119,13 @@
|
|||
// layers, so it looks better? Although hell, maybe applying
|
||||
// alpha to a cached raster just _is_ faster than applying it to
|
||||
// like 200 overlapping layers, that would just make sense...
|
||||
movieClip.cache(
|
||||
0,
|
||||
0,
|
||||
library.properties.width,
|
||||
library.properties.height
|
||||
);
|
||||
movieClip.on("tick", () => movieClip.updateCache());
|
||||
// movieClip.cache(
|
||||
// 0,
|
||||
// 0,
|
||||
// library.properties.width,
|
||||
// library.properties.height
|
||||
// );
|
||||
// movieClip.on("tick", () => movieClip.updateCache());
|
||||
|
||||
stage.addChild(movieClip);
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import React from "react";
|
||||
|
||||
import { safeImageUrl } from "../util";
|
||||
|
||||
const EaselContext = React.createContext({
|
||||
stage: null,
|
||||
addResizeListener: () => {},
|
||||
|
@ -26,9 +28,9 @@ function OutfitCanvas({ children, width, height }) {
|
|||
}
|
||||
|
||||
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]);
|
||||
|
||||
const addChild = React.useCallback(
|
||||
|
@ -182,6 +184,116 @@ export function OutfitCanvasImage({ src, zIndex }) {
|
|||
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.
|
||||
* 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;
|
||||
const script = document.createElement("script");
|
||||
script.onload = () => {
|
||||
loadScriptTag(src).then(() => {
|
||||
if (!canceled) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
script.src = src;
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
|
@ -246,4 +355,61 @@ export function loadImage(url) {
|
|||
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;
|
||||
|
|
|
@ -65,7 +65,15 @@ export function Heading2({ children, ...props }) {
|
|||
* safeImageUrl returns an HTTPS-safe image URL for Neopets assets!
|
||||
*/
|
||||
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,15 +2,38 @@ import React from "react";
|
|||
|
||||
import OutfitCanvas, {
|
||||
OutfitCanvasImage,
|
||||
OutfitCanvasMovie,
|
||||
} from "../app/components/OutfitCanvas";
|
||||
|
||||
export default {
|
||||
title: "Dress to Impress/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}>
|
||||
{args.pet === "Blue Acara" && (
|
||||
<>
|
||||
<OutfitCanvasImage
|
||||
src="http://images.neopets.com/cp/bio/data/000/000/002/2426_898928db88/2426.svg"
|
||||
zIndex={10}
|
||||
|
@ -35,5 +58,31 @@ export const BlueAcara = () => (
|
|||
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>
|
||||
);
|
||||
|
||||
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