Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 211 additions & 0 deletions app/scripts/CatLab/Easelbone/EaselJS/Pinner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
define(
['easeljs', 'CatLab/Easelbone/Utilities/DirtyFlag'],
function (createjs, DirtyFlag) {

'use strict';

// Pin state lives on the shared stage object so that separate easelbone
// bundles (game + library) rendering onto the SAME stage cooperate: both
// read/write stage._pinnedElements and stage._pinContainer, and whichever
// RootView owns the tick loop drives the sync for all of them.

function ensureState(stage) {
if (!stage._pinnedElements) {
stage._pinnedElements = [];
}
return stage._pinnedElements;
}

function ensureContainer(stage) {
if (stage._pinContainer && stage._pinContainer.stage === stage) {
return stage._pinContainer;
}
// Fallback for contexts with no designated pin layer (e.g. a
// standalone library portal with no blackout): a last-child
// container on the stage.
var container = new createjs.Container();
stage.addChild(container);
stage._pinContainer = container;
return container;
}

var Pinner = {

_props: {},

setContainer: function (stage, container) {
stage._pinContainer = container;
},

// Retry pinning on each tick until obj is attached to a stage.
// Gives up after ~5s (300 ticks) so a never-attached object can't
// leak a permanent retry loop.
_deferPin: function (obj, attempt) {
if (obj.stage) {
Pinner.pin(obj);
return;
}
if (attempt >= 300) {
return;
}
createjs.Ticker.on('tick', function () {
Pinner._deferPin(obj, attempt + 1);
}, null, true);
},

pin: function (obj) {
var stage = obj.stage;
if (!stage) {
// Not on a stage yet. We can't rely on obj's 'added'
// event here: obj is typically addChild'd to its
// placeholder BEFORE pinToTop is called, so 'added' has
// already fired and will NOT fire again when an ancestor
// (the placeholder) is later attached to the stage. Retry
// on the ticker until obj is actually on a stage.
Pinner._deferPin(obj, 0);
return;
}

// Idempotent: a second pin() on an object that's already
// pinned must not re-derive the anchor from obj.parent, since
// that's now the pin container, not the original anchor --
// which would orphan the real record (wrong key for
// _removeByAnchor) and push a bogus, never-torn-down one.
if (Pinner._isPinned(stage, obj)) {
return;
}

var anchor = obj.parent;
if (!anchor) {
return;
}

ensureState(stage);

// Re-pin replaces: drop any existing pin on the same anchor so a
// re-rendered QR (removeAllChildren + regenerate) never stacks.
Pinner._removeByAnchor(stage, anchor);

var wrapper = new createjs.Container();
var record = { obj: obj, anchor: anchor, wrapper: wrapper };

ensureContainer(stage).addChild(wrapper);
wrapper.addChild(obj); // reparents obj out of anchor; obj's own transform/regX/regY untouched

stage._pinnedElements.push(record);

// Immediate sync: correctly placed even without a running tick.
Pinner._syncRecord(stage, record);

// Reparenting doesn't itself mark the RootView dirty; under
// dirtyRendering:true this ensures the pin paints next frame
// instead of waiting for an unrelated event or the heartbeat.
DirtyFlag.invalidate();
},

unpin: function (obj) {
var stage = obj.stage;
var records = stage && stage._pinnedElements;
if (!records) {
return;
}
for (var i = 0; i < records.length; i++) {
if (records[i].obj === obj) {
Pinner._restore(records[i]);
records.splice(i, 1);
DirtyFlag.invalidate();
return;
}
}
},

_isPinned: function (stage, obj) {
var records = stage._pinnedElements;
if (!records) {
return false;
}
for (var i = 0; i < records.length; i++) {
if (records[i].obj === obj) {
return true;
}
}
return false;
},

sync: function (stage) {
var records = stage._pinnedElements;
if (!records || records.length === 0) {
return false;
}
for (var i = records.length - 1; i >= 0; i--) {
var record = records[i];
// Auto-teardown: anchor removed from the stage.
if (record.anchor.stage !== stage) {
Pinner._restore(record);
records.splice(i, 1);
continue;
}
Pinner._syncRecord(stage, record);
}
return records.length > 0;
},

_removeByAnchor: function (stage, anchor) {
var records = stage._pinnedElements;
for (var i = records.length - 1; i >= 0; i--) {
if (records[i].anchor === anchor) {
Pinner._restore(records[i]);
records.splice(i, 1);
}
}
},

_restore: function (record) {
if (record.obj.parent) {
record.obj.parent.removeChild(record.obj);
}
if (record.wrapper.parent) {
record.wrapper.parent.removeChild(record.wrapper);
}
if (record.anchor && record.anchor.stage) {
record.anchor.addChild(record.obj);
}
},

_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;
}

// Carry the anchor's bounds so bounds-sizing children (e.g. Fill) size correctly.
var bounds = record.anchor.getBounds();
if (bounds) {
wrapper.setBounds(0, 0, bounds.width, bounds.height);
}

// Place the wrapper in the anchor's coordinate space:
// wrapperLocal = pinContainerConcatenated^-1 * anchorConcatenated
var local = container.getConcatenatedMatrix().invert()
.appendMatrix(record.anchor.getConcatenatedMatrix());

var props = local.decompose(Pinner._props);
wrapper.x = props.x;
wrapper.y = props.y;
wrapper.scaleX = props.scaleX;
wrapper.scaleY = props.scaleY;
wrapper.rotation = props.rotation;
wrapper.skewX = props.skewX;
wrapper.skewY = props.skewY;
}
};

