summaryrefslogtreecommitdiff
path: root/threads.js
diff options
context:
space:
mode:
Diffstat (limited to 'threads.js')
-rw-r--r--threads.js537
1 files changed, 512 insertions, 25 deletions
diff --git a/threads.js b/threads.js
index af899dc..8e4f0c4 100644
--- a/threads.js
+++ b/threads.js
@@ -1,5 +1,5 @@
/*
-
+
threads.js
a tail call optimized blocks-based programming language interpreter
@@ -128,6 +128,15 @@ function snapEquals(a, b) {
return x === y;
}
+// stricter alternative to parseFloat
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat/
+function filterFloat(value) {
+ if(/^(\-|\+)?([0-9]+(\.[0-9]+)?|Infinity)$/
+ .test(value))
+ return Number(value);
+ return NaN;
+}
+
// ThreadManager ///////////////////////////////////////////////////////
function ThreadManager() {
@@ -441,11 +450,20 @@ Process.prototype.pause = function () {
if (this.context && this.context.startTime) {
this.pauseOffset = Date.now() - this.context.startTime;
}
+ if (this.context.activeNote) {
+ this.context.activeNote.stop();
+ }
};
Process.prototype.resume = function () {
this.isPaused = false;
this.pauseOffset = null;
+ if (this.context.activeNote) {
+ if (this.context.activeNote.oscillator === null) {
+ // prevents Note from resuming twice
+ this.context.activeNote.play();
+ }
+ }
};
Process.prototype.pauseStep = function () {
@@ -735,6 +753,13 @@ Process.prototype.expectReport = function () {
// Process Exception Handling
+Process.prototype.checkIfList = function (maybeList) {
+ if (!(maybeList instanceof List)) {
+ maybeList = new List();
+ }
+ return maybeList;
+};
+
Process.prototype.handleError = function (error, element) {
var m = element;
this.stop();
@@ -1157,7 +1182,8 @@ Process.prototype.doSetVar = function (varName, value) {
if (name.expression.selector === 'reportGetVar') {
name.variables.setVar(
name.expression.blockSpec,
- value
+ value,
+ this.blockReceiver()
);
return;
}
@@ -1173,7 +1199,8 @@ Process.prototype.doChangeVar = function (varName, value) {
if (name.expression.selector === 'reportGetVar') {
name.variables.changeVar(
name.expression.blockSpec,
- value
+ value,
+ this.blockReceiver()
);
return;
}
@@ -1223,7 +1250,7 @@ Process.prototype.doShowVar = function (varName) {
}
// if no watcher exists, create a new one
isGlobal = contains(
- this.homeContext.receiver.variables.parentFrame.names(),
+ this.homeContext.receiver.globalVariables().names(),
varName
);
if (isGlobal || target.owner) {
@@ -1302,6 +1329,77 @@ Process.prototype.doRemoveTemporaries = function () {
}
};
+// Peer to peer primitives
+
+Process.prototype.sendPeerMessage = function (message, peer) {
+ var myself = this;
+ var stage = this.homeContext.receiver.parentThatIsA(StageMorph),
+ ide = this.homeContext.receiver.parentThatIsA(IDE_Morph);
+
+ if (peer instanceof List) {
+ peer.asArray().forEach(function (singlePeer) {
+ myself.sendPeerMessage(message, singlePeer);
+ });
+ return;
+ }
+ if (peer == ide.peer.id) {
+ stage.newPeerMessage(message, peer);
+ return;
+ }
+
+ var connection = ide.peer.connect(peer, {reliable: true});
+ connection.on('open', function () {
+ var data;
+ if (typeof message == "string") {
+ data = message;
+ } else if (typeof message == "function") {
+ data = message.toString();
+ } else {
+ data = ide.serializer.serialize(message);
+ }
+ connection.send(data);
+ });
+};
+
+Process.prototype.reportPeerList = function () {
+ var myself = this;
+
+ if (!this.context.wait) {
+ this.context.wait = true;
+ var ide = this.homeContext.receiver.parentThatIsA(IDE_Morph);
+ ide.peer.listAllPeers(function (peers) {
+ myself.context.result = new List(peers);
+ });
+ } else if (this.context.result) {
+ return this.context.result;
+ }
+
+ this.pushContext('doYield');
+ this.pushContext();
+};
+
+Process.prototype.reportPeerId = function () {
+ var ide = this.homeContext.receiver.parentThatIsA(IDE_Morph);
+ return ide.peer.id;
+};
+
+// Process sprite inheritance primitives
+
+Process.prototype.doDeleteAttr = function (attrName) {
+ // currently only variables are deletable
+ var name = attrName,
+ rcvr = this.blockReceiver();
+
+ if (name instanceof Context) {
+ if (name.expression.selector === 'reportGetVar') {
+ name = name.expression.blockSpec;
+ }
+ }
+ if (contains(rcvr.inheritedVariableNames(true), name)) {
+ rcvr.deleteVariable(name);
+ }
+};
+
// Process lists primitives
Process.prototype.reportNewList = function (elements) {
@@ -1313,15 +1411,19 @@ Process.prototype.reportCONS = function (car, cdr) {
};
Process.prototype.reportCDR = function (list) {
+ list = this.checkIfList(list);
return list.cdr();
};
Process.prototype.doAddToList = function (element, list) {
+ list = this.checkIfList(list);
list.add(element);
};
Process.prototype.doDeleteFromList = function (index, list) {
var idx = index;
+ list = this.checkIfList(list);
+
if (this.inputOption(index) === 'all') {
return list.clear();
}
@@ -1338,6 +1440,8 @@ Process.prototype.doDeleteFromList = function (index, list) {
Process.prototype.doInsertInList = function (element, index, list) {
var idx = index;
+ list = this.checkIfList(list);
+
if (index === '') {
return null;
}
@@ -1352,6 +1456,8 @@ Process.prototype.doInsertInList = function (element, index, list) {
Process.prototype.doReplaceInList = function (index, list, element) {
var idx = index;
+ list = this.checkIfList(list);
+
if (index === '') {
return null;
}
@@ -1366,6 +1472,8 @@ Process.prototype.doReplaceInList = function (index, list, element) {
Process.prototype.reportListItem = function (index, list) {
var idx = index;
+ list = this.checkIfList(list);
+
if (index === '') {
return '';
}
@@ -1379,10 +1487,12 @@ Process.prototype.reportListItem = function (index, list) {
};
Process.prototype.reportListLength = function (list) {
+ list = this.checkIfList(list);
return list.length();
};
Process.prototype.reportListContainsItem = function (list, element) {
+ list = this.checkIfList(list);
return list.contains(element);
};
@@ -1438,7 +1548,7 @@ Process.prototype.doStopAll = function () {
if (this.homeContext.receiver) {
stage = this.homeContext.receiver.parentThatIsA(StageMorph);
if (stage) {
- stage.threads.resumeAll(stage);
+ //stage.threads.resumeAll(stage); // leads to a strange Note bug
stage.keysPressed = {};
stage.threads.stopAll();
stage.stopAllActiveSounds();
@@ -1641,6 +1751,8 @@ Process.prototype.reportMap = function (reporter, list) {
// documented in each of the variants' code (linked or arrayed) below
var next;
+ list = this.checkIfList(list);
+
if (list.isLinked) {
// this.context.inputs:
// [0] - reporter
@@ -1930,6 +2042,7 @@ Process.prototype.reportIsA = function (thing, typeString) {
Process.prototype.reportTypeOf = function (thing) {
// answer a string denoting the argument's type
var exp;
+
if (thing === null || (thing === undefined)) {
return 'nothing';
}
@@ -2249,6 +2362,20 @@ Process.prototype.reportTextSplit = function (string, delimiter) {
return new List(str.split(del));
};
+// Process notification operations
+
+Process.prototype.doNotify = function (title, content) {
+ // TODO webkitNotification
+ if (window.plugin) {
+ if (window.plugin.notification) {
+ window.plugin.notification.local.add({
+ message: content,
+ title: title
+ });
+ }
+ }
+};
+
// Process debugging
Process.prototype.alert = function (data) {
@@ -2490,6 +2617,35 @@ Process.prototype.reportColorIsTouchingColor = function (color1, color2) {
return false;
};
+Process.prototype.reportStreamingCamera = function () {
+ var stage = this.homeContext.receiver.parentThatIsA(StageMorph);
+ return stage.streamingCamera;
+};
+
+Process.prototype.reportCameraMotion = function () {
+ if (this.reportStreamingCamera()) {
+ var thisObj = this.blockReceiver();
+ var motionCanvas = this.getCameraMotionCanvas();
+ var motion = this.getCameraMotion(motionCanvas, thisObj);
+ if (motion > 10) {
+ return true;
+ }
+ }
+ return false;
+};
+
+Process.prototype.reportCameraDirection = function () {
+ if (this.reportStreamingCamera()) {
+ var thisObj = this.blockReceiver();
+ var motionCanvas = this.getCameraMotionCanvas();
+ var motion = this.getCameraMotion(motionCanvas, thisObj);
+ if (motion > 10) {
+ return this.getCameraDirection(motionCanvas);
+ }
+ }
+ return 0;
+};
+
Process.prototype.reportDistanceTo = function (name) {
var thisObj = this.blockReceiver(),
thatObj,
@@ -2667,6 +2823,114 @@ Process.prototype.reportDate = function (datefn) {
return result;
};
+Process.prototype.getCompassHeading = function () {
+ var stage = this.homeContext.receiver.parentThatIsA(StageMorph);
+
+ stage.compassHeading = null;
+ if (navigator.compass) {
+ navigator.compass.getCurrentHeading(
+ function (result) {
+ stage.compassHeading = result;
+ },
+ function () {
+ stage.compassHeading =
+ {'magneticHeading': 0, 'trueHeading': 0,
+ 'headingAccuracy': 0, 'timestamp': 0};
+ }
+ );
+ } else {
+ stage.compassHeading =
+ {'magneticHeading': 0, 'trueHeading': 0,
+ 'headingAccuracy': 0, 'timestamp': 0};
+ }
+};
+
+Process.prototype.reportCompassHeading = function () {
+ var stage = this.homeContext.receiver.parentThatIsA(StageMorph);
+
+ if (this.context.wait) {
+ if (stage.compassHeading !== null) {
+ return stage.compassHeading.magneticHeading;
+ }
+ } else {
+ this.context.wait = true;
+ this.getCompassHeading();
+ }
+ this.pushContext('doYield');
+ this.pushContext();
+};
+
+Process.prototype.getAcceleration = function () {
+ var stage = this.homeContext.receiver.parentThatIsA(StageMorph);
+
+ stage.acceleration = null;
+ if (navigator.accelerometer) {
+ navigator.accelerometer.getCurrentAcceleration(
+ function (result) {
+ stage.acceleration = result;
+ },
+ function () {
+ stage.acceleration =
+ {'timestamp': 0, 'z': 0, 'y': 0, 'x': 0};
+ }
+ );
+ } else {
+ stage.acceleration =
+ {'timestamp': 0, 'z': 0, 'y': 0, 'x': 0};
+ }
+};
+
+Process.prototype.reportAccelerationX = function () {
+ var stage = this.homeContext.receiver.parentThatIsA(StageMorph);
+
+ if (this.context.wait) {
+ if (stage.acceleration !== null) {
+ return stage.acceleration.x;
+ }
+ } else {
+ this.context.wait = true;
+ this.getAcceleration();
+ }
+ this.pushContext('doYield');
+ this.pushContext();
+};
+
+Process.prototype.reportAccelerationY = function () {
+ var stage = this.homeContext.receiver.parentThatIsA(StageMorph);
+
+ if (this.context.wait) {
+ if (stage.acceleration !== null) {
+ return stage.acceleration.y;
+ }
+ } else {
+ this.context.wait = true;
+ this.getAcceleration();
+ }
+ this.pushContext('doYield');
+ this.pushContext();
+};
+
+Process.prototype.reportAccelerationZ = function () {
+ var stage = this.homeContext.receiver.parentThatIsA(StageMorph);
+
+ if (this.context.wait) {
+ if (stage.acceleration !== null) {
+ return stage.acceleration.z;
+ }
+ } else {
+ this.context.wait = true;
+ this.getAcceleration();
+ }
+ this.pushContext('doYield');
+ this.pushContext();
+};
+
+Process.prototype.doVibrate = function (seconds) {
+ if ("vibrate" in navigator) {
+ window.navigator.vibrate(seconds * 1000);
+ }
+};
+
// Process code mapping
/*
@@ -2782,22 +3046,228 @@ Process.prototype.doPlayNote = function (pitch, beats) {
Process.prototype.doPlayNoteForSecs = function (pitch, secs) {
// interpolated
+ var receiver = this.homeContext.receiver;
+ var volume = receiver.volume;
+ var muted = receiver.parentThatIsA(StageMorph).muted;
+
+ if (muted === true) {
+ volume = 0;
+ }
+
if (!this.context.startTime) {
this.context.startTime = Date.now();
- this.context.activeNote = new Note(pitch);
+ this.context.activeNote = new Note(pitch, volume);
this.context.activeNote.play();
}
+
if ((Date.now() - this.context.startTime) >= (secs * 1000)) {
if (this.context.activeNote) {
this.context.activeNote.stop();
this.context.activeNote = null;
}
return null;
+ } else if (this.context.activeNote) {
+ if (this.context.activeNote.volume !== volume) {
+ this.context.activeNote.setVolume(volume);
+ }
+ }
+
+ this.pushContext('doYield');
+ this.pushContext();
+};
+
+// Process camera streaming primitives
+
+Process.prototype.doStreamCamera = function () {
+ if ((Date.now() - this.context.startTime) < 3000) {
+ // 20 FPS is enough
+ this.pushContext('doYield');
+ this.pushContext();
+ return;
+ }
+ var myself = this;
+ var video = this.context.activeStream || null;
+
+ var stage = this.homeContext.receiver.parentThatIsA(StageMorph);
+ if (!stage.trailsCanvas) {
+ stage.trailsCanvas = newCanvas(stage.dimensions);
+ }
+
+ error = function (msg) {
+ var err = { name: 'Camera', message: msg };
+ myself.handleError(err);
+ };
+
+ stage.streamingCamera = false;
+
+ if (video === null) {
+ video = document.createElement('video');
+ video.width = stage.dimensions.x;
+ video.height = stage.dimensions.y;
+
+ this.context.activeStream = video;
+
+ var videoObject = {'video': true, 'audio': false};
+
+ navigator.getUserMedia_ = (navigator.getUserMedia ||
+ navigator.webkitGetUserMedia ||
+ navigator.mozGetUserMedia ||
+ navigator.msGetUserMedia);
+
+ if (!! navigator.getUserMedia_) {
+ navigator.getUserMedia_(videoObject, function (stream) {
+ window.URL_ = window.URL || window.webkitURL;
+ video.src = window.URL_.createObjectURL(stream);
+ video.play();
+ }, error);
+ } else {
+ error('getUserMedia not supported');
+ }
+ stage.lastCameraCanvas = newCanvas(stage.dimensions);
+ } else {
+ var canvas = stage.trailsCanvas;
+ var context = canvas.getContext('2d');
+
+ if (video.readyState == 4) {
+ try {
+ // https://stackoverflow.com/questions/23840880/check-whether-canvas-is-black
+ // check whether lastCameraCanvas is white -> copy video canvas
+ // otherwise you would have a 'motion' when the first camera picture is loaded
+ var tmp = document.createElement('canvas'),
+ ctx = tmp.getContext('2d'), result;
+ tmp.width = tmp.height = 1;
+ ctx.drawImage(stage.lastCameraCanvas, 0, 0, 1, 1);
+ result = ctx.getImageData(0, 0, 1, 1);
+ if (result.data[0] + result.data[1] + result.data[2]
+ + result.data[3] === 0) {
+ stage.lastCameraCanvas.getContext('2d').
+ drawImage(video, 0, 0, video.width, video.height);
+ } else {
+ var dest = stage.lastCameraCanvas.getContext('2d');
+ dest.drawImage(stage.trailsCanvas, 0, 0);
+ }
+ context.drawImage(video, 0, 0, video.width, video.height);
+ stage.changed();
+ stage.streamingCamera = true;
+ } catch (e) {
+ if (e.name !== 'NS_ERROR_NOT_AVAILABLE') {
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=879717
+ throw e;
+ }
+ }
+ }
}
this.pushContext('doYield');
this.pushContext();
};
+Process.prototype.doStopCamera = function () {
+ var stage = this.homeContext.receiver.parentThatIsA(StageMorph);
+ if (stage) {
+ stage.streamingCamera = false;
+ stage.threads.processes.forEach(function (thread) {
+ if (thread.context) {
+ if (thread.context.activeStream) {
+ thread.popContext();
+ }
+ }
+ });
+ }
+};
+
+Process.prototype.getCameraMotionCanvas = function () {
+ // see https://www.adobe.com/devnet/html5/articles/javascript-motion-detection.html
+ fastAbs = function (value) // faster, but less acurate
+ { return (value ^ (value >> 31)) - (value >> 31); };
+
+ threshold = function (value)
+ { return (value > 0x15) ? 0xFF : 0; };
+
+ diff = function (target, data1, data2) {
+ if (data1.length != data2.length) return null;
+ var i = 0;
+ while (i < (data1.length * 0.25)) {
+ var average1 = (data1[4*i] + data1[4*i+1] + data1[4*i+2]) / 3;
+ var average2 = (data2[4*i] + data2[4*i+1] + data2[4*i+2]) / 3;
+ var diff = threshold(fastAbs(average1 - average2));
+ target[4*i] = diff;
+ target[4*i+1] = diff;
+ target[4*i+2] = diff;
+ target[4*i+3] = 0xFF;
+ ++i;
+ }
+ };
+
+ var stage = this.homeContext.receiver.parentThatIsA(StageMorph);
+ if (!stage.trailsCanvas || !stage.lastCameraCanvas) {
+ var canv = newCanvas(stage.dimensions);
+ var ctx = canv.getContext('2d');
+ ctx.fill();
+ return canv;
+ }
+
+ var canvasSource = stage.trailsCanvas;
+ var contextSource = canvasSource.getContext('2d');
+ var width = canvasSource.width,
+ height = canvasSource.height;
+ var sourceData = contextSource.getImageData(0, 0, width, height);
+ var lastImageData =
+ stage.lastCameraCanvas.getContext('2d').getImageData(0, 0, width, height);
+ var blendedData = contextSource.createImageData(width, height);
+ var blendedCanvas = newCanvas(stage.dimensions);
+ diff(blendedData.data, sourceData.data, lastImageData.data);
+ blendedCanvas.getContext('2d').putImageData(blendedData, 0, 0);
+ return blendedCanvas;
+};
+
+Process.prototype.getCameraMotion = function (motionCanvas, thisObj) {
+ var x = thisObj.xPosition() + 210, y = 170 - thisObj.yPosition();
+ // ouch! hardcoded ^ ^
+ var motionData = motionCanvas.getContext('2d').getImageData(
+ x, y, thisObj.width(), thisObj.height());
+ var i = 0, average = 0;
+ while (i < (motionData.data.length * 0.25)) {
+ average += (motionData.data[i*4] +
+ motionData.data[i*4+1] + motionData.data[i*4+2]) / 3;
+ ++i;
+ }
+ average = Math.round(average / (motionData.data.length * 0.25));
+ return average;
+};
+
+Process.prototype.getCameraDirection = function (motionCanvas) {
+ var stage = this.homeContext.receiver.parentThatIsA(StageMorph);
+ var canv = document.createElement('canvas'),
+ ctx = canv.getContext('2d');
+ var data = motionCanvas.getContext('2d')
+ .getImageData(0, 0, motionCanvas.width, motionCanvas.height);
+ // scale, saves some time
+ canv.width = motionCanvas.width / 10;
+ canv.height = motionCanvas.height / 10;
+ ctx.drawImage(motionCanvas, 0, 0, canv.width, canv.height);
+ var yval = 0, xval = 0, cnt = 0;
+ var motion, imageData = ctx.getImageData(0, 0, canv.width, canv.height);
+ // find the geometric center of the moving object
+ for (var j = 0; j < canv.height; j++) {
+ for (var k = 0; k < canv.width; k++) {
+ motion = imageData.data[(j*canv.width+k)*4];
+ yval += j * motion;
+ xval += k * motion;
+ cnt += motion;
+ }
+ }
+ // calculate the angle between this center and the last one
+ var resultX = Math.round(xval / cnt);
+ var resultY = Math.round(yval / cnt);
+ var diff = (resultX + resultY) -
+ (stage.lastCameraMotion.x - stage.lastCameraMotion.y);
+ var deg = 0;
+ deg = degrees(Math.atan2(resultX - stage.lastCameraMotion.x,
+ stage.lastCameraMotion.y - resultY));
+ stage.lastCameraMotion = new Point(resultX, resultY);
+ return deg;
+};
+
// Process constant input options
Process.prototype.inputOption = function (dta) {
@@ -2869,6 +3339,7 @@ Process.prototype.reportFrameCount = function () {
startValue initial value for interpolated operations
activeAudio audio buffer for interpolated operations, don't persist
activeNote audio oscillator for interpolated ops, don't persist
+ activeStream video element for camera streaming, don't persist
isCustomBlock marker for return ops
emptySlots caches the number of empty slots for reification
tag string or number to optionally identify the Context,
@@ -2895,6 +3366,7 @@ function Context(
this.startTime = null;
this.activeAudio = null;
this.activeNote = null;
+ this.activeStream = null;
this.isCustomBlock = false; // marks the end of a custom block's stack
this.emptySlots = 0; // used for block reification
this.tag = null; // lexical catch-tag for custom blocks
@@ -3115,35 +3587,50 @@ VariableFrame.prototype.silentFind = function (name) {
return null;
};
-VariableFrame.prototype.setVar = function (name, value) {
-/*
- change the specified variable if it exists
- else throw an error, because variables need to be
- declared explicitly (e.g. through a "script variables" block),
- before they can be accessed.
-*/
+VariableFrame.prototype.setVar = function (name, value, sender) {
+ // change the specified variable if it exists
+ // else throw an error, because variables need to be
+ // declared explicitly (e.g. through a "script variables" block),
+ // before they can be accessed.
+ // if the found frame is inherited by the sender sprite
+ // shadow it (create an explicit one for the sender)
+ // before setting the value ("create-on-write")
+
var frame = this.find(name);
if (frame) {
- frame.vars[name].value = value;
+ if (sender instanceof SpriteMorph &&
+ (frame.owner instanceof SpriteMorph) &&
+ (sender !== frame.owner)) {
+ sender.shadowVar(name, value);
+ } else {
+ frame.vars[name].value = value;
+ }
}
};
-VariableFrame.prototype.changeVar = function (name, delta) {
-/*
- change the specified variable if it exists
- else throw an error, because variables need to be
- declared explicitly (e.g. through a "script variables" block,
- before they can be accessed.
-*/
+VariableFrame.prototype.changeVar = function (name, delta, sender) {
+ // change the specified variable if it exists
+ // else throw an error, because variables need to be
+ // declared explicitly (e.g. through a "script variables" block,
+ // before they can be accessed.
+ // if the found frame is inherited by the sender sprite
+ // shadow it (create an explicit one for the sender)
+ // before changing the value ("create-on-write")
+
var frame = this.find(name),
- value;
+ value,
+ newValue;
if (frame) {
value = parseFloat(frame.vars[name].value);
- if (isNaN(value)) {
- frame.vars[name].value = delta;
+ newValue = isNaN(value) ? delta : value + parseFloat(delta);
+ if (sender instanceof SpriteMorph &&
+ (frame.owner instanceof SpriteMorph) &&
+ (sender !== frame.owner)) {
+ sender.shadowVar(name, newValue);
} else {
- frame.vars[name].value = value + parseFloat(delta);
+ frame.vars[name].value = newValue;
}
+
}
};