/*
* Copyright (c) 2013, Octagon Software LLC
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
// Provide a helpful error message if the user forgets to include pod.js first.
if (typeof(PodJS) === "undefined") {
throw new Error("Must include pod.js script before including pod_scratch.js.");
}
/**
* @class
* @classdesc pod.js pod that emulates the scratch programming language.
* <p>
* By default, when this pod initializes it looks for a div in the document with the id of "stage" (or whatever is specified in
* the options). It then attaches to that div and creates a resource called "stage" that is the Scratch stage.
*
* @param {type} options The following parameters are supported:
* <ul>
* <li>scratch_stage_div - The id of the div to which the scratch stage should bind. Optional. If not specified, uses the
* div called "stage".</li>
* </ul>
* @returns {PodJS.ScratchPod}
*/
PodJS.ScratchPod = function(options) {
// Call super constructor
PodJS.Pod.call(this, options);
var ScratchPod_this = this;
/**
* The environment this pod is in
*
* @private
* @instance
* @memberof PodJS.ScratchPod
*/
var _env = options.env;
/**
* The div that contains the stage and controls
*
* @private
* @instance
* @memberof PodJS.ScratchPod
*/
var _div;
/**
* The div that contains just the controls
*
* @private
* @instance
* @memberof PodJS.ScratchPod
*/
var _controlsDiv;
/**
* The img for the red stop button
*
* @private
* @instance
* @memberof PodJS.ScratchPod
*/
var _redStopButton;
/**
* The img for the green flag button
*
* @private
* @instance
* @memberof PodJS.ScratchPod
*/
var _greenFlagButton;
/**
* Records the last time the green flag was clicked, in millis since epoch.
*
* @private
* @instance
* @memberof PodJS.ScratchPod
*/
var _lastGreenFlagClickTime = 0;
/**
* True if the green flag is clicked and the scratch app is running or false if not.
*
* @private
* @instance
* @memberof PodJS.ScratchPod
*/
var _running = false;
/**
* The div that contains just the stage
*
* @private
* @instance
* @memberof PodJS.ScratchPod
*/
var _stageDiv;
/**
* The canvas inside the div that contains the stage
*
* @private
* @instance
* @memberof PodJS.ScratchPod
*/
var _canvas = document.createElement("canvas");
/**
* The createjs easel that uses the canvas to draw the stage and all sprites.
*
* @private
* @instance
* @memberof PodJS.ScratchPod
*/
var _easelStage = null;
/**
* The model of the stage
*
* @private
* @instance
* @memberof PodJS.ScratchPod
*/
var _stage;
/**
* Table of the last time each message was sent.
*
* @private
* @instance
* @memberof PodJS.ScratchPod
*/
var _broadcastMessages = {};
/**
* Global variables that apply to all sprites.
*
* @private
* @instance
* @memberof PodJS.ScratchPod
*/
var _variables = {};
/**
* Global list variables that apply to all sprites.
*
* @private
* @instance
* @memberof PodJS.ScratchPod
*/
var _listVariables = {};
/**
* Information about each audio resource. Key is spriteName::audioName or just ::audioName for the stage.
* Value is an AudioInfo object.
*/
var _audioFiles = {};
/**
* Path prefix to prepend to all resource paths.
*/
var _resourcesPathPrefix = "";
/**
* @private
* @instance
* @memberof PodJS.ScratchPod
*
* @property {string} prefix prefix of the audio id, either the sprite name or "" if the stage.
* @property {string} name the name of the audio file, from the resource's perspective
* @property {string} href of the source of the audio
* @property {boolean} loaded true if loaded, false if still loading.
*/
var AudioInfo = {
prefix : "",
name : "",
src : "",
loaded : false
};
/**
* Sanitize HTML
* Source: http://stackoverflow.com/questions/295566/sanitize-rewrite-html-on-the-client-side
*/
var _htmlEscape = function(html) {
var tagBody = '(?:[^"\'>]|"[^"]*"|\'[^\']*\')*';
var tagOrComment = new RegExp(
'<(?:'
// Comment body.
+ '!--(?:(?:-*[^->])*--+|-?)'
// Special "raw text" elements whose content should be elided.
+ '|script\\b' + tagBody + '>[\\s\\S]*?</script\\s*'
+ '|style\\b' + tagBody + '>[\\s\\S]*?</style\\s*'
// Regular name
+ '|/?[a-z]'
+ tagBody
+ ')>',
'gi');
var oldHtml;
do {
oldHtml = html;
html = html.replace(tagOrComment, '');
} while (html !== oldHtml);
return html.replace(/</g, '<');
};
// Private Variable class
var Variable = function(spriteName, variableName) {
/**
* HTML DOM element for rendering the variable
*
* @instance
* @member {HTMLElement} _varDiv
* @memberof PodJS.ScratchPod#Variable
*/
var _varDiv = document.createElement("div");
/**
* HTML DOM element for rendering the variable value
*
* @instance
* @member {HTMLElement} _valueDiv
* @memberof PodJS.ScratchPod#Variable
*/
var _valueSpan = document.createElement("span");
/**
* CreateJS Container for the visual display of this variable, if shown.
*
* @instance
* @member {createjs.DOMElement} _createJSDOMElement
* @memberof PodJS.ScratchPod#Variable
*/
var _createJSDOMElement = new createjs.DOMElement(_varDiv);
/**
* @instance
* @member {number|string} value
* @memberof PodJS.ScratchPod#Variable
*/
var _value = 0;
Object.defineProperty(this, "value", {
get : function() {
return _value;
},
set : function(value) {
_value = value;
_valueSpan.innerHTML = _htmlEscape(String(_value));
}
});
/**
* True if the variable can be seen on the stage, or false if it is hidden.
* @instance
* @member {boolean} hidden
* @memberof PodJS.ScratchPod#Variable
*/
var _shown = false;
Object.defineProperty(this, "shown", {
get : function() {
return _shown;
},
set : function(value) {
_shown = value;
if (value) {
_easelStage.addChild(_createJSDOMElement);
_varDiv.style.visibility = "visible";
} else {
_easelStage.removeChild(_createJSDOMElement);
_varDiv.style.visibility = "hidden";
}
}
});
/**
* X position of the variable on the stage, when shown.
*
* @instance
* @member {number} x
* @memberof PodJS.ScratchPod#Variable
*/
var _x = 0;
Object.defineProperty(this, "x", {
get : function() {
return _x;
},
set : function(value) {
_x = value;
_createJSDOMElement.x = value;
}
});
/**
* Y position of the variable on the stage, when shown.
*
* @instance
* @member {number} y
* @memberof PodJS.ScratchPod#Variable
*/
var _y = 0;
Object.defineProperty(this, "y", {
get : function() {
return _y;
},
set : function(value) {
_y = value;
_createJSDOMElement.y = value;
}
});
var construct = function() {
// mimic the style of Scratch's variable display but use a DOM object.
_varDiv.innerHTML = ((spriteName === null) ? "" : (spriteName + ": ")) + variableName;
_varDiv.className = "podjs_scratch_var_div";
_stageDiv.insertBefore(_varDiv, _stageDiv.firstChild);
_valueSpan.innerHTML = "0";
_valueSpan.className = "podjs_scratch_var_value";
_varDiv.appendChild(_valueSpan);
};
construct();
};
/**
* @class
* @classdesc List variable containing both the list contents and the display.
*/
this.ListVariable = function(spriteName, listVariableName) {
/**
* HTML DOM element for rendering the list variable
*
* @instance
* @member {HTMLElement} _varDiv
* @memberof PodJS.ScratchPod#ListVariable
*/
var _varDiv = document.createElement("div");
/**
* HTML DOM element for rendering the list variable
*
* @instance
* @member {HTMLElement} _valueDiv
* @memberof PodJS.ScratchPod#ListVariable
*/
var _valueDiv = document.createElement("div");
/**
* HTML DOM element for rendering the list variable
*
* @instance
* @member {HTMLElement} _domList
* @memberof PodJS.ScratchPod#ListVariable
*/
var _domList = document.createElement("ul");
/**
* HTML DOM element for rendering the list length
*
* @instance
* @member {HTMLElement} _lengthSpan
* @memberof PodJS.ScratchPod#ListVariable
*/
var _lengthDiv = document.createElement("div");
/**
* CreateJS Container for the visual display of this list variable, if shown.
*
* @instance
* @member {createjs.DOMElement} _createJSDOMElement
* @memberof PodJS.ScratchPod#ListVariable
*/
var _createJSDOMElement = new createjs.DOMElement(_varDiv);
/**
* @instance
* @member {object[]} value
* @memberof PodJS.ScratchPod#ListVariable
*/
var _list = [];
/**
* True if the list variable can be seen on the stage, or false if it is hidden.
* @instance
* @member {boolean} hidden
* @memberof PodJS.ScratchPod#ListVariable
*/
var _shown = false;
Object.defineProperty(this, "shown", {
get : function() {
return _shown;
},
set : function(value) {
_shown = value;
if (value) {
_easelStage.addChild(_createJSDOMElement);
_varDiv.style.visibility = "visible";
} else {
_easelStage.removeChild(_createJSDOMElement);
_varDiv.style.visibility = "hidden";
}
}
});
/**
* X position of the list variable on the stage, when shown.
*
* @instance
* @member {number} x
* @memberof PodJS.ScratchPod#ListVariable
*/
var _x = 0;
Object.defineProperty(this, "x", {
get : function() {
return _x;
},
set : function(value) {
_x = value;
_createJSDOMElement.x = value;
}
});
/**
* Y position of the list variable on the stage, when shown.
*
* @instance
* @member {number} y
* @memberof PodJS.ScratchPod#ListVariable
*/
var _y = 0;
Object.defineProperty(this, "y", {
get : function() {
return _y;
},
set : function(value) {
_y = value;
_createJSDOMElement.y = value;
}
});
/**
* Width of the list variable on the stage, when shown.
*
* @instance
* @member {number} width
* @memberof PodJS.ScratchPod#ListVariable
*/
var _width = null;
Object.defineProperty(this, "width", {
get : function() {
return _width;
},
set : function(value) {
_width = value;
if (value !== null) {
_varDiv.style.width = Number(value) + "px";
}
}
});
/**
* Height of the list variable on the stage, when shown.
*
* @instance
* @member {number} height
* @memberof PodJS.ScratchPod#ListVariable
*/
var _height = null;
Object.defineProperty(this, "height", {
get : function() {
return _height;
},
set : function(value) {
_height = value;
if (value !== null) {
_varDiv.style.height = Number(value) + "px";
}
}
});
/**
* Create the DOM list item for an item of the list.
*
* @private
* @instance
* @method _createListItem
* @param {string|number} value The value to display
* @return {HTMLElement} A list item HTML element.
*/
var _createListItem = function(value) {
var li = document.createElement("li");
li.className = "podjs_scratch_list_var_item";
var span = document.createElement("span");
span.className = "podjs_scratch_list_var_item_span";
span.innerHTML = _htmlEscape(String(value));
li.appendChild(span);
_updateLength();
return li;
};
/**
* Update the display of the length of this list.
*
* @private
* @instance
* @method _updateLength
*/
var _updateLength = function() {
_lengthDiv.innerHTML = "length: " + _list.length;
};
/**
* Add an item to the end of the list
*
* @instance
* @method add
* @param {number|string} value The item to add to the list
* @memberof PodJS.ScratchPod#ListVariable
*/
this.add = function(value) {
_list.push(value);
var li = _createListItem(value);
_domList.appendChild(li);
_updateLength();
};
/**
* Delete an item from the list
*
* @instance
* @method deleteAll
* @memberof PodJS.ScratchPod#ListVariable
*/
this.deleteAll = function() {
_list.length = 0;
_domList.innerHTML = "";
_updateLength();
};
/**
* Delete item from the list at the given index
*
* @instance
* @method deleteAt
* @param {number} index The index to delete from, 0-based.
* @memberof PodJS.ScratchPod#ListVariable
*/
this.deleteAt = function(index) {
if (index >= 0 && index < _list.length) {
_list.splice(index, 1);
_domList.removeChild(_domList.childNodes[index]);
_updateLength();
}
};
/**
* Insert item into the list at the given index
*
* @instance
* @method insertAt
* @param {number|string} value The value to insert into the list
* @param {number} index The index to insert at, 0-based. All other items are shifted towards the end of the list.
* @memberof PodJS.ScratchPod#ListVariable
*/
this.insertAt = function(value, index) {
if (index >= 0 && index <= _list.length) {
var atEnd = index === _list.length;
_list.splice(index, 0, value);
var li = _createListItem(value);
if (atEnd) {
_domList.appendChild(li);
} else {
_domList.insertBefore(li, _domList.childNodes[index]);
}
_updateLength();
}
};
/**
* Replace item at the given index
*
* @instance
* @method replaceAt
* @param {number|string} value The value to replace in the list
* @param {number} index The index to insert at, 0-based.
* @memberof PodJS.ScratchPod#ListVariable
*/
this.replaceAt = function(value, index) {
if (index >= 0 && index < _list.length) {
_list.splice(index, 1, value);
_domList.childNodes[index].getElementsByTagName("span")[0].innerHTML = _htmlEscape(String(value));
}
};
/**
* Return item at the given index
*
* @instance
* @method getAt
* @param {number} index The index of the element to retrieve
* @return {number|string} The value at the given index in the list.
* @memberof PodJS.ScratchPod#ListVariable
*/
this.getAt = function(index) {
var result = "";
if (index >= 0 && index < _list.length) {
result = _list[index];
}
return result;
};
/**
* Returns the list of the list
*
* @instance
* @method length
* @return {number} The number of items in the list
* @memberof PodJS.ScratchPod#ListVariable
*/
this.length = function() {
return _list.length;
};
/**
* Returns true if the list contains the given item, or false if not.
*
* @instance
* @method length
* @param {number|string} value The item to check the list for
* @return {boolean} true if the list contains the item or false if not.
* @memberof PodJS.ScratchPod#ListVariable
*/
this.contains = function(value) {
return _list.indexOf(value) !== -1;
};
var construct = function() {
// mimic the style of Scratch's variable display but use a DOM object.
_varDiv.innerHTML = ((spriteName === null) ? "" : (spriteName + ": ")) + listVariableName;
_varDiv.className = "podjs_scratch_list_var";
_stageDiv.insertBefore(_varDiv, _stageDiv.firstChild);
_domList.className = "podjs_scratch_list_var_list";
_varDiv.appendChild(_valueDiv);
_valueDiv.className = "podjs_scratch_list_value_div";
_valueDiv.appendChild(_domList);
_lengthDiv.className = "podjs_scratch_list_var_length_div";
_updateLength();
_varDiv.appendChild(_lengthDiv);
};
construct();
};
/**
* Returns true if the value provided should be considered true, or false if not.
*
* @private
* @instance
* @param val The value to evaluate for truthiness.
* @memberof PodJS.ScratchPod
*/
var truthy = function(val) {
return String(val) === "true";
};
/**
* Automatically set the starting position of the variable when shown.
*
* @instance
* @method _autoPositionVariable
* @param {PodJS.ScratchPod.Variable} variable The variable to position.
* @memberof PodJS.ScratchPod
*/
var _autoVariableX = -230;
var _autoVariableY = -170;
var _autoPositionVariable = function(variable) {
variable.x = _autoVariableX;
variable.y = _autoVariableY;
_autoVariableY += 30;
if (_autoVariableY > 200) {
_autoVariableY = -170;
_autoVariableX += 100;
}
};
/**
* Automatically set the starting position of the list variable when shown.
*
* @instance
* @method _autoPositionListVariable
* @param {PodJS.ScratchPod.ListVariable} listVariable The list variable to position.
* @memberof PodJS.ScratchPod
*/
var _autoPositionListVariable = function(listVariable) {
listVariable.x = _autoVariableX;
listVariable.y = _autoVariableY;
_autoVariableY += 200;
if (_autoVariableY > 200) {
_autoVariableY = -170;
_autoVariableX += 100;
}
};
/**
* Create a new variable for all sprites with the given name.
*
* @instance
* @method createVariable
* @param {string} name the name of the variable
* @memberof PodJS.ScratchPod
*/
this.createVariable = function(name) {
if (_variables.hasOwnProperty(name)) {
throw "All Sprites already has a variable called '" + name + "'";
}
var variable = new Variable(null, name);
_autoPositionVariable(variable);
_variables[name] = variable;
};
/**
* Create a new list variable for all sprites with the given name.
*
* @instance
* @method createListVariable
* @param {string} name the name of the list variable
* @memberof PodJS.ScratchPod
*/
this.createListVariable = function(name) {
if (_listVariables.hasOwnProperty(name)) {
throw "All Sprites already has a list variable called '" + name + "'";
}
var listVariable = new ScratchPod_this.ListVariable(null, name);
_autoPositionListVariable(listVariable);
_listVariables[name] = listVariable;
};
/**
* Set the value of the given variable to the given value.
*
* @instance
* @method setVariable
* @param {string} name the name of the variable
* @param {number|string} value the value to set the variable to
* @memberof PodJS.ScratchPod
*/
this.setVariable = function(name, value) {
if (!_variables.hasOwnProperty(name)) {
throw "All Sprites does not have a variable called '" + name + "'";
}
_variables[name].value = value;
};
/**
* Sets whether this variable is shown on the stage, and the location at which it is shown.
*
* @instance
* @method showVariable
* @param {string} name the name of the variable
* @param {boolean} shown true if the variable is to be shown, or false if not. Optional, defaults to true.
* @param {number} x x position of the variable on the stage (optional, defaults to 0)
* @param {number} y y position of the variable on the stage (optional, defaults to 0)
* @memberof PodJS.ScratchPod
*/
this.showVariable = function(name, shown, x, y) {
if (typeof(shown) === "undefined") {
shown = true;
}
if (typeof(x) !== "undefined") {
_variables[name].x = x;
}
if (typeof(y) !== "undefined") {
_variables[name].y = y;
}
if (!_variables.hasOwnProperty(name)) {
throw "All Sprites does not have a variable called '" + name + "'";
}
_variables[name].shown = shown;
};
/**
* Sets whether this list variable is shown on the stage, and the location at which it is shown.
*
* @instance
* @method showListVariable
* @param {string} name the name of the list variable
* @param {boolean} shown true if the list variable is to be shown, or false if not. Optional, defaults to true.
* @param {number} x x position of the list variable on the stage (optional, defaults to 0)
* @param {number} y y position of the list variable on the stage (optional, defaults to 0)
* @param {number} width width Width of the box to show (optional, defaults to enough width to show small ints)
* @param {number} height height of the box to show (optional, defaults to 8 rows worth of pixels)
* @memberof PodJS.ScratchPod
*/
this.showListVariable = function(name, shown, x, y, width, height) {
if (typeof(shown) === "undefined") {
shown = true;
}
if (typeof(x) !== "undefined" && x !== null) {
_listVariables[name].x = x;
}
if (typeof(y) !== "undefined" && y !== null) {
_listVariables[name].y = y;
}
if (typeof(width) !== "undefined") {
_listVariables[name].width = width;
}
if (typeof(height) !== "undefined") {
_listVariables[name].height = height;
}
if (!_listVariables.hasOwnProperty(name)) {
throw "All Sprites does not have a list variable called '" + name + "'";
}
_listVariables[name].shown = shown;
};
/**
* Get the value of the given variable.
*
* @instance
* @method getVariable
* @param {string} name the name of the variable
* @return The value of the variable.
* @memberof PodJS.ScratchPod
*/
this.getVariable = function(name) {
if (!_variables.hasOwnProperty(name)) {
throw "Sprite does not have a variable called '" + name + "'";
}
return _variables[name].value;
};
/**
* Get the list variable.
*
* @instance
* @method getListVariable
* @param {string} name the name of the list variable
* @return {PodJS.ScratchPod.ListVariable} The list variable.
* @memberof PodJS.ScratchPod
*/
this.getListVariable = function(name) {
if (!_listVariables.hasOwnProperty(name)) {
throw "Sprite does not have a list variable called '" + name + "'";
}
return _listVariables[name];
};
/**
* Returns true if the variable exists for all sprites, or false if not.
*
* @instance
* @method hasVariable
* @param {string} name the name of the variable
* @return {boolean} true if the variable exists or false if not.
* @memberof PodJS.ScratchPod
*/
this.hasVariable = function(name) {
return _variables.hasOwnProperty(name);
};
/**
* Returns true if the list variable exists for all sprites, or false if not.
*
* @instance
* @method hasListVariable
* @param {string} name the name of the list variable
* @return {boolean} true if the list variable exists or false if not.
* @memberof PodJS.ScratchPod
*/
this.hasListVariable = function(name) {
return _listVariables.hasOwnProperty(name);
};
/**
* Returns the sprite with the given name
*
* @instance
* @method sprite
* @param {string} name the name of the sprite
* @return {PodJS.ScratchPod.Sprite} the sprite
* @memberof PodJS.ScratchPod
*/
this.sprite = function(name) {
var result = this.getResourceByName(name);
if (result === null || result.resourceType !== "sprite") {
throw new Error("No sprite with the name '" + name + "' found.");
}
return result;
}
/**
* Part of the Pod standard interface - return information about the blocks provided
* by the scratch pod.
*
* @public
* @instance
* @method getBlockTypes
* @memberof PodJS.ScratchPod
* @return {object[]} One info object for each block.
*/
this.getBlockTypes = function() {
return [
//////////////////////////////////////////////////////////////
// Motion Blocks
{
blockType : "change_x",
description : "The block moves its sprite costume center's X position by the specified amount.",
parameterInfo : [
{ name : "steps" }
],
returnsValue : false,
compatibleWith : function(resource) {
return resource.resourceType === "sprite";
},
tick : function(context) {
var arg = context.blockScript.nextArgument();
console.log("change_x " + arg);
var sprite = context.resource;
sprite.translate(arg, 0);
context.blockScript.nextBlock();
context.blockScript.yield = true;
}
},
{
blockType : "change_y",
description : "The block moves its sprite costume center's Y position by the specified amount.",
parameterInfo : [
{ name : "steps" }
],
returnsValue : false,
compatibleWith : function(resource) {
return resource.resourceType === "sprite";
},
tick : function(context) {
var arg = context.blockScript.nextArgument();
console.log("change_y " + arg);
var sprite = context.resource;
sprite.translate(0, arg);
context.blockScript.nextBlock();
context.blockScript.yield = true;
}
},
{
blockType : "move",
description : "Moves its sprite forward the specified amount of steps in the direction it is facing, a " +
"step being 1 pixel length.",
parameterInfo : [
{ name : "steps" }
],
returnsValue : false,
compatibleWith : function(resource) {
return resource.resourceType === "sprite";
},
tick : function(context) {
var arg = context.blockScript.nextArgument();
console.log("move " + arg);
var sprite = context.resource;
sprite.moveSteps(arg);
context.blockScript.nextBlock();
context.blockScript.yield = true;
}
},
{
blockType : "point_dir",
description : "The block points its sprite in the specified direction; this rotates the sprite.",
parameterInfo : [
{ name : "degrees" }
],
returnsValue : false,
compatibleWith : function(resource) {
return resource.resourceType === "sprite";
},
tick : function(context) {
var arg = context.blockScript.nextArgument();
console.log("point_dir " + arg);
var sprite = context.resource;
sprite.setDirection(arg);
context.blockScript.nextBlock();
context.blockScript.yield = true;
}
},
{
blockType : "go_xy",
description : "sets the sprite's X and Y position to the specified amounts.",
parameterInfo : [
{ name : "x" },
{ name : "y" }
],
returnsValue : false,
compatibleWith : function(resource) {
return resource.resourceType === "sprite";
},
tick : function(context) {
var x = context.blockScript.nextArgument();
var y = context.blockScript.nextArgument();
console.log("go_xy " + x + " " + y);
var sprite = context.resource;
sprite.goXY(x, y);
context.blockScript.nextBlock();
context.blockScript.yield = true;
}
},
//////////////////////////////////////////////////////////////
// Looks Blocks
{
blockType : "costume",
description : "Changes the Sprite's costume to the specified one.",
parameterInfo : [
{ name : "costume" }
],
returnsValue : false,
compatibleWith : function(resource) {
return resource.resourceType === "sprite";
},
tick : function(context) {
var arg = context.blockScript.nextArgument();
console.log("costume " + arg);
var sprite = context.resource;
sprite.setCostume(arg);
context.blockScript.nextBlock();
}
},
{
blockType : "hide",
description : "If the block's sprite is shown, it will hide the sprite - if the sprite is already hidden, nothing happens.",
parameterInfo : [],
returnsValue : false,
compatibleWith : function(resource) {
return resource.resourceType === "sprite";
},
tick : function(context) {
console.log("hide");
var sprite = context.resource;
if (sprite.isShown()) {
sprite.setShown(false);
}
context.blockScript.nextBlock();
}
},
{
blockType : "show",
description : "If the block's sprite is hidden, it will show the sprite - if the sprite is already showing, nothing will change.",
parameterInfo : [],
returnsValue : false,
compatibleWith : function(resource) {
return resource.resourceType === "sprite";
},
tick : function(context) {
console.log("show");
var sprite = context.resource;
if (!sprite.isShown()) {
sprite.setShown(true);
}
context.blockScript.nextBlock();
}
},
//////////////////////////////////////////////////////////////
// Sound Blocks
{
blockType : "play_sound",
description : "The block will play the specified sound, with no pause to its script",
parameterInfo : [
{ name : "audioId" }
],
returnsValue : false,
compatibleWith : function(resource) {
return resource.resourceType === "sprite" || resource.resourceType === "stage";
},
tick : function(context) {
var audioId = context.blockScript.nextArgument();
console.log("play_sound " + audioId);
context.resource.playSound(audioId);
context.blockScript.nextBlock();
}
},
{
blockType : "stop_all_sounds",
description : "The block will stop any sounds currently being played on all sprites and the Stage.",
parameterInfo : [],
returnsValue : false,
compatibleWith : function(resource) {
return resource.resourceType === "sprite" || resource.resourceType === "stage";
},
tick : function(context) {
console.log("stop_all_sounds");
ScratchPod_this.stopAllSounds();
context.blockScript.nextBlock();
}
},
//////////////////////////////////////////////////////////////
// EventBlocks
{
blockType : "broadcast",
description : "Sends a broadcast throughout the whole Scratch program. Any scripts in any sprites that " +
"are hatted with the When I Receive () block that is set to a specified broadcast will activate.",
parameterInfo : [ { "name" : "message" } ],
returnsValue : false,
compatibleWith : function(resource) {
return true;
},
tick : function(context) {
var message = context.blockScript.nextArgument();
console.log("broadcast " + message);
_broadcastMessages[message] = Date.now();
context.blockScript.nextBlock();
context.blockScript.yield = true;
}
},
{
blockType : "when_green_flag_clicked",
description : "Scripts that wear this block will activate once the Green Flag has been clicked - " +
"these scripts can activate other scripts and enable the entire program.",
parameterInfo : [],
returnsValue : false,
eventBlock : true,
compatibleWith : function(resource) {
return true;
},
tick : function(context) {
var lastGreenFlagClickTime = context.block.hasOwnProperty("lastGreenFlagClickTime") ?
context.block.lastGreenFlagClickTime : null;
if (lastGreenFlagClickTime === null) {
var now = Date.now();
lastGreenFlagClickTime = now;
context.block.lastGreenFlagClickTime = lastGreenFlagClickTime;
} else if (_lastGreenFlagClickTime > lastGreenFlagClickTime) {
context.block.lastGreenFlagClickTime = _lastGreenFlagClickTime;
console.log("when_green_flag_clicked");
context.blockScript.nextBlock();
}
context.blockScript.yield = true;
}
},
{
blockType : "when_receive",
description : "Scripts that begin with this block will be invoked once the specified broadcast has " +
"been sent by a calling script.",
parameterInfo : [ { "name" : "message" }],
returnsValue : false,
eventBlock : true,
compatibleWith : function(resource) {
return true;
},
reset : function(context) {
delete context.block.messageName;
delete context.block.nextIP;
},
tick : function(context) {
var messageName;
if (context.block.hasOwnProperty("messageName")) {
messageName = context.block.messageName;
} else {
var ip = context.blockScript.index;
messageName = context.blockScript.nextArgument();
context.block.nextIP = context.blockScript.index + 1;
context.blockScript.index = ip;
context.block.messageName = messageName;
}
var lastEventTime = context.block.hasOwnProperty("lastEventTime") ? context.block.lastEventTime : null;
if (lastEventTime === null) {
var now = Date.now();
lastEventTime = now;
context.block.lastEventTime = lastEventTime;
} else if (_broadcastMessages.hasOwnProperty(messageName) && _broadcastMessages[messageName] > lastEventTime) {
context.block.lastEventTime = _broadcastMessages[messageName];
console.log("when_receive " + messageName);
context.blockScript.nextBlock();
context.blockScript.index = context.block.nextIP;
this.reset(context);
}
context.blockScript.yield = true;
}
},
{
blockType : "when_sprite_clicked",
description : "Scripts that wear the block will activate once its sprite is clicked.",
parameterInfo : [],
returnsValue : false,
eventBlock : true,
compatibleWith : function(resource) {
return resource.resourceType === "sprite";
},
tick : function(context) {
var sprite = context.resource;
var lastSpriteClickTime = context.block.hasOwnProperty("lastSpriteClickTime") ?
context.block.lastSpriteClickTime : null;
if (lastSpriteClickTime === null) {
var now = Date.now();
lastSpriteClickTime = now;
context.block.lastSpriteClickTime = lastSpriteClickTime;
} else if (sprite.lastClickTime > lastSpriteClickTime) {
context.block.lastSpriteClickTime = sprite.lastClickTime;
console.log("when_sprite_clicked");
context.blockScript.nextBlock();
}
context.blockScript.yield = true;
}
},
//////////////////////////////////////////////////////////////
// Control Blocks
{
blockType : "otherwise",
description : "This block should be placed immediately after an if_then begin/end block to be executed if the " +
"condition is false. The otherwise should have its own begin/end block. This block is not legal when used " +
"by itself.",
parameterInfo : [],
returnsValue : false,
compatibleWith : function(resource) {
return true;
},
reset : function(context) {
},
tick : function(context) {
throw new Error("otherwise block cannot be used by itself - it must be placed after an if_then " +
"begin/end block. A common cause of this error is a missing 'end' before an 'otherwise'.");
}
},
{
blockType : "forever",
description : "Blocks held inside this block will be in a loop - just like the Repeat () block and the " +
"Repeat Until () block, except that the loop never ends (unless the stop sign is clicked, the Stop All " +
"block is activated, or the stop script block is activated within the loop).",
parameterInfo : [],
returnsValue : false,
compatibleWith : function(resource) {
return true;
},
tick : function(context) {
console.log("forever");
context.blockScript.pushIP();
context.blockScript.nextBlock();
}
},
{
blockType : "if_then",
description : "The block will check its boolean condition: if the condition is true, the code held inside the " +
"first begin/end will activate, and then the script will continue; if the condition is false, then if " +
"an otherwise block is present immediately after the end the code inside the second begin/end will activate.",
parameterInfo : [ { name : "condition" } ],
returnsValue : false,
compatibleWith : function(resource) {
return true;
},
reset : function(context) {
delete context.block.evaluated;
delete context.block.nextIP;
},
tick : function(context) {
if (typeof(context.block.evaluated) === "undefined") {
var ipOfIfElse = context.blockScript.index;
context.block.evaluated = true;
var condition = truthy(context.blockScript.nextArgument());
context.blockScript.nextBlock();
context.block.nextIP = context.blockScript.index;
console.log("if_then " + condition);
if (condition) {
context.blockScript.index = ipOfIfElse;
context.blockScript.pushIP();
context.blockScript.index = context.block.nextIP;
} else {
context.blockScript.skipBeginEndBlock();
var b = context.blockScript.peekBlock();
if (b != null && b.blockType === "otherwise") {
// execute the otherwise block
context.blockScript.nextBlock();
var beginIP = context.blockScript.index;
console.log("otherwise");
context.blockScript.index = ipOfIfElse;
context.blockScript.pushIP();
context.blockScript.index = beginIP;
}
}
} else {
// hit the end block and now we're back to the if or otherwise block to see where to go next.
// Always skip begin/end.
context.blockScript.index = context.block.nextIP;
context.blockScript.skipBeginEndBlock();
var b = context.blockScript.peekBlock();
if (b.blockType === "otherwise") {
context.blockScript.nextBlock();
// skip past begin/end of otherwise as well.
context.blockScript.skipBeginEndBlock();
}
delete context.block.evaluated;
delete context.block.nextIP;
}
}
},
{
blockType : "repeat",
description : "Blocks held inside this block will loop a given amount of times, before allowing the " +
"script to continue.",
parameterInfo : [
{ name : "count" }
],
returnsValue : false,
compatibleWith : function(resource) {
return true;
},
reset : function(context) {
delete context.block.remaining;
delete context.block.nextIP;
},
tick : function(context) {
var ipOfRepeat = context.blockScript.index;
if (typeof(context.block.remaining) === "undefined") {
context.block.remaining = Math.ceil(Number(context.blockScript.nextArgument()));
context.blockScript.nextBlock();
context.block.nextIP = context.blockScript.index;
} else {
context.blockScript.index = context.block.nextIP;
}
if (context.block.remaining > 0) {
console.log("repeat " + context.block.remaining);
context.block.remaining--;
var beginIP = context.blockScript.index;
context.blockScript.index = ipOfRepeat;
context.blockScript.pushIP();
context.blockScript.index = beginIP;
} else {
context.blockScript.skipBeginEndBlock();
this.reset(context);
}
context.blockScript.yield = true;
}
},
{
blockType : "repeat_until",
description : "Blocks held inside this block will loop until the specified boolean statement is true, " +
"in which case the code beneath the block (if any) will execute.",
parameterInfo : [
{ name : "condition" }
],
returnsValue : false,
compatibleWith : function(resource) {
return true;
},
reset : function(context) {
},
tick : function(context) {
var ipOfRepeatUntil = context.blockScript.index;
var condition = truthy(context.blockScript.nextArgument());
context.blockScript.nextBlock();
console.log("repeat_until " + condition);
if (condition) {
context.blockScript.skipBeginEndBlock();
} else {
var beginIP = context.blockScript.index;
context.blockScript.index = ipOfRepeatUntil;
context.blockScript.pushIP();
context.blockScript.index = beginIP;
}
context.blockScript.yield = true;
}
},
{
blockType : "wait",
description : "pauses its script for the specified amount of seconds - the wait can also be a decimal number.",
parameterInfo : [
{ name : "seconds" }
],
returnsValue : false,
compatibleWith : function(resource) {
return true;
},
reset : function(context) {
delete context.block.endTime;
delete context.block.nextIP;
},
tick : function(context) {
var now = Date.now();
var endTime = context.block.hasOwnProperty("endTime") ? context.block.endTime : null;
if (endTime === null) {
console.log("wait");
var ip = context.blockScript.index;
var delay = context.blockScript.nextArgument();
context.block.nextIP = context.blockScript.index + 1;
context.blockScript.index = ip;
endTime = now + delay * 1000;
context.block.endTime = endTime;
}
if (now >= endTime) {
context.blockScript.index = context.block.nextIP;
this.reset(context);
} else {
context.blockScript.yield = true;
}
}
},
{
blockType : "wait_until",
description : "The block pauses its script until the specified boolean condition is true.",
parameterInfo : [
{ name : "condition" }
],
returnsValue : false,
compatibleWith : function(resource) {
return true;
},
reset : function(context) {
},
tick : function(context) {
var ip = context.blockScript.index;
var condition = truthy(context.blockScript.nextArgument());
console.log("wait_until " + condition);
if (condition) {
context.blockScript.nextBlock();
} else {
context.blockScript.index = ip;
}
context.blockScript.yield = true;
}
},
//////////////////////////////////////////////////////////////
// Sensing Blocks
{
blockType : "touching",
description : "The block checks if its sprite is touching the mouse-pointer (use the string 'mouse_pointer'), " +
"edge (use the string 'edge'), or another sprite (a Reporter block holding the sprite's name can be " +
"used). If the sprite is touching the selected object, the block returns true; if it is not, it " +
"returns false.",
parameterInfo : [
{ name : "object" }
],
returnsValue : true,
compatibleWith : function(resource) {
return true;
},
tick : function(context) {
var result = false;
var obj = context.blockScript.nextArgument();
var sprite = context.resource;
var bitmap = null;
var costume = sprite._getCurrentCostumeObject();
if (costume !== null) {
bitmap = costume.getEaselBitmap();
}
if (obj === "mouse-pointer") {
// This should return true even if the object is hidden
if (bitmap !== null) {
var spritePoint = bitmap.globalToLocal(_easelStage.mouseX, _easelStage.mouseY);
result = bitmap.hitTest(spritePoint.x, spritePoint.y);
}
} else if (obj === "edge") {
// This should return true even if the object is hidden
// TODO: Implement this
} else {
// assume a sprite name.
var otherSprite = ScratchPod_this.getResourceByName(obj);
if (otherSprite === null) {
console.log("No sprite found by the name of '" + obj + "'.");
result = false;
} else {
var otherBitmap = null;
var otherCostume = otherSprite._getCurrentCostumeObject();
if (otherCostume !== null) {
otherBitmap = otherCostume.getEaselBitmap();
}
if (bitmap !== null && otherBitmap !== null) {
result = ndgmr.checkPixelCollision(bitmap, otherBitmap) ? true : false;
}
}
// Should not return true if either sprite is hidden
}
console.log("touching " + obj + " == " + result);
return result;
}
},
//////////////////////////////////////////////////////////////
// Operators Blocks
{
blockType : "equals",
description : "The block checks if the first value is equal to the other value. If the values are equal, " +
"the block returns true; if not, false.",
parameterInfo : [
{ name : "firstValue" },
{ name : "secondValue" }
],
returnsValue : true,
compatibleWith : function(resource) {
return true;
},
tick : function(context) {
var firstValue = context.blockScript.nextArgument();
var secondValue = context.blockScript.nextArgument();
var result = (firstValue.toString() === secondValue.toString()).toString();
console.log("equals " + firstValue + " " + secondValue + " == " + result);
return result;
}
},
{
blockType : "greater",
description : "The block checks if the first value is greater than the second value. If it is greater, the " +
"block returns true; if not, it returns false.",
parameterInfo : [
{ name : "firstValue" },
{ name : "secondValue" }
],
returnsValue : true,
compatibleWith : function(resource) {
return true;
},
tick : function(context) {
var firstValue = context.blockScript.nextArgument();
var secondValue = context.blockScript.nextArgument();
var result = (Number(firstValue) > Number(secondValue)).toString();
console.log("greater " + firstValue + " " + secondValue + " == " + result);
return result;
}
},
{
blockType : "join",
description : "concatenates, or \"links\" the two values together and reports the result - for example, " +
"if \"hello\" and \"world\" were put in the block, it would report \"helloworld\". ",
parameterInfo : [
{ name : "firstValue" },
{ name : "secondValue" }
],
returnsValue : true,
compatibleWith : function(resource) {
return true;
},
tick : function(context) {
var firstValue = context.blockScript.nextArgument();
var secondValue = context.blockScript.nextArgument();
var result = String(firstValue) + String(secondValue);
console.log("join " + firstValue + " " + secondValue + " == " + result);
return result;
}
},
{
blockType : "less",
description : "The block checks if the first value is less than the second value. If it is less, the " +
"block returns true; if not, it returns false.",
parameterInfo : [
{ name : "firstValue" },
{ name : "secondValue" }
],
returnsValue : true,
compatibleWith : function(resource) {
return true;
},
tick : function(context) {
var firstValue = context.blockScript.nextArgument();
var secondValue = context.blockScript.nextArgument();
var result = (Number(firstValue) < Number(secondValue)).toString();
console.log("less " + firstValue + " " + secondValue + " == " + result);
return result;
}
},
{
blockType : "not",
description : "The block checks if the boolean inside it is false - if it is false, the block returns " +
"true; if the condition is true, it returns false.",
parameterInfo : [
{ name : "value" }
],
returnsValue : true,
compatibleWith : function(resource) {
return true;
},
tick : function(context) {
var value = context.blockScript.nextArgument();
var result = String(!truthy(value));
console.log("not " + value + " == " + result);
return result;
}
},
{
blockType : "random_from_to",
description : "picks a psuedorandom number ranging from the first given number to the second, including " +
"both endpoints. If both numbers have no decimals, it will report a whole number. For example, " +
"if a 1 and a 3 were imputed, the block could return a 1, 2 or 3. If one of the numbers has a decimal " +
"point, even .0, it reports a number with a decimal.",
parameterInfo : [
{ name : "from" },
{ name : "to" }
],
returnsValue : true,
compatibleWith : function(resource) {
return true;
},
tick : function(context) {
var from = context.blockScript.nextArgument();
var to = context.blockScript.nextArgument();
var floatingPoint = (String(from).indexOf('.') !== -1) || (String(to).indexOf('.') !== -1);
from = Number(from);
to = Number(to);
var result;
if (floatingPoint) {
// return floating-point number
result = from + Math.random() * (to - from);
} else {
// return int number
result = Math.floor(from + Math.random() * (to - from + 1));
}
console.log("random_from_to " + from + " to " + to + " == " + result);
return result;
}
},
//////////////////////////////////////////////////////////////
// Data Blocks
{
blockType : "add_to",
description : "Adds an item to the specified list, the item containing the given text.",
parameterInfo : [
{ name : "value" },
{ name : "listVariable" }
],
returnsValue : false,
compatibleWith : function(resource) {
return resource.resourceType === "sprite" || resource.resourceType === "stage";
},
tick : function(context) {
var resource = context.resource;
var value = context.blockScript.nextArgument();
var listVariable = context.blockScript.nextArgument();
if (resource.resourceType === "sprite" && resource.hasListVariable(listVariable)) {
var sprite = resource;
var list = sprite.getListVariable(listVariable);
list.add(value);
} else if (ScratchPod_this.hasListVariable(listVariable)) {
var list = ScratchPod_this.getListVariable(listVariable);
list.add(value);
} else {
throw new Error("List variable '" + listVariable + "' is not defined.");
}
context.blockScript.nextBlock();
console.log("add_to " + value + " " + listVariable);
}
},
{
blockType : "change_by",
description : "The block will change the specified variable by the given amount. If the variable is a " +
"string and not a number, the variable will be set to the amount that the block was supposed to " +
"change the variable by",
parameterInfo : [
{ name : "variable" },
{ name : "delta" }
],
returnsValue : false,
compatibleWith : function(resource) {
return resource.resourceType === "sprite" || resource.resourceType === "stage";
},
tick : function(context) {
var resource = context.resource;
var variable = context.blockScript.nextArgument();
var delta = context.blockScript.nextArgument();
if (typeof(delta) !== "number") {
throw new Error("For change_by block, delta must be a number.");
}
var oldValue;
var newValue;
if (resource.resourceType === "sprite" && resource.hasVariable(variable)) {
var sprite = resource;
oldValue = sprite.getVariable(variable);
if (typeof(oldValue) === "string") {
oldValue = 0;
}
newValue = oldValue + delta;
sprite.setVariable(variable, newValue);
} else if (ScratchPod_this.hasVariable(variable)) {
oldValue = ScratchPod_this.getVariable(variable);
if (typeof(oldValue) === "string") {
oldValue = 0;
}
newValue = oldValue + delta;
ScratchPod_this.setVariable(variable, newValue);
} else {
throw new Error("Variable '" + variable + "' is not defined.");
}
context.blockScript.nextBlock();
console.log("change_by " + variable + " " + oldValue + " + " + delta + " == " + newValue);
}
},
{
blockType : "delete_of",
description : "Delete the item at the given index (1-based), the last item (pass in the word 'last'), or " +
"all items (pass in the word 'all') of the specified list depending on the option selected.",
parameterInfo : [
{ name : "what" },
{ name : "listVariable" }
],
returnsValue : false,
compatibleWith : function(resource) {
return resource.resourceType === "sprite" || resource.resourceType === "stage";
},
tick : function(context) {
var resource = context.resource;
var what = context.blockScript.nextArgument();
var listVariable = context.blockScript.nextArgument();
var list = null;
if (resource.resourceType === "sprite" && resource.hasListVariable(listVariable)) {
var sprite = resource;
list = sprite.getListVariable(listVariable);
} else if (ScratchPod_this.hasListVariable(listVariable)) {
list = ScratchPod_this.getListVariable(listVariable);
} else {
throw new Error("List variable '" + listVariable + "' is not defined.");
}
if (list !== null) {
if (what === "all") {
list.deleteAll();
} else if (what === "last") {
if (list.length() > 0) {
list.deleteAt(list.length() - 1);
}
} else {
var index = Number(what) - 1;
if (index >= 0 && index <= list.length()) {
list.deleteAt(index);
}
}
}
context.blockScript.nextBlock();
console.log("delete_of " + what + " " + listVariable);
}
},
{
blockType : "hide_list",
description : "Hides the specified list variable's Stage monitor.",
parameterInfo : [
{ name : "listVariable" }
],
returnsValue : false,
compatibleWith : function(resource) {
return resource.resourceType === "sprite" || resource.resourceType === "stage";
},
tick : function(context) {
var resource = context.resource;
var listVariable = context.blockScript.nextArgument();
if (resource.resourceType === "sprite" && resource.hasListVariable(listVariable)) {
var sprite = resource;
sprite.showListVariable(listVariable, false);
} else if (ScratchPod_this.hasListVariable(listVariable)) {
ScratchPod_this.showListVariable(listVariable, false);
} else {
throw new Error("List variable '" + listVariable + "' is not defined.");
}
context.blockScript.nextBlock();
console.log("hide_list " + listVariable);
}
},
{
blockType : "hide_variable",
description : "Hides the specified variable's Stage monitor.",
parameterInfo : [
{ name : "variable" }
],
returnsValue : false,
compatibleWith : function(resource) {
return resource.resourceType === "sprite" || resource.resourceType === "stage";
},
tick : function(context) {
var resource = context.resource;
var variable = context.blockScript.nextArgument();
if (resource.resourceType === "sprite" && resource.hasVariable(variable)) {
var sprite = resource;
sprite.showVariable(variable, false);
} else if (ScratchPod_this.hasVariable(variable)) {
ScratchPod_this.showVariable(variable, false);
} else {
throw new Error("Variable '" + variable + "' is not defined.");
}
context.blockScript.nextBlock();
console.log("hide_variable " + variable);
}
},
{
blockType : "item_of",
description : "Reports the contents of the specified item on a list. Pass a 1-based number or the string " +
"'last' to get the value of the last item or 'random' to get the value of a random item.",
parameterInfo : [
{ name : "what" },
{ name : "listVariable" }
],
returnsValue : true,
compatibleWith : function(resource) {
return resource.resourceType === "sprite" || resource.resourceType === "stage";
},
tick : function(context) {
var resource = context.resource;
var what = context.blockScript.nextArgument();
var listVariable = context.blockScript.nextArgument();
var list;
if (resource.resourceType === "sprite" && resource.hasListVariable(listVariable)) {
var sprite = resource;
list = sprite.getListVariable(listVariable);
} else if (ScratchPod_this.hasListVariable(listVariable)) {
list = ScratchPod_this.getListVariable(listVariable);
} else {
throw new Error("List variable '" + listVariable + "' is not defined.");
}
var result;
if (what === "last") {
result = list.getAt(list.length() - 1);
} else if (what === "random") {
var index = Math.floor(Math.random() * list.length());
result = list.getAt(index);
} else {
var index = Number(what) - 1;
result = list.getAt(index);
}
console.log("item_of " + what + " " + listVariable + " == " + result);
return result;
}
},
{
blockType : "length_of",
description : "Reports how many items a list contains",
parameterInfo : [
{ name : "listVariable" }
],
returnsValue : true,
compatibleWith : function(resource) {
return resource.resourceType === "sprite" || resource.resourceType === "stage";
},
tick : function(context) {
var resource = context.resource;
var listVariable = context.blockScript.nextArgument();
var result;
if (resource.resourceType === "sprite" && resource.hasListVariable(listVariable)) {
var sprite = resource;
result = sprite.getListVariable(listVariable).length();
} else if (ScratchPod_this.hasListVariable(listVariable)) {
result = ScratchPod_this.getListVariable(listVariable).length();
} else {
throw new Error("List variable '" + listVariable + "' is not defined.");
}
console.log("length_of " + listVariable + " == " + result);
return result;
}
},
{
blockType : "set_to",
description : "The block will set the specified variable to the given value: a string or number",
parameterInfo : [
{ name : "variable" },
{ name : "value" }
],
returnsValue : false,
compatibleWith : function(resource) {
return resource.resourceType === "sprite" || resource.resourceType === "stage";
},
tick : function(context) {
var resource = context.resource;
var variable = context.blockScript.nextArgument();
var value = context.blockScript.nextArgument();
if (resource.resourceType === "sprite" && resource.hasVariable(variable)) {
var sprite = resource;
sprite.setVariable(variable, value);
} else if (ScratchPod_this.hasVariable(variable)) {
ScratchPod_this.setVariable(variable, value);
} else {
throw new Error("Variable '" + variable + "' is not defined.");
}
context.blockScript.nextBlock();
console.log("set_to " + variable + " '" + value + "'");
}
},
{
blockType : "show_list",
description : "Shows the specified list's Stage monitor.",
parameterInfo : [
{ name : "listVariable" }
],
returnsValue : false,
compatibleWith : function(resource) {
return resource.resourceType === "sprite" || resource.resourceType === "stage";
},
tick : function(context) {
var resource = context.resource;
var variable = context.blockScript.nextArgument();
if (resource.resourceType === "sprite" && resource.hasListVariable(variable)) {
var sprite = resource;
sprite.showListVariable(variable, true);
} else if (ScratchPod_this.hasListVariable(variable)) {
ScratchPod_this.showListVariable(variable, true);
} else {
throw new Error("List variable '" + variable + "' is not defined.");
}
context.blockScript.nextBlock();
console.log("show_list " + variable);
}
},
{
blockType : "show_variable",
description : "Shows the specified variable's Stage monitor.",
parameterInfo : [
{ name : "variable" }
],
returnsValue : false,
compatibleWith : function(resource) {
return resource.resourceType === "sprite" || resource.resourceType === "stage";
},
tick : function(context) {
var resource = context.resource;
var variable = context.blockScript.nextArgument();
if (resource.resourceType === "sprite" && resource.hasVariable(variable)) {
var sprite = resource;
sprite.showVariable(variable, true);
} else if (ScratchPod_this.hasVariable(variable)) {
ScratchPod_this.showVariable(variable, true);
} else {
throw new Error("Variable '" + variable + "' is not defined.");
}
context.blockScript.nextBlock();
console.log("show_variable " + variable);
}
},
{
blockType : "variable",
description : "Returns the value of the variable with the provided name.",
parameterInfo : [
{ name : "variable" }
],
returnsValue : true,
compatibleWith : function(resource) {
return resource.resourceType === "sprite" || resource.resourceType === "stage";
},
tick : function(context) {
var result;
var resource = context.resource;
var variable = context.blockScript.nextArgument();
if (resource.resourceType === "sprite" && resource.hasVariable(variable)) {
var sprite = resource;
result = sprite.getVariable(variable);
} else if (ScratchPod_this.hasVariable(variable)) {
result = ScratchPod_this.getVariable(variable);
} else {
throw new Error("Variable '" + variable + "' is not defined.");
}
console.log("variable " + variable + " == " + result);
return result;
}
}
];
};
/**
* Part of the Pod standard interface - return information about the resources provided
* by the scratch pod.
*
* @public
* @instance
* @method getResourceTypes
* @memberof PodJS.ScratchPod
* @return {object[]} One info object for each resource.
*/
this.getResourceTypes = function() {
return [
{
resourceType : "sprite"
},
{
resourceType : "stage"
}
];
};
/**
* @private
* @instance
* @class AudioFeature
* @classdesc Equips a resource with the capability to play audio. Each AudioFeature can only play one sound at a time and
* handles loading, playing and stopping of the audio.
*
* @param {string} prefix The prefix to use to keep audio resources unique.
*/
var AudioFeature = function(prefix) {
var AudioFeature_this = this;
// Sound currently being played
var _currentSound = null;
/**
* True if the sound for this sprite / stage is done playing, or false if actively playing.
*
* @member
* @memberof PodJS.ScratchPod.AudioFeature
*/
this.soundComplete = true;
this.loadSound = function(name, src) {
var audioId = prefix + "::" + name;
if (_audioFiles.hasOwnProperty(audioId)) {
throw new Error("Already have a sound resource called '" + name + "'");
}
var audioInfo = Object.create(AudioInfo);
audioInfo.prefix = prefix;
audioInfo.name = name;
var audioSrc = _resourcesPathPrefix + src;
// Add .ogg version of sound as well, for Firefox
var mp3Index = src.indexOf(".mp3");
if (mp3Index !== -1) {
audioSrc += "|" + _resourcesPathPrefix + src.substring(0, mp3Index) + ".ogg";
}
audioInfo.src = audioSrc;
console.log(audioSrc);
_audioFiles[audioId] = audioInfo;
createjs.Sound.registerSound(audioInfo.src, audioId);
};
var _handleComplete = function(event) {
this.soundComplete = true;
};
this.playSound = function(name) {
var audioId = prefix + "::" + name;
this.stopAllSounds();
if (_audioFiles.hasOwnProperty(audioId) && _audioFiles[audioId].loaded) {
this.soundComplete = false;
_currentSound = createjs.Sound.play(audioId);
_currentSound.addEventListener("complete", createjs.proxy(_handleComplete, AudioFeature_this));
} else {
console.log("Warning: Sound '" + audioId + "' not loaded yet, so not playing.");
}
};
this.stopAllSounds = function() {
if (_currentSound !== null) {
_currentSound.stop();
_currentSound.removeAllEventListeners();
_currentSound = null;
this.soundComplete = true;
}
};
};
/**
* Internal factory method for new Sprite class instances.
*
* @private
* @instance
* @method createSprite
* @param parentObject The resource super-object from PodJS
* @param spriteName The name of the sprite
* @return A new Sprite instance
*/
var createSprite = function(parentObject, spriteName) {
// Private Costume class
var Costume = function(src, scale) {
scale = scale || 1.0;
var _easelBitmap;
this.getEaselBitmap = function() {
return _easelBitmap;
};
var construct = function() {
var img = new Image();
img.onload = function() {
_easelBitmap.x = -img.width * _easelBitmap.scaleX / 2;
_easelBitmap.y = -img.height * _easelBitmap.scaleY / 2;
};
img.src = src;
_easelBitmap = new createjs.Bitmap(img);
_easelBitmap.scaleX = scale;
_easelBitmap.scaleY = scale;
};
construct();
};
/**
* @class PodJS.ScratchPod.Sprite
* @classdesc Model for the Scratch Sprite class
*/
var Sprite = function(spriteName) {
var Sprite_this = this;
var _costumes = {};
var _variables = {};
var _listVariables = {};
var _currentCostume = null;
var _show = true;
var _x = 0;
var _y = 0;
var _direction = 90;
var _audio = new AudioFeature(spriteName);
/**
* Converts direction (which is in Scratch format, 0 = up, -90 = left, 90 = right, 180 = down) to normalized
* degrees (0 = right, 90 = up, 180 = left, 270 = down).
*/
var _normalizedDirection = function() {
return ((-_direction) + 90 + 360) % 360;
};
/**
* The last time this Sprite was clicked, or 0 if never clicked, in millis since epoch.
*
* @instance
* @member {number} lastClickTime
* @memberof PodJS.ScratchPod.Sprite
*/
this.lastClickTime = 0;
/**
* Create a new variable with the given name.
*
* @instance
* @method createVariable
* @param {string} name the name of the variable
* @memberof PodJS.ScratchPod.Sprite
*/
this.createVariable = function(name) {
if (_variables.hasOwnProperty(name)) {
throw "Sprite already has a variable called '" + name + "'";
}
var variable = new Variable(spriteName, name);
_autoPositionVariable(variable);
_variables[name] = variable;
return this;
};
/**
* Create a new list variable with the given name.
*
* @instance
* @method createListVariable
* @param {string} name the name of the list variable
* @memberof PodJS.ScratchPod.Sprite
*/
this.createListVariable = function(name) {
if (_listVariables.hasOwnProperty(name)) {
throw "Sprite already has a list variable called '" + name + "'";
}
var listVariable = new ScratchPod_this.ListVariable(spriteName, name);
_autoPositionListVariable(listVariable);
_listVariables[name] = listVariable;
return this;
};
/**
* Set the value of the given variable to the given value.
*
* @instance
* @method setVariable
* @param {string} name the name of the variable
* @param {number|string} value the value to set the variable to
* @memberof PodJS.ScratchPod.Sprite
*/
this.setVariable = function(name, value) {
if (!_variables.hasOwnProperty(name)) {
throw "Sprite does not have a variable called '" + name + "'";
}
_variables[name].value = value;
return this;
};
/**
* Get the value of the given variable.
*
* @instance
* @method getVariable
* @param {string} name the name of the variable
* @return The value of the variable.
* @memberof PodJS.ScratchPod.Sprite
*/
this.getVariable = function(name) {
if (!_variables.hasOwnProperty(name)) {
throw "Sprite does not have a variable called '" + name + "'";
}
return _variables[name].value;
};
/**
* Get the given list variable.
*
* @instance
* @method getListVariable
* @param {string} name the name of the list variable
* @return The list variable.
* @memberof PodJS.ScratchPod.Sprite
*/
this.getListVariable = function(name) {
if (!_listVariables.hasOwnProperty(name)) {
throw "Sprite does not have a list variable called '" + name + "'";
}
return _listVariables[name];
};
/**
* Returns the names of all the variables for this sprite.
*
* @instance
* @method getVariableNames
* @return {string[]} The names of all the variables
* @memberof PodJS.ScratchPod.Sprite
*/
this.getVariableNames = function() {
var result = [];
for (var name in _variables) {
if (_variables.hasOwnProperty(name)) {
result.push(name);
}
}
return result;
};
/**
* Returns true if the variable exists for this sprite, or false if not.
*
* @instance
* @method hasVariable
* @param {string} name the name of the variable
* @return {boolean} true if the variable exists or false if not.
* @memberof PodJS.ScratchPod.Sprite
*/
this.hasVariable = function(name) {
return _variables.hasOwnProperty(name);
};
/**
* Returns true if the list variable exists for this sprite, or false if not.
*
* @instance
* @method hasListVariable
* @param {string} name the name of the list variable
* @return {boolean} true if the list variable exists or false if not.
* @memberof PodJS.ScratchPod.Sprite
*/
this.hasListVariable = function(name) {
return _listVariables.hasOwnProperty(name);
};
/**
* Sets whether this variable is shown on the stage, and the location at which it is shown.
*
* @instance
* @method showVariable
* @param {string} name the name of the variable
* @param {boolean} shown true if the variable is to be shown, or false if not. Optional, defaults to true.
* @param {number} x x position of the variable on the stage (optional, defaults to 0)
* @param {number} y y position of the variable on the stage (optional, defaults to 0)
* @memberof PodJS.ScratchPod.Sprite
*/
this.showVariable = function(name, shown, x, y) {
if (typeof(shown) === "undefined") {
shown = true;
}
if (typeof(x) !== "undefined") {
_variables[name].x = x;
}
if (typeof(y) !== "undefined") {
_variables[name].y = y;
}
if (!_variables.hasOwnProperty(name)) {
throw "Sprite does not have a variable called '" + name + "'";
}
_variables[name].shown = shown;
return this;
};
/**
* Sets whether this list variable is shown on the stage, and the location at which it is shown.
*
* @instance
* @method showListVariable
* @param {string} name the name of the list variable
* @param {boolean} shown true if the list variable is to be shown, or false if not. Optional, defaults to true.
* @param {number} x x position of the list variable on the stage (optional, defaults to 0)
* @param {number} y y position of the list variable on the stage (optional, defaults to 0)
* @param {number} width width Width of the box to show (optional, defaults to enough width to show small ints)
* @param {number} height height of the box to show (optional, defaults to 8 rows worth of pixels)
* @memberof PodJS.ScratchPod.Sprite
*/
this.showListVariable = function(name, shown, x, y, width, height) {
if (typeof(shown) === "undefined") {
shown = true;
}
if (typeof(x) !== "undefined" && x !== null) {
_listVariables[name].x = x;
}
if (typeof(y) !== "undefined" && y !== null) {
_listVariables[name].y = y;
}
if (typeof(width) !== "undefined") {
_listVariables[name].width = width;
}
if (typeof(height) !== "undefined") {
_listVariables[name].height = height;
}
if (!_listVariables.hasOwnProperty(name)) {
throw "Sprite does not have a list variable called '" + name + "'";
}
_listVariables[name].shown = shown;
return this;
};
/**
* Load and register a new costume for this sprite.
*
* @instance
* @method loadCostume
* @param {string} name the name of the costume
* @param {string} src the href of where to find the image of the costume.
* @param {number} scale (optional) - if specified, scale the image up or down by this amount, defaults to 1.0.
* @memberof PodJS.ScratchPod.Sprite
*/
this.loadCostume = function(name, src, scale) {
scale = scale || 1.0;
if (_costumes.hasOwnProperty(name)) {
throw "Sprite already has a costume called '" + name + "'";
}
var costume = new Costume(_resourcesPathPrefix + src, scale);
var bitmap = costume.getEaselBitmap();
bitmap.addEventListener("click", function(event) {
Sprite_this.lastClickTime = Date.now();
});
_costumes[name] = costume;
if (_currentCostume === null) {
this.setCostume(name);
}
return this;
};
/**
* Load and register an audio file for this sprite.
*
* @instance
* @method loadSound
* @param {string} name the name of the audio
* @param {string} src the href of where to find the audio file.
* @memberof PodJS.ScratchPod.Sprite
*/
this.loadSound = function(name, src) {
_audio.loadSound(name, src);
return this;
};
/**
* Stop playing any sounds and play a new sound.
*
* @instance
* @method playSound
* @param {string} name the id of the sound to play
* @memberof PodJS.ScratchPod.Sprite
*/
this.playSound = function(name) {
_audio.playSound(name);
return this;
};
/**
* Stop playing any sounds for this sprite.
*
* @instance
* @method stopAllSounds
* @memberof PodJS.ScratchPod.Sprite
*/
this.stopAllSounds = function() {
_audio.stopAllSounds();
return this;
};
/**
* Change costume for this Sprite
*
* @instance
* @method setCostume
* @param {string} name the name of the costume to change into, matching the name
* provided to loadCostume.
* @memberof PodJS.ScratchPod.Sprite
*/
this.setCostume = function(name) {
if (!_costumes.hasOwnProperty(name)) {
throw "Sprite does not have a costume called '" + name + "'";
}
if (name !== _currentCostume) {
var index = -1;
if (_currentCostume !== null) {
// Remove from stage
var easelBitmap = _costumes[_currentCostume].getEaselBitmap();
index = _easelStage.getChildIndex(easelBitmap);
if (index !== -1) {
// Unfortuantely, there's no replaceAt, so we remove and re-add at a cost of 2n.
_easelStage.removeChildAt(index);
}
}
var newCostume = _costumes[name];
var newBitmap = newCostume.getEaselBitmap();
newBitmap.visible = _show;
if (index !== -1) {
// Keep the same index
_easelStage.addChildAt(newBitmap, index);
} else {
_easelStage.addChild(newBitmap);
}
_currentCostume = name;
}
return this;
};
/**
* Return the currently active costume object for this Sprite
* <p>
* This method is intended for internal use and may change in the future.
*
* @instance
* @method _getCurrentCostumeObject
* @return {PodJS.ScratchPod.Costume}
* @memberof PodJS.ScratchPod.Sprite
*/
this._getCurrentCostumeObject = function() {
return _costumes[_currentCostume];
};
/**
* Changes whether this sprite is being shown or not.
*
* @instance
* @method setShown
* @param {boolean} show true if this is to be shown or false if not.
* @memberof PodJS.ScratchPod.Sprite
*/
this.setShown = function(show) {
_show = show;
if (_currentCostume !== null) {
var costume = _costumes[_currentCostume];
var easelBitmap = costume.getEaselBitmap();
easelBitmap.visible = show;
}
return this;
};
/**
* Shortcut for setShown(false)
*
* @instance
* @method hide
* @memberof PodJS.ScratchPod.Sprite
*/
this.hide = function() {
return this.setShown(false);
};
/**
* Shortcut for setShown(true)
*
* @instance
* @method show
* @memberof PodJS.ScratchPod.Sprite
*/
this.show = function() {
return this.setShown(true);
};
/**
* Returns whether this sprite is being shown or not.
*
* @instance
* @method isShown
* @return {boolean} true if this is to be shown or false if not.
* @memberof PodJS.ScratchPod.Sprite
*/
this.isShown = function() {
return _show;
};
/**
* Gets called periodically by the environment when the next action is to take place.
*
* @method tick
* @memberof PodJS.ScratchPod.Sprite
* @instance
*/
this.tick = function() {
// Update appearance of Sprite on the easel
if (_currentCostume !== null) {
var costume = _costumes[_currentCostume];
var bitmap = costume.getEaselBitmap();
bitmap.show = _show;
bitmap.regX = Math.floor(bitmap.image.width / 2);
bitmap.regY = Math.floor(bitmap.image.height / 2);
bitmap.x = _x;
bitmap.y = -_y;
bitmap.rotation = _normalizedDirection();
}
};
/**
* Move this sprite the given number of steps.
*
* @method moveSteps
* @param {number} steps The number of steps to move the Sprite, in the forward direction.
* @memberof PodJS.ScratchPod.Sprite
* @instance
*/
this.moveSteps = function(steps) {
var rad = _normalizedDirection() * Math.PI / 180;
var dx = Math.cos(rad);
var dy = Math.sin(rad);
_x += dx * steps;
_y -= dy * steps;
return this;
};
/**
* Move this sprite the given number of steps in the X and Y directions.
*
* @method translate
* @param {number} x The number of steps to move the Sprite, in the x direction.
* @param {number} y The number of steps to move the Sprite, in the y direction.
* @memberof PodJS.ScratchPod.Sprite
* @instance
*/
this.translate = function(x, y) {
_x += x;
_y += y;
return this;
};
/**
* Returns the current direction of the sprite, in degrees normalized to 0 (inclusive) to 360 (exclusive).
*
* @method getDirection
* @memberof PodJS.ScratchPod.Sprite
* @instance
*/
this.getDirection = function() {
return _direction;
};
/**
* Point this sprite in the direction given
*
* @method setDirection
* @param {number} degrees The number of steps to move the Sprite, in the x direction.
* @memberof PodJS.ScratchPod.Sprite
* @instance
*/
this.setDirection = function(degrees) {
_direction = degrees;
return this;
};
/**
* Sets the sprite's X and Y position to the specified amounts.
*
* @method go_xy
* @param {number} x The x position, in pixels.
* @param {number} y The y position, in pixels.
* @memberof PodJS.ScratchPod.Sprite
* @instance
*/
this.goXY = function(x, y) {
_x = x;
_y = y;
return this;
};
};
Sprite.prototype = parentObject;
var result = new Sprite(spriteName);
result.register(result);
return result;
};
/**
* Internal method to create the model class that controls the stage.
* <p>
* The interfaces to these classes are all internal - the official public way to access these classes
* is through the blocks returned by this pod.
*
* @private
* @instance
* @method createStage
* @param parentObject The resource super-object from PodJS
* @memberOf PodJS.ScratchPod
*/
var createStage = function(parentObject) {
// Private Backdrop class
var Backdrop = function(src) {
var _easelBitmap;
this.getEaselBitmap = function() {
return _easelBitmap;
};
var construct = function() {
var img = new Image();
img.onload = function() {
_easelBitmap.setTransform(-img.width / 2, -img.height / 2);
};
img.src = src;
_easelBitmap = new createjs.Bitmap(img);
};
construct();
};
// Private Stage class
var Stage = function() {
var _currentBackdrop = null;
var _audio = new AudioFeature("");
var _backdrops = {};
this.loadBackdrop = function(name, src) {
if (_backdrops.hasOwnProperty(name)) {
throw "Stage already has a backdrop called '" + name + "'";
}
_backdrops[name] = new Backdrop(_resourcesPathPrefix + src);
return this;
};
this.switchBackdrop = function(name) {
// Assert that the backdrop with the given name exists
var newBackdrop = _backdrops[name];
if (typeof(newBackdrop) === "undefined") {
throw "Stage does not contain a backdrop with name '" + name + "'";
}
var newBitmap = newBackdrop.getEaselBitmap();
// Remove previous bitmap from stage (backdrop bitmap is always at index 0)
if (_easelStage.getNumChildren() > 0) {
_easelStage.removeChildAt(0);
}
// Add new bitmap to stage
newBitmap.x = -_canvas.width / 2;
newBitmap.y = -_canvas.height / 2;
_easelStage.addChildAt(newBitmap, 0);
_easelStage.update();
_currentBackdrop = newBackdrop;
return this;
};
/**
* Load and register an audio file for the stage.
*
* @instance
* @method loadSound
* @param {string} name the name of the audio
* @param {string} src the href of where to find the audio file.
* @memberof PodJS.ScratchPod.Stage
*/
this.loadSound = function(name, src) {
_audio.loadSound(name, src);
return this;
};
/**
* Stop playing any sounds for the stage and play a new sound.
*
* @instance
* @method playSound
* @param {string} name the id of the sound to play
* @memberof PodJS.ScratchPod.Stage
*/
this.playSound = function(name) {
_audio.playSound(name);
return this;
};
/**
* Stop playing any sounds for the stage.
*
* @instance
* @method stopAllSounds
* @memberof PodJS.ScratchPod.Stage
*/
this.stopAllSounds = function() {
_audio.stopAllSounds();
return this;
};
};
Stage.prototype = parentObject;
var result = new Stage();
// Set default backdrop:
result.loadBackdrop("backdrop1", "img/blank.png");
result.switchBackdrop("backdrop1");
// Complete registration
result.register(result);
return result;
};
/**
* Part of the Pod standard interface - called when the environment or an application wishes to create a
* new resource of the given type.
*
* @method newResource
* @memberof PodJS.ScratchPod
* @instance
* @param {string} resourceType The type of resource to be created (e.g. "sprite"). Must be one of the resource types
* returned by {@link getResourceTypes}.
* @param {string} resourceName The name of the resource to create. This name must be unique for the type of resource so that
* the resource can be later retrieved and, if necessary, deleted.
* @param {object} [options] Set of parameters to be used when creating the resource.
* @returns {PodJS.Pod#Resource} Returns the instance of the resource.
* @throws {Error} If the resource type provided was not valid. This checking is handled by {@link PodJS.Pod#newResourceClass}.
* @throws {Error} If a resource of this type already exists with the given name. This checking is handled by
* {@link PodJS.Pod#newResourceClass}.
*/
this.newResource = function(resourceType, resourceName, options) {
var result;
var resourceBase = this.newResourceClass(resourceType, resourceName, options);
var resource = Object.create(resourceBase);
if (resourceType === "sprite") {
result = createSprite(resource, resourceName);
} else if (resourceType === "stage") {
result = createStage(resource);
} else {
result = resource;
}
return result;
};
/**
* Convenience method to create a new sprite resource.
*
* @method newSprite
* @memberof PodJS.ScratchPod
* @instance
* @param {string} name The name of the sprite to create. This name must be unique for all sprites.
* @returns {PodJS.ScratchPod.Sprite} Returns the instance of the Sprite.
* @throws {Error} If a resource of this type already exists with the given name. This checking is handled by
* {@link PodJS.Pod#newResourceClass}.
*/
this.newSprite = function(name) {
return this.newResource("sprite", name);
};
/**
* Part of the Pod standard interface - called when a {@link PodJS.ScriptBuilder} wishes to create a
* new instance of a block.
*
* @method newBlock
* @memberof PodJS.ScratchPod
* @instance
* @param {string} blockType The type of block to be created (e.g. "gotoXY"). Must be one of the block types
* returned by {@link PodJS.Pod#getBlockTypes}.
* @param {PodJS.Block#Resource} resource The resource this block is to be bound to.
* @param {PodJS.Script} script The script this block is bound to.
* @returns {PodJS.Pod#Block} The instance of the block.
* @throws {Error} If the block type provided was not one of the valid block types returned by {@link PodJS.Pod#getBlockTypes}.
* This check is performed by {@link PodJS.Pod#newBlockClass}.
* @throws {Error} If the block to be returned would not be compatible with the resource provided. This check
* must be performed by the subclass.
*/
this.newBlock = function(blockType, resource, script) {
var blockClass = this.newBlockClass(blockType, resource, script);
var block = Object.create(blockClass);
return block;
};
/**
* Returns the stage.
*
* @method getStage
* @memberof PodJS.ScratchPod
* @instance
* @returns {PodJS.ScratchPod.Stage} The stage
*/
this.getStage = function() {
return _stage;
};
/**
* Stop playing all sounds being played by scratch resources.
*
* @method stopAllSounds
* @memberof PodJS.ScratchPod
* @instance
*/
this.stopAllSounds = function() {
var resources = ScratchPod_this.getAllResources()
for (var resType in resources) {
if (resources.hasOwnProperty(resType)) {
var resourceByType = resources[resType];
for (var resName in resourceByType) {
if (resourceByType.hasOwnProperty(resName)) {
var resource = resourceByType[resName];
if (resource.hasOwnProperty("stopAllSounds")) {
resource.stopAllSounds();
}
}
}
}
}
};
/**
* Sets the directory that all resources are relative to.
*
* @method resourcesPath
* @memberof PodJS.ScratchPod
* @param {string} path The path prefix for all resources loaded by this pod. If it does not end in '/', a '/' will be added.
* @instance
*/
this.setResourcesPath = function(path) {
if (path.charAt(path.length - 1) !== '/') {
path += '/';
}
_resourcesPathPrefix = path;
return ScratchPod_this;
};
/**
* Internal method to find the div, attach the canvas, and initialize createjs' easel.
*
* @private
* @instance
* @method attachToDiv
* @memberOf PodJS.ScratchPod
*/
var attachToDiv = function() {
var style = document.createElement("style");
style.innerHTML = "\n\
.podjs_scratch_list_var { \n\
z-index: 100; \n\
padding: 2px 6px; \n\
border: 1px solid #949191; \n\
background: #c1c4c7; \n\
font-weight: bold; \n\
font-family: sans-serif; \n\
border-radius: 4px; \n\
font-size: 10pt; \n\
visibility: hidden; \n\
height: 185px;\n\
} \n\
.podjs_scratch_list_value_div {\n\
height: calc(100% - 45px); \n\
overflow-y: auto; \n\
padding-right: 16px; \n\
margin-top: 10px;\n\
min-width: 100px;\n\
}\n\
.podjs_scratch_list_var_list { \n\
padding-left: 0px;\n\
margin: 0px; \n\
list-style-type: decimal; \n\
} \n\
.podjs_scratch_list_var_item { \n\
margin-left: 30px; \n\
color: #000000; \n\
padding: 0px 4px; \n\
} \n\
.podjs_scratch_list_var_item_span { \n\
background: #ee7d16; \n\
color: #ffffff; \n\
min-width: 34px; \n\
padding: 0px 4px; \n\
border: 1px solid #ffffff; \n\
border-radius: 4px; \n\
text-align: left; \n\
display: block; \n\
} \n\
.podjs_scratch_list_var_length_div { \n\
font-weight: normal;\n\
margin-top: 5px;\n\
} \n\
.podjs_scratch_var_div { \n\
z-index: 100; \n\
padding: 2px 6px; \n\
border: 1px solid #949191; \n\
background: #c1c4c7; \n\
font-weight: bold; \n\
font-family: sans-serif; \n\
border-radius: 4px; \n\
font-size: 10pt; \n\
visibility: hidden; \n\
} \n\
.podjs_scratch_var_value { \n\
margin-left: 6px; \n\
background: #ee7d16; \n\
color: #ffffff; \n\
min-width: 34px; \n\
padding: 0px 4px; \n\
border: 1px solid #ffffff; \n\
border-radius: 4px; \n\
float: right; \n\
text-align: center; \n\
} \n\
";
document.head.appendChild(style);
var divId = options.scratch_stage_div;
if (typeof(divId) === "undefined") {
divId = "stage";
}
_div = document.getElementById(divId);
if (_div === null) {
throw new Error("Could not find div with id '" + divId + "' to bind scratch stage to.");
}
_div.innerHTML = "";
var gradientCss = "background: #e6e8e8; \
background: -moz-linear-gradient(top, #ffffff 0%, #e6e8e8 100%); /* FF3.6+ */ \
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(100%,#e6e8e8)); /* Chrome,Safari4+ */ \
background: -webkit-linear-gradient(top, #ffffff 0%,#e6e8e8 100%); /* Chrome10+,Safari5.1+ */ \
background: -o-linear-gradient(top, #ffffff 0%,#e6e8e8 100%); /* Opera 11.10+ */ \
background: -ms-linear-gradient(top, #ffffff 0%,#e6e8e8 100%); /* IE10+ */ \
background: linear-gradient(top, #ffffff 0%,#e6e8e8 100%); /* W3C */ \
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#e6e8e8',GradientType=0 ); /* IE6-9 */";
_controlsDiv = document.createElement("div");
_controlsDiv.setAttribute("style", "height: 32px; width: 100%; " + gradientCss);
_redStopButton = document.createElement("img");
_redStopButton.setAttribute("style", "width: 24px; height: 24px; float: right; margin-top: 4px; margin-right: 8px;");
_redStopButton.onmouseover = function() { this.src='img/red_stop_on.png'; };
_redStopButton.onmouseout = function() {
if (_running) {
this.src='img/red_stop_off.png';
} else {
this.src='img/red_stop_on.png';
}
};
_redStopButton.onclick = function() {
_running = false;
_lastGreenFlagClickTime = 0;
_env.resetAllScripts();
_redStopButton.onmouseout();
_greenFlagButton.onmouseout();
ScratchPod_this.stopAllSounds();
};
_redStopButton.onmouseout();
_controlsDiv.appendChild(_redStopButton);
_greenFlagButton = document.createElement("img");
_greenFlagButton.setAttribute("style", "width: 24px; height: 24px; float: right; margin-top: 4px; margin-right: 8px;");
_greenFlagButton.onmouseover = function() { this.src='img/green_flag_on.png'; };
_greenFlagButton.onmouseout = function() {
if (_running) {
this.src='img/green_flag_on.png';
} else {
this.src='img/green_flag_off.png';
}
};
_greenFlagButton.onclick = function() {
_running = true;
_lastGreenFlagClickTime = Date.now();
_env.resetAllScripts();
_redStopButton.onmouseout();
_greenFlagButton.onmouseout();
};
_greenFlagButton.onmouseout();
_controlsDiv.appendChild(_greenFlagButton);
_stageDiv = document.createElement("div");
_stageDiv.setAttribute("style", "height: " + (_div.offsetHeight - 34) + "px; width: 100%;");
_div.appendChild(_controlsDiv);
_div.appendChild(_stageDiv);
// Add canvas to _stageDiv
_canvas.style.width = "100%";
_canvas.style.height = "100%";
_canvas.width = _stageDiv.offsetWidth;
_canvas.height = _stageDiv.offsetHeight;
_stageDiv.appendChild(_canvas);
// Attach createjs to canvas
_easelStage = new createjs.Stage(_canvas);
_easelStage.setTransform(_canvas.width / 2, _canvas.height / 2);
};
/**
* Gets called periodically by the environment when the next action is to take place. This is part of the standard PodJS
* interface.
*
* @method tick
* @memberof PodJS.Pod
* @instance
*/
this.tick = function() {
if (_easelStage !== null) {
_easelStage.update();
}
// TODO: This can be made much more efficient
// Update running status: check if all scripts are at the beginning. If so, virtually click stop.
var allScriptsAtZero = true;
var resources = this.getAllResources();
for (var resType in resources) {
if (resources.hasOwnProperty(resType)) {
var resourceByType = resources[resType];
for (var resName in resourceByType) {
if (resourceByType.hasOwnProperty(resName)) {
var resource = resourceByType[resName];
var scripts = resource.scripts;
for (var i = 0; i < scripts.length; i++) {
var script = scripts[i];
if (script.index !== 0) {
allScriptsAtZero = false;
}
}
}
}
}
}
if (allScriptsAtZero) {
if (_running) {
_running = false;
_redStopButton.onmouseout();
_greenFlagButton.onmouseout();
}
} else {
if (!_running) {
_running = true;
_redStopButton.onmouseout();
_greenFlagButton.onmouseout();
}
}
};
var _audioLoadHandler = function(event) {
var audioId = event.id;
if (_audioFiles.hasOwnProperty(audioId)) {
var info = _audioFiles[audioId];
info.loaded = true;
}
};
var _initializeAudio = function() {
// TODO: Initialize audio once the user touches, so this works on mobile devices.
// See http://www.createjs.com/tutorials/Mobile%20Safe%20Approach/
if (!createjs.Sound.initializeDefaultPlugins()) {
console.log("Could not initialize SoundJS. Audio may not work correctly.");
} else {
createjs.Sound.addEventListener("fileload", createjs.proxy(_audioLoadHandler, ScratchPod_this));
}
};
var construct = function() {
// Ensure createjs is loaded:
if (typeof(createjs) === "undefined") {
throw new Error("You must add <script src='js/createjs-2013.09.25.min.js'></script> " +
"to your html for pod_scratch.js to work.");
}
// Find stage div and attach to it
attachToDiv();
// Initialize stage and add a default backdrop
_stage = ScratchPod_this.newResource("stage", "stage");
_initializeAudio();
};
construct();
};
PodJS.ScratchPod.prototype = Object.create(PodJS.Pod.prototype);
PodJS.ScratchPod.constructor = PodJS.ScratchPod;
PodJS.REGISTER_POD_CLASS("scratch", PodJS.ScratchPod);