From 75ceeba6e263be187b90dd07ec60657ebda5e455 Mon Sep 17 00:00:00 2001 From: Matchu Date: Wed, 16 Jun 2021 18:00:25 -0700 Subject: [PATCH] Bundle CreateJS, instead of loading async So I finally started looking into the race condition that makes item previews sometimes fail to load, and as expected, it was that we were trying to load the movie before CreateJS had necessarily loaded. Usually the timing worked out, esp after a reload, but not under certain circumstances! Anyway, I've been wanting for a while to just bundle them instead. That'll help us more eagerly load them when we need them, and not depend on external CDNs, and remove a bunch of loading state! So yeah, I had to learn how the `easeljs` and `tweenjs` NPM packages did their bundling, and how to use `imports-loader` to let them just register straight onto `window`! But we got there and it's pretty nice tbh! --- package.json | 4 ++ src/app/components/OutfitMovieLayer.js | 67 ++++---------------------- src/app/components/OutfitPreview.js | 10 ++-- yarn.lock | 25 ++++++++++ 4 files changed, 41 insertions(+), 65 deletions(-) diff --git a/package.json b/package.json index 0b7c090..1e29a72 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "canvas": "^2.6.1", "dataloader": "^2.0.0", "dompurify": "^2.2.0", + "easeljs": "^1.0.2", "escape-html": "^1.0.3", "framer-motion": "^4.1.11", "graphql": "^15.5.0", @@ -49,6 +50,7 @@ "react-scripts": "^4.0.1", "react-transition-group": "^4.3.0", "simple-markdown": "^0.7.2", + "tweenjs": "^1.0.2", "typescript": "^4.1.3", "xmlrpc": "^1.3.2" }, @@ -91,6 +93,7 @@ } ], "import/first": "off", + "import/no-webpack-loader-syntax": "off", "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": [ "warn", @@ -128,6 +131,7 @@ "es6-promise-pool": "^2.5.0", "eslint-plugin-cypress": "^2.11.2", "husky": "^6.0.0", + "imports-loader": "^3.0.0", "inquirer": "^7.3.3", "jest-image-snapshot": "^4.3.0", "lint-staged": "^10.5.4", diff --git a/src/app/components/OutfitMovieLayer.js b/src/app/components/OutfitMovieLayer.js index 64cbfc2..61a2991 100644 --- a/src/app/components/OutfitMovieLayer.js +++ b/src/app/components/OutfitMovieLayer.js @@ -4,6 +4,12 @@ import { useToast } from "@chakra-ui/react"; import { loadImage, logAndCapture, safeImageUrl } from "../util"; +// Import EaselJS and TweenJS directly into the `window` object! The bundled +// scripts are built to attach themselves to `window.createjs`, and +// `window.createjs` is where the Neopets movie libraries expects to find them! +require("imports-loader?wrapper=window!easeljs/lib/easeljs"); +require("imports-loader?wrapper=window!tweenjs/lib/tweenjs"); + function OutfitMovieLayer({ libraryUrl, width, @@ -19,8 +25,6 @@ function OutfitMovieLayer({ const hasShownErrorMessageRef = React.useRef(false); const toast = useToast(); - 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; @@ -63,7 +67,7 @@ function OutfitMovieLayer({ React.useLayoutEffect(() => { const canvas = canvasRef.current; - if (loadingDeps || !canvas) { + if (!canvas) { return; } @@ -101,15 +105,11 @@ function OutfitMovieLayer({ canvas.height = 0; } }; - }, [loadingDeps, libraryUrl, toast]); + }, [libraryUrl, toast]); // This effect gives us the `library` and `movieClip`, based on the incoming // `libraryUrl`. React.useEffect(() => { - if (loadingDeps) { - return; - } - let canceled = false; loadMovieLibrary(libraryUrl) @@ -132,7 +132,7 @@ function OutfitMovieLayer({ setLibrary(null); setMovieClip(null); }; - }, [loadingDeps, libraryUrl]); + }, [libraryUrl]); // This effect puts the `movieClip` on the `stage`, when both are ready. React.useEffect(() => { @@ -230,55 +230,6 @@ function OutfitMovieLayer({ ); } -/** - * 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"); diff --git a/src/app/components/OutfitPreview.js b/src/app/components/OutfitPreview.js index da3804a..7a2996f 100644 --- a/src/app/components/OutfitPreview.js +++ b/src/app/components/OutfitPreview.js @@ -16,7 +16,6 @@ import OutfitMovieLayer, { buildMovieClip, hasAnimations, loadMovieLibrary, - useEaselDependenciesLoader, } from "./OutfitMovieLayer"; import HangerSpinner from "./HangerSpinner"; import { loadImage, safeImageUrl, useLocalStorage } from "../util"; @@ -163,13 +162,10 @@ export function OutfitLayers({ false ); - const { loading: loadingEasel } = useEaselDependenciesLoader(); - const loadingAnything = loading || loadingEasel; - // When we start in a loading state, or re-enter a loading state, start the // loading delay timer. React.useEffect(() => { - if (loadingAnything) { + if (loading) { setLoadingDelayHasPassed(false); const t = setTimeout( () => setLoadingDelayHasPassed(true), @@ -177,7 +173,7 @@ export function OutfitLayers({ ); return () => clearTimeout(t); } - }, [loadingDelayMs, loadingAnything]); + }, [loadingDelayMs, loading]); React.useLayoutEffect(() => { function computeAndSaveCanvasSize() { @@ -291,7 +287,7 @@ export function OutfitLayers({ // also use a timeout to delay the fade-in by 0.5s, but don't delay the // fade-out at all. (The timeout was an awkward choice, it was hard to // find a good CSS way to specify this delay well!) - opacity={loadingAnything && loadingDelayHasPassed ? 1 : 0} + opacity={loading && loadingDelayHasPassed ? 1 : 0} transition="opacity 0.2s" > {spinnerVariant === "overlay" && ( diff --git a/yarn.lock b/yarn.lock index e02d4e3..fd75e2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9226,6 +9226,11 @@ duplexify@^3.4.2, duplexify@^3.6.0: readable-stream "^2.0.0" stream-shift "^1.0.0" +easeljs@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/easeljs/-/easeljs-1.0.2.tgz#68fdcc69d0f217394e2ebf51ae047428a81240de" + integrity sha512-PQTsiud32vrUIqZCbynjOJjCzoEp0xH+MRusRCdsZ1MzL4LCE2vp4Sa5cr6aShB3mK4vMZ8LFPnts4xuFhjEmg== + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -11426,6 +11431,16 @@ import-local@^3.0.2: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" +imports-loader@1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/imports-loader/-/imports-loader-1.2.0.tgz#b06823d0bb42e6f5ff89bc893829000eda46693f" + integrity sha512-zPvangKEgrrPeqeUqH0Uhc59YqK07JqZBi9a9cQ3v/EKUIqrbJHY4CvUrDus2lgQa5AmPyXuGrWP8JJTqzE5RQ== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + source-map "^0.6.1" + strip-comments "^2.0.1" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -17718,6 +17733,11 @@ strip-comments@^1.0.2: babel-extract-comments "^1.0.0" babel-plugin-transform-object-rest-spread "^6.26.0" +strip-comments@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-comments/-/strip-comments-2.0.1.tgz#4ad11c3fbcac177a67a40ac224ca339ca1c1ba9b" + integrity sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw== + strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" @@ -18348,6 +18368,11 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tweenjs@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tweenjs/-/tweenjs-1.0.2.tgz#768869c4d4ba91fdb4c5ccc661969c58138555af" + integrity sha512-WnFozCNkUkmJtLqJyGrToxVojW2Srzudktr8BzFKQijQRVcmlq7Fc+qfo75ccnwIJGiRWbXKfg7qU67Tzbb1bg== + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"