From 2be9b1fc748d4ab86e4eb98af84af50fdc4913f0 Mon Sep 17 00:00:00 2001 From: Thijs Van der Schaeghe Date: Tue, 14 Jul 2026 23:36:33 +0200 Subject: [PATCH 1/6] Release v1.2.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 631298e..b11e707 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "easelbone", - "version": "1.2.8", + "version": "1.2.9", "main": "dist/scripts/easelbone.js", "author": "CatLab Interactive", "devDependencies": { From 449e806007491dda95b80a8497548c9527383b5f Mon Sep 17 00:00:00 2001 From: Thijs Van der Schaeghe Date: Wed, 15 Jul 2026 13:36:59 +0200 Subject: [PATCH 2/6] Release v1.2.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b11e707..10f05d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "easelbone", - "version": "1.2.9", + "version": "1.2.10", "main": "dist/scripts/easelbone.js", "author": "CatLab Interactive", "devDependencies": { From 42c36b307a12b1ec9b08710d2b12f8ba34558beb Mon Sep 17 00:00:00 2001 From: Thijs Van der Schaeghe Date: Wed, 15 Jul 2026 16:17:01 +0200 Subject: [PATCH 3/6] feat(pinner): park pinned objects at their anchor while suppressed While stage._pinsHidden is set, pinned objects now drop back into their regular spot in the display list (under an open modal) instead of being hidden, and lift back when the flag clears. --- .../CatLab/Easelbone/EaselJS/Pinner.js | 64 ++++++-- package.json | 3 +- tools/fixtures/pin-suppress.html | 152 ++++++++++++++++++ tools/pin-suppress-test.js | 85 ++++++++++ tools/pin-test.js | 4 +- 5 files changed, 297 insertions(+), 11 deletions(-) create mode 100644 tools/fixtures/pin-suppress.html create mode 100644 tools/pin-suppress-test.js diff --git a/app/scripts/CatLab/Easelbone/EaselJS/Pinner.js b/app/scripts/CatLab/Easelbone/EaselJS/Pinner.js index 9fa0250..2107ebb 100644 --- a/app/scripts/CatLab/Easelbone/EaselJS/Pinner.js +++ b/app/scripts/CatLab/Easelbone/EaselJS/Pinner.js @@ -101,8 +101,14 @@ define( stage._pinnedElements.push(record); - // Immediate sync: correctly placed even without a running tick. - Pinner._syncRecord(stage, record); + // Immediate placement: if the stage is currently suppressed + // (e.g. a modal is open) the new pin starts parked; otherwise + // sync it into place even without a running tick. + if (stage._pinsHidden) { + Pinner._park(record); + } else { + Pinner._syncRecord(stage, record); + } // Reparenting doesn't itself mark the RootView dirty; under // dirtyRendering:true this ensures the pin paints next frame @@ -139,11 +145,49 @@ define( return false; }, + // Park: move a pinned object out of its wrapper and back into its + // anchor -- it renders in its regular spot and z-order (e.g. under + // an open modal). The record is kept so the pin can resume. + _park: function (record) { + if (record._parked) { + return; + } + record._parked = true; + record.obj._pinnedToTop = false; + if (record.obj.parent === record.wrapper) { + record.wrapper.removeChild(record.obj); + } + record.anchor.addChild(record.obj); + DirtyFlag.invalidate(); + }, + + // Unpark: lift the object back into its pin wrapper; the pin + // resumes exactly as before. wrapper.visible is forced true in + // case an older Pinner copy hid it while we were parked. + _unpark: function (record) { + if (!record._parked) { + return; + } + record._parked = false; + if (record.obj.parent) { + record.obj.parent.removeChild(record.obj); + } + record.wrapper.addChild(record.obj); + record.wrapper.visible = true; + record.obj._pinnedToTop = true; + DirtyFlag.invalidate(); + }, + sync: function (stage) { var records = stage._pinnedElements; if (!records || records.length === 0) { return false; } + // _pinsHidden is the shared-stage suppression protocol: set by + // the suppressor registry, or manually by older callers. While + // set, pins are PARKED at their anchors (regular spot), not + // positioned in the pin layer. + var suppressed = !!stage._pinsHidden; for (var i = records.length - 1; i >= 0; i--) { var record = records[i]; // Auto-teardown: anchor removed from the stage. @@ -152,7 +196,12 @@ define( records.splice(i, 1); continue; } - Pinner._syncRecord(stage, record); + if (suppressed) { + Pinner._park(record); + } else { + Pinner._unpark(record); + Pinner._syncRecord(stage, record); + } } return records.length > 0; }, @@ -169,13 +218,14 @@ define( _restore: function (record) { record.obj._pinnedToTop = false; - if (record.obj.parent) { + record._parked = false; + if (record.obj.parent && record.obj.parent !== record.anchor) { record.obj.parent.removeChild(record.obj); } if (record.wrapper.parent) { record.wrapper.parent.removeChild(record.wrapper); } - if (record.anchor && record.anchor.stage) { + if (record.obj.parent !== record.anchor && record.anchor && record.anchor.stage) { record.anchor.addChild(record.obj); } }, @@ -183,10 +233,6 @@ define( _syncRecord: function (stage, record) { var wrapper = record.wrapper; - // Callers can hide all pinned overlays (e.g. while a modal is - // open over the pinned QR) by setting stage._pinsHidden. - wrapper.visible = !stage._pinsHidden; - var container = wrapper.parent; if (!container) { return; diff --git a/package.json b/package.json index 10f05d7..dfe5b8b 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "smoke": "node tools/smoke-test.js", "test:baseline": "node tools/baseline-test.js", "test:pin": "node tools/pin-test.js", - "test:pin:placeholder": "node tools/pin-placeholder-test.js" + "test:pin:placeholder": "node tools/pin-placeholder-test.js", + "test:pin:suppress": "node tools/pin-suppress-test.js" } } diff --git a/tools/fixtures/pin-suppress.html b/tools/fixtures/pin-suppress.html new file mode 100644 index 0000000..468b86c --- /dev/null +++ b/tools/fixtures/pin-suppress.html @@ -0,0 +1,152 @@ + + + + Pinner suppression fixture + + + + + + + + diff --git a/tools/pin-suppress-test.js b/tools/pin-suppress-test.js new file mode 100644 index 0000000..e5f25a6 --- /dev/null +++ b/tools/pin-suppress-test.js @@ -0,0 +1,85 @@ +/** + * Behavioral test for Pinner suppression (park-at-anchor). + * + * Renders tools/fixtures/pin-suppress.html headlessly. + * + * Semantics under test: while a stage is suppressed, pinned objects are + * PARKED back into their anchors (regular spot, regular z-order) -- not + * hidden. Unsuppressing lifts them back into their pin wrappers. + * + * Usage: node tools/pin-suppress-test.js [port] + */ +var spawn = require('child_process').spawn; +var chromium = require('playwright').chromium; + +var FIXTURE = 'tools/fixtures/pin-suppress.html'; +var PORT = parseInt(process.argv[2], 10) || 8127; + +function pixel(page, x, y) { + return page.evaluate(function (p) { + var ctx = document.getElementById('canvas').getContext('2d'); + var d = ctx.getImageData(p.x, p.y, 1, 1).data; + return [d[0], d[1], d[2]]; + }, { x: x, y: y }); +} + +function isGreen(rgb) { return rgb[1] > 150 && rgb[0] < 100 && rgb[2] < 100; } +function isBlue(rgb) { return rgb[2] > 150 && rgb[0] < 100 && rgb[1] < 100; } + +async function main() { + var server = spawn('node', ['node_modules/http-server/bin/http-server', '.', '-p', String(PORT), '-c-1'], { stdio: 'ignore' }); + await new Promise(function (r) { setTimeout(r, 1500); }); + var browser = await chromium.launch(); + var failures = []; + try { + var page = await browser.newPage(); + var errors = []; + page.on('pageerror', function (e) { errors.push(e.message); }); + page.on('console', function (m) { if (m.type() === 'error') errors.push(m.text()); }); + await page.goto('http://localhost:' + PORT + '/' + FIXTURE, { waitUntil: 'load', timeout: 30000 }); + await page.waitForFunction('window.__ready === true', { timeout: 10000 }); + + // Baseline: pinned + lifted. Green above the blue occluder at the + // anchor spot (125,125); record obj lives in the wrapper. + var spot = await pixel(page, 125, 125); + if (!isGreen(spot)) { failures.push('baseline: expected GREEN at (125,125), got ' + spot); } + if ((await page.evaluate('window.__pinnedParent()')) !== 'wrapper') { failures.push('baseline: obj should be in wrapper'); } + if ((await page.evaluate('window.__pinnedToTop()')) !== true) { failures.push('baseline: _pinnedToTop should be true'); } + + // Manual flag on -> PARKED: obj back in the anchor (regular spot, + // under the occluder -> BLUE), _pinnedToTop false, record kept. + await page.evaluate('window.__setPinsHidden(true)'); + spot = await pixel(page, 125, 125); + if (!isBlue(spot)) { failures.push('parked: expected BLUE at (125,125), got ' + spot); } + if ((await page.evaluate('window.__pinnedParent()')) !== 'anchor') { failures.push('parked: obj should be in anchor'); } + if ((await page.evaluate('window.__pinnedToTop()')) !== false) { failures.push('parked: _pinnedToTop should be false'); } + if ((await page.evaluate('window.__pinnedCount()')) !== 1) { failures.push('parked: record must be kept'); } + + // A pin created WHILE suppressed starts parked (replaces the old + // record on the same anchor -> still exactly 1 record). + await page.evaluate('window.__pinNewBox()'); + if ((await page.evaluate('window.__pinnedCount()')) !== 1) { failures.push('pin-while-parked: expected 1 record'); } + if ((await page.evaluate('window.__pinnedParent()')) !== 'anchor') { failures.push('pin-while-parked: obj should start in anchor'); } + spot = await pixel(page, 125, 125); + if (!isBlue(spot)) { failures.push('pin-while-parked: expected BLUE at (125,125), got ' + spot); } + + // Manual flag off -> UNPARKED: lifted again, green on top. + await page.evaluate('window.__setPinsHidden(false)'); + spot = await pixel(page, 125, 125); + if (!isGreen(spot)) { failures.push('unparked: expected GREEN at (125,125), got ' + spot); } + if ((await page.evaluate('window.__pinnedParent()')) !== 'wrapper') { failures.push('unparked: obj should be back in wrapper'); } + if ((await page.evaluate('window.__pinnedToTop()')) !== true) { failures.push('unparked: _pinnedToTop should be true'); } + + if (errors.length) { failures.push('page errors: ' + errors.join(' | ')); } + } finally { + await browser.close(); + server.kill(); + } + if (failures.length) { + console.error('FAIL pin-suppress-test:\n - ' + failures.join('\n - ')); + process.exit(1); + } + console.log('PASS pin-suppress-test'); +} + +main().catch(function (e) { console.error(e); process.exit(1); }); diff --git a/tools/pin-test.js b/tools/pin-test.js index b1ecde5..2ebe280 100644 --- a/tools/pin-test.js +++ b/tools/pin-test.js @@ -47,7 +47,9 @@ async function main() { // falling back to a 100x100 default. This requires a per-pin wrapper // container -- it fails against pre-fix code that reparents obj // directly into the (bounds-less) pin container. - // Suppression: hiding pins makes the pinned wrapper invisible; restoring shows it. + // Suppression: _pinsHidden parks the pinned box back into its anchor + // (under the occluder here), so the spot stops being green; clearing + // the flag lifts it back on top. await page.evaluate('window.__setPinsHidden(true)'); var hiddenSpot = await pixel(page, 125, 125); if (isGreen(hiddenSpot)) { failures.push('pins hidden: expected GREEN hidden at (125,125), still green'); } From 6d674cf05cef1847605b661e9cf0ffd554f7bdeb Mon Sep 17 00:00:00 2001 From: Thijs Van der Schaeghe Date: Wed, 15 Jul 2026 16:19:11 +0200 Subject: [PATCH 4/6] feat(pinner): attachment-based pin suppressors (suppressPins/releasePins) Register a modal's container with easelbone.suppressPins(obj): while it is on the stage, pins are parked at their anchors; they lift back when the modal leaves (or on explicit releasePins). Nests across multiple modals, expires never-attached registrations after ~5s, and writes stage._pinsHidden for cross-bundle compatibility. --- .../CatLab/Easelbone/EaselJS/Pinner.js | 101 ++++++++++++++++++ .../CatLab/Easelbone/FrontController.js | 14 +++ tools/fixtures/pin-suppress.html | 11 +- tools/pin-suppress-test.js | 64 +++++++++++ 4 files changed, 189 insertions(+), 1 deletion(-) diff --git a/app/scripts/CatLab/Easelbone/EaselJS/Pinner.js b/app/scripts/CatLab/Easelbone/EaselJS/Pinner.js index 2107ebb..a2da831 100644 --- a/app/scripts/CatLab/Easelbone/EaselJS/Pinner.js +++ b/app/scripts/CatLab/Easelbone/EaselJS/Pinner.js @@ -132,6 +132,107 @@ define( } }, + // ---- Modal suppression ------------------------------------- + // suppress(obj) registers a display object (typically a modal + // view's container). While ANY registered object is attached to a + // stage, that stage's pins are parked at their anchors. State is + // evaluated on the shared Ticker (not the RootView sync) so it + // works even when an older easelbone copy owns the render loop; + // the outcome is materialized into stage._pinsHidden, which IS + // the cross-bundle protocol. + + _suppressors: [], + _suppressedStages: [], + _suppressTickerHandle: null, + + suppress: function (obj) { + for (var i = 0; i < Pinner._suppressors.length; i++) { + if (Pinner._suppressors[i].obj === obj) { + return; + } + } + Pinner._suppressors.push({ obj: obj, wasAttached: !!obj.stage, attempts: 0 }); + Pinner._evaluateSuppression(); + if (!Pinner._suppressTickerHandle && Pinner._suppressors.length) { + Pinner._suppressTickerHandle = createjs.Ticker.on('tick', function () { + Pinner._evaluateSuppression(); + }); + } + }, + + release: function (obj) { + for (var i = 0; i < Pinner._suppressors.length; i++) { + if (Pinner._suppressors[i].obj === obj) { + Pinner._suppressors.splice(i, 1); + break; + } + } + Pinner._evaluateSuppression(); + }, + + _evaluateSuppression: function () { + var i; + var stages = []; + var suppressors = Pinner._suppressors; + + for (i = suppressors.length - 1; i >= 0; i--) { + var entry = suppressors[i]; + var stage = entry.obj.stage; + if (stage) { + entry.wasAttached = true; + if (stages.indexOf(stage) === -1) { + stages.push(stage); + } + } else if (entry.wasAttached) { + // Modal left the stage: suppression over, one-shot. + suppressors.splice(i, 1); + } else if (++entry.attempts >= 300) { + // Never attached within ~5s: give up (same grace as + // _deferPin) so a never-shown modal can't leak. + suppressors.splice(i, 1); + } + } + + for (i = 0; i < stages.length; i++) { + if (Pinner._suppressedStages.indexOf(stages[i]) === -1) { + Pinner._setSuppressed(stages[i], true); + Pinner._suppressedStages.push(stages[i]); + } + } + for (i = Pinner._suppressedStages.length - 1; i >= 0; i--) { + if (stages.indexOf(Pinner._suppressedStages[i]) === -1) { + Pinner._setSuppressed(Pinner._suppressedStages[i], false); + Pinner._suppressedStages.splice(i, 1); + } + } + + if (!suppressors.length && !Pinner._suppressedStages.length && Pinner._suppressTickerHandle) { + createjs.Ticker.off('tick', Pinner._suppressTickerHandle); + Pinner._suppressTickerHandle = null; + } + }, + + // Flip a stage's suppression state: write the shared _pinsHidden + // flag and park/unpark its records right away (don't wait for a + // sync that an older bundle's Pinner might be driving). + _setSuppressed: function (stage, suppressed) { + stage._pinsHidden = suppressed; + var records = stage._pinnedElements; + if (records) { + for (var i = 0; i < records.length; i++) { + if (records[i].anchor.stage !== stage) { + continue; // sync() will tear this one down + } + if (suppressed) { + Pinner._park(records[i]); + } else { + Pinner._unpark(records[i]); + } + } + } + DirtyFlag.invalidate(); + }, + _isPinned: function (stage, obj) { var records = stage._pinnedElements; if (!records) { diff --git a/app/scripts/CatLab/Easelbone/FrontController.js b/app/scripts/CatLab/Easelbone/FrontController.js index cf9f555..d42fcad 100644 --- a/app/scripts/CatLab/Easelbone/FrontController.js +++ b/app/scripts/CatLab/Easelbone/FrontController.js @@ -122,6 +122,20 @@ define( return Pinner.unpin(displayObject); }, + // Register a modal-like display object: while it is attached to a + // stage, that stage's pinned objects are parked back at their + // anchors (regular spot, under the modal). Auto-releases when the + // object leaves the stage. + suppressPins: function (displayObject) { + return Pinner.suppress(displayObject); + }, + + // Explicit counterpart for modals that linger on the stage after + // closing (e.g. covered by the next screen instead of removed). + releasePins: function (displayObject) { + return Pinner.release(displayObject); + }, + FakeWebremote: { KeyboardUser: KeyboardUser }, diff --git a/tools/fixtures/pin-suppress.html b/tools/fixtures/pin-suppress.html index 468b86c..add58cd 100644 --- a/tools/fixtures/pin-suppress.html +++ b/tools/fixtures/pin-suppress.html @@ -106,7 +106,10 @@ var box = new createjs.Shape(); box.graphics.beginFill('#888888').drawRect(0, 0, 200, 200); modal.addChild(box); - modal.x = 100; modal.y = 100; + // Bottom-right quadrant: must NOT cover the probed anchor + // spot (125,125) so pixel checks see park/lift state while + // the modal is attached. + modal.x = 200; modal.y = 200; modals[name] = modal; easelbone.suppressPins(modal); }; @@ -143,6 +146,12 @@ return !!root.stage._pinsHidden; }; + // Force a repaint: suppression state changes on Ticker ticks, + // which don't repaint a non-dirty-rendering root by themselves. + window.__render = function () { + root.render(); + }; + root.render(); window.__ready = true; }); diff --git a/tools/pin-suppress-test.js b/tools/pin-suppress-test.js index e5f25a6..61d883f 100644 --- a/tools/pin-suppress-test.js +++ b/tools/pin-suppress-test.js @@ -70,6 +70,70 @@ async function main() { if ((await page.evaluate('window.__pinnedParent()')) !== 'wrapper') { failures.push('unparked: obj should be back in wrapper'); } if ((await page.evaluate('window.__pinnedToTop()')) !== true) { failures.push('unparked: _pinnedToTop should be true'); } + // ---- Suppressor registry ---- + + // Registering a detached modal does nothing yet. + await page.evaluate('window.__registerModal("a")'); + await page.waitForTimeout(200); + await page.evaluate('window.__render()'); + spot = await pixel(page, 125, 125); + if (!isGreen(spot)) { failures.push('detached suppressor: pins must stay lifted, got ' + spot); } + if ((await page.evaluate('window.__suppressorCount()')) !== 1) { failures.push('detached suppressor: expected 1 registered'); } + + // Attaching the modal parks the pins (next ticks). + await page.evaluate('window.__attachModal("a")'); + await page.waitForTimeout(300); + await page.evaluate('window.__render()'); + spot = await pixel(page, 125, 125); + if (!isBlue(spot)) { failures.push('modal attached: expected parked/BLUE, got ' + spot); } + if ((await page.evaluate('window.__pinnedParent()')) !== 'anchor') { failures.push('modal attached: obj should be in anchor'); } + if ((await page.evaluate('window.__pinsHiddenFlag()')) !== true) { failures.push('modal attached: _pinsHidden must be written for old-bundle compat'); } + + // Nesting: second modal attached, then first detached -> stays parked. + await page.evaluate('window.__registerModal("b")'); + await page.evaluate('window.__attachModal("b")'); + await page.waitForTimeout(200); + await page.evaluate('window.__render()'); + await page.evaluate('window.__detachModal("a")'); + await page.waitForTimeout(300); + await page.evaluate('window.__render()'); + spot = await pixel(page, 125, 125); + if (!isBlue(spot)) { failures.push('nesting: expected still parked after first modal closed, got ' + spot); } + if ((await page.evaluate('window.__suppressorCount()')) !== 1) { failures.push('nesting: detached suppressor must auto-deregister'); } + + // Last modal detached -> unparked automatically, registry empty. + await page.evaluate('window.__detachModal("b")'); + await page.waitForTimeout(300); + await page.evaluate('window.__render()'); + spot = await pixel(page, 125, 125); + if (!isGreen(spot)) { failures.push('last modal closed: expected GREEN again, got ' + spot); } + if ((await page.evaluate('window.__pinnedParent()')) !== 'wrapper') { failures.push('last modal closed: obj should be back in wrapper'); } + if ((await page.evaluate('window.__suppressorCount()')) !== 0) { failures.push('last modal closed: registry should be empty'); } + if ((await page.evaluate('window.__pinsHiddenFlag()')) !== false) { failures.push('last modal closed: _pinsHidden must be cleared'); } + + // Explicit release while still attached -> unparks immediately. + await page.evaluate('window.__registerModal("c")'); + await page.evaluate('window.__attachModal("c")'); + await page.waitForTimeout(300); + await page.evaluate('window.__render()'); + await page.evaluate('window.__releaseModal("c")'); + await page.waitForTimeout(200); + await page.evaluate('window.__render()'); + spot = await pixel(page, 125, 125); + if (!isGreen(spot)) { failures.push('explicit release: expected GREEN, got ' + spot); } + if ((await page.evaluate('window.__suppressorCount()')) !== 0) { failures.push('explicit release: registry should be empty'); } + await page.evaluate('window.__detachModal("c")'); + await page.waitForTimeout(200); + await page.evaluate('window.__render()'); + + // Grace period: a suppressor that never attaches expires after ~300 + // evaluations and never affects the pins. + await page.evaluate('window.__registerModal("never")'); + await page.evaluate('window.__tickSuppression(301)'); + if ((await page.evaluate('window.__suppressorCount()')) !== 0) { failures.push('grace: never-attached suppressor should expire'); } + spot = await pixel(page, 125, 125); + if (!isGreen(spot)) { failures.push('grace: pins must stay lifted, got ' + spot); } + if (errors.length) { failures.push('page errors: ' + errors.join(' | ')); } } finally { await browser.close(); From 79320ae75e5e9538dca186a98559be79947d33de Mon Sep 17 00:00:00 2001 From: Thijs Van der Schaeghe Date: Wed, 15 Jul 2026 16:20:41 +0200 Subject: [PATCH 5/6] build: rebuild dist with pin suppressor support --- dist/scripts/easelbone.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/scripts/easelbone.js b/dist/scripts/easelbone.js index 5b4ffed..ccda1a6 100644 --- a/dist/scripts/easelbone.js +++ b/dist/scripts/easelbone.js @@ -8,7 +8,7 @@ define("CatLab/Easelbone/Utilities/MovieClipHelper",["easeljs","CatLab/Easelbone define("CatLab/Easelbone/Views/Base",["backbone","easeljs","CatLab/Easelbone/Utilities/GlobalProperties","CatLab/Easelbone/Utilities/MovieClipHelper","CatLab/Easelbone/Utilities/Deferred"],(function(e,t,i,r,n){"use strict";return e.View.extend({el:"div",easelScreen:null,setRootView:function(e){this.set("root",e)},getWidth:function(){return this.getStage().canvas.width},getHeight:function(){return this.getStage().canvas.height},getStage:function(){return void 0!==this.el.stage?this.el.stage:void 0!==this.el.getStage?this.el.getStage():void 0},setScreen:function(e){return this.easelScreen=e,this},getScreen:function(){return this.easelScreen},hasLabeledFrame:function(e,t){if(void 0===t&&(t=this.easelScreen),!t)throw new Error("hasLabeledFrame requires a screen to be set or a container to be provided.");return r.hasLabeledFrame(e,t)},jumpToFrame:function(e,t){if(void 0===t&&(t=this.easelScreen)&&!t.parent){var i=new n;return this._pendingFrameJumps||(this._pendingFrameJumps=[]),this._pendingFrameJumps.push({label:e,deferred:i}),i.promise()}if(!t)throw new Error("hasLabeledFrame requires a screen to be set or a container to be provided.");return r.jumpToFrame(e,t)},_replayPendingFrameJumps:function(){if(this._pendingFrameJumps){var e=this._pendingFrameJumps;this._pendingFrameJumps=null,e.forEach(function(e){this.jumpToFrame(e.label).done((function(){e.deferred.resolve()}))}.bind(this))}},pause:function(e){if(void 0===e&&(e=this.easelScreen),!e)throw new Error("pause requires a screen to be set or a container to be provided.");return r.pause(this.easelScreen)},resume:function(e){if(void 0===e&&(e=this.easelScreen),!e)throw new Error("resume requires a screen to be set or a container to be provided.");return r.resume(this.easelScreen)},scale:function(e){var t=this.getScale();e.scaleX=t.x,e.scaleY=t.y},getScale:function(e,t,r){null==e&&(e=i.getWidth()),null==t&&(t=i.getHeight()),void 0===r&&(r=!1);var n=this.getWidth()/e,a=this.getHeight()/t,s=r?Math.max(n,a):Math.min(n,a);return{x:s,y:s}},addCenter:function(e,r,n,a,s){null==r&&(r=i.getWidth()),null==n&&(n=i.getHeight()),null==s&&(s=1);var h=this.getScale(r,n,a),o=!0;if(this.getWidth()===r&&this.getHeight()===n&&(o=!1),o&&(h.x=h.x*s,h.y=h.y*s,e.x=(this.getWidth()-r*h.x)/2,e.y=(this.getHeight()-n*h.y)/2,e.scaleX=h.x,e.scaleY=h.y),this.el.addChild(e),o){var l=new t.Graphics;e.x>=1&&(l.beginFill(this.getBackground()).drawRect(0,0,Math.ceil(e.x),this.getHeight()),l.beginFill(this.getBackground()).drawRect(this.getWidth()-Math.ceil(e.x),0,Math.ceil(e.x),this.getHeight())),e.y>=1&&(l.beginFill(this.getBackground()).drawRect(0,0,this.getWidth(),Math.ceil(e.y)),l.beginFill(this.getBackground()).drawRect(0,this.getHeight()-Math.ceil(e.y),this.getWidth(),Math.ceil(e.y)));var d=new t.Shape(l);this.el.addChild(d)}},clear:function(){this.el.removeAllChildren();var e=new t.Graphics;e.beginFill(this.getBackground()).drawRect(0,0,this.getWidth(),this.getHeight());var i=new t.Shape(e);this.el.addChild(i)},getBackground:function(){return"#000000"},render:function(){if(this.easelScreen)return this.addCenter(this.easelScreen),this._replayPendingFrameJumps(),this;var e=new t.Container;e.setBounds(0,0,this.getWidth(),this.getHeight());var i=new t.BigText("easelbone view: no screen set and render function was not overridden...","Arial","#000000");e.addChild(i),this.el.addChild(e)},findPlaceholders:function(e,t){return void 0===t&&(t=[],this.easelScreen&&t.push(this.easelScreen)),r.findPlaceholders(e,t)},findTextPlaceholders:function(e,t){return this.findPlaceholdersPreferPostfix(e,t,"text")},findPlaceholdersPreferPostfix:function(e,t,i){var r=[];return Array.isArray(e)?e.forEach((function(e){r.push(e+".text"),r.push(e)})):(r.push(e+".text"),r.push(e)),this.findPlaceholders(r,t)},findFromNames:function(e,t){return void 0===t&&(t=[],this.easelScreen&&t.push(this.easelScreen)),r.findFromNames(e,t)},findFromNameInContainer:function(e,t,i){return r.findFromNameInContainer(e,t,i)},forEachNamedChild:function(e,t){return r.forEachNamedChild(e,t)},tick:function(){return!0},afterRender:function(){},onRemove:function(){},scrollIntoView:function(e){for(var t=e;t.parent;){if(void 0!==t._parentScrollArea)return void t._parentScrollArea.focus(e,200);t=t.parent}}})})); define("CatLab/Easelbone/Views/LoadingView",["CatLab/Easelbone/Views/Base"],(function(t){"use strict";var e=new Image;e.crossOrigin="anonymous",e.src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAWCAYAAAA1vze2AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH3wMRCycoY5y7DgAAAeBJREFUSMel1j9sTlEYBvD3fv2DIqmmmmjTBSXVkYSBhAixWNRmMYvBgMZQSUeLQWIw2hqLWIRBYpCIQYcyYDBJNAQpBk21P8u5cdzcfv1835Pc3JznOed9zn3f95zciCbAAbzEIk5VtI04jxmMRzvAKD76i1VszvSZTFvESDsmk1jxLyYz/UtFu7pWrEYTn9EafUcyGIiIgYo23o5Jdw23kt79NVrR1AQNbEEe+F0WtMSLiIiiKN5HxGqdluL1oB89JdGFabzGLWxKfB9ms5zfqdTsYqbNoT+LN5tqdq+cfLrSQUcrOxrGCLoqJg1sxxg2ZPyZSsNcCTyvdMlUdABMpc2W+NyIiP2VecMtBjuMEzVSX2U82B0RSxHRk5H5gTsbEbcjYig1wpuI2BoRx7I5cxFxqCiK5URtq3SawINKui6nxfvwU2u4npneqGjzkQr3LRFPMZil40eLJnfLxsAQHuErHpfx1sr5Xnxq0WS6Wf2anfhzKb/r4XeqVcvt14sL//EFJZ6Uh3g9gzF80D7u4yQm1rzU8DYi9kRnWE333WBRFN/rarIQnaMREb/SU5uu3VjWOS6haFaX41iqWbjQosE8eltpgAk8TKf9FXYl/kgawzMcTPxOXMPN/B8gxx+XbHMxap3gAwAAAABJRU5ErkJggg==";var i=0,s={x:{start:0,end:100},y:{start:0,end:100}},a={x:s.x.end/2,y:s.y.end/2},n=[],h=!1;return t.extend({initialize:function(t){this.gameVersion=null,void 0!==t.gameVersion&&(this.gameVersion=t.gameVersion),this.dRotation=0,this.speed=10,this.angle=Math.random()*Math.PI*2,this.targetAngle=null,this.flippedX=!1,this.flippedY=!1,this.rotationSpeedRad=.2,this.screenMargin=50},render:function(){s={x:{start:0,end:this.el.stage.canvas.width},y:{start:0,end:this.el.stage.canvas.height}},this.loadingText=new createjs.Text("Loading","15px monospace","#ffffff"),this.loadingText.textAlign="center",this.loadingText.lineHeight=20,this.el.addChild(this.loadingText),this.versionText=null,this.gameVersion&&(this.versionText=new createjs.Text("v"+this.gameVersion,"12px monospace","#ffffff"),this.el.addChild(this.versionText),this.versionText.cache(-100,-100,200,200),this.loadingText.cache(-100,-100,200,200)),this.el.setBounds(0,0,this.el.stage.canvas.width,this.el.stage.canvas.height),this.updatePositions(),a={x:Math.random()*(s.x.end-2*this.screenMargin)+this.screenMargin,y:Math.random()*(s.y.end-2*this.screenMargin)+this.screenMargin}},tick:function(){this.updatePositions();var t=this.angle;null!==this.targetAngle&&(t=this.targetAngle),(a.xs.x.end-this.screenMargin&&Math.cos(t)>0)&&(this.targetAngle=Math.PI-t),(a.ys.y.end-this.screenMargin&&Math.sin(t)>0)&&(this.targetAngle=2*Math.PI-t),null!==this.targetAngle&&(this.targetAngle-this.angle>this.rotationSpeedRad?this.angle+=this.rotationSpeedRad:this.targetAngle-this.angle<0-this.rotationSpeedRad?this.angle-=this.rotationSpeedRad:this.targetAngle=null);var e=Math.cos(this.angle)*this.speed,h=Math.sin(this.angle)*this.speed;a.x+=e,a.y+=h,++i%6==0&&this.addPaw();for(var r=0;r0&&g.push(n[r]);return n=g,!0},updatePositions:function(){this.loadingText.x=this.el.getBounds().width/2,this.loadingText.y=this.el.getBounds().height/2,this.versionText&&(this.versionText.x=this.el.getBounds().width-60,this.versionText.y=this.el.getBounds().height-20)},stop:function(){},start:function(){},addPaw:function(){var t=10;(h=!h)&&(t*=-1);var i=new createjs.Bitmap(e),s=this.angle;i.regX=10,i.regY=10,i.x=a.x+Math.cos(s+Math.PI/2)*t,i.y=a.y+Math.sin(s+Math.PI/2)*t,i.scaleX=20/e.width,i.scaleY=20/e.height,i.rotation=(s+Math.PI/2)*(180/Math.PI),this.el.addChild(i),n.push(i)},setProgress:function(t){t=Math.floor(100*t),this.loadingText.text="Loading\n"+t+"%",this.loadingText.cache(-100,-100,200,200)}})})); define("CatLab/Easelbone/Views/Layer",["easeljs","underscore","backbone","CatLab/Easelbone/Utilities/Deferred"],(function(e,i,t,n){var r=function(n){i.extend(this,t.Events),void 0===n&&(n={}),void 0!==n.container?this.container=n.container:this.container=new e.Container,this.view=null};return r.prototype.setView=function(i){var t=this.view;this.view=i;var n=new e.Container;this.view.setElement(n),this.container.addChild(n),this.view.on("all",function(e){this.trigger("view:"+e)}.bind(this)),this.view.trigger("stage:added"),t&&this.container.children.length>0&&this.container.setChildIndex(t.el,this.container.children.length-1),t&&this._destroyView(t).then((function(){t=null}))},r.prototype.clearView=function(){this.view&&this._destroyView(this.view).pipe(function(){this.view=null}.bind(this))},r.prototype._destroyView=function(e){return this._waitForViewDestroy(e).pipe(function(){null!==e&&(e.trigger("stage:removed"),this.container.removeChild(e.el),e=null)}.bind(this))},r.prototype._waitForViewDestroy=function(e){var i=new n;return e&&void 0!==e.hasLabeledFrame&&void 0!==e.easelScreen&&e.easelScreen&&e.hasLabeledFrame("view:destroy")?(e.jumpToFrame("view:destroy").then((function(){i.resolve()})),i.promise()):(i.resolve(),i.promise())},r.prototype.render=function(){this.view&&(this.view.el.removeAllChildren(),this.view.trigger("render:before"),this.view.render(),this.view.trigger("render"),this.view.trigger("render:after"))},r.prototype.tick=function(e){return!!this.view&&this.view.tick(e)},r})); -define("CatLab/Easelbone/EaselJS/Pinner",["easeljs","CatLab/Easelbone/Utilities/DirtyFlag"],(function(n,e){"use strict";var r={_props:{},setContainer:function(n,e){n._pinContainer=e},_deferPin:function(e,t){e.stage?r.pin(e):t>=300||n.Ticker.on("tick",(function(){r._deferPin(e,t+1)}),null,!0)},pin:function(t){var i=t.stage;if(i){if(!r._isPinned(i,t)){var a=t.parent;if(a){!function(n){n._pinnedElements||(n._pinnedElements=[]),n._pinnedElements}(i),r._removeByAnchor(i,a);var o=new n.Container,s={obj:t,anchor:a,wrapper:o};(function(e){if(e._pinContainer&&e._pinContainer.stage===e)return e._pinContainer;var r=new n.Container;return e.addChild(r),e._pinContainer=r,r})(i).addChild(o),o.addChild(t),t._pinnedToTop=!0,i._pinnedElements.push(s),r._syncRecord(i,s),e.invalidate()}}}else r._deferPin(t,0)},unpin:function(n){var t=n.stage,i=t&&t._pinnedElements;if(i)for(var a=0;a=0;t--){var i=e[t];i.anchor.stage===n?r._syncRecord(n,i):(r._restore(i),e.splice(t,1))}return e.length>0},_removeByAnchor:function(n,e){for(var t=n._pinnedElements,i=t.length-1;i>=0;i--)t[i].anchor===e&&(r._restore(t[i]),t.splice(i,1))},_restore:function(n){n.obj._pinnedToTop=!1,n.obj.parent&&n.obj.parent.removeChild(n.obj),n.wrapper.parent&&n.wrapper.parent.removeChild(n.wrapper),n.anchor&&n.anchor.stage&&n.anchor.addChild(n.obj)},_syncRecord:function(n,e){var t=e.wrapper;t.visible=!n._pinsHidden;var i=t.parent;if(i){var a,o;try{a=e.anchor.getConcatenatedMatrix(),o=i.getConcatenatedMatrix()}catch(n){return}if(a&&o){var s=null;try{s=e.anchor.getBounds()}catch(n){s=null}!s||e._bw===s.width&&e._bh===s.height||(e._bw=s.width,e._bh=s.height,t.setBounds(0,0,s.width,s.height));var c=o.invert().appendMatrix(a),d=e._m||(e._m={a:0,b:0,c:0,d:0,tx:0,ty:0});if(d.a!==c.a||d.b!==c.b||d.c!==c.c||d.d!==c.d||d.tx!==c.tx||d.ty!==c.ty){d.a=c.a,d.b=c.b,d.c=c.c,d.d=c.d,d.tx=c.tx,d.ty=c.ty;var p=c.decompose(r._props);t.x=p.x,t.y=p.y,t.scaleX=p.scaleX,t.scaleY=p.scaleY,t.rotation=p.rotation,t.skewX=p.skewX,t.skewY=p.skewY}}}}};return r})); +define("CatLab/Easelbone/EaselJS/Pinner",["easeljs","CatLab/Easelbone/Utilities/DirtyFlag"],(function(e,n){"use strict";var r={_props:{},setContainer:function(e,n){e._pinContainer=n},_deferPin:function(n,s){n.stage?r.pin(n):s>=300||e.Ticker.on("tick",(function(){r._deferPin(n,s+1)}),null,!0)},pin:function(s){var t=s.stage;if(t){if(!r._isPinned(t,s)){var a=s.parent;if(a){!function(e){e._pinnedElements||(e._pinnedElements=[]),e._pinnedElements}(t),r._removeByAnchor(t,a);var p=new e.Container,i={obj:s,anchor:a,wrapper:p};(function(n){if(n._pinContainer&&n._pinContainer.stage===n)return n._pinContainer;var r=new e.Container;return n.addChild(r),n._pinContainer=r,r})(t).addChild(p),p.addChild(s),s._pinnedToTop=!0,t._pinnedElements.push(i),t._pinsHidden?r._park(i):r._syncRecord(t,i),n.invalidate()}}}else r._deferPin(s,0)},unpin:function(e){var s=e.stage,t=s&&s._pinnedElements;if(t)for(var a=0;a=0;n--){var a=t[n],p=a.obj.stage;p?(a.wasAttached=!0,-1===s.indexOf(p)&&s.push(p)):(a.wasAttached||++a.attempts>=300)&&t.splice(n,1)}for(n=0;n=0;n--)-1===s.indexOf(r._suppressedStages[n])&&(r._setSuppressed(r._suppressedStages[n],!1),r._suppressedStages.splice(n,1));t.length||r._suppressedStages.length||!r._suppressTickerHandle||(e.Ticker.off("tick",r._suppressTickerHandle),r._suppressTickerHandle=null)},_setSuppressed:function(e,s){e._pinsHidden=s;var t=e._pinnedElements;if(t)for(var a=0;a=0;t--){var a=n[t];a.anchor.stage===e?s?r._park(a):(r._unpark(a),r._syncRecord(e,a)):(r._restore(a),n.splice(t,1))}return n.length>0},_removeByAnchor:function(e,n){for(var s=e._pinnedElements,t=s.length-1;t>=0;t--)s[t].anchor===n&&(r._restore(s[t]),s.splice(t,1))},_restore:function(e){e.obj._pinnedToTop=!1,e._parked=!1,e.obj.parent&&e.obj.parent!==e.anchor&&e.obj.parent.removeChild(e.obj),e.wrapper.parent&&e.wrapper.parent.removeChild(e.wrapper),e.obj.parent!==e.anchor&&e.anchor&&e.anchor.stage&&e.anchor.addChild(e.obj)},_syncRecord:function(e,n){var s=n.wrapper,t=s.parent;if(t){var a,p;try{a=n.anchor.getConcatenatedMatrix(),p=t.getConcatenatedMatrix()}catch(e){return}if(a&&p){var i=null;try{i=n.anchor.getBounds()}catch(e){i=null}!i||n._bw===i.width&&n._bh===i.height||(n._bw=i.width,n._bh=i.height,s.setBounds(0,0,i.width,i.height));var o=p.invert().appendMatrix(a),d=n._m||(n._m={a:0,b:0,c:0,d:0,tx:0,ty:0});if(d.a!==o.a||d.b!==o.b||d.c!==o.c||d.d!==o.d||d.tx!==o.tx||d.ty!==o.ty){d.a=o.a,d.b=o.b,d.c=o.c,d.d=o.d,d.tx=o.tx,d.ty=o.ty;var _=o.decompose(r._props);s.x=_.x,s.y=_.y,s.scaleX=_.scaleX,s.scaleY=_.scaleY,s.rotation=_.rotation,s.skewX=_.skewX,s.skewY=_.skewY}}}}};return r})); define("CatLab/Easelbone/Views/Root",["backbone","easeljs","CatLab/Easelbone/Views/Layer","CatLab/Easelbone/Utilities/DirtyFlag","CatLab/Easelbone/EaselJS/Pinner"],(function(i,t,e,n,a){var s;return i.View.extend({stage:null,container:null,hudcontainer:null,view:null,hud:null,maxCanvasSize:null,width:null,height:null,initialize:function(i){this.initializeRootView(i)},initializeRootView:function(i){if(void 0!==i.canvas)this.canvas=i.canvas,this.container=this.canvas.parentNode;else{if(void 0===i.container)throw new Error("Container must be defined for root view.");this.canvas=document.createElement("canvas"),this.container=i.container,this.container.appendChild(this.canvas)}if(void 0!==i.width&&void 0!==i.height&&(this.width=i.width,this.height=i.height),this.stage=this.createStage(i),this.layers=[],this.layerMap={},this.dirty=!1,this.mainLayer=this.nextLayer("main"),this.snapToPixel=void 0!==i.snapToPixel&&i.snapToPixel,this.dirtyRendering=void 0!==i.dirtyRendering&&!!i.dirtyRendering,this.heartbeatInterval=1e3,this._lastPaintTime=0,this.dirtyRendering){this.stage.tickOnUpdate=!1,n.installMovieClipHook(t);var e=this.invalidate.bind(this);this.stage.on("stagemousedown",e),this.stage.on("stagemouseup",e),this.stage.on("stagemousemove",e)}t.Ticker.addEventListener("tick",function(i){t.Ticker.paused||this.tick(i)}.bind(this)),this.resize()},createStage:function(i){var e;return(e=void 0!==i.webgl&&i.webgl?new t.StageGL(this.canvas):new t.Stage(this.canvas)).snapToPixelEnabled=this.snapToPixel,e},setMaxCanvasSize:function(i,t){this.maxCanvasSize=[i,t]},nextLayer:function(i){void 0===i&&(i="Layer"+this.layers.length);var t=new e;return t.container.snapToPixel=this.snapToPixel,this.stage.addChild(t.container),this.layers.push(t),this.layerMap[i]=t,t},getLayer:function(i){return this.layerMap[i]},setPinContainer:function(i){a.setContainer(this.stage,i)},setView:function(i){this.mainLayer.setView(i),this.render()},invalidate:function(){n.invalidate()},tick:function(i){for(this.trigger("tick:before",i),this.dirty=!1,s=0;s=this.heartbeatInterval&&(this.dirty=!0),this.dirty&&(this._lastPaintTime=i.time,this.update())):this.dirty&&this.update(),this.trigger("tick:after")},render:function(){for(s=0;s0&&0==t&&this.trigger("click"),r=t},this.getLabel=function(){return''+o+""},this.setStaticLabel=function(t,n){return o=t,e=n,this},this.setLabel=function(i,e){return n.emit("button:label",{id:t,label:i}),this},this.click=function(t){return this.on("click",t),this},this.on=function(t,n){return void 0===s[t]&&(s[t]=[]),s[t].push(n),this},this.trigger=function(t){var n=[];if(void 0!==s[t])for(i=0;i Date: Wed, 15 Jul 2026 16:20:42 +0200 Subject: [PATCH 6/6] Release v1.2.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dfe5b8b..1aed785 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "easelbone", - "version": "1.2.10", + "version": "1.2.11", "main": "dist/scripts/easelbone.js", "author": "CatLab Interactive", "devDependencies": {