diff --git a/src/BodySegmentation/index.js b/src/BodySegmentation/index.js index 5a5428fb..53e309be 100644 --- a/src/BodySegmentation/index.js +++ b/src/BodySegmentation/index.js @@ -25,9 +25,6 @@ class BodySegmentation { * @param {function} [callback] - A callback to be called when the model is ready. */ constructor(modelName = "SelfieSegmentation", options, callback) { - // for compatibility with p5's preload() - if (this.p5PreLoadExists()) window._incrementPreload(); - this.modelName = modelName; this.video = video; this.model = null; @@ -187,9 +184,6 @@ class BodySegmentation { modelConfig ); - // for compatibility with p5's preload() - if (this.p5PreLoadExists) window._decrementPreload(); - return this; } /** @@ -340,21 +334,6 @@ class BodySegmentation { return imageData; } } - - /** - * Check if p5.js' preload() function is present - * @returns {boolean} true if preload() exists - * - * @private - */ - p5PreLoadExists() { - if (typeof window === "undefined") return false; - if (typeof window.p5 === "undefined") return false; - if (typeof window.p5.prototype === "undefined") return false; - if (typeof window.p5.prototype.registerPreloadMethod === "undefined") - return false; - return true; - } } /** diff --git a/src/FaceMesh/index.js b/src/FaceMesh/index.js index 59deeb44..5a6b6747 100644 --- a/src/FaceMesh/index.js +++ b/src/FaceMesh/index.js @@ -36,9 +36,6 @@ class FaceMesh { * @private */ constructor(options, callback) { - // for compatibility with p5's preload() - if (this.p5PreLoadExists()) window._incrementPreload(); - this.model = null; this.config = options; this.runtimeConfig = {}; @@ -115,9 +112,6 @@ class FaceMesh { modelConfig ); - // for compatibility with p5's preload() - if (this.p5PreLoadExists) window._decrementPreload(); - return this; } @@ -284,21 +278,6 @@ class FaceMesh { } return faces; } - - /** - * Check if p5.js' preload() function is present - * @returns {boolean} true if preload() exists - * - * @private - */ - p5PreLoadExists() { - if (typeof window === "undefined") return false; - if (typeof window.p5 === "undefined") return false; - if (typeof window.p5.prototype === "undefined") return false; - if (typeof window.p5.prototype.registerPreloadMethod === "undefined") - return false; - return true; - } } /** diff --git a/src/HandPose/index.js b/src/HandPose/index.js index 47f06c8c..b9e1ad41 100644 --- a/src/HandPose/index.js +++ b/src/HandPose/index.js @@ -37,9 +37,6 @@ class HandPose { * @private */ constructor(options, callback) { - // for compatibility with p5's preload() - if (this.p5PreLoadExists()) window._incrementPreload(); - this.model = null; this.config = options; this.runtimeConfig = {}; @@ -114,9 +111,6 @@ class HandPose { await tf.ready(); this.model = await handPoseDetection.createDetector(pipeline, modelConfig); - // for compatibility with p5's preload() - if (this.p5PreLoadExists) window._decrementPreload(); - return this; } @@ -239,20 +233,6 @@ class HandPose { }); return result; } - - /** - * Check if p5.js' preload() function is present in the current environment. - * @returns {boolean} True if preload() exists. False otherwise. - * @private - */ - p5PreLoadExists() { - if (typeof window === "undefined") return false; - if (typeof window.p5 === "undefined") return false; - if (typeof window.p5.prototype === "undefined") return false; - if (typeof window.p5.prototype.registerPreloadMethod === "undefined") - return false; - return true; - } } /** diff --git a/src/NeuralNetwork/index.js b/src/NeuralNetwork/index.js index 00dfd420..5b004cfb 100644 --- a/src/NeuralNetwork/index.js +++ b/src/NeuralNetwork/index.js @@ -124,7 +124,7 @@ class DiyNeuralNetwork { // will take a URL to model.json, an object, or files array this.ready = this.load(this.options.modelUrl, callback); } else { - this.ready = true; + this.ready = Promise.resolve(this); } } diff --git a/src/Sentiment/index.js b/src/Sentiment/index.js index 0f67c5bf..ee3aa922 100644 --- a/src/Sentiment/index.js +++ b/src/Sentiment/index.js @@ -51,8 +51,8 @@ class Sentiment { */ constructor(modelName, callback) { /** - * Boolean value that specifies if the model has loaded. - * @type {boolean} + * Promise that resolves when the model has loaded. + * @type {Promise} * @public */ this.ready = callCallback(this.loadModel(modelName), callback); diff --git a/src/index.js b/src/index.js index 2f896780..6af539e8 100644 --- a/src/index.js +++ b/src/index.js @@ -10,22 +10,26 @@ import setBackend from "./utils/setBackend"; import bodySegmentation from "./BodySegmentation"; import communityStatement from "./utils/communityStatement"; import imageClassifier from "./ImageClassifier"; -import preloadRegister from "./utils/p5PreloadHelper"; const withPreload = { + bodyPose, + bodySegmentation, + faceMesh, + handPose, imageClassifier, + neuralNetwork, + sentiment, }; -export default Object.assign({ p5Utils }, preloadRegister(withPreload), { +const ml5 = Object.assign({ p5Utils }, withPreload, { tf, tfvis, - neuralNetwork, - handPose, - sentiment, - faceMesh, - bodyPose, setBackend, - bodySegmentation, + setP5: p5Utils.setP5.bind(p5Utils), }); +p5Utils.shouldPreload(ml5, Object.keys(withPreload)); + communityStatement(); + +export default ml5; diff --git a/src/utils/p5PreloadHelper.js b/src/utils/p5PreloadHelper.js deleted file mode 100644 index f75aaa1c..00000000 --- a/src/utils/p5PreloadHelper.js +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2019 ml5 -// -// This software is released under the MIT License. -// https://opensource.org/licenses/MIT - -/** - * a list to store all functions to hook p5 preload - * @param {obj} object or prototype to wrap with - * @returns obj - */ -export default function registerPreload(obj) { - if (typeof window === "undefined") return obj; - if (typeof window.p5 === "undefined") return obj; - if (typeof window.p5.prototype === "undefined") return obj; - if (typeof window.p5.prototype.registerPreloadMethod === "undefined") - return obj; - - const preloadFn = obj; - Object.keys(obj).forEach((key) => { - const fn = obj[key]; - - preloadFn[key] = function preloads(...args) { - let originCallback = null; - let argLen = args.length; - if (typeof args[argLen - 1] === "function") { - // find callback function attached - originCallback = args[argLen - 1]; - argLen -= 1; - } - return fn.apply(obj, [ - ...args.slice(0, argLen), - function doingPreloads() { - const targetPreloadFn = "_decrementPreload"; - try { - if (originCallback) originCallback(); - } catch (err) { - console.error(err); - } - if (window[targetPreloadFn]) return window[targetPreloadFn](); - return null; - }, - ]); - }; - window.p5.prototype.registerPreloadMethod(`${key}`, obj); - }); - - return obj; -} diff --git a/src/utils/p5Utils.js b/src/utils/p5Utils.js index e38783f8..fceed4e0 100644 --- a/src/utils/p5Utils.js +++ b/src/utils/p5Utils.js @@ -1,50 +1,178 @@ -// Copyright (c) 2018 ml5 +// Copyright (c) 2018 - 2024 ml5 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT +function isP5Constructor(source) { + return Boolean( + source && + typeof source === "function" && + source.prototype && + source.prototype.registerMethod + ); +} + +function isP5Extensions(source) { + return Boolean(source && typeof source.loadImage === "function"); +} + class P5Util { constructor() { + /** + * @type {boolean} + */ + this.didSetupPreload = false; + /** + * The `p5` variable, which can be instantiated via `new p5()` and has a `.prototype` property. + * In browser environments this is `window.p5`. + * When loading p5 via npm it must be manually provided using `ml5.setP5()`. + */ + this.p5Constructor = undefined; + /** + * Object with all of the constants (HSL etc.) and methods like loadImage(). + * In global mode, this is the Window object. + */ + this.p5Extensions = undefined; + + /** + * Keep a reference to the arguments of `shouldPreload()` so that preloads + * can be set up after the fact if p5 becomes available. + */ + this.ml5Library = undefined; + this.methodsToPreload = []; + + // Check for p5 on the window. + this.findAndSetP5(); + } + + /** + * @private + * Check the window or globalThis for p5. + * Can run this repeatedly in case p5 is loaded after ml5 is loaded. + */ + findAndSetP5() { + let source; if (typeof window !== "undefined") { - /** - * Store the window as a private property regardless of whether p5 is present. - * Can also set this property by calling method setP5Instance(). - * @property {Window | p5 | {p5: p5} | undefined} m_p5Instance - * @private - */ - this.m_p5Instance = window; + source = window; + } else if (typeof globalThis !== "undefined") { + source = globalThis; + } + + if (!source) return; + + if (isP5Constructor(source.p5)) { + this.p5Constructor = source.p5; + this.registerPreloads(); + } + if (isP5Extensions(source)) { + this.p5Extensions = source; } } /** - * Set p5 instance globally in order to enable p5 features throughout ml5. - * Call this function with the p5 instance when using p5 in instance mode. - * @param {p5 | {p5: p5}} p5Instance + * @public + * Set p5 in order to enable p5 features throughout ml5. + * This manual setup is only necessary when importing `p5` as a module + * rather than loading it on the window. + * Can be used in ml5 unit tests to check p5 behavior. + * + * @example + * import p5 from "p5"; + * import ml5 "ml5"; + * + * ml5.setP5(p5); + * + * @param {import('p5')} p5 */ - setP5Instance(p5Instance) { - this.m_p5Instance = p5Instance; + setP5(p5) { + if (isP5Constructor(p5)) { + this.p5Constructor = p5; + this.p5Extensions = p5.prototype; + this.registerPreloads(); + } else { + console.warn("Invalid p5 object provided to ml5.setP5()."); + } } /** + * @internal + * Pass in the ml5 methods which require p5 preload behavior. + * Preload functions must return an object with a property `ready` which is a `Promise`. + * Preloading will be set up immediately if p5 is available on the window. + * Store the references in case p5 is added later. + * + * @param {*} ml5Library - the `ml5` variable. + * @param {Array} methodNames - an array of ml5 functions to preload. + */ + shouldPreload(ml5Library, methodNames) { + this.methodsToPreload = methodNames; + this.ml5Library = ml5Library; + if (this.checkP5()) { + this.registerPreloads(); + } + } + + /** + * @private + * Execute the p5 preload setup using the stored references, provided by shouldPreload(). + * Won't do anything if `shouldPreload()` has not been called or if p5 is not found. + */ + registerPreloads() { + if (this.didSetupPreload) return; + const p5 = this.p5Constructor; + const ml5 = this.ml5Library; + const preloadMethods = this.methodsToPreload; + if (!p5 || !ml5) return; + + // Must shallow copy so that it doesn't reference the replaced method. + const original = { ...ml5 }; + // Must alias `this` so that it can be used inside functions with their own `this` context. + const self = this; + + // Function to be called when a sketch is created, either in global or instance mode. + p5.prototype.ml5Init = function () { + // Bind to this specific p5 instance. + const increment = this._incrementPreload.bind(this); + const decrement = this._decrementPreload.bind(this); + // Replace each preloaded on the ml5 object with a wrapped version which + // increments and decrements the p5 preload counter when called. + preloadMethods.forEach((method) => { + ml5[method] = function (...args) { + increment(); + const result = original[method](...args); + result.ready.then(() => { + decrement(); + }); + return result; + }; + }); + self.didSetupPreload = true; + }; + + // Function to be called when a sketch is destroyed. + p5.prototype.ml5Remove = function () { + // Resets each ml5 method back to its original version. + preloadMethods.forEach((method) => { + ml5[method] = original[method]; + }); + self.didSetupPreload = false; + }; + + p5.prototype.registerMethod("init", p5.prototype.ml5Init); + p5.prototype.registerMethod("remove", p5.prototype.ml5Remove); + } + + /** + * @internal * Dynamic getter checks if p5 is loaded and will return undefined if p5 cannot be found, * or will return an object containing all of the global p5 properties. - * It first checks if p5 is in the window, and then if it is in the p5 property of this.m_p5Instance. - * @returns {p5 | undefined} + * @returns {import('p5').p5InstanceExtensions | undefined} */ get p5Instance() { - if ( - typeof this.m_p5Instance !== "undefined" && - typeof this.m_p5Instance.loadImage === "function" - ) - return this.m_p5Instance; - - if ( - typeof this.m_p5Instance.p5 !== "undefined" && - typeof this.m_p5Instance.p5.Image !== "undefined" && - typeof this.m_p5Instance.p5.Image === "function" - ) - return this.m_p5Instance.p5; - return undefined; + if (!this.p5Extensions) { + this.findAndSetP5(); + } + return this.p5Extensions; } /** @@ -77,7 +205,7 @@ class P5Util { /** * Load a p5.Image from a URL in an async way. * @param {string} url - * @return {Promise} + * @return {Promise} */ loadAsync(url) { return new Promise((resolve, reject) => { @@ -140,7 +268,7 @@ class P5Util { * Convert Blob to P5.Image * @param {Blob} blob * Note: may want to reject instead of returning null. - * @returns {Promise} + * @returns {Promise} */ async blobToP5Image(blob) { if (this.checkP5() && typeof URL !== "undefined") {