From 08bdf560a4fed330cb93201f7038fa36530c8325 Mon Sep 17 00:00:00 2001 From: Matchu Date: Tue, 22 Sep 2020 03:03:01 -0700 Subject: [PATCH] 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 --- public/animate-test.html | 40 +++---- src/app/components/OutfitCanvas.js | 180 ++++++++++++++++++++++++++-- src/app/util.js | 10 +- src/stories/OutfitCanvas.stories.js | 99 +++++++++++---- 4 files changed, 276 insertions(+), 53 deletions(-) diff --git a/public/animate-test.html b/public/animate-test.html index 0e32ad8..9752391 100644 --- a/public/animate-test.html +++ b/public/animate-test.html @@ -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); diff --git a/src/app/components/OutfitCanvas.js b/src/app/components/OutfitCanvas.js index 6b4230e..20360d1 100644 --- a/src/app/components/OutfitCanvas.js +++ b/src/app/components/OutfitCanvas.js @@ -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; diff --git a/src/app/util.js b/src/app/util.js index c27c8e0..ff4357a 100644 --- a/src/app/util.js +++ b/src/app/util.js @@ -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; } /** diff --git a/src/stories/OutfitCanvas.stories.js b/src/stories/OutfitCanvas.stories.js index 67e63e5..60aae55 100644 --- a/src/stories/OutfitCanvas.stories.js +++ b/src/stories/OutfitCanvas.stories.js @@ -2,38 +2,87 @@ 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) => ( - - - - - - + {args.pet === "Blue Acara" && ( + <> + + + + + + + + )} + {args.items.includes("Bubbles In Water Foreground") && ( + + )} ); + +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"], +};