summaryrefslogtreecommitdiff
path: root/threads.js
diff options
context:
space:
mode:
Diffstat (limited to 'threads.js')
-rw-r--r--threads.js527
1 files changed, 524 insertions, 3 deletions
diff --git a/threads.js b/threads.js
index 50af8dd..2bc5142 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();
@@ -1295,6 +1320,56 @@ Process.prototype.doRemoveTemporaries = function () {
}
};
+// Peer to peer primitives
+
+Process.prototype.sendPeerMessage = function (message, peer) {
+ var myself = this;
+
+ if (peer instanceof List) {
+ peer.asArray().forEach(function (singlePeer) {
+ myself.sendPeerMessage(message, singlePeer);
+ });
+ return;
+ }
+
+ var stage = this.homeContext.receiver.parentThatIsA(StageMorph),
+ ide = this.homeContext.receiver.parentThatIsA(IDE_Morph);
+ var connection = stage.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) {
+ var stage = this.homeContext.receiver.parentThatIsA(StageMorph);
+ stage.peer.listAllPeers(function (peers) {
+ myself.context.result = new List(peers);
+ });
+ this.context.wait = true;
+ } else if (this.context.result) {
+ return this.context.result;
+ }
+
+ this.pushContext('doYield');
+ this.pushContext();
+};
+
+Process.prototype.reportPeerId = function () {
+ var stage = this.homeContext.receiver.parentThatIsA(StageMorph);
+ return stage.peerId;
+};
+
// Process lists primitives
Process.prototype.reportNewList = function (elements) {
@@ -1306,15 +1381,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();
}
@@ -1329,6 +1408,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;
}
@@ -1343,6 +1424,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;
}
@@ -1357,6 +1440,8 @@ Process.prototype.doReplaceInList = function (index, list, element) {
Process.prototype.reportListItem = function (index, list) {
var idx = index;
+ list = this.checkIfList(list);
+
if (index === '') {
return '';
}
@@ -1370,10 +1455,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);
};
@@ -1429,7 +1516,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();
@@ -1632,6 +1719,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
@@ -1920,6 +2009,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';
}
@@ -2236,6 +2326,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) {
@@ -2477,6 +2581,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,
@@ -2626,6 +2759,78 @@ Process.prototype.reportTimer = function () {
return 0;
};
+Process.prototype.reportLanguage = function () {
+ var ide = this.homeContext.receiver.parentThatIsA(IDE_Morph);
+ if (ide) {
+ var lang = ide.userLanguage;
+ if (lang) {
+ return lang;
+ }
+ }
+ return 'en';
+};
+
+Process.prototype.reportLocation = function (name) {
+ var myself = this;
+
+ if (!myself.context.jsonpScript) {
+ if (!navigator.geolocation) {
+ myself.handleError({name: 'Location',
+ message: 'navigator.geolocation is not supported'});
+ }
+ window.OSM_JSONP_Callback = function (data) {
+ myself.context.json = data;
+ window.OSM_JSONP_Callback = undefined;
+ };
+ myself.context.jsonpScript = document.createElement('script');
+
+ navigator.geolocation.getCurrentPosition(
+ function (pos) {
+ var crd = pos.coords;
+
+ myself.context.jsonpScript.src =
+ 'https://nominatim.openstreetmap.org/reverse'
+ + '?format=json&json_callback=OSM_JSONP_Callback'
+ + '&zoom=18&addressdetails=1'
+ + '&lat='
+ + crd.latitude
+ + '&lon='
+ + crd.longitude;
+ document.getElementsByTagName('HEAD')[0]
+ .appendChild(myself.context.jsonpScript);
+ },
+ function (err) {
+ console.log(err);
+ myself.handleError({name: 'Location', message: err});
+ },
+ { enableHightAccuracy: true, timeout: 30000, maximumAge: 0}
+ );
+ } else {
+ if (myself.context.json) {
+ switch (myself.inputOption(name)) {
+ case 'all':
+ return myself.context.json.display_name;
+ case 'state district':
+ return myself.context.json.address.state_district;
+ case 'house number':
+ return myself.context.json.address.house_number;
+ case 'licence':
+ return myself.context.json.licence;
+ case 'country':
+ case 'state':
+ case 'suburb':
+ case 'city':
+ case 'road':
+ return myself.context.
+ json.address[myself.inputOption(name)];
+ }
+ }
+ }
+
+ this.pushContext('doYield');
+ this.pushContext();
+};
+
// Process Dates and times in Snap
// Map block options to built-in functions
var dateMap = {
@@ -2655,6 +2860,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
/*
@@ -2770,22 +3083,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) {
@@ -2857,6 +3376,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,
@@ -2883,6 +3403,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