summaryrefslogtreecommitdiff
path: root/threads.js
diff options
context:
space:
mode:
Diffstat (limited to 'threads.js')
-rw-r--r--threads.js419
1 files changed, 418 insertions, 1 deletions
diff --git a/threads.js b/threads.js
index e07059f..06454e3 100644
--- a/threads.js
+++ b/threads.js
@@ -1,5 +1,5 @@
/*
-
+
threads.js
a tail call optimized blocks-based programming language interpreter
@@ -2236,6 +2236,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 +2491,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 +2669,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 +2770,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
/*
@@ -2786,6 +3009,198 @@ Process.prototype.doPlayNoteForSecs = function (pitch, secs) {
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 +3272,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 +3299,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