diff --git a/Makefile b/Makefile index 835c16f79..c3614f5ac 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ watch: $(WEBPACK) --watch serve: - $(WEBPACK_DEV_SERVER) --content-base ./ + $(WEBPACK_DEV_SERVER) --host 0.0.0.0 --content-base ./ # ------------------------------------------------------------------------------ @@ -31,4 +31,4 @@ coverage: # ------------------------------------------------------------------------------ -.PHONY: build lint test coverage benchmark +.PHONY: build lint test coverage benchmark serve diff --git a/index.html b/index.html new file mode 100644 index 000000000..42edce430 --- /dev/null +++ b/index.html @@ -0,0 +1,7 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="refresh" content="0; URL='/playground'" /> + <title>Redirect to playground</title> +</head> +</html> diff --git a/package.json b/package.json index 2d4d54773..9f28cbd83 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "highlightjs": "8.7.0", "json-loader": "0.5.4", "scratch-blocks": "git+https://git@github.com/LLK/scratch-blocks.git", + "scratch-render": "git+https://git@github.com/LLK/scratch-render.git", "tap": "5.7.1", "webpack": "1.13.0", "webpack-dev-server": "1.14.1" diff --git a/playground/index.html b/playground/index.html index 82535807b..bcfb4426f 100644 --- a/playground/index.html +++ b/playground/index.html @@ -13,9 +13,14 @@ <button id="greenflag">Green flag</button> <button id="stopall">Stop</button> <p> + <a id="renderexplorer-link" href="#">Renderer</a><br /> <a id="threadexplorer-link" href="#">VM Threads</a><br /> <a id="blockexplorer-link" href="#">VM Block Representation</a> </p> + <div id="tab-renderexplorer"> + Render<br /> + <canvas id="scratch-stage" style="width: 480px; height: 360px;"></canvas> + </div> <div id="tab-threadexplorer"> Thread explorer <pre id="threadexplorer"></pre> @@ -28,58 +33,536 @@ <div id="blocks"></div> <xml id="toolbox" style="display: none"> - <category name="Events"> - <block type="event_whenflagclicked"></block> - <block type="event_whenbroadcastreceived"> - <value name="BROADCAST_OPTION"> - <shadow type="event_broadcast_menu"></shadow> - </value> + <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="COSTUME"> + <shadow type="looks_backdrops"></shadow> + </value> + </block> + <block type="looks_switchbackdroptoandwait"> + <value name="COSTUME"> + <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="math_number"> + <field name="NUM">0.25</field> + </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">1</field> + </shadow> + </value> + <value name="BEATS"> + <shadow type="math_number"> + <field name="NUM">0.5</field> + </shadow> + </value> + </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"> + <block type="data_variable"> + <value name="VARIABLE"> + <shadow type="data_variablemenu"></shadow> + </value> + </block> + <block type="data_setvariableto"> + <value name="VARIABLE"> + <shadow type="data_variablemenu"></shadow> + </value> + <value name="VALUE"> + <shadow type="math_number"> + <field name="NUM">0</field> + </shadow> + </value> + </block> + <block type="data_changevariableby"> + <value name="VARIABLE"> + <shadow type="data_variablemenu"></shadow> + </value> + <value name="VALUE"> + <shadow type="math_number"> + <field name="NUM">0</field> + </shadow> + </value> + </block> + <block type="data_showvariable"> + <value name="VARIABLE"> + <shadow type="data_variablemenu"></shadow> + </value> + </block> + <block type="data_hidevariable"> + <value name="VARIABLE"> + <shadow type="data_variablemenu"></shadow> + </value> + </block> + </category> + <category name="Events" colour="#FFD500"> + <block type="event_whenflagclicked"></block> + <block type="event_whenkeypressed"> + <value name="KEY_OPTION"> + <shadow type="event_keyoptions"></shadow> + </value> + </block> + <block type="event_whenthisspriteclicked"></block> + <block type="event_whenbackdropswitchesto"> + <value name="BACKDROP"> + <shadow type="event_backdrops"></shadow> + </value> + </block> + <block type="event_whengreaterthan"> + <value name="WHENGREATERTHANMENU"> + <shadow type="event_whengreaterthanmenu"></shadow> + </value> + <value name="VALUE"> + <shadow type="math_number"> + <field name="NUM">10</field> + </shadow> + </value> + </block> + <block type="event_whenbroadcastreceived"> + <value name="BROADCAST_OPTION"> + <shadow type="event_broadcast_menu"></shadow> + </value> + </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_OPTIONS"> + <shadow type="sensing_keyoptions"></shadow> + </value> </block> - <block type="event_broadcast"> - <value name="BROADCAST_OPTION"> - <shadow type="event_broadcast_menu"></shadow> - </value> + <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="event_broadcastandwait"> - <value name="BROADCAST_OPTION"> - <shadow type="event_broadcast_menu"></shadow> - </value> + <block type="sensing_videotoggle"> + <value name="VIDEOTOGGLEMENU"> + <shadow type="sensing_videotogglemenu"></shadow> + </value> </block> - </category> - <category name="Control"> - <block type="control_wait"> - <value name="DURATION"> + <block type="sensing_setvideotransparency"> + <value name="TRANSPARENCY"> <shadow type="math_number"> - <field name="NUM">1</field> + <field name="NUM">50</field> </shadow> </value> - </block> - <block type="control_repeat"> - <value name="TIMES"> - <shadow type="math_number"> - <field name="NUM">4</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="Operators"> + </block> + <block type="sensing_timer"></block> + <block type="sensing_resettimer"></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"> @@ -193,7 +676,7 @@ </block> <block type="operator_letter_of"> <value name="LETTER"> - <shadow type="math_number"> + <shadow type="math_whole_number"> <field name="NUM">1</field> </shadow> </value> @@ -239,8 +722,9 @@ </shadow> </value> </block> - </category> - </xml> + </category> + <category name="More Blocks" colour="#FF6680"></category> + </xml> <!-- Syntax highlighter --> <script src="../node_modules/highlightjs/highlight.pack.min.js"></script> @@ -249,6 +733,9 @@ <script src="../node_modules/scratch-blocks/blockly_compressed_vertical.js"></script> <script src="../node_modules/scratch-blocks/blocks_compressed.js"></script> <script src="../node_modules/scratch-blocks/blocks_compressed_vertical.js"></script> + <script src="../node_modules/scratch-blocks/msg/messages.js"></script> + <!-- Renderer --> + <script src="../node_modules/scratch-render/render.js"></script> <!-- VM Worker --> <script src="../vm.worker.js"></script> <!-- Playground --> diff --git a/playground/playground.css b/playground/playground.css index 451a7cad5..a15e1d81c 100644 --- a/playground/playground.css +++ b/playground/playground.css @@ -32,6 +32,6 @@ a { font-size: 10pt; } -#tab-blockexplorer { +#tab-blockexplorer, #tab-threadexplorer { display: none; } diff --git a/playground/playground.js b/playground/playground.js index be683ceb7..47631b201 100644 --- a/playground/playground.js +++ b/playground/playground.js @@ -3,6 +3,10 @@ window.onload = function() { var vm = new window.VirtualMachine(); window.vm = vm; + var canvas = document.getElementById('scratch-stage'); + window.renderer = new window.RenderWebGLLocal(canvas); + window.renderer.connectWorker(window.vm.vmWorker); + var toolbox = document.getElementById('toolbox'); var workspace = window.Blockly.inject('blocks', { toolbox: toolbox, @@ -47,11 +51,14 @@ window.onload = function() { } }; + // Only request data from the VM thread if the appropriate tab is open. + window.exploreTabOpen = false; var getPlaygroundData = function () { vm.getPlaygroundData(); - window.requestAnimationFrame(getPlaygroundData); + if (window.exploreTabOpen) { + window.requestAnimationFrame(getPlaygroundData); + } }; - getPlaygroundData(); vm.on('playgroundData', function(data) { updateThreadExplorer(data.threads); @@ -71,10 +78,20 @@ window.onload = function() { vm.on('BLOCK_GLOW_OFF', function(data) { workspace.glowBlock(data.id, false); }); + vm.on('VISUAL_REPORT', function(data) { + workspace.reportValue(data.id, data.value); + }); // Run threads vm.start(); + // Inform VM of animation frames. + var animate = function() { + window.vm.animationFrame(); + requestAnimationFrame(animate); + }; + requestAnimationFrame(animate); + // Handlers for green flag and stop all. document.getElementById('greenflag').addEventListener('click', function() { vm.greenFlag(); @@ -85,16 +102,30 @@ window.onload = function() { var tabBlockExplorer = document.getElementById('tab-blockexplorer'); var tabThreadExplorer = document.getElementById('tab-threadexplorer'); + var tabRenderExplorer = document.getElementById('tab-renderexplorer'); // Handlers to show different explorers. document.getElementById('threadexplorer-link').addEventListener('click', function () { + window.exploreTabOpen = true; + getPlaygroundData(); tabBlockExplorer.style.display = 'none'; + tabRenderExplorer.style.display = 'none'; tabThreadExplorer.style.display = 'block'; }); document.getElementById('blockexplorer-link').addEventListener('click', function () { + window.exploreTabOpen = true; + getPlaygroundData(); tabBlockExplorer.style.display = 'block'; + tabRenderExplorer.style.display = 'none'; + tabThreadExplorer.style.display = 'none'; + }); + document.getElementById('renderexplorer-link').addEventListener('click', + function () { + window.exploreTabOpen = false; + tabBlockExplorer.style.display = 'none'; + tabRenderExplorer.style.display = 'block'; tabThreadExplorer.style.display = 'none'; }); }; diff --git a/src/blocks/scratch3_control.js b/src/blocks/scratch3_control.js index e13edefd9..d3cd2fdf6 100644 --- a/src/blocks/scratch3_control.js +++ b/src/blocks/scratch3_control.js @@ -15,6 +15,7 @@ function Scratch3ControlBlocks(runtime) { Scratch3ControlBlocks.prototype.getPrimitives = function() { return { 'control_repeat': this.repeat, + 'control_repeat_until': this.repeatUntil, 'control_forever': this.forever, 'control_wait': this.wait, 'control_if': this.if, @@ -28,16 +29,50 @@ Scratch3ControlBlocks.prototype.repeat = function(args, util) { if (util.stackFrame.loopCounter === undefined) { util.stackFrame.loopCounter = parseInt(args.TIMES); } - // Decrease counter - util.stackFrame.loopCounter--; - // If we still have some left, start the substack - if (util.stackFrame.loopCounter >= 0) { - util.startSubstack(); + // Only execute once per frame. + // When the substack finishes, `repeat` will be executed again and + // the second branch will be taken, yielding for the rest of the frame. + if (!util.stackFrame.executedInFrame) { + util.stackFrame.executedInFrame = true; + // Decrease counter + util.stackFrame.loopCounter--; + // If we still have some left, start the substack + if (util.stackFrame.loopCounter >= 0) { + util.startSubstack(); + } + } else { + util.stackFrame.executedInFrame = false; + util.yieldFrame(); + } +}; + +Scratch3ControlBlocks.prototype.repeatUntil = function(args, util) { + // Only execute once per frame. + // When the substack finishes, `repeat` will be executed again and + // the second branch will be taken, yielding for the rest of the frame. + if (!util.stackFrame.executedInFrame) { + util.stackFrame.executedInFrame = true; + // If the condition is true, start the substack. + if (!args.CONDITION) { + util.startSubstack(); + } + } else { + util.stackFrame.executedInFrame = false; + util.yieldFrame(); } }; Scratch3ControlBlocks.prototype.forever = function(args, util) { - util.startSubstack(); + // Only execute once per frame. + // When the substack finishes, `forever` will be executed again and + // the second branch will be taken, yielding for the rest of the frame. + if (!util.stackFrame.executedInFrame) { + util.stackFrame.executedInFrame = true; + util.startSubstack(); + } else { + util.stackFrame.executedInFrame = false; + util.yieldFrame(); + } }; Scratch3ControlBlocks.prototype.wait = function(args) { @@ -51,8 +86,8 @@ Scratch3ControlBlocks.prototype.wait = function(args) { Scratch3ControlBlocks.prototype.if = function(args, util) { // Only execute one time. `if` will be returned to // when the substack finishes, but it shouldn't execute again. - if (util.stackFrame.executed === undefined) { - util.stackFrame.executed = true; + if (util.stackFrame.executedInFrame === undefined) { + util.stackFrame.executedInFrame = true; if (args.CONDITION) { util.startSubstack(); } @@ -62,8 +97,8 @@ Scratch3ControlBlocks.prototype.if = function(args, util) { Scratch3ControlBlocks.prototype.ifElse = function(args, util) { // Only execute one time. `ifElse` will be returned to // when the substack finishes, but it shouldn't execute again. - if (util.stackFrame.executed === undefined) { - util.stackFrame.executed = true; + if (util.stackFrame.executedInFrame === undefined) { + util.stackFrame.executedInFrame = true; if (args.CONDITION) { util.startSubstack(1); } else { diff --git a/src/blocks/scratch3_looks.js b/src/blocks/scratch3_looks.js new file mode 100644 index 000000000..33e1ef3bf --- /dev/null +++ b/src/blocks/scratch3_looks.js @@ -0,0 +1,98 @@ +function Scratch3LooksBlocks(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; +} + +/** + * Retrieve the block primitives implemented by this package. + * @return {Object.<string, Function>} Mapping of opcode to Function. + */ +Scratch3LooksBlocks.prototype.getPrimitives = function() { + return { + 'looks_say': this.say, + 'looks_sayforsecs': this.sayforsecs, + 'looks_think': this.think, + 'looks_thinkforsecs': this.sayforsecs, + 'looks_show': this.show, + 'looks_hide': this.hide, + 'looks_effectmenu': this.effectMenu, + 'looks_changeeffectby': this.changeEffect, + 'looks_seteffectto': this.setEffect, + 'looks_cleargraphiceffects': this.clearEffects, + 'looks_changesizeby': this.changeSize, + 'looks_setsizeto': this.setSize, + 'looks_size': this.getSize + }; +}; + +Scratch3LooksBlocks.prototype.say = function (args, util) { + util.target.setSay('say', args.MESSAGE); +}; + +Scratch3LooksBlocks.prototype.sayforsecs = function (args, util) { + util.target.setSay('say', args.MESSAGE); + return new Promise(function(resolve) { + setTimeout(function() { + // Clear say bubble and proceed. + util.target.setSay(); + resolve(); + }, 1000 * args.SECS); + }); +}; + +Scratch3LooksBlocks.prototype.think = function (args, util) { + util.target.setSay('think', args.MESSAGE); +}; + +Scratch3LooksBlocks.prototype.thinkforsecs = function (args, util) { + util.target.setSay('think', args.MESSAGE); + return new Promise(function(resolve) { + setTimeout(function() { + // Clear say bubble and proceed. + util.target.setSay(); + resolve(); + }, 1000 * args.SECS); + }); +}; + +Scratch3LooksBlocks.prototype.show = function (args, util) { + util.target.setVisible(true); +}; + +Scratch3LooksBlocks.prototype.hide = function (args, util) { + util.target.setVisible(false); +}; + +Scratch3LooksBlocks.prototype.effectMenu = function (args) { + return args.EFFECT.toLowerCase(); +}; + +Scratch3LooksBlocks.prototype.changeEffect = function (args, util) { + var newValue = args.CHANGE + util.target.effects[args.EFFECT]; + util.target.setEffect(args.EFFECT, newValue); +}; + +Scratch3LooksBlocks.prototype.setEffect = function (args, util) { + util.target.setEffect(args.EFFECT, args.VALUE); +}; + +Scratch3LooksBlocks.prototype.clearEffects = function (args, util) { + util.target.clearEffects(); +}; + +Scratch3LooksBlocks.prototype.changeSize = function (args, util) { + util.target.setSize(util.target.size + args.CHANGE); +}; + +Scratch3LooksBlocks.prototype.setSize = function (args, util) { + util.target.setSize(args.SIZE); +}; + +Scratch3LooksBlocks.prototype.getSize = function (args, util) { + return util.target.size; +}; + +module.exports = Scratch3LooksBlocks; diff --git a/src/blocks/scratch3_motion.js b/src/blocks/scratch3_motion.js new file mode 100644 index 000000000..730eee03f --- /dev/null +++ b/src/blocks/scratch3_motion.js @@ -0,0 +1,83 @@ +var MathUtil = require('../util/math-util'); + +function Scratch3MotionBlocks(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; +} + +/** + * Retrieve the block primitives implemented by this package. + * @return {Object.<string, Function>} Mapping of opcode to Function. + */ +Scratch3MotionBlocks.prototype.getPrimitives = function() { + return { + 'motion_movesteps': this.moveSteps, + 'motion_gotoxy': this.goToXY, + 'motion_turnright': this.turnRight, + 'motion_turnleft': this.turnLeft, + 'motion_pointindirection': this.pointInDirection, + 'motion_changexby': this.changeX, + 'motion_setx': this.setX, + 'motion_changeyby': this.changeY, + 'motion_sety': this.setY, + 'motion_xposition': this.getX, + 'motion_yposition': this.getY, + 'motion_direction': this.getDirection + }; +}; + +Scratch3MotionBlocks.prototype.moveSteps = function (args, util) { + var radians = MathUtil.degToRad(util.target.direction); + var dx = args.STEPS * Math.cos(radians); + var dy = args.STEPS * Math.sin(radians); + util.target.setXY(util.target.x + dx, util.target.y + dy); +}; + +Scratch3MotionBlocks.prototype.goToXY = function (args, util) { + util.target.setXY(args.X, args.Y); +}; + +Scratch3MotionBlocks.prototype.turnRight = function (args, util) { + util.target.setDirection(util.target.direction + args.DEGREES); +}; + +Scratch3MotionBlocks.prototype.turnLeft = function (args, util) { + util.target.setDirection(util.target.direction - args.DEGREES); +}; + +Scratch3MotionBlocks.prototype.pointInDirection = function (args, util) { + util.target.setDirection(args.DIRECTION); +}; + +Scratch3MotionBlocks.prototype.changeX = function (args, util) { + util.target.setXY(util.target.x + args.DX, util.target.y); +}; + +Scratch3MotionBlocks.prototype.setX = function (args, util) { + util.target.setXY(args.X, util.target.y); +}; + +Scratch3MotionBlocks.prototype.changeY = function (args, util) { + util.target.setXY(util.target.x, util.target.y + args.DY); +}; + +Scratch3MotionBlocks.prototype.setY = function (args, util) { + util.target.setXY(util.target.x, args.Y); +}; + +Scratch3MotionBlocks.prototype.getX = function (args, util) { + return util.target.x; +}; + +Scratch3MotionBlocks.prototype.getY = function (args, util) { + return util.target.y; +}; + +Scratch3MotionBlocks.prototype.getDirection = function (args, util) { + return util.target.direction; +}; + +module.exports = Scratch3MotionBlocks; diff --git a/src/blocks/scratch3_operators.js b/src/blocks/scratch3_operators.js index 75077061c..fa43950ae 100644 --- a/src/blocks/scratch3_operators.js +++ b/src/blocks/scratch3_operators.js @@ -1,5 +1,3 @@ -var Promise = require('promise'); - function Scratch3OperatorsBlocks(runtime) { /** * The runtime instantiating this block package. @@ -15,6 +13,9 @@ function Scratch3OperatorsBlocks(runtime) { Scratch3OperatorsBlocks.prototype.getPrimitives = function() { return { 'math_number': this.number, + 'math_positive_number': this.number, + 'math_whole_number': this.number, + 'math_angle': this.number, 'text': this.text, 'operator_add': this.add, 'operator_subtract': this.subtract, @@ -31,7 +32,12 @@ Scratch3OperatorsBlocks.prototype.getPrimitives = function() { }; Scratch3OperatorsBlocks.prototype.number = function (args) { - return Number(args.NUM); + var num = Number(args.NUM); + if (num !== num) { + // NaN + return 0; + } + return num; }; Scratch3OperatorsBlocks.prototype.text = function (args) { @@ -79,16 +85,16 @@ Scratch3OperatorsBlocks.prototype.not = function (args) { }; Scratch3OperatorsBlocks.prototype.random = function (args) { - // As a demo, this implementation of random returns after 1 second of yield. - // @todo Match Scratch 2.0 implementation with int-truncation. - // See: http://bit.ly/1Qc0GzC - var examplePromise = new Promise(function(resolve) { - setTimeout(function() { - var res = (Math.random() * (args.TO - args.FROM)) + args.FROM; - resolve(res); - }, 1000); - }); - return examplePromise; + var low = args.FROM <= args.TO ? args.FROM : args.TO; + var high = args.FROM <= args.TO ? args.TO : args.FROM; + if (low == high) return low; + // If both low and high are ints, truncate the result to an int. + var lowInt = low == parseInt(low); + var highInt = high == parseInt(high); + if (lowInt && highInt) { + return low + parseInt(Math.random() * ((high + 1) - low)); + } + return (Math.random() * (high - low)) + low; }; module.exports = Scratch3OperatorsBlocks; diff --git a/src/engine/execute.js b/src/engine/execute.js index 188f0bf52..8d750eee3 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -7,12 +7,13 @@ var Thread = require('./thread'); */ var execute = function (sequencer, thread) { var runtime = sequencer.runtime; + var target = runtime.targetForThread(thread); // Current block to execute is the one on the top of the stack. var currentBlockId = thread.peekStack(); var currentStackFrame = thread.peekStackFrame(); - var opcode = runtime.blocks.getOpcode(currentBlockId); + var opcode = target.blocks.getOpcode(currentBlockId); if (!opcode) { console.warn('Could not get opcode for block: ' + currentBlockId); @@ -29,13 +30,13 @@ var execute = function (sequencer, thread) { var argValues = {}; // Add all fields on this block to the argValues. - var fields = runtime.blocks.getFields(currentBlockId); + var fields = target.blocks.getFields(currentBlockId); for (var fieldName in fields) { argValues[fieldName] = fields[fieldName].value; } // Recursively evaluate input blocks. - var inputs = runtime.blocks.getInputs(currentBlockId); + var inputs = target.blocks.getInputs(currentBlockId); for (var inputName in inputs) { var input = inputs[inputName]; var inputBlockId = input.block; @@ -62,14 +63,21 @@ var execute = function (sequencer, thread) { var primitiveReportedValue = null; primitiveReportedValue = blockFunction(argValues, { - yield: thread.yield.bind(thread), + yield: function() { + thread.setStatus(Thread.STATUS_YIELD); + }, + yieldFrame: function() { + thread.setStatus(Thread.STATUS_YIELD_FRAME); + }, done: function() { + thread.setStatus(Thread.STATUS_RUNNING); sequencer.proceedThread(thread); }, stackFrame: currentStackFrame.executionContext, startSubstack: function (substackNum) { sequencer.stepToSubstack(thread, substackNum); - } + }, + target: target }); // Deal with any reported value. @@ -82,21 +90,33 @@ var execute = function (sequencer, thread) { if (isPromise) { if (thread.status === Thread.STATUS_RUNNING) { // Primitive returned a promise; automatically yield thread. - thread.status = Thread.STATUS_YIELD; + thread.setStatus(Thread.STATUS_YIELD); } // Promise handlers primitiveReportedValue.then(function(resolvedValue) { // Promise resolved: the primitive reported a value. thread.pushReportedValue(resolvedValue); + // Report the value visually if necessary. + if (typeof resolvedValue !== 'undefined' && + thread.peekStack() === thread.topBlock) { + runtime.visualReport(thread.peekStack(), resolvedValue); + } + thread.setStatus(Thread.STATUS_RUNNING); sequencer.proceedThread(thread); }, function(rejectionReason) { // Promise rejected: the primitive had some error. // Log it and proceed. console.warn('Primitive rejected promise: ', rejectionReason); + thread.setStatus(Thread.STATUS_RUNNING); sequencer.proceedThread(thread); }); } else if (thread.status === Thread.STATUS_RUNNING) { thread.pushReportedValue(primitiveReportedValue); + // Report the value visually if necessary. + if (typeof primitiveReportedValue !== 'undefined' && + thread.peekStack() === thread.topBlock) { + runtime.visualReport(thread.peekStack(), primitiveReportedValue); + } } }; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 8c4c862e1..f92a91db7 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -6,23 +6,25 @@ var util = require('util'); var defaultBlockPackages = { 'scratch3_control': require('../blocks/scratch3_control'), 'scratch3_event': require('../blocks/scratch3_event'), + 'scratch3_looks': require('../blocks/scratch3_looks'), + 'scratch3_motion': require('../blocks/scratch3_motion'), 'scratch3_operators': require('../blocks/scratch3_operators') }; /** - * Manages blocks, stacks, and the sequencer. - * @param {!Blocks} blocks Blocks instance for this runtime. + * Manages targets, stacks, and the sequencer. + * @param {!Array.<Target>} targets List of targets for this runtime. */ -function Runtime (blocks) { +function Runtime (targets) { // Bind event emitter EventEmitter.call(this); // State for the runtime /** - * Block management and storage + * Target management and storage. */ - this.blocks = blocks; + this.targets = targets; /** * A list of threads that are currently running in the VM. @@ -67,6 +69,12 @@ Runtime.BLOCK_GLOW_ON = 'BLOCK_GLOW_ON'; */ Runtime.BLOCK_GLOW_OFF = 'BLOCK_GLOW_OFF'; +/** + * Event name for visual value report. + * @const {string} + */ +Runtime.VISUAL_REPORT = 'VISUAL_REPORT'; + /** * Inherit from EventEmitter */ @@ -75,7 +83,7 @@ util.inherits(Runtime, EventEmitter); /** * How rapidly we try to step threads, in ms. */ -Runtime.THREAD_STEP_INTERVAL = 1000 / 30; +Runtime.THREAD_STEP_INTERVAL = 1000 / 60; // ----------------------------------------------------------------------------- @@ -163,32 +171,13 @@ Runtime.prototype.greenFlag = function () { this._removeThread(this.threads[i]); } // Add all top stacks with green flag - var stacks = this.blocks.getStacks(); - for (var j = 0; j < stacks.length; j++) { - var topBlock = stacks[j]; - if (this.blocks.getBlock(topBlock).opcode === 'event_whenflagclicked') { - this._pushThread(stacks[j]); - } - } -}; - -/** - * Distance sensor hack - */ -Runtime.prototype.startDistanceSensors = function () { - // Add all top stacks with distance sensor - var stacks = this.blocks.getStacks(); - for (var j = 0; j < stacks.length; j++) { - var topBlock = stacks[j]; - if (this.blocks.getBlock(topBlock).opcode === - 'wedo_whendistanceclose') { - var alreadyRunning = false; - for (var k = 0; k < this.threads.length; k++) { - if (this.threads[k].topBlock === topBlock) { - alreadyRunning = true; - } - } - if (!alreadyRunning) { + for (var t = 0; t < this.targets.length; t++) { + var target = this.targets[t]; + var stacks = target.blocks.getStacks(); + for (var j = 0; j < stacks.length; j++) { + var topBlock = stacks[j]; + if (target.blocks.getBlock(topBlock).opcode === + 'event_whenflagclicked') { this._pushThread(stacks[j]); } } @@ -228,9 +217,6 @@ Runtime.prototype._step = function () { * @param {boolean} isGlowing True to turn on glow; false to turn off. */ Runtime.prototype.glowBlock = function (blockId, isGlowing) { - if (!this.blocks.getBlock(blockId)) { - return; - } if (isGlowing) { this.emit(Runtime.BLOCK_GLOW_ON, blockId); } else { @@ -239,28 +225,45 @@ Runtime.prototype.glowBlock = function (blockId, isGlowing) { }; /** - * setInterval implementation that works in a WebWorker or not. - * @param {?Function} fcn Function to call. - * @param {number} interval Interval at which to call it. - * @return {number} Value returned by setInterval. + * Emit value for reporter to show in the blocks. + * @param {string} blockId ID for the block. + * @param {string} value Value to show associated with the block. */ -Runtime.prototype._setInterval = function(fcn, interval) { - var setInterval = null; - if (typeof window !== 'undefined' && window.setInterval) { - setInterval = window.setInterval; - } else if (typeof self !== 'undefined' && self.setInterval) { - setInterval = self.setInterval; - } else { - return; +Runtime.prototype.visualReport = function (blockId, value) { + this.emit(Runtime.VISUAL_REPORT, blockId, String(value)); +}; + +/** + * Return the Target for a particular thread. + * @param {!Thread} thread Thread to determine target for. + * @return {?Target} Target object, if one exists. + */ +Runtime.prototype.targetForThread = function (thread) { + // @todo This is a messy solution, + // but prevents having circular data references. + // Have a map or some other way to associate target with threads. + for (var t = 0; t < this.targets.length; t++) { + var target = this.targets[t]; + if (target.blocks.getBlock(thread.topBlock)) { + return target; + } + } +}; + +/** + * Handle an animation frame from the main thread. + */ +Runtime.prototype.animationFrame = function () { + if (self.renderer) { + self.renderer.draw(); } - return setInterval(fcn, interval); }; /** * Set up timers to repeatedly step in a browser */ Runtime.prototype.start = function () { - this._setInterval(function() { + self.setInterval(function() { this._step(); }.bind(this), Runtime.THREAD_STEP_INTERVAL); }; diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 1498d6320..175fc68d1 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -36,6 +36,13 @@ Sequencer.prototype.stepThreads = function (threads) { var inactiveThreads = []; // If all of the threads are yielding, we should yield. var numYieldingThreads = 0; + // Clear all yield statuses that were for the previous frame. + for (var t = 0; t < threads.length; t++) { + if (threads[t].status === Thread.STATUS_YIELD_FRAME) { + threads[t].setStatus(Thread.STATUS_RUNNING); + } + } + // While there are still threads to run and we are within WORK_TIME, // continue executing threads. while (threads.length > 0 && @@ -51,9 +58,10 @@ Sequencer.prototype.stepThreads = function (threads) { if (activeThread.status === Thread.STATUS_RUNNING) { // Normal-mode thread: step. this.startThread(activeThread); - } else if (activeThread.status === Thread.STATUS_YIELD) { + } else if (activeThread.status === Thread.STATUS_YIELD || + activeThread.status === Thread.STATUS_YIELD_FRAME) { // Yielding thread: do nothing for this step. - continue; + numYieldingThreads++; } if (activeThread.stack.length === 0 && activeThread.status === Thread.STATUS_DONE) { @@ -77,17 +85,14 @@ Sequencer.prototype.stepThreads = function (threads) { Sequencer.prototype.startThread = function (thread) { var currentBlockId = thread.peekStack(); if (!currentBlockId) { - // A "null block" - empty substack. Pop the stack. + // A "null block" - empty substack. + // Yield for the frame. thread.popStack(); - thread.status = Thread.STATUS_DONE; + thread.setStatus(Thread.STATUS_YIELD_FRAME); return; } - // Start showing run feedback in the editor. - this.runtime.glowBlock(currentBlockId, true); - // Execute the current block execute(this, thread); - // If the block executed without yielding and without doing control flow, // move to done. if (thread.status === Thread.STATUS_RUNNING && @@ -106,7 +111,7 @@ Sequencer.prototype.stepToSubstack = function (thread, substackNum) { substackNum = 1; } var currentBlockId = thread.peekStack(); - var substackId = this.runtime.blocks.getSubstack( + var substackId = this.runtime.targetForThread(thread).blocks.getSubstack( currentBlockId, substackNum ); @@ -147,23 +152,17 @@ Sequencer.prototype.stepToReporter = function (thread, blockId, inputName) { Sequencer.prototype.proceedThread = function (thread) { var currentBlockId = thread.peekStack(); // Mark the status as done and proceed to the next block. - this.runtime.glowBlock(currentBlockId, false); - // If the block was yielding, move back to running state. - thread.status = Thread.STATUS_RUNNING; // Pop from the stack - finished this level of execution. thread.popStack(); // Push next connected block, if there is one. - var nextBlockId = this.runtime.blocks.getNextBlock(currentBlockId); + var nextBlockId = (this.runtime.targetForThread(thread). + blocks.getNextBlock(currentBlockId)); if (nextBlockId) { thread.pushStack(nextBlockId); } - // Pop from the stack until we have a next block. - while (thread.peekStack() === null && thread.stack.length > 0) { - thread.popStack(); - } - // If we still can't find a next block to run, mark the thread as done. - if (thread.peekStack() === null) { - thread.status = Thread.STATUS_DONE; + // If we can't find a next block to run, mark the thread as done. + if (!thread.peekStack()) { + thread.setStatus(Thread.STATUS_DONE); } }; diff --git a/src/engine/target.js b/src/engine/target.js new file mode 100644 index 000000000..ad7d192c6 --- /dev/null +++ b/src/engine/target.js @@ -0,0 +1,20 @@ +var Blocks = require('./blocks'); + +/** + * @fileoverview + * A Target is an abstract "code-running" object for the Scratch VM. + * Examples include sprites/clones or potentially physical-world devices. + */ + +/** + * @param {?Blocks} blocks Blocks instance for the blocks owned by this target. + * @constructor + */ +function Target (blocks) { + if (!blocks) { + blocks = new Blocks(this); + } + this.blocks = blocks; +} + +module.exports = Target; diff --git a/src/engine/thread.js b/src/engine/thread.js index e5c4df2e4..d1bd73fc0 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -46,12 +46,18 @@ Thread.STATUS_RUNNING = 0; */ Thread.STATUS_YIELD = 1; +/** + * Thread status for a single-frame yield. + * @const + */ +Thread.STATUS_YIELD_FRAME = 2; + /** * Thread status for a finished/done thread. * Thread is in this state when there are no more blocks to execute. * @const */ -Thread.STATUS_DONE = 2; +Thread.STATUS_DONE = 3; /** * Push stack and update stack frames appropriately. @@ -118,10 +124,11 @@ Thread.prototype.pushReportedValue = function (value) { }; /** - * Yields the thread. + * Set thread status. + * @param {number} status Enum representing thread status. */ -Thread.prototype.yield = function () { - this.status = Thread.STATUS_YIELD; +Thread.prototype.setStatus = function (status) { + this.status = status; }; module.exports = Thread; diff --git a/src/index.js b/src/index.js index 174f05b1c..db99f1d15 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ var EventEmitter = require('events'); var util = require('util'); -var Blocks = require('./engine/blocks'); +var Sprite = require('./sprites/sprite'); var Runtime = require('./engine/runtime'); /** @@ -21,18 +21,23 @@ function VirtualMachine () { // Bind event emitter and runtime to VM instance // @todo Post message (Web Worker) polyfill EventEmitter.call(instance); - instance.blocks = new Blocks(); - instance.runtime = new Runtime(instance.blocks); + // @todo support multiple targets/sprites. + // This is just a demo/example. + var exampleSprite = new Sprite(); + exampleSprite.createClone(); + var exampleTargets = [exampleSprite.clones[0]]; + instance.exampleSprite = exampleSprite; + instance.runtime = new Runtime(exampleTargets); /** * Event listeners for scratch-blocks. */ instance.blockListener = ( - instance.blocks.generateBlockListener(false, instance.runtime) + exampleSprite.blocks.generateBlockListener(false, instance.runtime) ); instance.flyoutBlockListener = ( - instance.blocks.generateBlockListener(true, instance.runtime) + exampleSprite.blocks.generateBlockListener(true, instance.runtime) ); // Runtime emits are passed along as VM emits. @@ -48,6 +53,9 @@ function VirtualMachine () { instance.runtime.on(Runtime.BLOCK_GLOW_OFF, function (id) { instance.emit(Runtime.BLOCK_GLOW_OFF, {id: id}); }); + instance.runtime.on(Runtime.VISUAL_REPORT, function (id, value) { + instance.emit(Runtime.VISUAL_REPORT, {id: id, value: value}); + }); } /** @@ -81,17 +89,28 @@ VirtualMachine.prototype.stopAll = function () { */ VirtualMachine.prototype.getPlaygroundData = function () { this.emit('playgroundData', { - blocks: this.blocks, + blocks: this.exampleSprite.blocks, threads: this.runtime.threads }); }; +/** + * Handle an animation frame. + */ +VirtualMachine.prototype.animationFrame = function () { + this.runtime.animationFrame(); +}; + /* * Worker handlers: for all public methods available above, * we must also provide a message handler in case the VM is run * from a worker environment. */ if (ENV_WORKER) { + self.importScripts( + './node_modules/scratch-render/render-worker.js' + ); + self.renderer = new self.RenderWebGLWorker(); self.vmInstance = new VirtualMachine(); self.onmessage = function (e) { var messageData = e.data; @@ -114,12 +133,19 @@ if (ENV_WORKER) { case 'getPlaygroundData': self.postMessage({ method: 'playgroundData', - blocks: self.vmInstance.blocks, + blocks: self.vmInstance.exampleSprite.blocks, threads: self.vmInstance.runtime.threads }); break; + case 'animationFrame': + self.vmInstance.animationFrame(); + break; default: - throw 'Unknown method' + messageData.method; + if (e.data.id == 'RendererConnected') { + //initRenderWorker(); + } + self.renderer.onmessage(e); + break; } }; // Bind runtime's emitted events to postmessages. @@ -135,6 +161,9 @@ if (ENV_WORKER) { self.vmInstance.runtime.on(Runtime.BLOCK_GLOW_OFF, function (id) { self.postMessage({method: Runtime.BLOCK_GLOW_OFF, id: id}); }); + self.vmInstance.runtime.on(Runtime.VISUAL_REPORT, function (id, value) { + self.postMessage({method: Runtime.VISUAL_REPORT, id: id, value: value}); + }); } /** diff --git a/src/sprites/clone.js b/src/sprites/clone.js new file mode 100644 index 000000000..8370ac73e --- /dev/null +++ b/src/sprites/clone.js @@ -0,0 +1,187 @@ +var util = require('util'); +var MathUtil = require('../util/math-util'); +var Target = require('../engine/target'); + +/** + * Clone (instance) of a sprite. + * @param {!Blocks} spriteBlocks Reference to the sprite's blocks. + * @constructor + */ +function Clone(spriteBlocks) { + Target.call(this, spriteBlocks); + /** + * Reference to the global renderer for this VM, if one exists. + * @type {?RenderWebGLWorker} + */ + this.renderer = null; + // If this is not true, there is no renderer (e.g., running in a test env). + if (typeof self !== 'undefined' && self.renderer) { + // Pull from `self.renderer`. + this.renderer = self.renderer; + } + /** + * ID of the drawable for this clone returned by the renderer, if rendered. + * @type {?Number} + */ + this.drawableID = null; + + this.initDrawable(); +} +util.inherits(Clone, Target); + +/** + * Create a clone's drawable with the this.renderer. + */ +Clone.prototype.initDrawable = function () { + if (this.renderer) { + var createPromise = this.renderer.createDrawable(); + var instance = this; + createPromise.then(function (id) { + instance.drawableID = id; + }); + } +}; + +// Clone-level properties. +/** + * Scratch X coordinate. Currently should range from -240 to 240. + * @type {!number} + */ +Clone.prototype.x = 0; + +/** + * Scratch Y coordinate. Currently should range from -180 to 180. + * @type {!number} + */ +Clone.prototype.y = 0; + +/** + * Scratch direction. Currently should range from -179 to 180. + * @type {!number} + */ +Clone.prototype.direction = 90; + +/** + * Whether the clone is currently visible. + * @type {!boolean} + */ +Clone.prototype.visible = true; + +/** + * Size of clone as a percent of costume size. Ranges from 5% to 535%. + * @type {!number} + */ +Clone.prototype.size = 100; + +/** + * Map of current graphic effect values. + * @type {!Object.<string, number>} + */ +Clone.prototype.effects = { + 'color': 0, + 'fisheye': 0, + 'whirl': 0, + 'pixelate': 0, + 'mosaic': 0, + 'brightness': 0, + 'ghost': 0 +}; +// End clone-level properties. + +/** + * Set the X and Y coordinates of a clone. + * @param {!number} x New X coordinate of clone, in Scratch coordinates. + * @param {!number} y New Y coordinate of clone, in Scratch coordinates. + */ +Clone.prototype.setXY = function (x, y) { + this.x = x; + this.y = y; + if (this.renderer) { + this.renderer.updateDrawableProperties(this.drawableID, { + position: [this.x, this.y] + }); + } +}; + +/** + * Set the direction of a clone. + * @param {!number} direction New direction of clone. + */ +Clone.prototype.setDirection = function (direction) { + // Keep direction between -179 and +180. + this.direction = MathUtil.wrapClamp(direction, -179, 180); + if (this.renderer) { + this.renderer.updateDrawableProperties(this.drawableID, { + direction: this.direction + }); + } +}; + +/** + * Set a say bubble on this clone. + * @param {?string} type Type of say bubble: "say", "think", or null. + * @param {?string} message Message to put in say bubble. + */ +Clone.prototype.setSay = function (type, message) { + // @todo: Render to stage. + if (!type || !message) { + console.log('Clearing say bubble'); + return; + } + console.log('Setting say bubble:', type, message); +}; + +/** + * Set visibility of the clone; i.e., whether it's shown or hidden. + * @param {!boolean} visible True if the sprite should be shown. + */ +Clone.prototype.setVisible = function (visible) { + this.visible = visible; + if (this.renderer) { + this.renderer.updateDrawableProperties(this.drawableID, { + visible: this.visible + }); + } +}; + +/** + * Set size of the clone, as a percentage of the costume size. + * @param {!number} size Size of clone, from 5 to 535. + */ +Clone.prototype.setSize = function (size) { + // Keep size between 5% and 535%. + this.size = MathUtil.clamp(size, 5, 535); + if (this.renderer) { + this.renderer.updateDrawableProperties(this.drawableID, { + scale: [this.size, this.size] + }); + } +}; + +/** + * Set a particular graphic effect on this clone. + * @param {!string} effectName Name of effect (see `Clone.prototype.effects`). + * @param {!number} value Numerical magnitude of effect. + */ +Clone.prototype.setEffect = function (effectName, value) { + this.effects[effectName] = value; + if (this.renderer) { + var props = {}; + props[effectName] = this.effects[effectName]; + this.renderer.updateDrawableProperties(this.drawableID, props); + } +}; + +/** + * Clear all graphic effects on this clone. + */ +Clone.prototype.clearEffects = function () { + for (var effectName in this.effects) { + this.effects[effectName] = 0; + } + if (this.renderer) { + this.renderer.updateDrawableProperties(this.drawableID, this.effects); + } +}; + +module.exports = Clone; diff --git a/src/sprites/sprite.js b/src/sprites/sprite.js new file mode 100644 index 000000000..b4abdbbb5 --- /dev/null +++ b/src/sprites/sprite.js @@ -0,0 +1,29 @@ +var Clone = require('./clone'); +var Blocks = require('../engine/blocks'); + +/** + * Sprite to be used on the Scratch stage. + * All clones of a sprite have shared blocks, shared costumes, shared variables. + * @param {?Blocks} blocks Shared blocks object for all clones of sprite. + * @constructor + */ +function Sprite (blocks) { + if (!blocks) { + // Shared set of blocks for all clones. + blocks = new Blocks(); + } + this.blocks = blocks; + this.clones = []; +} + +/** + * Create a clone of this sprite. + * @returns {!Clone} Newly created clone. + */ +Sprite.prototype.createClone = function () { + var newClone = new Clone(this.blocks); + this.clones.push(newClone); + return newClone; +}; + +module.exports = Sprite; diff --git a/src/util/math-util.js b/src/util/math-util.js new file mode 100644 index 000000000..b53698ef1 --- /dev/null +++ b/src/util/math-util.js @@ -0,0 +1,48 @@ +function MathUtil () {} + +/** + * Convert a value from degrees to radians. + * @param {!number} deg Value in degrees. + * @return {!number} Equivalent value in radians. + */ +MathUtil.degToRad = function (deg) { + return (Math.PI * (90 - deg)) / 180; +}; + +/** + * Convert a value from radians to degrees. + * @param {!number} rad Value in radians. + * @return {!number} Equivalent value in degrees. + */ +MathUtil.radToDeg = function (rad) { + return rad * 180 / Math.PI; +}; + +/** + * Clamp a number between two limits. + * If n < min, return min. If n > max, return max. Else, return n. + * @param {!number} n Number to clamp. + * @param {!number} min Minimum limit. + * @param {!number} max Maximum limit. + * @return {!number} Value of n clamped to min and max. + */ +MathUtil.clamp = function (n, min, max) { + return Math.min(Math.max(n, min), max); +}; + +/** + * Keep a number between two limits, wrapping "extra" into the range. + * e.g., wrapClamp(7, 1, 5) == 2 + * wrapClamp(0, 1, 5) == 5 + * wrapClamp(-11, -10, 6) == 6, etc. + * @param {!number} n Number to wrap. + * @param {!number} min Minimum limit. + * @param {!number} max Maximum limit. + * @return {!number} Value of n wrapped between min and max. + */ +MathUtil.wrapClamp = function (n, min, max) { + var range = (max - min) + 1; + return n - Math.floor((n - min) / range) * range; +}; + +module.exports = MathUtil; diff --git a/src/worker.js b/src/worker.js index e96c57bf3..70e6da5cb 100644 --- a/src/worker.js +++ b/src/worker.js @@ -63,6 +63,10 @@ VirtualMachine.prototype.stopAll = function () { this.vmWorker.postMessage({method: 'stopAll'}); }; +VirtualMachine.prototype.animationFrame = function () { + this.vmWorker.postMessage({method: 'animationFrame'}); +}; + /** * Export and bind to `window` */