return Pinner;
}
);
16 changes: 14 additions & 2 deletions app/scripts/CatLab/Easelbone/FrontController.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ define(
'CatLab/Easelbone/Utilities/GlobalProperties',
'CatLab/Easelbone/Utilities/MovieClipHelper',

'CatLab/FakeWebremote/Models/KeyboardUser'
'CatLab/FakeWebremote/Models/KeyboardUser',

'CatLab/Easelbone/EaselJS/Pinner'
],
function (
Loader,
Expand Down Expand Up @@ -65,7 +67,9 @@ define(
GlobalProperties,
MovieClipHelper,

KeyboardUser
KeyboardUser,

Pinner
) {

return {
Expand Down Expand Up @@ -110,6 +114,14 @@ define(
}
},

pinToTop: function (displayObject) {
return Pinner.pin(displayObject);
},

unpinFromTop: function (displayObject) {
return Pinner.unpin(displayObject);
},

FakeWebremote: {
KeyboardUser: KeyboardUser
},
Expand Down
16 changes: 14 additions & 2 deletions app/scripts/CatLab/Easelbone/Views/Root.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ define(
'easeljs',

'CatLab/Easelbone/Views/Layer',
'CatLab/Easelbone/Utilities/DirtyFlag'
'CatLab/Easelbone/Utilities/DirtyFlag',
'CatLab/Easelbone/EaselJS/Pinner'
],
function (Backbone, createjs, Layer, DirtyFlag) {
function (Backbone, createjs, Layer, DirtyFlag, Pinner) {
var i;
var layer;

Expand Down Expand Up @@ -150,6 +151,16 @@ define(
return this.layerMap[name];
},

/**
* Designate the container that pinToTop reparents objects into.
* Lets the host control z-order (e.g. above the smiley layer but
* below a blackout overlay).
* @param container
*/
setPinContainer: function (container) {
Pinner.setContainer(this.stage, container);
},

/**
* Set a view on the main layer.
* @param view
Expand Down Expand Up @@ -239,6 +250,7 @@ define(
// Direct paints (render/resize) also reset the heartbeat.
this._lastPaintTime = createjs.Ticker.getTime();
}
Pinner.sync(this.stage);
this.stage.update();
},

Expand Down
Loading
Loading