simplify canvas code, just use separate elements
Previously I tried to be clever and pre-optimize by putting all the layers onto one canvas… I think this probably helped by batching their paints, but it made fades less smooth by not taking advantage of native CSS transitions, and it made us dip into JS way more often than necessary.
Here, I take the simpler approach: just layers of <img> and <canvas> tags, with each animated layer on its own canvas, and letting the browser handle transitions and compositing, and separate `setInterval` timers to manage their framerates.
I have a suspicion that batching the paints could help performance more, but honestly, maybe that batching is already happening somehow, because things look pretty great on my big-screen stress test now; and so if it _is_ relevant, I want to wait and see after testing on low-power devices.
2020-10-08 04:13:47 -07:00
|
|
|
import React from "react";
|
2021-01-23 12:43:17 -08:00
|
|
|
import { useToast } from "@chakra-ui/react";
|
simplify canvas code, just use separate elements
Previously I tried to be clever and pre-optimize by putting all the layers onto one canvas… I think this probably helped by batching their paints, but it made fades less smooth by not taking advantage of native CSS transitions, and it made us dip into JS way more often than necessary.
Here, I take the simpler approach: just layers of <img> and <canvas> tags, with each animated layer on its own canvas, and letting the browser handle transitions and compositing, and separate `setInterval` timers to manage their framerates.
I have a suspicion that batching the paints could help performance more, but honestly, maybe that batching is already happening somehow, because things look pretty great on my big-screen stress test now; and so if it _is_ relevant, I want to wait and see after testing on low-power devices.
2020-10-08 04:13:47 -07:00
|
|
|
|
2021-01-23 12:43:17 -08:00
|
|
|
import { loadImage, logAndCapture, safeImageUrl } from "../util";
|
simplify canvas code, just use separate elements
Previously I tried to be clever and pre-optimize by putting all the layers onto one canvas… I think this probably helped by batching their paints, but it made fades less smooth by not taking advantage of native CSS transitions, and it made us dip into JS way more often than necessary.
Here, I take the simpler approach: just layers of <img> and <canvas> tags, with each animated layer on its own canvas, and letting the browser handle transitions and compositing, and separate `setInterval` timers to manage their framerates.
I have a suspicion that batching the paints could help performance more, but honestly, maybe that batching is already happening somehow, because things look pretty great on my big-screen stress test now; and so if it _is_ relevant, I want to wait and see after testing on low-power devices.
2020-10-08 04:13:47 -07:00
|
|
|
|
2020-10-10 03:46:23 -07:00
|
|
|
function OutfitMovieLayer({
|
|
|
|
libraryUrl,
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
isPaused = false,
|
|
|
|
onLoad = null,
|
|
|
|
}) {
|
simplify canvas code, just use separate elements
Previously I tried to be clever and pre-optimize by putting all the layers onto one canvas… I think this probably helped by batching their paints, but it made fades less smooth by not taking advantage of native CSS transitions, and it made us dip into JS way more often than necessary.
Here, I take the simpler approach: just layers of <img> and <canvas> tags, with each animated layer on its own canvas, and letting the browser handle transitions and compositing, and separate `setInterval` timers to manage their framerates.
I have a suspicion that batching the paints could help performance more, but honestly, maybe that batching is already happening somehow, because things look pretty great on my big-screen stress test now; and so if it _is_ relevant, I want to wait and see after testing on low-power devices.
2020-10-08 04:13:47 -07:00
|
|
|
const [stage, setStage] = React.useState(null);
|
|
|
|
const [library, setLibrary] = React.useState(null);
|
|
|
|
const [movieClip, setMovieClip] = React.useState(null);
|
|
|
|
const canvasRef = React.useRef(null);
|
2021-01-23 12:43:17 -08:00
|
|
|
const hasShownErrorMessageRef = React.useRef(false);
|
|
|
|
const toast = useToast();
|
simplify canvas code, just use separate elements
Previously I tried to be clever and pre-optimize by putting all the layers onto one canvas… I think this probably helped by batching their paints, but it made fades less smooth by not taking advantage of native CSS transitions, and it made us dip into JS way more often than necessary.
Here, I take the simpler approach: just layers of <img> and <canvas> tags, with each animated layer on its own canvas, and letting the browser handle transitions and compositing, and separate `setInterval` timers to manage their framerates.
I have a suspicion that batching the paints could help performance more, but honestly, maybe that batching is already happening somehow, because things look pretty great on my big-screen stress test now; and so if it _is_ relevant, I want to wait and see after testing on low-power devices.
2020-10-08 04:13:47 -07:00
|
|
|
|
|
|
|
const loadingDeps = useEaselDependenciesLoader();
|
|
|
|
|
|
|
|
// Set the canvas's internal dimensions to be higher, if the device has high
|
|
|
|
// DPI like retina. But we'll keep the layout width/height as expected!
|
|
|
|
const internalWidth = width * window.devicePixelRatio;
|
|
|
|
const internalHeight = height * window.devicePixelRatio;
|
|
|
|
|
2021-01-23 12:43:17 -08:00
|
|
|
const updateStage = React.useCallback(() => {
|
|
|
|
if (!stage) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
stage.update();
|
|
|
|
} catch (e) {
|
|
|
|
// If rendering the frame fails, log it and proceed. If it's an
|
|
|
|
// animation, then maybe the next frame will work? Also alert the user,
|
|
|
|
// just as an FYI. (This is pretty uncommon, so I'm not worried about
|
|
|
|
// being noisy!)
|
|
|
|
if (!hasShownErrorMessageRef.current) {
|
|
|
|
console.error(`Error rendering movie clip ${libraryUrl}`);
|
|
|
|
logAndCapture(e);
|
|
|
|
toast({
|
2021-02-01 18:55:05 -08:00
|
|
|
status: "warning",
|
2021-01-23 12:43:17 -08:00
|
|
|
title:
|
|
|
|
"Hmm, we're maybe having trouble playing one of these animations.",
|
|
|
|
description:
|
|
|
|
"If it looks wrong, try pausing and playing, or reloading the " +
|
|
|
|
"page. Sorry!",
|
|
|
|
duration: 10000,
|
|
|
|
isClosable: true,
|
|
|
|
});
|
|
|
|
// We do this via a ref, not state, because I want to guarantee that
|
|
|
|
// future calls see the new value. With state, React's effects might
|
|
|
|
// not happen in the right order for it to work!
|
|
|
|
hasShownErrorMessageRef.current = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [stage, toast, libraryUrl]);
|
|
|
|
|
simplify canvas code, just use separate elements
Previously I tried to be clever and pre-optimize by putting all the layers onto one canvas… I think this probably helped by batching their paints, but it made fades less smooth by not taking advantage of native CSS transitions, and it made us dip into JS way more often than necessary.
Here, I take the simpler approach: just layers of <img> and <canvas> tags, with each animated layer on its own canvas, and letting the browser handle transitions and compositing, and separate `setInterval` timers to manage their framerates.
I have a suspicion that batching the paints could help performance more, but honestly, maybe that batching is already happening somehow, because things look pretty great on my big-screen stress test now; and so if it _is_ relevant, I want to wait and see after testing on low-power devices.
2020-10-08 04:13:47 -07:00
|
|
|
// This effect gives us a `stage` corresponding to the canvas element.
|
|
|
|
React.useLayoutEffect(() => {
|
2021-02-01 18:55:05 -08:00
|
|
|
const canvas = canvasRef.current;
|
|
|
|
|
|
|
|
if (loadingDeps || !canvas) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (canvas.getContext("2d") == null) {
|
|
|
|
console.warn(`Out of memory, can't use canvas for ${libraryUrl}.`);
|
|
|
|
toast({
|
|
|
|
status: "warning",
|
|
|
|
title: "Oops, too many animations!",
|
|
|
|
description:
|
|
|
|
`Your device is out of memory, so we can't show any more ` +
|
|
|
|
`animations. Try removing some items, or using another device.`,
|
|
|
|
duration: null,
|
|
|
|
isClosable: true,
|
|
|
|
});
|
simplify canvas code, just use separate elements
Previously I tried to be clever and pre-optimize by putting all the layers onto one canvas… I think this probably helped by batching their paints, but it made fades less smooth by not taking advantage of native CSS transitions, and it made us dip into JS way more often than necessary.
Here, I take the simpler approach: just layers of <img> and <canvas> tags, with each animated layer on its own canvas, and letting the browser handle transitions and compositing, and separate `setInterval` timers to manage their framerates.
I have a suspicion that batching the paints could help performance more, but honestly, maybe that batching is already happening somehow, because things look pretty great on my big-screen stress test now; and so if it _is_ relevant, I want to wait and see after testing on low-power devices.
2020-10-08 04:13:47 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
setStage((stage) => {
|
2021-02-01 18:55:05 -08:00
|
|
|
if (stage && stage.canvas === canvas) {
|
simplify canvas code, just use separate elements
Previously I tried to be clever and pre-optimize by putting all the layers onto one canvas… I think this probably helped by batching their paints, but it made fades less smooth by not taking advantage of native CSS transitions, and it made us dip into JS way more often than necessary.
Here, I take the simpler approach: just layers of <img> and <canvas> tags, with each animated layer on its own canvas, and letting the browser handle transitions and compositing, and separate `setInterval` timers to manage their framerates.
I have a suspicion that batching the paints could help performance more, but honestly, maybe that batching is already happening somehow, because things look pretty great on my big-screen stress test now; and so if it _is_ relevant, I want to wait and see after testing on low-power devices.
2020-10-08 04:13:47 -07:00
|
|
|
return stage;
|
|
|
|
}
|
|
|
|
|
2021-02-01 18:55:05 -08:00
|
|
|
return new window.createjs.Stage(canvas);
|
simplify canvas code, just use separate elements
Previously I tried to be clever and pre-optimize by putting all the layers onto one canvas… I think this probably helped by batching their paints, but it made fades less smooth by not taking advantage of native CSS transitions, and it made us dip into JS way more often than necessary.
Here, I take the simpler approach: just layers of <img> and <canvas> tags, with each animated layer on its own canvas, and letting the browser handle transitions and compositing, and separate `setInterval` timers to manage their framerates.
I have a suspicion that batching the paints could help performance more, but honestly, maybe that batching is already happening somehow, because things look pretty great on my big-screen stress test now; and so if it _is_ relevant, I want to wait and see after testing on low-power devices.
2020-10-08 04:13:47 -07:00
|
|
|
});
|
2021-02-01 18:55:05 -08:00
|
|
|
|
|
|
|
return () => {
|
|
|
|
setStage(null);
|
|
|
|
|
|
|
|
if (canvas) {
|
|
|
|
// There's a Safari bug where it doesn't reliably garbage-collect
|
|
|
|
// canvas data. Clean it up ourselves, rather than leaking memory over
|
|
|
|
// time! https://stackoverflow.com/a/52586606/107415
|
|
|
|
// https://bugs.webkit.org/show_bug.cgi?id=195325
|
|
|
|
canvas.width = 0;
|
|
|
|
canvas.height = 0;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}, [loadingDeps, libraryUrl, toast]);
|
simplify canvas code, just use separate elements
Previously I tried to be clever and pre-optimize by putting all the layers onto one canvas… I think this probably helped by batching their paints, but it made fades less smooth by not taking advantage of native CSS transitions, and it made us dip into JS way more often than necessary.
Here, I take the simpler approach: just layers of <img> and <canvas> tags, with each animated layer on its own canvas, and letting the browser handle transitions and compositing, and separate `setInterval` timers to manage their framerates.
I have a suspicion that batching the paints could help performance more, but honestly, maybe that batching is already happening somehow, because things look pretty great on my big-screen stress test now; and so if it _is_ relevant, I want to wait and see after testing on low-power devices.
2020-10-08 04:13:47 -07:00
|
|
|
|
|
|
|
// This effect gives us the `library` and `movieClip`, based on the incoming
|
|
|
|
// `libraryUrl`.
|
|
|
|
React.useEffect(() => {
|
|
|
|
if (loadingDeps) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let canceled = false;
|
|
|
|
|
|
|
|
loadMovieLibrary(libraryUrl)
|
|
|
|
.then((library) => {
|
|
|
|
if (canceled) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
setLibrary(library);
|
|
|
|
|
|
|
|
const movieClip = buildMovieClip(library, libraryUrl);
|
|
|
|
setMovieClip(movieClip);
|
|
|
|
})
|
|
|
|
.catch((e) => {
|
2021-02-01 18:55:05 -08:00
|
|
|
console.error(`Error loading outfit movie layer: ${libraryUrl}`, e);
|
simplify canvas code, just use separate elements
Previously I tried to be clever and pre-optimize by putting all the layers onto one canvas… I think this probably helped by batching their paints, but it made fades less smooth by not taking advantage of native CSS transitions, and it made us dip into JS way more often than necessary.
Here, I take the simpler approach: just layers of <img> and <canvas> tags, with each animated layer on its own canvas, and letting the browser handle transitions and compositing, and separate `setInterval` timers to manage their framerates.
I have a suspicion that batching the paints could help performance more, but honestly, maybe that batching is already happening somehow, because things look pretty great on my big-screen stress test now; and so if it _is_ relevant, I want to wait and see after testing on low-power devices.
2020-10-08 04:13:47 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
canceled = true;
|
|
|
|
setLibrary(null);
|
|
|
|
setMovieClip(null);
|
|
|
|
};
|
|
|
|
}, [loadingDeps, libraryUrl]);
|
|
|
|
|
|
|
|
// This effect puts the `movieClip` on the `stage`, when both are ready.
|
|
|
|
React.useEffect(() => {
|
|
|
|
if (!stage || !movieClip) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
stage.addChild(movieClip);
|
|
|
|
|
|
|
|
// Render the movie's first frame. If it's animated and we're not paused,
|
|
|
|
// then another effect will perform subsequent updates.
|
2021-01-23 12:43:17 -08:00
|
|
|
updateStage();
|
simplify canvas code, just use separate elements
Previously I tried to be clever and pre-optimize by putting all the layers onto one canvas… I think this probably helped by batching their paints, but it made fades less smooth by not taking advantage of native CSS transitions, and it made us dip into JS way more often than necessary.
Here, I take the simpler approach: just layers of <img> and <canvas> tags, with each animated layer on its own canvas, and letting the browser handle transitions and compositing, and separate `setInterval` timers to manage their framerates.
I have a suspicion that batching the paints could help performance more, but honestly, maybe that batching is already happening somehow, because things look pretty great on my big-screen stress test now; and so if it _is_ relevant, I want to wait and see after testing on low-power devices.
2020-10-08 04:13:47 -07:00
|
|
|
|
2020-10-10 03:46:23 -07:00
|
|
|
// This is when we trigger `onLoad`: once we're actually showing it!
|
|
|
|
if (onLoad) {
|
|
|
|
onLoad();
|
|
|
|
}
|
|
|
|
|
simplify canvas code, just use separate elements
Previously I tried to be clever and pre-optimize by putting all the layers onto one canvas… I think this probably helped by batching their paints, but it made fades less smooth by not taking advantage of native CSS transitions, and it made us dip into JS way more often than necessary.
Here, I take the simpler approach: just layers of <img> and <canvas> tags, with each animated layer on its own canvas, and letting the browser handle transitions and compositing, and separate `setInterval` timers to manage their framerates.
I have a suspicion that batching the paints could help performance more, but honestly, maybe that batching is already happening somehow, because things look pretty great on my big-screen stress test now; and so if it _is_ relevant, I want to wait and see after testing on low-power devices.
2020-10-08 04:13:47 -07:00
|
|
|
return () => stage.removeChild(movieClip);
|
2021-01-23 12:43:17 -08:00
|
|
|
}, [stage, updateStage, movieClip, onLoad]);
|
simplify canvas code, just use separate elements
Previously I tried to be clever and pre-optimize by putting all the layers onto one canvas… I think this probably helped by batching their paints, but it made fades less smooth by not taking advantage of native CSS transitions, and it made us dip into JS way more often than necessary.
Here, I take the simpler approach: just layers of <img> and <canvas> tags, with each animated layer on its own canvas, and letting the browser handle transitions and compositing, and separate `setInterval` timers to manage their framerates.
I have a suspicion that batching the paints could help performance more, but honestly, maybe that batching is already happening somehow, because things look pretty great on my big-screen stress test now; and so if it _is_ relevant, I want to wait and see after testing on low-power devices.
2020-10-08 04:13:47 -07:00
|
|
|
|
|
|
|
// This effect updates the `stage` according to the `library`'s framerate,
|
|
|
|
// but only if there's actual animation to do - i.e., there's more than one
|
|
|
|
// frame to show, and we're not paused.
|
|
|
|
React.useEffect(() => {
|
|
|
|
if (!stage || !movieClip || !library) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isPaused || !hasAnimations(movieClip)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const intervalId = setInterval(
|
2021-01-23 12:43:17 -08:00
|
|
|
() => updateStage(),
|
simplify canvas code, just use separate elements
Previously I tried to be clever and pre-optimize by putting all the layers onto one canvas… I think this probably helped by batching their paints, but it made fades less smooth by not taking advantage of native CSS transitions, and it made us dip into JS way more often than necessary.
Here, I take the simpler approach: just layers of <img> and <canvas> tags, with each animated layer on its own canvas, and letting the browser handle transitions and compositing, and separate `setInterval` timers to manage their framerates.
I have a suspicion that batching the paints could help performance more, but honestly, maybe that batching is already happening somehow, because things look pretty great on my big-screen stress test now; and so if it _is_ relevant, I want to wait and see after testing on low-power devices.
2020-10-08 04:13:47 -07:00
|
|
|
1000 / library.properties.fps
|
|
|
|
);
|
|
|
|
return () => clearInterval(intervalId);
|
2021-01-23 12:43:17 -08:00
|
|
|
}, [stage, updateStage, movieClip, library, isPaused]);
|
simplify canvas code, just use separate elements
Previously I tried to be clever and pre-optimize by putting all the layers onto one canvas… I think this probably helped by batching their paints, but it made fades less smooth by not taking advantage of native CSS transitions, and it made us dip into JS way more often than necessary.
Here, I take the simpler approach: just layers of <img> and <canvas> tags, with each animated layer on its own canvas, and letting the browser handle transitions and compositing, and separate `setInterval` timers to manage their framerates.
I have a suspicion that batching the paints could help performance more, but honestly, maybe that batching is already happening somehow, because things look pretty great on my big-screen stress test now; and so if it _is_ relevant, I want to wait and see after testing on low-power devices.
2020-10-08 04:13:47 -07:00
|
|
|
|
|
|
|
// This effect keeps the `movieClip` scaled correctly, based on the canvas
|
|
|
|
// size and the `library`'s natural size declaration. (If the canvas size
|
|
|
|
// changes on window resize, then this will keep us responsive, so long as
|
|
|
|
// the parent updates our width/height props on window resize!)
|
|
|
|
React.useEffect(() => {
|
|
|
|
if (!stage || !movieClip || !library) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
movieClip.scaleX = internalWidth / library.properties.width;
|
|
|
|
movieClip.scaleY = internalHeight / library.properties.height;
|
|
|
|
|
2020-10-10 04:51:53 -07:00
|
|
|
// Redraw the stage with the new dimensions - but with `tickOnUpdate` set
|
|
|
|
// to `false`, so that we don't advance by a frame. This keeps us
|
|
|
|
// really-paused if we're paused, and avoids skipping ahead by a frame if
|
|
|
|
// we're playing.
|
|
|
|
stage.tickOnUpdate = false;
|
2021-01-23 12:43:17 -08:00
|
|
|
updateStage();
|
2020-10-10 04:51:53 -07:00
|
|
|
stage.tickOnUpdate = true;
|
2021-01-23 12:43:17 -08:00
|
|
|
}, [stage, updateStage, library, movieClip, internalWidth, internalHeight]);
|
simplify canvas code, just use separate elements
Previously I tried to be clever and pre-optimize by putting all the layers onto one canvas… I think this probably helped by batching their paints, but it made fades less smooth by not taking advantage of native CSS transitions, and it made us dip into JS way more often than necessary.
Here, I take the simpler approach: just layers of <img> and <canvas> tags, with each animated layer on its own canvas, and letting the browser handle transitions and compositing, and separate `setInterval` timers to manage their framerates.
I have a suspicion that batching the paints could help performance more, but honestly, maybe that batching is already happening somehow, because things look pretty great on my big-screen stress test now; and so if it _is_ relevant, I want to wait and see after testing on low-power devices.
2020-10-08 04:13:47 -07:00
|
|
|
|
|
|
|
return (
|
|
|
|
<canvas
|
|
|
|
ref={canvasRef}
|
|
|
|
width={internalWidth}
|
|
|
|
height={internalHeight}
|
|
|
|
style={{ width: width, height: height }}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* useEaselDependenciesLoader loads the CreateJS scripts we use in OutfitCanvas.
|
|
|
|
* We load it as part of OutfitCanvas, but callers can also use this to preload
|
|
|
|
* the scripts and track loading progress.
|
|
|
|
*/
|
|
|
|
export function useEaselDependenciesLoader() {
|
|
|
|
// NOTE: I couldn't find an official NPM source for this that worked with
|
|
|
|
// Webpack, and I didn't want to rely on random people's ports, and I
|
|
|
|
// couldn't get a bundled version to work quite right. So we load
|
|
|
|
// createjs async!
|
|
|
|
const loadingEaselJS = useScriptTag(
|
|
|
|
"https://code.createjs.com/1.0.0/easeljs.min.js"
|
|
|
|
);
|
|
|
|
const loadingTweenJS = useScriptTag(
|
|
|
|
"https://code.createjs.com/1.0.0/tweenjs.min.js"
|
|
|
|
);
|
|
|
|
const loadingDeps = loadingEaselJS || loadingTweenJS;
|
|
|
|
|
|
|
|
return loadingDeps;
|
|
|
|
}
|
|
|
|
|
|
|
|
function useScriptTag(src) {
|
|
|
|
const [loading, setLoading] = React.useState(true);
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
const existingScript = document.querySelector(
|
|
|
|
`script[src=${CSS.escape(src)}]`
|
|
|
|
);
|
|
|
|
if (existingScript) {
|
|
|
|
setLoading(false);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let canceled = false;
|
|
|
|
loadScriptTag(src).then(() => {
|
|
|
|
if (!canceled) {
|
|
|
|
setLoading(false);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
canceled = true;
|
|
|
|
setLoading(true);
|
|
|
|
};
|
|
|
|
}, [src, setLoading]);
|
|
|
|
|
|
|
|
return loading;
|
|
|
|
}
|
|
|
|
|
|
|
|
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 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!
|
|
|
|
//
|
|
|
|
// I'm not _sure_ this method is reliable, but it seems to be stable so far
|
|
|
|
// in Firefox for me. The things I think I'm observing are:
|
|
|
|
// - Script execution order should match insert order,
|
|
|
|
// - Onload execution order should match insert order,
|
|
|
|
// - BUT, script executions might be batched before onloads.
|
|
|
|
// - So, each script grabs the _first_ composition from the list, and
|
|
|
|
// deletes it after grabbing. That way, it serves as a FIFO queue!
|
|
|
|
// I'm not suuure this is happening as I'm expecting, vs I'm just not seeing
|
|
|
|
// the race anymore? But fingers crossed!
|
|
|
|
await loadScriptTag(safeImageUrl(librarySrc));
|
|
|
|
const [compositionId, composition] = Object.entries(
|
|
|
|
window.AdobeAn.compositions
|
|
|
|
)[0];
|
|
|
|
if (Object.keys(window.AdobeAn.compositions).length > 1) {
|
|
|
|
console.warn(
|
|
|
|
`Grabbing composition ${compositionId}, but there are >1 here: `,
|
|
|
|
Object.keys(window.AdobeAn.compositions).length
|
|
|
|
);
|
|
|
|
}
|
|
|
|
delete window.AdobeAn.compositions[compositionId];
|
|
|
|
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,
|
add CORS for movie clip asset loading
I think the issue with the Spring Topiary Garden Background is that EaselJS is trying to do intermediate canvas reads in order to apply computed filters, but loading from our asset proxy counts as tainted data.
Here's the traceback I got in Chrome for it:
```
Error building movie clips DOMException: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data.
at a.b._applyFilters (https://code.createjs.com/1.0.0/easeljs.min.js:15:12029)
at a.b._drawToCache (https://code.createjs.com/1.0.0/easeljs.min.js:15:11806)
at a.b.update (https://code.createjs.com/1.0.0/easeljs.min.js:15:8638)
at a.b.define (https://code.createjs.com/1.0.0/easeljs.min.js:15:8148)
at lib.Flowerfront.b.cache (https://code.createjs.com/1.0.0/easeljs.min.js:13:3361)
at new lib.Bg (https://images.neopets-asset-proxy.openneo.net/cp/items/data/000/000/441/441520_f4a43d48bf/441520HTML5.js:4266:16)
at new lib._441520HTML5 (https://images.neopets-asset-proxy.openneo.net/cp/items/data/000/000/441/441520_f4a43d48bf/441520HTML5.js:5291:18)
at x (https://impress-2020.openneo.net/static/js/11.3a356cfe.chunk.js:1:12286)
at https://impress-2020.openneo.net/static/js/11.3a356cfe.chunk.js:1:17768
at Array.map (<anonymous>)
```
To try to fix this, I've updated our Fastly config to version 8, which should accept CORS requests from https://impress-2020.openneo.net. And here, I've updated the movie clip assets to be requested CORS-style, so that the Origin header will actually be set.
It's hard to test this without just, pushing it to prod. I've confirmed in isolation that setting the `Origin` header in the request yields the expected `Access-Control-Allow-Origin` response header, and that the `Vary` header is set correctly too. But, end-to-end, I don't really have great mockability here—maybe with a good proxy setup I could do it? But nah, let's just push and find out!
2020-10-23 00:20:50 -07:00
|
|
|
loadImage({
|
|
|
|
src: safeImageUrl(librarySrcDir + "/" + src),
|
|
|
|
crossOrigin: "anonymous",
|
|
|
|
}),
|
simplify canvas code, just use separate elements
Previously I tried to be clever and pre-optimize by putting all the layers onto one canvas… I think this probably helped by batching their paints, but it made fades less smooth by not taking advantage of native CSS transitions, and it made us dip into JS way more often than necessary.
Here, I take the simpler approach: just layers of <img> and <canvas> tags, with each animated layer on its own canvas, and letting the browser handle transitions and compositing, and separate `setInterval` timers to manage their framerates.
I have a suspicion that batching the paints could help performance more, but honestly, maybe that batching is already happening somehow, because things look pretty great on my big-screen stress test now; and so if it _is_ relevant, I want to wait and see after testing on low-power devices.
2020-10-08 04:13:47 -07:00
|
|
|
])
|
|
|
|
);
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function buildMovieClip(library, libraryUrl) {
|
|
|
|
let constructorName;
|
|
|
|
try {
|
fix bug with items with %20 in movie clip URL
"Beautiful Green Painting Background" wasn't loading! https://impress-2020.openneo.net/items/75594
```Error building movie clips Error: Expected JS movie library http://images.neopets.com/cp/items/data/000/000/491/491273_31368b3745/491273_2_HTML5%20Canvas.js to contain a constructor named _491273_2_HTML5%20Canvas, but it did not: ssMetadata,Bitmap3,Bitmap5,CachedTexturedBitmap_4183,CachedTexturedBitmap_4184,CachedTexturedBitmap_4185,CachedTexturedBitmap_4186,CachedTexturedBitmap_4187,Symbol20,Symbol8,Symbol4,Symbol7,Symbol2,Symbol1,Symbol9,Symbol2copy,Symbol2_1,_491273_2_HTML5Canvas,properties,Stage```
We already had code to strip out spaces, but not encoded spaces like %20. Now, we decode the URL first, so that space-stripping will work even if it was encoded.
2020-10-28 00:29:32 -07:00
|
|
|
const fileName = decodeURI(libraryUrl).split("/").pop();
|
simplify canvas code, just use separate elements
Previously I tried to be clever and pre-optimize by putting all the layers onto one canvas… I think this probably helped by batching their paints, but it made fades less smooth by not taking advantage of native CSS transitions, and it made us dip into JS way more often than necessary.
Here, I take the simpler approach: just layers of <img> and <canvas> tags, with each animated layer on its own canvas, and letting the browser handle transitions and compositing, and separate `setInterval` timers to manage their framerates.
I have a suspicion that batching the paints could help performance more, but honestly, maybe that batching is already happening somehow, because things look pretty great on my big-screen stress test now; and so if it _is_ relevant, I want to wait and see after testing on low-power devices.
2020-10-08 04:13:47 -07:00
|
|
|
const fileNameWithoutExtension = fileName.split(".")[0];
|
|
|
|
constructorName = fileNameWithoutExtension.replace(/[ -]/g, "");
|
|
|
|
if (constructorName.match(/^[0-9]/)) {
|
|
|
|
constructorName = "_" + constructorName;
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
throw new Error(
|
|
|
|
`Movie libraryUrl ${JSON.stringify(
|
|
|
|
libraryUrl
|
|
|
|
)} did not match expected format: ${e.message}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const LibraryMovieClipConstructor = library[constructorName];
|
|
|
|
if (!LibraryMovieClipConstructor) {
|
|
|
|
throw new Error(
|
|
|
|
`Expected JS movie library ${libraryUrl} to contain a constructor ` +
|
|
|
|
`named ${constructorName}, but it did not: ${Object.keys(library)}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
const movieClip = new LibraryMovieClipConstructor();
|
|
|
|
|
|
|
|
return movieClip;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Recursively scans the given MovieClip (or child createjs node), to see if
|
|
|
|
* there are any animated areas.
|
|
|
|
*/
|
|
|
|
export function hasAnimations(createjsNode) {
|
|
|
|
return (
|
|
|
|
createjsNode.totalFrames > 1 ||
|
|
|
|
(createjsNode.children || []).some(hasAnimations)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export default OutfitMovieLayer;
|