diff --git a/contributor_docs/creating_libraries.md b/contributor_docs/creating_libraries.md index 35fd893399..2c3b1ae854 100644 --- a/contributor_docs/creating_libraries.md +++ b/contributor_docs/creating_libraries.md @@ -92,6 +92,10 @@ Method names you can register and unregister include the following list. Note th * **post** — Called at the end of `draw()`. * **remove** — Called when `remove()` is called. * **init** — Called when the sketch is first initialized, just before the sketch initialization function (the one that was passed into the `p5` constructor) is executed. This is also called before any global mode setup, so your library can add anything to the sketch and it will automatically be copied to `window` if global mode is active. + * **beforePreload** — Called before the `preload()` function is executed. + * **afterPreload** — Called after the `preload()` function is executed. + * **beforeSetup** — Called before the `setup()` function is executed. + * **afterSetup** — Called after the `setup()` function is executed. More to come shortly, lining up roughly with this list: https://GitHub.com/processing/processing/wiki/Library-Basics#library-methods diff --git a/src/core/main.js b/src/core/main.js index f7f94dfbd6..4e4e37cef5 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -231,6 +231,20 @@ class p5 { this._events.devicemotion = null; } + // Function to invoke registered hooks before or after events such as preload, setup, and pre/post draw. + p5.prototype.callRegisteredHooksFor = function (hookName) { + const target = this || p5.prototype; + const context = this._isGlobal ? window : this; + if (target._registeredMethods.hasOwnProperty(hookName)) { + const methods = target._registeredMethods[hookName]; + for (const method of methods) { + if (typeof method === 'function') { + method.call(context); + } + } + } + }; + this._start = () => { // Find node if id given if (this._userNode) { @@ -241,6 +255,7 @@ class p5 { const context = this._isGlobal ? window : this; if (context.preload) { + this.callRegisteredHooksFor('beforePreload'); // Setup loading screen // Set loading screen into dom if not present // Otherwise displays and removes user provided loading screen @@ -286,6 +301,7 @@ class p5 { if (loadingScreen) { loadingScreen.parentNode.removeChild(loadingScreen); } + this.callRegisteredHooksFor('afterPreload'); if (!this._setupDone) { this._lastTargetFrameTime = window.performance.now(); this._lastRealFrameTime = window.performance.now(); @@ -322,6 +338,7 @@ class p5 { }; this._setup = () => { + this.callRegisteredHooksFor('beforeSetup'); // Always create a default canvas. // Later on if the user calls createCanvas, this default one // will be replaced @@ -369,6 +386,7 @@ class p5 { if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { this._updateAccsOutput(); } + this.callRegisteredHooksFor('afterSetup'); }; this._draw = () => { diff --git a/src/core/structure.js b/src/core/structure.js index 85f66004dc..539a9c1192 100644 --- a/src/core/structure.js +++ b/src/core/structure.js @@ -6,7 +6,6 @@ */ import p5 from './main'; - /** * Stops p5.js from continuously executing the code within draw(). * If loop() is called, the code in draw() @@ -471,9 +470,6 @@ p5.prototype.redraw = function(n) { if (typeof context.setup === 'undefined') { context.scale(context._pixelDensity, context._pixelDensity); } - const callMethod = f => { - f.call(context); - }; for (let idxRedraw = 0; idxRedraw < numberOfRedraws; idxRedraw++) { context.resetMatrix(); if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { @@ -483,14 +479,14 @@ p5.prototype.redraw = function(n) { context._renderer._update(); } context._setProperty('frameCount', context.frameCount + 1); - context._registeredMethods.pre.forEach(callMethod); + this.callRegisteredHooksFor('pre'); this._inUserDraw = true; try { context.draw(); } finally { this._inUserDraw = false; } - context._registeredMethods.post.forEach(callMethod); + this.callRegisteredHooksFor('post'); } } }; diff --git a/test/unit/core/main.js b/test/unit/core/main.js index bc73fab76c..7df683542f 100644 --- a/test/unit/core/main.js +++ b/test/unit/core/main.js @@ -1,68 +1,157 @@ -const { expect } = require('chai'); +const { expect, assert } = require('chai'); -suite('Core', function() { - suite('p5.prototype.registerMethod', function() { - test('should register and call "init" methods', function() { - var originalInit = p5.prototype._registeredMethods.init; +suite('Core', function () { + suite('p5.prototype.registerMethod', function () { + teardown(function() { + p5.prototype._registeredMethods.init = []; + p5.prototype._registeredMethods.beforePreload = []; + p5.prototype._registeredMethods.preload = []; + p5.prototype._registeredMethods.afterPreload = []; + p5.prototype._registeredMethods.beforeSetup = []; + p5.prototype._registeredMethods.setup = []; + p5.prototype._registeredMethods.afterSetup = []; + p5.prototype._registeredMethods.pre = []; + p5.prototype._registeredMethods.draw = []; + p5.prototype._registeredMethods.post = []; + }); + test('should register and call "init" methods', function () { var myp5, myInitCalled; + p5.prototype.registerMethod('init', function myInit() { + assert( + !myInitCalled, + 'myInit should only be called once during test suite' + ); + myInitCalled = true; - p5.prototype._registeredMethods.init = []; + this.myInitCalled = true; + }); + + myp5 = new p5(function (sketch) { + assert(sketch.hasOwnProperty('myInitCalled')); + assert(sketch.myInitCalled); - try { - p5.prototype.registerMethod('init', function myInit() { - assert( - !myInitCalled, - 'myInit should only be called once during test suite' - ); - myInitCalled = true; + sketch.sketchFunctionCalled = true; + }); + + assert(myp5.sketchFunctionCalled); + }); + test('should register and call before and after "preload" hooks', function () { + return new Promise(resolve => { + let beforePreloadCalled = false; + let preloadCalled = false; + let afterPreloadCalled = false; - this.myInitCalled = true; + p5.prototype.registerMethod('beforePreload', () => { + beforePreloadCalled = true; }); - myp5 = new p5(function(sketch) { - assert(sketch.hasOwnProperty('myInitCalled')); - assert(sketch.myInitCalled); + p5.prototype.registerMethod('preload', () => { + assert.equal(beforePreloadCalled, true); + preloadCalled = true; + }); - sketch.sketchFunctionCalled = true; + p5.prototype.registerMethod('afterPreload', () => { + if (beforePreloadCalled && preloadCalled) afterPreloadCalled = true; }); - assert(myp5.sketchFunctionCalled); - } finally { - p5.prototype._registeredMethods.init = originalInit; - } + myp5 = new p5(function (sketch) { + sketch.preload = () => {}; + sketch.setup = () => { + assert.equal(afterPreloadCalled, true); + resolve(); + }; + }); + }); + }); + test('should register and call before and after "setup" hooks', function () { + return new Promise(resolve => { + let beforeSetupCalled = false; + let setupCalled = false; + let afterSetupCalled = false; + + p5.prototype.registerMethod('beforeSetup', () => { + beforeSetupCalled = true; + }); + + p5.prototype.registerMethod('setup', () => { + assert.equal(beforeSetupCalled, true); + setupCalled = true; + }); + + p5.prototype.registerMethod('afterSetup', () => { + if (beforeSetupCalled && setupCalled) afterSetupCalled = true; + }); + + myp5 = new p5(function (sketch) { + sketch.setup = () => {}; + sketch.draw = () => { + assert.equal(afterSetupCalled, true); + resolve(); + }; + }); + }); + }); + test('should register and call pre and post "draw" hooks', function () { + return new Promise(resolve => { + let preDrawCalled = false; + let drawCalled = false; + let postDrawCalled = false; + + p5.prototype.registerMethod('pre', () => { + preDrawCalled = true; + }); + + p5.prototype.registerMethod('draw', () => { + assert.equal(preDrawCalled, true); + drawCalled = true; + }); + + p5.prototype.registerMethod('post', () => { + if (preDrawCalled && drawCalled) postDrawCalled = true; + }); + + myp5 = new p5(function (sketch) { + sketch.draw = () => { + if (sketch.frameCount === 2) { + assert.equal(postDrawCalled, true); + resolve(); + } + }; + }); + }); }); }); - suite('new p5() / global mode', function() { + suite('new p5() / global mode', function () { var iframe; - teardown(function() { + teardown(function () { if (iframe) { iframe.teardown(); iframe = null; } }); - test('is triggered when "setup" is in window', function() { - return new Promise(function(resolve, reject) { + test('is triggered when "setup" is in window', function () { + return new Promise(function (resolve, reject) { iframe = createP5Iframe(); - iframe.elt.contentWindow.setup = function() { + iframe.elt.contentWindow.setup = function () { resolve(); }; }); }); - test('is triggered when "draw" is in window', function() { - return new Promise(function(resolve, reject) { + test('is triggered when "draw" is in window', function () { + return new Promise(function (resolve, reject) { iframe = createP5Iframe(); - iframe.elt.contentWindow.draw = function() { + iframe.elt.contentWindow.draw = function () { resolve(); }; }); }); - test('works when p5.js is loaded asynchronously', function() { - return new Promise(function(resolve, reject) { + test('works when p5.js is loaded asynchronously', function () { + return new Promise(function (resolve, reject) { iframe = createP5Iframe(`