Merge branch 'develop' of https://github.com/LLK/scratch-vm into sound

# Conflicts:
#	playground/index.html
This commit is contained in:
Eric Rosenbaum 2016-10-17 17:13:43 -04:00
commit 7bd0142deb
25 changed files with 828 additions and 831 deletions

1
.gitignore vendored
View file

@ -11,6 +11,7 @@ npm-*
/dist.js
/vm.js
/vm.min.js
/playground/assets
/playground/media
/playground/vendor.js
/playground/vm.js

View file

@ -1,14 +1,14 @@
language: node_js
node_js:
- "4"
- "stable"
- '4'
- stable
sudo: false
cache:
directories:
- node_modules
before_install:
# Install the most up to date scratch-* dependencies
- rm -rf node_modules/scratch-*
install:
- npm install
- npm update
after_script:
- |
# RELEASE_BRANCHES and NPM_TOKEN defined in Travis settings panel

View file

@ -12,7 +12,8 @@
"main": "./dist.js",
"scripts": {
"prepublish": "./node_modules/.bin/webpack --bail",
"start": "make serve",
"start": "webpack-dev-server",
"build": "webpack --colors --progress",
"test": "make test",
"version": "./node_modules/.bin/json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\""
},

View file

@ -51,751 +51,6 @@
<div id="blocks"></div>
<xml id="toolbox" style="display: none">
<category name="Motion" colour="#4C97FF">
<block type="motion_movesteps">
<value name="STEPS">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="motion_turnright">
<value name="DEGREES">
<shadow type="math_number">
<field name="NUM">15</field>
</shadow>
</value>
</block>
<block type="motion_turnleft">
<value name="DEGREES">
<shadow type="math_number">
<field name="NUM">15</field>
</shadow>
</value>
</block>
<block type="motion_pointindirection">
<value name="DIRECTION">
<shadow type="math_angle">
<field name="NUM">90</field>
</shadow>
</value>
</block>
<block type="motion_pointtowards">
<value name="TOWARDS">
<shadow type="motion_pointtowards_menu">
</shadow>
</value>
</block>
<block type="motion_gotoxy">
<value name="X">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
<value name="Y">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="motion_goto">
<value name="TO">
<shadow type="motion_goto_menu">
</shadow>
</value>
</block>
<block type="motion_glidesecstoxy">
<value name="SECS">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="X">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
<value name="Y">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="motion_changexby">
<value name="DX">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="motion_setx">
<value name="X">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="motion_changeyby">
<value name="DY">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="motion_sety">
<value name="Y">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="motion_ifonedgebounce"></block>
<block type="motion_setrotationstyle">
<value name="STYLE">
<shadow type="motion_setrotationstyle_menu"></shadow>
</value>
</block>
<block type="motion_xposition"></block>
<block type="motion_yposition"></block>
<block type="motion_direction"></block>
</category>
<category name="Looks" colour="#9966FF">
<block type="looks_sayforsecs">
<value name="MESSAGE">
<shadow type="text">
<field name="TEXT">Hello!</field>
</shadow>
</value>
<value name="SECS">
<shadow type="math_number">
<field name="NUM">2</field>
</shadow>
</value>
</block>
<block type="looks_say">
<value name="MESSAGE">
<shadow type="text">
<field name="TEXT">Hello!</field>
</shadow>
</value>
</block>
<block type="looks_thinkforsecs">
<value name="MESSAGE">
<shadow type="text">
<field name="TEXT">Hmm...</field>
</shadow>
</value>
<value name="SECS">
<shadow type="math_number">
<field name="NUM">2</field>
</shadow>
</value>
</block>
<block type="looks_think">
<value name="MESSAGE">
<shadow type="text">
<field name="TEXT">Hmm...</field>
</shadow>
</value>
</block>
<block type="looks_show"></block>
<block type="looks_hide"></block>
<block type="looks_switchcostumeto">
<value name="COSTUME">
<shadow type="looks_costume"></shadow>
</value>
</block>
<block type="looks_nextcostume"></block>
<block type="looks_nextbackdrop"></block>
<block type="looks_switchbackdropto">
<value name="BACKDROP">
<shadow type="looks_backdrops"></shadow>
</value>
</block>
<block type="looks_switchbackdroptoandwait">
<value name="BACKDROP">
<shadow type="looks_backdrops"></shadow>
</value>
</block>
<block type="looks_changeeffectby">
<value name="EFFECT">
<shadow type="looks_effectmenu"></shadow>
</value>
<value name="CHANGE">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="looks_seteffectto">
<value name="EFFECT">
<shadow type="looks_effectmenu"></shadow>
</value>
<value name="VALUE">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="looks_cleargraphiceffects"></block>
<block type="looks_changesizeby">
<value name="CHANGE">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="looks_setsizeto">
<value name="SIZE">
<shadow type="math_number">
<field name="NUM">100</field>
</shadow>
</value>
</block>
<block type="looks_gotofront"></block>
<block type="looks_gobacklayers">
<value name="NUM">
<shadow type="math_integer">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="looks_costumeorder"></block>
<block type="looks_backdroporder"></block>
<block type="looks_backdropname"></block>
<block type="looks_size"></block>
</category>
<category name="Sound" colour="#D65CD6">
<block type="sound_play">
<value name="SOUND_MENU">
<shadow type="sound_sounds_option">
</shadow>
</value>
</block>
<block type="sound_playuntildone">
<value name="SOUND_MENU">
<shadow type="sound_sounds_option">
</shadow>
</value>
</block>
<block type="sound_stopallsounds"></block>
<block type="sound_playdrumforbeats">
<value name="DRUMTYPE">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="BEATS">
<shadow type="sound_beats_menu">
</shadow>
</value>
</block>
<block type="sound_restforbeats">
<value name="BEATS">
<shadow type="math_number">
<field name="NUM">0.25</field>
</shadow>
</value>
</block>
<block type="sound_playnoteforbeats">
<value name="NOTE">
<shadow type="math_number">
<field name="NUM">60</field>
</shadow>
</value>
<value name="BEATS">
<shadow type="sound_beats_menu">
</shadow>
</value>
</block>
<block type="sound_seteffectto">
<value name="EFFECT">
<shadow type="sound_effects_menu"></shadow>
</value>
<value name="VALUE">
<shadow type="math_number">
<field name="NUM">100</field>
</shadow>
</value>
</block>
<block type="sound_changeeffectby">
<value name="EFFECT">
<shadow type="sound_effects_menu"></shadow>
</value>
<value name="VALUE">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="sound_cleareffects"></block>
<block type="sound_setinstrumentto">
<value name="INSTRUMENT">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="sound_changevolumeby">
<value name="VOLUME">
<shadow type="math_number">
<field name="NUM">-10</field>
</shadow>
</value>
</block>
<block type="sound_setvolumeto">
<value name="VOLUME">
<shadow type="math_number">
<field name="NUM">100</field>
</shadow>
</value>
</block>
<block type="sound_volume"></block>
<block type="sound_changetempoby">
<value name="TEMPO">
<shadow type="math_number">
<field name="NUM">20</field>
</shadow>
</value>
</block>
<block type="sound_settempotobpm">
<value name="TEMPO">
<shadow type="math_number">
<field name="NUM">60</field>
</shadow>
</value>
</block>
<block type="sound_tempo"></block>
</category>
<category name="Pen" colour="#00B295">
<block type="pen_clear"></block>
<block type="pen_stamp"></block>
<block type="pen_pendown"></block>
<block type="pen_penup"></block>
<block type="pen_setpencolortocolor">
<value name="COLOR">
<shadow type="colour_picker">
</shadow>
</value>
</block>
<block type="pen_changepencolorby">
<value name="COLOR">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="pen_setpencolortonum">
<value name="COLOR">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="pen_changepenshadeby">
<value name="SHADE">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="pen_setpenshadeto">
<value name="SHADE">
<shadow type="math_number">
<field name="NUM">50</field>
</shadow>
</value>
</block>
<block type="pen_changepensizeby">
<value name="SIZE">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="pen_setpensizeto">
<value name="SIZE">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
</category>
<category name="Data" colour="#FF8C1A" custom="VARIABLE">
</category>
<category name="Lists" colour="#FF8C1A">
<block type="data_listcontents"></block>
<block type="data_addtolist">
<value name="ITEM">
<shadow type="text">
<field name="TEXT">thing</field>
</shadow>
</value>
</block>
<block type="data_deleteoflist">
<value name="INDEX">
<shadow type="math_integer">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="data_insertatlist">
<value name="INDEX">
<shadow type="math_integer">
<field name="NUM">1</field>
</shadow>
</value>
<value name="ITEM">
<shadow type="text">
<field name="TEXT">thing</field>
</shadow>
</value>
</block>
<block type="data_replaceitemoflist">
<value name="INDEX">
<shadow type="math_integer">
<field name="NUM">1</field>
</shadow>
</value>
<value name="ITEM">
<shadow type="text">
<field name="TEXT">thing</field>
</shadow>
</value>
</block>
<block type="data_itemoflist">
<value name="INDEX">
<shadow type="math_integer">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="data_lengthoflist"></block>
<block type="data_listcontainsitem">
<value name="ITEM">
<shadow type="text">
<field name="TEXT">thing</field>
</shadow>
</value>
</block>
<block type="data_showlist"></block>
<block type="data_hidelist"></block>
</category>
<category name="Events" colour="#FFD500">
<block type="event_whenflagclicked"></block>
<block type="event_whenkeypressed">
</block>
<block type="event_whenthisspriteclicked"></block>
<block type="event_whenbackdropswitchesto">
</block>
<block type="event_whengreaterthan">
<value name="VALUE">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="event_whenbroadcastreceived">
</block>
<block type="event_broadcast">
<value name="BROADCAST_OPTION">
<shadow type="event_broadcast_menu"></shadow>
</value>
</block>
<block type="event_broadcastandwait">
<value name="BROADCAST_OPTION">
<shadow type="event_broadcast_menu"></shadow>
</value>
</block>
</category>
<category name="Control" colour="#FFAB19">
<block type="control_wait">
<value name="DURATION">
<shadow type="math_positive_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="control_repeat">
<value name="TIMES">
<shadow type="math_whole_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="control_forever"></block>
<block type="control_if"></block>
<block type="control_if_else"></block>
<block type="control_wait_until"></block>
<block type="control_repeat_until"></block>
<block type="control_stop">
<value name="STOP_OPTION">
<shadow type="control_stop_menu"></shadow>
</value>
</block>
<block type="control_start_as_clone"></block>
<block type="control_create_clone_of">
<value name="CLONE_OPTION">
<shadow type="control_create_clone_of_menu"></shadow>
</value>
</block>
<block type="control_delete_this_clone"></block>
</category>
<category name="Sensing" colour="#4CBFE6">
<block type="sensing_touchingobject">
<value name="TOUCHINGOBJECTMENU">
<shadow type="sensing_touchingobjectmenu"></shadow>
</value>
</block>
<block type="sensing_touchingcolor">
<value name="COLOR">
<shadow type="colour_picker"></shadow>
</value>
</block>
<block type="sensing_coloristouchingcolor">
<value name="COLOR">
<shadow type="colour_picker"></shadow>
</value>
<value name="COLOR2">
<shadow type="colour_picker"></shadow>
</value>
</block>
<block type="sensing_distanceto">
<value name="DISTANCETOMENU">
<shadow type="sensing_distancetomenu"></shadow>
</value>
</block>
<block type="sensing_askandwait">
<value name="QUESTION">
<shadow type="text">
<field name="TEXT">What's your name?</field>
</shadow>
</value>
</block>
<block type="sensing_answer"></block>
<block type="sensing_keypressed">
<value name="KEY_OPTION">
<shadow type="sensing_keyoptions"></shadow>
</value>
</block>
<block type="sensing_mousedown"></block>
<block type="sensing_mousex"></block>
<block type="sensing_mousey"></block>
<block type="sensing_loudness"></block>
<block type="sensing_videoon">
<value name="VIDEOONMENU1">
<shadow type="sensing_videoonmenuone"></shadow>
</value>
<value name="VIDEOONMENU2">
<shadow type="sensing_videoonmenutwo"></shadow>
</value>
</block>
<block type="sensing_videotoggle">
<value name="VIDEOTOGGLEMENU">
<shadow type="sensing_videotogglemenu"></shadow>
</value>
</block>
<block type="sensing_setvideotransparency">
<value name="TRANSPARENCY">
<shadow type="math_number">
<field name="NUM">50</field>
</shadow>
</value>
</block>
<block type="sensing_timer"></block>
<block type="sensing_resettimer"></block>
<block type="sensing_of">
<value name="PROPERTY">
<shadow type="sensing_of_property_menu"></shadow>
</value>
<value name="OBJECT">
<shadow type="sensing_of_object_menu"></shadow>
</value>
</block>
<block type="sensing_current">
<value name="CURRENTMENU">
<shadow type="sensing_currentmenu"></shadow>
</value>
</block>
<block type="sensing_dayssince2000"></block>
<block type="sensing_username"></block>
</category>
<category name="Operators" colour="#40BF4A">
<block type="operator_add">
<value name="NUM1">
<shadow type="math_number">
<field name="NUM"></field>
</shadow>
</value>
<value name="NUM2">
<shadow type="math_number">
<field name="NUM"></field>
</shadow>
</value>
</block>
<block type="operator_subtract">
<value name="NUM1">
<shadow type="math_number">
<field name="NUM"></field>
</shadow>
</value>
<value name="NUM2">
<shadow type="math_number">
<field name="NUM"></field>
</shadow>
</value>
</block>
<block type="operator_multiply">
<value name="NUM1">
<shadow type="math_number">
<field name="NUM"></field>
</shadow>
</value>
<value name="NUM2">
<shadow type="math_number">
<field name="NUM"></field>
</shadow>
</value>
</block>
<block type="operator_divide">
<value name="NUM1">
<shadow type="math_number">
<field name="NUM"></field>
</shadow>
</value>
<value name="NUM2">
<shadow type="math_number">
<field name="NUM"></field>
</shadow>
</value>
</block>
<block type="operator_random">
<value name="FROM">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="TO">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="operator_lt">
<value name="OPERAND1">
<shadow type="text">
<field name="TEXT"></field>
</shadow>
</value>
<value name="OPERAND2">
<shadow type="text">
<field name="TEXT"></field>
</shadow>
</value>
</block>
<block type="operator_equals">
<value name="OPERAND1">
<shadow type="text">
<field name="TEXT"></field>
</shadow>
</value>
<value name="OPERAND2">
<shadow type="text">
<field name="TEXT"></field>
</shadow>
</value>
</block>
<block type="operator_gt">
<value name="OPERAND1">
<shadow type="text">
<field name="TEXT"></field>
</shadow>
</value>
<value name="OPERAND2">
<shadow type="text">
<field name="TEXT"></field>
</shadow>
</value>
</block>
<block type="operator_and"></block>
<block type="operator_or"></block>
<block type="operator_not"></block>
<block type="operator_join">
<value name="STRING1">
<shadow type="text">
<field name="TEXT">hello</field>
</shadow>
</value>
<value name="STRING2">
<shadow type="text">
<field name="TEXT">world</field>
</shadow>
</value>
</block>
<block type="operator_letter_of">
<value name="LETTER">
<shadow type="math_whole_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="STRING">
<shadow type="text">
<field name="TEXT">world</field>
</shadow>
</value>
</block>
<block type="operator_length">
<value name="STRING">
<shadow type="text">
<field name="TEXT">world</field>
</shadow>
</value>
</block>
<block type="operator_mod">
<value name="NUM1">
<shadow type="math_number">
<field name="NUM"></field>
</shadow>
</value>
<value name="NUM2">
<shadow type="math_number">
<field name="NUM"></field>
</shadow>
</value>
</block>
<block type="operator_round">
<value name="NUM">
<shadow type="math_number">
<field name="NUM"></field>
</shadow>
</value>
</block>
<block type="operator_mathop">
<value name="OPERATOR">
<shadow type="operator_mathop_menu"></shadow>
</value>
<value name="NUM">
<shadow type="math_number">
<field name="NUM"></field>
</shadow>
</value>
</block>
</category>
<category name="More Blocks" colour="#FF6680" custom="PROCEDURE"></category>
</xml>
<!-- FPS counter, Syntax highlighter, Blocks, Renderer -->
<script src="./vendor.js"></script>
<!-- VM Worker -->

