diff --git a/Makefile b/Makefile
index 835c16f7..c3614f5a 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 00000000..42edce43
--- /dev/null
+++ b/index.html
@@ -0,0 +1,7 @@
+
+
+
+
Thread explorer
@@ -28,58 +33,536 @@
-
-
-
-
-
-
+
+
+
+
+ 10
+
+
+
+
+
+
+ 15
+
+
+
+
+
+
+ 15
+
+
+
+
+
+
+ 90
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+ 0
+
+
+
+
+ 0
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hello!
+
+
+
+
+ 2
+
+
+
+
+
+
+ Hello!
+
+
+
+
+
+
+ Hmm...
+
+
+
+
+ 2
+
+
+
+
+
+
+ Hmm...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+ 100
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+ 0.25
+
+
+
+
+
+
+ 0.25
+
+
+
+
+
+
+ 1
+
+
+
+
+ 0.5
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+ -10
+
+
+
+
+
+
+ 100
+
+
+
+
+
+
+
+ 20
+
+
+
+
+
+
+ 60
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+ 50
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ What's your name?
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
- 1
+ 50
-
-
-
-
- 4
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
@@ -193,7 +676,7 @@
-
+
1
@@ -239,8 +722,9 @@
-
-
+
+
+
@@ -249,6 +733,9 @@
+
+
+
diff --git a/playground/playground.css b/playground/playground.css
index 451a7cad..a15e1d81 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 be683ceb..47631b20 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 e13edefd..d3cd2fdf 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 00000000..33e1ef3b
--- /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.
} 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 00000000..730eee03
--- /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.} 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 75077061..fa43950a 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 188f0bf5..8d750eee 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 8c4c862e..f92a91db 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.} 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 1498d632..175fc68d 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 00000000..ad7d192c
--- /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 e5c4df2e..d1bd73fc 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 174f05b1..db99f1d1 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 00000000..8370ac73
--- /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.}
+ */
+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 00000000..b4abdbbb
--- /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 00000000..b53698ef
--- /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 e96c57bf..70e6da5c 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`
*/