diff --git a/src/blocks/scratch3_looks.js b/src/blocks/scratch3_looks.js index cef818f70..fee2ecd30 100644 --- a/src/blocks/scratch3_looks.js +++ b/src/blocks/scratch3_looks.js @@ -2,6 +2,7 @@ const Cast = require('../util/cast'); const Clone = require('../util/clone'); const RenderedTarget = require('../sprites/rendered-target'); const uid = require('../util/uid'); +const StageLayering = require('../engine/stage-layering'); /** * @typedef {object} BubbleState - the bubble state associated with a particular target. @@ -91,7 +92,7 @@ class Scratch3LooksBlocks { _onTargetWillExit (target) { const bubbleState = this._getBubbleState(target); if (bubbleState.drawableId && bubbleState.skinId) { - this.runtime.renderer.destroyDrawable(bubbleState.drawableId); + this.runtime.renderer.destroyDrawable(bubbleState.drawableId, StageLayering.BUBBLE_LAYER); this.runtime.renderer.destroySkin(bubbleState.skinId); bubbleState.drawableId = null; bubbleState.skinId = null; @@ -195,10 +196,9 @@ class Scratch3LooksBlocks { bubbleState.onSpriteRight = false; } - bubbleState.drawableId = this.runtime.renderer.createDrawable(); + bubbleState.drawableId = this.runtime.renderer.createDrawable(StageLayering.BUBBLE_LAYER); bubbleState.skinId = this.runtime.renderer.createTextSkin(type, text, bubbleState.onSpriteRight, [0, 0]); - this.runtime.renderer.setDrawableOrder(bubbleState.drawableId, Infinity); this.runtime.renderer.updateDrawableProperties(bubbleState.drawableId, { skinId: bubbleState.skinId }); diff --git a/src/engine/runtime.js b/src/engine/runtime.js index e55f93ec8..2aaa88613 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -12,6 +12,7 @@ const TargetType = require('../extension-support/target-type'); const Thread = require('./thread'); const log = require('../util/log'); const maybeFormatMessage = require('../util/maybe-format-message'); +const StageLayering = require('./stage-layering'); // Virtual I/O devices. const Clock = require('../io/clock'); @@ -915,6 +916,7 @@ class Runtime extends EventEmitter { */ attachRenderer (renderer) { this.renderer = renderer; + this.renderer.setLayerGroupOrdering(StageLayering.LAYER_GROUPS); } /** diff --git a/src/engine/stage-layering.js b/src/engine/stage-layering.js new file mode 100644 index 000000000..1e7912c04 --- /dev/null +++ b/src/engine/stage-layering.js @@ -0,0 +1,63 @@ +class StageLayering { + static get BACKGROUND_LAYER () { + return 'background'; + } + + static get EXTENSION_LAYER () { + return 'extensions'; + } + + static get SPRITE_LAYER () { + return 'sprite'; + } + + static get BUBBLE_LAYER () { + return 'bubble'; + } + + static get BACKGROUND_ORDER () { + return 0; + } + + // Video should be in the back of the extension group + static get VIDEO_ORDER () { + return 0; + } + + static get PEN_ORDER () { + return 1; + } + + // Order of layer groups relative to each other, + // and ordering style of each + // Currently extensions are the only layer group + // that have an explicit ordering (e.g. video must be behind pen). + // All other groups here are ordered based on when they get added + static get LAYER_GROUPS () { + return [ + { + group: StageLayering.BACKGROUND_LAYER, + // This is a weird use case for a layer group ordering style, + // because in the main Scratch use case, this group has only one item, + // so ordering of the items doesn't really matter. + explicitOrdering: false + + }, + { + group: StageLayering.EXTENSION_LAYER, + explicitOrdering: true + }, + { + group: StageLayering.SPRITE_LAYER, + explicitOrdering: false + }, + { + group: StageLayering.BUBBLE_LAYER, + explicitOrdering: false + } + + ]; + } +} + +module.exports = StageLayering; diff --git a/src/extensions/scratch3_pen/index.js b/src/extensions/scratch3_pen/index.js index f1b79b409..4e5399770 100644 --- a/src/extensions/scratch3_pen/index.js +++ b/src/extensions/scratch3_pen/index.js @@ -7,6 +7,7 @@ const formatMessage = require('format-message'); const MathUtil = require('../../util/math-util'); const RenderedTarget = require('../../sprites/rendered-target'); const log = require('../../util/log'); +const StageLayering = require('../../engine/stage-layering'); /** * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. @@ -87,15 +88,6 @@ class Scratch3PenBlocks { }; } - /** - * Place the pen layer in front of the backdrop but behind everything else. - * We should probably handle this somewhere else... somewhere central that knows about pen, backdrop, video, etc. - * Maybe it should be in the GUI? - * @type {int} - */ - static get PEN_ORDER () { - return 1; - } /** * The minimum and maximum allowed pen size. @@ -136,8 +128,8 @@ class Scratch3PenBlocks { _getPenLayerID () { if (this._penSkinId < 0 && this.runtime.renderer) { this._penSkinId = this.runtime.renderer.createPenSkin(); - this._penDrawableId = this.runtime.renderer.createDrawable(); - this.runtime.renderer.setDrawableOrder(this._penDrawableId, Scratch3PenBlocks.PEN_ORDER); + this._penDrawableId = this.runtime.renderer.createDrawable( + StageLayering.EXTENSION_LAYER, StageLayering.PEN_ORDER); this.runtime.renderer.updateDrawableProperties(this._penDrawableId, {skinId: this._penSkinId}); } return this._penSkinId; diff --git a/src/io/video.js b/src/io/video.js index c01141c1e..26ec30630 100644 --- a/src/io/video.js +++ b/src/io/video.js @@ -1,3 +1,5 @@ +const StageLayering = require('../engine/stage-layering'); + class Video { constructor (runtime) { this.runtime = runtime; @@ -144,11 +146,7 @@ class Video { if (this._skinId === -1 && this._skin === null && this._drawable === -1) { this._skinId = renderer.createPenSkin(); this._skin = renderer._allSkins[this._skinId]; - this._drawable = renderer.createDrawable(); - renderer.setDrawableOrder( - this._drawable, - Video.ORDER - ); + this._drawable = renderer.createDrawable(StageLayering.EXTENSION_LAYER, StageLayering.VIDEO_ORDER); renderer.updateDrawableProperties(this._drawable, { skinId: this._skinId }); diff --git a/src/serialization/sb2.js b/src/serialization/sb2.js index 95d6d2ffb..516d9e797 100644 --- a/src/serialization/sb2.js +++ b/src/serialization/sb2.js @@ -327,7 +327,7 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip) // Blocks container for this object. const blocks = new Blocks(); // @todo: For now, load all Scratch objects (stage/sprites) as a Sprite. - const sprite = new Sprite(blocks, runtime); + const sprite = new Sprite(blocks, runtime, topLevel /* whether this sprite is a stge or not */); // Sprite/stage name from JSON. if (object.hasOwnProperty('objName')) { sprite.name = topLevel ? 'Stage' : object.objName; diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index 6f292399b..06b237789 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -684,7 +684,7 @@ const parseScratchObject = function (object, runtime, extensions, zip) { const blocks = new Blocks(); // @todo: For now, load all Scratch objects (stage/sprites) as a Sprite. - const sprite = new Sprite(blocks, runtime); + const sprite = new Sprite(blocks, runtime, object.isStage); // Sprite/stage name from JSON. if (object.hasOwnProperty('name')) { diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index c55042e67..3af4f3129 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -2,6 +2,7 @@ const log = require('../util/log'); const MathUtil = require('../util/math-util'); const StringUtil = require('../util/string-util'); const Target = require('../engine/target'); +const StageLayering = require('../engine/stage-layering'); /** * Rendered target: instance of a sprite (clone), or the stage. @@ -156,9 +157,10 @@ class RenderedTarget extends Target { /** * Create a drawable with the this.renderer. */ - initDrawable () { + initDrawable (isStage) { if (this.renderer) { - this.drawableID = this.renderer.createDrawable(); + this.drawableID = this.renderer.createDrawable(isStage ? + StageLayering.BACKGROUND_LAYER : StageLayering.SPRITE_LAYER); } // If we're a clone, start the hats. if (!this.isOriginal) { @@ -800,18 +802,22 @@ class RenderedTarget extends Target { /** * Move to the front layer. */ - goToFront () { + goToFront () { // This should only ever be used for sprites if (this.renderer) { - this.renderer.setDrawableOrder(this.drawableID, Infinity); + // Let the renderer re-order the sprite based on its knowledge + // of what layers are present + this.renderer.setDrawableOrder(this.drawableID, Infinity, StageLayering.SPRITE_LAYER); } } /** * Move to the back layer. */ - goToBack () { + goToBack () { // This should only ever be used for sprites if (this.renderer) { - this.renderer.setDrawableOrder(this.drawableID, -Infinity, false, 1); + // Let the renderer re-order the sprite based on its knowledge + // of what layers are present + this.renderer.setDrawableOrder(this.drawableID, -Infinity, StageLayering.SPRITE_LAYER, false); } } @@ -821,7 +827,7 @@ class RenderedTarget extends Target { */ goForwardLayers (nLayers) { if (this.renderer) { - this.renderer.setDrawableOrder(this.drawableID, nLayers, true, 1); + this.renderer.setDrawableOrder(this.drawableID, nLayers, StageLayering.SPRITE_LAYER, true); } } @@ -831,7 +837,7 @@ class RenderedTarget extends Target { */ goBackwardLayers (nLayers) { if (this.renderer) { - this.renderer.setDrawableOrder(this.drawableID, -nLayers, true, 1); + this.renderer.setDrawableOrder(this.drawableID, -nLayers, StageLayering.SPRITE_LAYER, true); } } @@ -842,8 +848,8 @@ class RenderedTarget extends Target { goBehindOther (other) { if (this.renderer) { const otherLayer = this.renderer.setDrawableOrder( - other.drawableID, 0, true); - this.renderer.setDrawableOrder(this.drawableID, otherLayer); + other.drawableID, 0, StageLayering.SPRITE_LAYER, true); + this.renderer.setDrawableOrder(this.drawableID, otherLayer, StageLayering.SPRITE_LAYER); } } @@ -912,7 +918,7 @@ class RenderedTarget extends Target { newClone.effects = JSON.parse(JSON.stringify(this.effects)); newClone.variables = JSON.parse(JSON.stringify(this.variables)); newClone.lists = JSON.parse(JSON.stringify(this.lists)); - newClone.initDrawable(); + newClone.initDrawable(false); // this,sprite is not a stage if we're calling makeClone on it newClone.updateAllDrawableProperties(); // Place behind the current target. newClone.goBehindOther(this); @@ -1049,7 +1055,7 @@ class RenderedTarget extends Target { this.runtime.stopForTarget(this); this.sprite.removeClone(this); if (this.renderer && this.drawableID !== null) { - this.renderer.destroyDrawable(this.drawableID); + this.renderer.destroyDrawable(this.drawableID, StageLayering.SPRITE_LAYER); if (this.visible) { this.emit(RenderedTarget.EVENT_TARGET_VISUAL_CHANGE, this); this.runtime.requestRedraw(); diff --git a/src/sprites/sprite.js b/src/sprites/sprite.js index 9f156f58f..173ea4c19 100644 --- a/src/sprites/sprite.js +++ b/src/sprites/sprite.js @@ -10,9 +10,10 @@ class Sprite { * All clones of a sprite have shared blocks, shared costumes, shared variables. * @param {?Blocks} blocks Shared blocks object for all clones of sprite. * @param {Runtime} runtime Reference to the runtime. + * @param {boolean=} isStage Whether or not this sprite is a stage * @constructor */ - constructor (blocks, runtime) { + constructor (blocks, runtime, isStage) { this.runtime = runtime; if (!blocks) { // Shared set of blocks for all clones. @@ -46,8 +47,12 @@ class Sprite { * @type {Array.} */ this.clones = []; + + // Needed for figuring out whether the associated drawable should + // go in the background layer or sprite layer + this.isStage = isStage || false; } - + /** * Add an array of costumes, taking care to avoid duplicate names. * @param {!Array} costumes Array of objects representing costumes. @@ -103,7 +108,7 @@ class Sprite { this.clones.push(newClone); newClone.initAudio(); if (newClone.isOriginal) { - newClone.initDrawable(); + newClone.initDrawable(this.isStage); this.runtime.fireTargetWasCreated(newClone); } else { this.runtime.fireTargetWasCreated(newClone, this.clones[0]);