View file

@ -1,6 +1,12 @@
var NEW_PROJECT_HASH = 'createEmptyProject';
var loadProject = function () {
var id = location.hash.substring(1);
if (id.length < 1) {
if (id === NEW_PROJECT_HASH) {
window.vm.createEmptyProject();
return;
}
if (id.length < 1 || !isFinite(id)) {
id = '119615668';
}
var url = 'https://projects.scratch.mit.edu/internalapi/project/' +
@ -32,7 +38,7 @@ window.onload = function() {
};
document.getElementById('createEmptyProject').addEventListener('click',
function() {
document.location = '#' + 'createEmptyProject';
document.location = '#' + NEW_PROJECT_HASH;
location.reload();
});
loadProject();
@ -47,9 +53,7 @@ window.onload = function() {
window.audioEngine = new window.AudioEngine();
// Instantiate scratch-blocks and attach it to the DOM.
var toolbox = document.getElementById('toolbox');
var workspace = window.Blockly.inject('blocks', {
toolbox: toolbox,
media: './media/',
zoom: {
controls: true,
@ -70,8 +74,9 @@ window.onload = function() {
window.workspace = workspace;
// Attach scratch-blocks events to VM.
// @todo: Re-enable flyout listening after fixing GH-69.
workspace.addChangeListener(vm.blockListener);
var flyoutWorkspace = workspace.getFlyout().getWorkspace();
flyoutWorkspace.addChangeListener(vm.flyoutBlockListener);
// Create FPS counter.
var stats = new window.Stats();
@ -116,11 +121,9 @@ window.onload = function() {
// Receipt of new block XML for the selected target.
vm.on('workspaceUpdate', function (data) {
window.Blockly.Events.disable();
workspace.clear();
var dom = window.Blockly.Xml.textToDom(data.xml);
window.Blockly.Xml.domToWorkspace(dom, workspace);
window.Blockly.Events.enable();
});
// Receipt of new list of targets, selected target update.

View file

@ -133,9 +133,16 @@ Scratch3ControlBlocks.prototype.ifElse = function(args, util) {
}
};
Scratch3ControlBlocks.prototype.stop = function() {
// @todo - don't use this.runtime
this.runtime.stopAll();
Scratch3ControlBlocks.prototype.stop = function(args, util) {
var option = args.STOP_OPTION;
if (option == 'all') {
util.stopAll();
} else if (option == 'other scripts in sprite' ||
option == 'other scripts in stage') {
util.stopOtherTargetThreads();
} else if (option == 'this script') {
util.stopThread();
}
};
// @todo (GH-146): remove.
@ -160,6 +167,7 @@ Scratch3ControlBlocks.prototype.createClone = function (args, util) {
};
Scratch3ControlBlocks.prototype.deleteClone = function (args, util) {
if (util.target.isOriginal) return;
this.runtime.disposeTarget(util.target);
this.runtime.stopForTarget(util.target);
};

View file

@ -17,7 +17,7 @@ Scratch3DataBlocks.prototype.getPrimitives = function () {
'data_variable': this.getVariable,
'data_setvariableto': this.setVariableTo,
'data_changevariableby': this.changeVariableBy,
'data_list': this.getListContents,
'data_listcontents': this.getListContents,
'data_addtolist': this.addToList,
'data_deleteoflist': this.deleteOfList,
'data_insertatlist': this.insertAtList,

View file

@ -37,7 +37,7 @@ Scratch3MotionBlocks.prototype.getPrimitives = function() {
Scratch3MotionBlocks.prototype.moveSteps = function (args, util) {
var steps = Cast.toNumber(args.STEPS);
var radians = MathUtil.degToRad(util.target.direction);
var radians = MathUtil.degToRad(90 - util.target.direction);
var dx = steps * Math.cos(radians);
var dy = steps * Math.sin(radians);
util.target.setXY(util.target.x + dx, util.target.y + dy);

View file

@ -13,7 +13,8 @@ function Scratch3ProcedureBlocks(runtime) {
Scratch3ProcedureBlocks.prototype.getPrimitives = function() {
return {
'procedures_defnoreturn': this.defNoReturn,
'procedures_callnoreturn': this.callNoReturn
'procedures_callnoreturn': this.callNoReturn,
'procedures_param': this.param
};
};
@ -23,10 +24,24 @@ Scratch3ProcedureBlocks.prototype.defNoReturn = function () {
Scratch3ProcedureBlocks.prototype.callNoReturn = function (args, util) {
if (!util.stackFrame.executed) {
var procedureName = args.mutation.name;
var procedureName = args.mutation.proccode;
var paramNames = util.getProcedureParamNames(procedureName);
for (var i = 0; i < paramNames.length; i++) {
if (args.hasOwnProperty('input' + i)) {
util.pushParam(paramNames[i], args['input' + i]);
}
}
util.stackFrame.executed = true;
util.startProcedure(procedureName);
}
};
Scratch3ProcedureBlocks.prototype.param = function (args, util) {
var value = util.getParam(args.mutation.paramname);
if (!value) {
return 0;
}
return value;
};
module.exports = Scratch3ProcedureBlocks;

View file

@ -23,7 +23,8 @@ Scratch3SensingBlocks.prototype.getPrimitives = function() {
'sensing_mousey': this.getMouseY,
'sensing_mousedown': this.getMouseDown,
'sensing_keypressed': this.getKeyPressed,
'sensing_current': this.current
'sensing_current': this.current,
'sensing_dayssince2000': this.daysSince2000
};
};
@ -99,4 +100,15 @@ Scratch3SensingBlocks.prototype.getKeyPressed = function (args, util) {
return util.ioQuery('keyboard', 'getKeyIsDown', args.KEY_OPTION);
};
Scratch3SensingBlocks.prototype.daysSince2000 = function()
{
var msPerDay = 24 * 60 * 60 * 1000;
var start = new Date(2000, 1-1, 1);
var today = new Date();
var dstAdjust = today.getTimezoneOffset() - start.getTimezoneOffset();
var mSecsSinceStart = today.valueOf() - start.valueOf();
mSecsSinceStart += ((today.getTimezoneOffset() - dstAdjust) * 60 * 1000);
return mSecsSinceStart / msPerDay;
};
module.exports = Scratch3SensingBlocks;

View file

@ -151,24 +151,40 @@ Blocks.prototype.getProcedureDefinition = function (name) {
var block = this._blocks[id];
if ((block.opcode == 'procedures_defnoreturn' ||
block.opcode == 'procedures_defreturn') &&
block.fields['NAME'].value == name) {
block.mutation.proccode == name) {
return id;
}
}
return null;
};
/**
* Get the procedure definition for a given name.
* @param {?string} name Name of procedure to query.
* @return {?string} ID of procedure definition.
*/
Blocks.prototype.getProcedureParamNames = function (name) {
for (var id in this._blocks) {
var block = this._blocks[id];
if ((block.opcode == 'procedures_defnoreturn' ||
block.opcode == 'procedures_defreturn') &&
block.mutation.proccode == name) {
return JSON.parse(block.mutation.argumentnames);
}
}
return null;
};
// ---------------------------------------------------------------------
/**
* Create event listener for blocks. Handles validation and serves as a generic
* adapter between the blocks and the runtime interface.
* @param {Object} e Blockly "block" event
* @param {boolean} isFlyout If true, create a listener for flyout events.
* @param {?Runtime} opt_runtime Optional runtime to forward click events to.
*/
Blocks.prototype.blocklyListen = function (e, isFlyout, opt_runtime) {
Blocks.prototype.blocklyListen = function (e, opt_runtime) {
// Validate event
if (typeof e !== 'object') return;
if (typeof e.blockId !== 'string') return;
@ -187,7 +203,7 @@ Blocks.prototype.blocklyListen = function (e, isFlyout, opt_runtime) {
var newBlocks = adapter(e);
// A create event can create many blocks. Add them all.
for (var i = 0; i < newBlocks.length; i++) {
this.createBlock(newBlocks[i], isFlyout);
this.createBlock(newBlocks[i]);
}
break;
case 'change':
@ -209,8 +225,10 @@ Blocks.prototype.blocklyListen = function (e, isFlyout, opt_runtime) {
});
break;
case 'delete':
// Don't accept delete events for shadow blocks being obscured.
if (this._blocks[e.blockId].shadow) {
// Don't accept delete events for missing blocks,
// or shadow blocks being obscured.
if (!this._blocks.hasOwnProperty(e.blockId) ||
this._blocks[e.blockId].shadow) {
return;
}
// Inform any runtime to forget about glows on this script.
@ -229,9 +247,8 @@ Blocks.prototype.blocklyListen = function (e, isFlyout, opt_runtime) {
/**
* Block management: create blocks and scripts from a `create` event
* @param {!Object} block Blockly create event to be processed
* @param {boolean} opt_isFlyoutBlock Whether the block is in the flyout.
*/
Blocks.prototype.createBlock = function (block, opt_isFlyoutBlock) {
Blocks.prototype.createBlock = function (block) {
// Does the block already exist?
// Could happen, e.g., for an unobscured shadow.
if (this._blocks.hasOwnProperty(block.id)) {
@ -241,9 +258,8 @@ Blocks.prototype.createBlock = function (block, opt_isFlyoutBlock) {
this._blocks[block.id] = block;
// Push block id to scripts array.
// Blocks are added as a top-level stack if they are marked as a top-block
// (if they were top-level XML in the event) and if they are not
// flyout blocks.
if (!opt_isFlyoutBlock && block.topLevel) {
// (if they were top-level XML in the event).
if (block.topLevel) {
this._addScript(block.id);
}
};
@ -271,6 +287,10 @@ Blocks.prototype.changeBlock = function (args) {
* @param {!Object} e Blockly move event to be processed
*/
Blocks.prototype.moveBlock = function (e) {
if (!this._blocks.hasOwnProperty(e.id)) {
return;
}
// Move coordinate changes.
if (e.newCoordinate) {
this._blocks[e.id].x = e.newCoordinate.x;
@ -434,7 +454,9 @@ Blocks.prototype.mutationToXML = function (mutation) {
var mutationString = '<' + mutation.tagName;
for (var prop in mutation) {
if (prop == 'children' || prop == 'tagName') continue;
mutationString += ' ' + prop + '="' + mutation[prop] + '"';
var mutationValue = (typeof mutation[prop] === 'string') ?
xmlEscape(mutation[prop]) : mutation[prop];
mutationString += ' ' + prop + '="' + mutationValue + '"';
}
mutationString += '>';
for (var i = 0; i < mutation.children.length; i++) {

View file

@ -22,19 +22,34 @@ var execute = function (sequencer, thread) {
var currentBlockId = thread.peekStack();
var currentStackFrame = thread.peekStackFrame();
// Verify that the block still exists.
if (!target ||
typeof target.blocks.getBlock(currentBlockId) === 'undefined') {
// Check where the block lives: target blocks or flyout blocks.
var targetHasBlock = (
typeof target.blocks.getBlock(currentBlockId) !== 'undefined'
);
var flyoutHasBlock = (
typeof runtime.flyoutBlocks.getBlock(currentBlockId) !== 'undefined'
);
// Stop if block or target no longer exists.
if (!target || (!targetHasBlock && !flyoutHasBlock)) {
// No block found: stop the thread; script no longer exists.
sequencer.retireThread(thread);
return;
}
// Query info about the block.
var opcode = target.blocks.getOpcode(currentBlockId);
var blockContainer = null;
if (targetHasBlock) {
blockContainer = target.blocks;
} else {
blockContainer = runtime.flyoutBlocks;
}
var opcode = blockContainer.getOpcode(currentBlockId);
var fields = blockContainer.getFields(currentBlockId);
var inputs = blockContainer.getInputs(currentBlockId);
var blockFunction = runtime.getOpcodeFunction(opcode);
var isHat = runtime.getIsHat(opcode);
var fields = target.blocks.getFields(currentBlockId);
var inputs = target.blocks.getInputs(currentBlockId);
if (!opcode) {
console.warn('Could not get opcode for block: ' + currentBlockId);
@ -133,7 +148,7 @@ var execute = function (sequencer, thread) {
}
// Add any mutation to args (e.g., for procedures).
var mutation = target.blocks.getMutation(currentBlockId);
var mutation = blockContainer.getMutation(currentBlockId);
if (mutation) {
argValues.mutation = mutation;
}
@ -161,9 +176,27 @@ var execute = function (sequencer, thread) {
startBranch: function (branchNum) {
sequencer.stepToBranch(thread, branchNum);
},
stopAll: function () {
runtime.stopAll();
},
stopOtherTargetThreads: function() {
runtime.stopForTarget(target, thread);
},
stopThread: function() {
sequencer.retireThread(thread);
},
startProcedure: function (procedureName) {
sequencer.stepToProcedure(thread, procedureName);
},
getProcedureParamNames: function (procedureName) {
return blockContainer.getProcedureParamNames(procedureName);
},
pushParam: function (paramName, paramValue) {
thread.pushParam(paramName, paramValue);
},
getParam: function (paramName) {
return thread.getParam(paramName);
},
startHats: function(requestedHat, opt_matchFields, opt_target) {
return (
runtime.startHats(requestedHat, opt_matchFields, opt_target)

View file

@ -1,5 +1,6 @@
var EventEmitter = require('events');
var Sequencer = require('./sequencer');
var Blocks = require('./blocks');
var Thread = require('./thread');
var util = require('util');
@ -45,6 +46,8 @@ function Runtime () {
/** @type {!Sequencer} */
this.sequencer = new Sequencer(this);
this.flyoutBlocks = new Blocks();
/**
* Map to look up a block primitive's implementation function by its opcode.
* This is a two-step lookup: package name first, then primitive name.
@ -244,6 +247,9 @@ Runtime.prototype._pushThread = function (id, target) {
* @param {?Thread} thread Thread object to remove from actives
*/
Runtime.prototype._removeThread = function (thread) {
// Inform sequencer to stop executing that thread.
this.sequencer.retireThread(thread);
// Remove from the list.
var i = this.threads.indexOf(thread);
if (i > -1) {
this.threads.splice(i, 1);
@ -342,7 +348,7 @@ Runtime.prototype.startHats = function (requestedHatOpcode,
// any existing threads starting with the top block.
for (var i = 0; i < instance.threads.length; i++) {
if (instance.threads[i].topBlock === topBlockId &&
(!opt_target || instance.threads[i].target == opt_target)) {
instance.threads[i].target == target) {
instance._removeThread(instance.threads[i]);
}
}
@ -351,7 +357,7 @@ Runtime.prototype.startHats = function (requestedHatOpcode,
// give up if any threads with the top block are running.
for (var j = 0; j < instance.threads.length; j++) {
if (instance.threads[j].topBlock === topBlockId &&
(!opt_target || instance.threads[j].target == opt_target)) {
instance.threads[j].target == target) {
// Some thread is already running.
return;
}
@ -364,26 +370,38 @@ Runtime.prototype.startHats = function (requestedHatOpcode,
};
/**
* Dispose of a target.
* @param {!Target} target Target to dispose of.
* Dispose all targets. Return to clean state.
*/
Runtime.prototype.disposeTarget = function (target) {
// Allow target to do dispose actions.
target.dispose();
// Remove from list of targets.
var index = this.targets.indexOf(target);
if (index > -1) {
this.targets.splice(index, 1);
}
Runtime.prototype.dispose = function () {
this.stopAll();
this.targets.map(this.disposeTarget, this);
};
/**
* Dispose of a target.
* @param {!Target} disposingTarget Target to dispose of.
*/
Runtime.prototype.disposeTarget = function (disposingTarget) {
this.targets = this.targets.filter(function (target) {
if (disposingTarget !== target) return true;
// Allow target to do dispose actions.
target.dispose();
// Remove from list of targets.
return false;
});
};
/**
* Stop any threads acting on the target.
* @param {!Target} target Target to stop threads for.
* @param {Thread=} opt_threadException Optional thread to skip.
*/
Runtime.prototype.stopForTarget = function (target) {
Runtime.prototype.stopForTarget = function (target, opt_threadException) {
// Stop any threads on the target.
for (var i = 0; i < this.threads.length; i++) {
if (this.threads[i] === opt_threadException) {
continue;
}
if (this.threads[i].target == target) {
this._removeThread(this.threads[i]);
}
@ -464,6 +482,10 @@ Runtime.prototype._updateScriptGlows = function () {
if (thread.requestScriptGlowInFrame && target == this._editingTarget) {
var blockForThread = thread.peekStack() || thread.topBlock;
var script = target.blocks.getTopLevelScript(blockForThread);
if (!script) {
// Attempt to find in flyout blocks.
script = this.flyoutBlocks.getTopLevelScript(blockForThread);
}
if (script) {
requestedGlowsThisFrame.push(script);
}

View file

@ -132,6 +132,11 @@ Sequencer.prototype.stepToBranch = function (thread, branchNum) {
Sequencer.prototype.stepToProcedure = function (thread, procedureName) {
var definition = thread.target.blocks.getProcedureDefinition(procedureName);
thread.pushStack(definition);
// Check if the call is recursive. If so, yield.
// @todo: Have behavior match Scratch 2.0.
if (thread.stack.indexOf(definition) > -1) {
thread.setStatus(Thread.STATUS_YIELD_FRAME);
}
};
/**

View file

@ -83,6 +83,7 @@ Thread.prototype.pushStack = function (blockId) {
this.stackFrames.push({
reported: {}, // Collects reported input values.
waitingReporter: null, // Name of waiting reporter.
params: {}, // Procedure parameters.
executionContext: {} // A context passed to block implementations.
});
}
@ -135,6 +136,21 @@ Thread.prototype.pushReportedValue = function (value) {
}
};
Thread.prototype.pushParam = function (paramName, value) {
var stackFrame = this.peekStackFrame();
stackFrame.params[paramName] = value;
};
Thread.prototype.getParam = function (paramName) {
for (var i = this.stackFrames.length - 1; i >= 0; i--) {
var frame = this.stackFrames[i];
if (frame.params.hasOwnProperty(paramName)) {
return frame.params[paramName];
}
}
return null;
};
/**
* Whether the current execution of a thread is at the top of the stack.
* @return {Boolean} True if execution is at top of the stack.

View file

@ -218,6 +218,40 @@ function flatten (blocks) {
return finalBlocks;
}
/**
* Convert a Scratch 2.0 procedure string (e.g., "my_procedure %s %b %n")
* into an argument map. This allows us to provide the expected inputs
* to a mutated procedure call.
* @param {string} procCode Scratch 2.0 procedure string.
* @return {Object} Argument map compatible with those in sb2specmap.
*/
function parseProcedureArgMap (procCode) {
var argMap = [
{} // First item in list is op string.
];
var INPUT_PREFIX = 'input';
var inputCount = 0;
// Split by %n, %b, %s.
var parts = procCode.split(/(?=[^\\]\%[nbs])/);
for (var i = 0; i < parts.length; i++) {
var part = parts[i].trim();
if (part.substring(0, 1) == '%') {
var argType = part.substring(1, 2);
var arg = {
type: 'input',
inputName: INPUT_PREFIX + (inputCount++)
};
if (argType == 'n') {
arg.inputOp = 'math_number';
} else if (argType == 's') {
arg.inputOp = 'text';
}
argMap.push(arg);
}
}
return argMap;
}
/**
* Parse a single SB2 JSON-formatted block and its children.
* @param {!Object} sb2block SB2 JSON-formatted block.
@ -242,6 +276,10 @@ function parseBlock (sb2block) {
shadow: false, // No shadow blocks in an SB2 by default.
children: [] // Store any generated children, flattened in `flatten`.
};
// For a procedure call, generate argument map from proc string.
if (oldOpcode == 'call') {
blockMetadata.argMap = parseProcedureArgMap(sb2block[1]);
}
// Look at the expected arguments in `blockMetadata.argMap.`
// The basic problem here is to turn positional SB2 arguments into
// non-positional named Scratch VM arguments.
@ -269,8 +307,14 @@ function parseBlock (sb2block) {
// Single block occupies the input.
innerBlocks = [parseBlock(providedArg)];
}
var previousBlock = null;
for (var j = 0; j < innerBlocks.length; j++) {
innerBlocks[j].parent = activeBlock.id;
if (j == 0) {
innerBlocks[j].parent = activeBlock.id;
} else {
innerBlocks[j].parent = previousBlock;
}
previousBlock = innerBlocks[j].id;
}
// Obscures any shadow.
shadowObscured = true;
@ -343,11 +387,44 @@ function parseBlock (sb2block) {
}
}
// Special cases to generate mutations.
if (oldOpcode == 'call') {
if (oldOpcode == 'stopScripts') {
// Mutation for stop block: if the argument is 'other scripts',
// the block needs a next connection.
if (sb2block[1] == 'other scripts in sprite' ||
sb2block[1] == 'other scripts in stage') {
activeBlock.mutation = {
tagName: 'mutation',
hasnext: 'true',
children: []
};
}
} else if (oldOpcode == 'procDef') {
// Mutation for procedure definition:
// store all 2.0 proc data.
var procData = sb2block.slice(1);
activeBlock.mutation = {
tagName: 'mutation',
proccode: procData[0], // e.g., "abc %n %b %s"
argumentnames: JSON.stringify(procData[1]), // e.g. ['arg1', 'arg2']
argumentdefaults: JSON.stringify(procData[2]), // e.g., [1, 'abc']
warp: procData[3], // Warp mode, e.g., true/false.
children: []
};
} else if (oldOpcode == 'call') {
// Mutation for procedure call:
// string for proc code (e.g., "abc %n %b %s").
activeBlock.mutation = {
tagName: 'mutation',
children: [],
name: sb2block[1]
proccode: sb2block[1]
};
} else if (oldOpcode == 'getParam') {
// Mutation for procedure parameter.
activeBlock.mutation = {
tagName: 'mutation',
children: [],
paramname: sb2block[1], // Name of parameter.
shape: sb2block[2] // Shape - in 2.0, 'r' or 'b'.
};
}
return activeBlock;

View file

@ -752,9 +752,8 @@ var specMap = {
'opcode':'control_stop',
'argMap':[
{
'type':'input',
'inputOp':'control_stop_menu',
'inputName':'STOP_OPTION'
'type':'field',
'fieldName':'STOP_OPTION'
}
]
},
@ -1375,15 +1374,10 @@ var specMap = {
},
'procDef':{
'opcode':'procedures_defnoreturn',
'argMap':[
{
'type':'field',
'fieldName':'NAME'
}
]
'argMap':[]
},
'getParam':{
'opcode':'proc_param',
'opcode':'procedures_param',
'argMap':[]
},
'call':{

View file

@ -44,6 +44,7 @@ function VirtualMachine () {
});
this.blockListener = this.blockListener.bind(this);
this.flyoutBlockListener = this.flyoutBlockListener.bind(this);
}
/**
@ -72,6 +73,15 @@ VirtualMachine.prototype.stopAll = function () {
this.runtime.stopAll();
};
/**
* Clear out current running project data.
*/
VirtualMachine.prototype.clear = function () {
this.runtime.dispose();
this.editingTarget = null;
this.emitTargetsUpdate();
};
/**
* Get data for playground. Data comes back in an emitted event.
*/
@ -115,6 +125,7 @@ VirtualMachine.prototype.postIOData = function (device, data) {
* @param {?string} json JSON string representing the project.
*/
VirtualMachine.prototype.loadProject = function (json) {
this.clear();
// @todo: Handle other formats, e.g., Scratch 1.4, Scratch 3.0.
sb2import(json, this.runtime);
// Select the first target for editing, e.g., the stage.
@ -132,10 +143,10 @@ VirtualMachine.prototype.loadProject = function (json) {
VirtualMachine.prototype.createEmptyProject = function () {
// Stage.
var blocks2 = new Blocks();
var stage = new Sprite(blocks2);
var stage = new Sprite(blocks2, this.runtime);
stage.name = 'Stage';
stage.costumes.push({
skin: '/assets/stage.png',
skin: './assets/stage.png',
name: 'backdrop1',
bitmapResolution: 2,
rotationCenterX: 480,
@ -151,10 +162,10 @@ VirtualMachine.prototype.createEmptyProject = function () {
target2.isStage = true;
// Sprite1 (cat).
var blocks1 = new Blocks();
var sprite = new Sprite(blocks1);
var sprite = new Sprite(blocks1, this.runtime);
sprite.name = 'Sprite1';
sprite.costumes.push({
skin: '/assets/scratch_cat.svg',
skin: './assets/scratch_cat.svg',
name: 'costume1',
bitmapResolution: 1,
rotationCenterX: 47,
@ -186,14 +197,18 @@ VirtualMachine.prototype.attachRenderer = function (renderer) {
*/
VirtualMachine.prototype.blockListener = function (e) {
if (this.editingTarget) {
this.editingTarget.blocks.blocklyListen(
e,
false,
this.runtime
);
this.editingTarget.blocks.blocklyListen(e, this.runtime);
}
};
/**
* Handle a Blockly event for the flyout.
* @param {!Blockly.Event} e Any Blockly event.
*/
VirtualMachine.prototype.flyoutBlockListener = function (e) {
this.runtime.flyoutBlocks.blocklyListen(e, this.runtime);
};
/**
* Set an editing target. An editor UI can use this function to switch
* between editing different targets, sprites, etc.
@ -232,7 +247,7 @@ VirtualMachine.prototype.emitTargetsUpdate = function () {
return [target.id, target.getName()];
}),
// Currently editing target id.
editingTarget: this.editingTarget.id
editingTarget: this.editingTarget ? this.editingTarget.id : null
});
};

View file

@ -438,9 +438,6 @@ Clone.prototype.onGreenFlag = function () {
* Dispose of this clone, destroying any run-time properties.
*/
Clone.prototype.dispose = function () {
if (this.isOriginal) { // Don't allow a non-clone to delete itself.
return;
}
this.runtime.changeCloneCounter(-1);
if (this.renderer && this.drawableID !== null) {
this.renderer.destroyDrawable(this.drawableID);

View file

@ -6,7 +6,7 @@ function MathUtil () {}
* @return {!number} Equivalent value in radians.
*/
MathUtil.degToRad = function (deg) {
return (Math.PI * (90 - deg)) / 180;
return deg * Math.PI / 180;
};
/**

71
test/fixtures/default.json vendored Normal file
View file

@ -0,0 +1,71 @@
{
"objName": "Stage",
"sounds": [{
"soundName": "pop",
"soundID": -1,
"md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
"sampleCount": 258,
"rate": 11025,
"format": ""
}],
"costumes": [{
"costumeName": "backdrop1",
"baseLayerID": -1,
"baseLayerMD5": "739b5e2a2435f6e1ec2993791b423146.png",
"bitmapResolution": 1,
"rotationCenterX": 240,
"rotationCenterY": 180
}],
"currentCostumeIndex": 0,
"penLayerMD5": "5c81a336fab8be57adc039a8a2b33ca9.png",
"penLayerID": -1,
"tempoBPM": 60,
"videoAlpha": 0.5,
"children": [{
"objName": "Sprite1",
"sounds": [{
"soundName": "meow",
"soundID": -1,
"md5": "83c36d806dc92327b9e7049a565c6bff.wav",
"sampleCount": 18688,
"rate": 22050,
"format": ""
}],
"costumes": [{
"costumeName": "costume1",
"baseLayerID": -1,
"baseLayerMD5": "09dc888b0b7df19f70d81588ae73420e.svg",
"bitmapResolution": 1,
"rotationCenterX": 47,
"rotationCenterY": 55
},
{
"costumeName": "costume2",
"baseLayerID": -1,
"baseLayerMD5": "3696356a03a8d938318876a593572843.svg",
"bitmapResolution": 1,
"rotationCenterX": 47,
"rotationCenterY": 55
}],
"currentCostumeIndex": 0,
"scratchX": 0,
"scratchY": 0,
"scale": 1,
"direction": 90,
"rotationStyle": "normal",
"isDraggable": false,
"indexInLibrary": 1,
"visible": true,
"spriteInfo": {
}
}],
"info": {
"videoOn": false,
"userAgent": "Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/50.0.2661.102 Safari\/537.36",
"swfVersion": "v446",
"scriptCount": 0,
"spriteCount": 1,
"hasCloudData": false,
"flashVersion": "MAC 21,0,0,242"
}
}

359
test/fixtures/demo.json vendored Normal file
View file

@ -0,0 +1,359 @@
{
"objName": "Stage",
"variables": [{
"name": "x",
"value": "1",
"isPersistent": false
},
{
"name": "y",
"value": "1",
"isPersistent": false
},
{
"name": "z",
"value": "1",
"isPersistent": false
},
{
"name": "d",
"value": "1",
"isPersistent": false
},
{
"name": "a",
"value": 4,
"isPersistent": false
}],
"lists": [{
"listName": "D# Minor Pentatonic",
"contents": ["78",
"75",
"73",
"75",
"70",
"78",
"73",
"75",
"75",
"78",
"75",
"73",
"75",
"70",
"75",
"78",
"73",
"75",
"78",
"75",
"73",
"75",
"70",
"73",
"68",
"70",
"66",
"68",
"63"],
"isPersistent": false,
"x": 5,
"y": 32,
"width": 125,
"height": 206,
"visible": true
}],
"scripts": [[52,
8,
[["whenIReceive", "start"],
["setVar:to:", "a", "1"],
["doRepeat",
["lineCountOfList:", "D# Minor Pentatonic"],
[["noteOn:duration:elapsed:from:", ["getLine:ofList:", ["readVariable", "a"], "D# Minor Pentatonic"], 0.5], ["changeVar:by:", "a", 1]]]]],
[53,
186,
[["whenIReceive", "start"],
["setVar:to:", "x", "1"],
["rest:elapsed:from:", 7.25],
["doRepeat",
["lineCountOfList:", "D# Minor Pentatonic"],
[["noteOn:duration:elapsed:from:", ["getLine:ofList:", ["readVariable", "x"], "D# Minor Pentatonic"], 0.25], ["changeVar:by:", "x", 1]]]]],
[48,
557,
[["whenIReceive", "start"],
["setVar:to:", "z", "1"],
["rest:elapsed:from:", 13],
["doRepeat",
["lineCountOfList:", "D# Minor Pentatonic"],
[["noteOn:duration:elapsed:from:", ["getLine:ofList:", ["readVariable", "z"], "D# Minor Pentatonic"], 0.0625], ["changeVar:by:", "z", 1]]]]],
[49,
368,
[["whenIReceive", "start"],
["setVar:to:", "y", "1"],
["rest:elapsed:from:", 11],
["doRepeat",
["lineCountOfList:", "D# Minor Pentatonic"],
[["noteOn:duration:elapsed:from:", ["getLine:ofList:", ["readVariable", "y"], "D# Minor Pentatonic"], 0.125], ["changeVar:by:", "y", 1]]]]],
[52,
745,
[["whenIReceive", "start"],
["setVar:to:", "d", "1"],
["rest:elapsed:from:", 13.5],
["doRepeat",
["lineCountOfList:", "D# Minor Pentatonic"],
[["noteOn:duration:elapsed:from:", ["getLine:ofList:", ["readVariable", "d"], "D# Minor Pentatonic"], 0.03125], ["changeVar:by:", "d", 1]]]]]],
"sounds": [{
"soundName": "pop",
"soundID": 0,
"md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
"sampleCount": 258,
"rate": 11025,
"format": ""
}],
"costumes": [{
"costumeName": "backdrop1",
"baseLayerID": 4,
"baseLayerMD5": "b61b1077b0ea1931abee9dbbfa7903ff.png",
"bitmapResolution": 2,
"rotationCenterX": 480,
"rotationCenterY": 360
}],
"currentCostumeIndex": 0,
"penLayerMD5": "5c81a336fab8be57adc039a8a2b33ca9.png",
"penLayerID": 0,
"tempoBPM": 60,
"videoAlpha": 0.5,
"children": [{
"objName": "Indicator",
"scripts": [[247.85,
32.8,
[["procDef", "foo %n", ["bar"], [1], false],
["hide"],
["clearPenTrails"],
["penColor:", 5968094],
["say:", ["getParam", "bar", "r"]],
["stopScripts", "this script"]]],
[41, 36, [["whenGreenFlag"], ["call", "foo %n", 1]]]],
"sounds": [{
"soundName": "pop",
"soundID": 0,
"md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
"sampleCount": 258,
"rate": 11025,
"format": ""
}],
"costumes": [{
"costumeName": "costume1",
"baseLayerID": 1,
"baseLayerMD5": "d36f6603ec293d2c2198d3ea05109fe0.png",
"bitmapResolution": 2,
"rotationCenterX": 0,
"rotationCenterY": 0
}],
"currentCostumeIndex": 0,
"scratchX": 22,
"scratchY": -26,
"scale": 1,
"direction": 90,
"rotationStyle": "normal",
"isDraggable": false,
"indexInLibrary": 3,
"visible": false,
"spriteInfo": {
}
},
{
"target": "Stage",
"cmd": "timer",
"param": null,
"color": 2926050,
"label": "timer",
"mode": 1,
"sliderMin": 0,
"sliderMax": 100,
"isDiscrete": true,
"x": 5,
"y": 5,
"visible": false
},
{
"target": "Stage",
"cmd": "getVar:",
"param": "x",
"color": 15629590,
"label": "x",
"mode": 1,
"sliderMin": 0,
"sliderMax": 100,
"isDiscrete": true,
"x": 5,
"y": 268,
"visible": true
},
{
"target": "Stage",
"cmd": "getVar:",
"param": "y",
"color": 15629590,
"label": "y",
"mode": 1,
"sliderMin": 0,
"sliderMax": 100,
"isDiscrete": true,
"x": 5,
"y": 295,
"visible": true
},
{
"target": "Stage",
"cmd": "getVar:",
"param": "z",
"color": 15629590,
"label": "z",
"mode": 1,
"sliderMin": 0,
"sliderMax": 100,
"isDiscrete": true,
"x": 78,
"y": 268,
"visible": true
},
{
"objName": "Play",
"scripts": [[32, 33, [["whenClicked"], ["broadcast:", "start"]]]],
"sounds": [{
"soundName": "pop",
"soundID": 0,
"md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
"sampleCount": 258,
"rate": 11025,
"format": ""
}],
"costumes": [{
"costumeName": "costume1",
"baseLayerID": 2,
"baseLayerMD5": "30f811366ae3a53e6447932cc7f0212d.png",
"bitmapResolution": 2,
"rotationCenterX": 68,
"rotationCenterY": 115
}],
"currentCostumeIndex": 0,
"scratchX": 2,
"scratchY": -48,
"scale": 1,
"direction": 90,
"rotationStyle": "normal",
"isDraggable": false,
"indexInLibrary": 1,
"visible": true,
"spriteInfo": {
}
},
{
"target": "Stage",
"cmd": "getVar:",
"param": "d",
"color": 15629590,
"label": "d",
"mode": 1,
"sliderMin": 0,
"sliderMax": 100,
"isDiscrete": true,
"x": 5,
"y": 241,
"visible": true
},
{
"target": "Stage",
"cmd": "getVar:",
"param": "a",
"color": 15629590,
"label": "a",
"mode": 1,
"sliderMin": 0,
"sliderMax": 100,
"isDiscrete": true,
"x": 78,
"y": 241,
"visible": true
},
{
"objName": "Stop",
"scripts": [[45, 104, [["whenClicked"], ["stopScripts", "all"]]]],
"sounds": [{
"soundName": "pop",
"soundID": 0,
"md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
"sampleCount": 258,
"rate": 11025,
"format": ""
}],
"costumes": [{
"costumeName": "costume1",
"baseLayerID": 3,
"baseLayerMD5": "3de406f265b8d664406adf7c70762514.png",
"bitmapResolution": 2,
"rotationCenterX": 68,
"rotationCenterY": 70
}],
"currentCostumeIndex": 0,
"scratchX": 121,
"scratchY": -33,
"scale": 1,
"direction": 90,
"rotationStyle": "normal",
"isDraggable": false,
"indexInLibrary": 2,
"visible": true,
"spriteInfo": {
}
},
{
"listName": "D# Minor Pentatonic",
"contents": ["78",
"75",
"73",
"75",
"70",
"78",
"73",
"75",
"75",
"78",
"75",
"73",
"75",
"70",
"75",
"78",
"73",
"75",
"78",
"75",
"73",
"75",
"70",
"73",
"68",
"70",
"66",
"68",
"63"],
"isPersistent": false,
"x": 5,
"y": 32,
"width": 125,
"height": 206,
"visible": true
}],
"info": {
"spriteCount": 3,
"projectID": "118381369",
"videoOn": false,
"hasCloudData": false,
"userAgent": "Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/53.0.2785.143 Safari\/537.36",
"scriptCount": 9,
"flashVersion": "MAC 23,0,0,185",
"swfVersion": "v450.1"
}
}

88
test/unit/import_sb2.js Normal file
View file

@ -0,0 +1,88 @@
var fs = require('fs');
var path = require('path');
var test = require('tap').test;
var clone = require('../../src/sprites/clone');
var runtime = require('../../src/engine/runtime');
var sb2 = require('../../src/import/sb2import');
test('spec', function (t) {
t.type(sb2, 'function');
t.end();
});
test('default', function (t) {
// Get SB2 JSON (string)
var uri = path.resolve(__dirname, '../fixtures/default.json');
var file = fs.readFileSync(uri, 'utf8');
// Create runtime instance & load SB2 into it
var rt = new runtime();
sb2(file, rt);
// Test
t.type(file, 'string');
t.type(rt, 'object');
t.type(rt.targets, 'object');
t.ok(rt.targets[0] instanceof clone);
t.type(rt.targets[0].id, 'string');
t.type(rt.targets[0].blocks, 'object');
t.type(rt.targets[0].variables, 'object');
t.type(rt.targets[0].lists, 'object');
t.equal(rt.targets[0].isOriginal, true);
t.equal(rt.targets[0].currentCostume, 0);
t.equal(rt.targets[0].isOriginal, true);
t.equal(rt.targets[0].isStage, true);
t.ok(rt.targets[1] instanceof clone);
t.type(rt.targets[1].id, 'string');
t.type(rt.targets[1].blocks, 'object');
t.type(rt.targets[1].variables, 'object');
t.type(rt.targets[1].lists, 'object');
t.equal(rt.targets[1].isOriginal, true);
t.equal(rt.targets[1].currentCostume, 0);
t.equal(rt.targets[1].isOriginal, true);
t.equal(rt.targets[1].isStage, false);
t.end();
});
test('demo', function (t) {
// Get SB2 JSON (string)
var uri = path.resolve(__dirname, '../fixtures/demo.json');
var file = fs.readFileSync(uri, 'utf8');
// Create runtime instance & load SB2 into it
var rt = new runtime();
sb2(file, rt);
// Test
t.type(file, 'string');
t.type(rt, 'object');
t.type(rt.targets, 'object');
t.ok(rt.targets[0] instanceof clone);
t.type(rt.targets[0].id, 'string');
t.type(rt.targets[0].blocks, 'object');
t.type(rt.targets[0].variables, 'object');
t.type(rt.targets[0].lists, 'object');
t.equal(rt.targets[0].isOriginal, true);
t.equal(rt.targets[0].currentCostume, 0);
t.equal(rt.targets[0].isOriginal, true);
t.equal(rt.targets[0].isStage, true);
t.ok(rt.targets[1] instanceof clone);
t.type(rt.targets[1].id, 'string');
t.type(rt.targets[1].blocks, 'object');
t.type(rt.targets[1].variables, 'object');
t.type(rt.targets[1].lists, 'object');
t.equal(rt.targets[1].isOriginal, true);
t.equal(rt.targets[1].currentCostume, 0);
t.equal(rt.targets[1].isOriginal, true);
t.equal(rt.targets[1].isStage, false);
t.end();
});

View file

@ -2,12 +2,11 @@ var test = require('tap').test;
var math = require('../../src/util/math-util');
test('degToRad', function (t) {
// @todo This is incorrect
t.strictEqual(math.degToRad(0), 1.5707963267948966);
t.strictEqual(math.degToRad(1), 1.5533430342749535);
t.strictEqual(math.degToRad(180), -1.5707963267948966);
t.strictEqual(math.degToRad(360), -4.71238898038469);
t.strictEqual(math.degToRad(720), -10.995574287564276);
t.strictEqual(math.degToRad(0), 0);
t.strictEqual(math.degToRad(1), 0.017453292519943295);
t.strictEqual(math.degToRad(180), Math.PI);
t.strictEqual(math.degToRad(360), 2 * Math.PI);
t.strictEqual(math.degToRad(720), 4 * Math.PI);
t.end();
});

View file

@ -52,6 +52,7 @@ module.exports = [
entry: {
'dist': './src/index.js'
},
output: {
library: 'VirtualMachine',
libraryTarget: 'commonjs2',
@ -108,6 +109,9 @@ module.exports = [
to: 'media'
}, {
from: 'node_modules/highlightjs/styles/zenburn.css'
}, {
from: 'assets',
to: 'assets'
}])
])
})