summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xbinary.js1099
-rwxr-xr-xbinary.sh101
-rw-r--r--blocks.js19
-rw-r--r--config.xml16
-rwxr-xr-xdesktop.sh34
-rw-r--r--gui.js3
-rwxr-xr-xmobile.sh65
-rw-r--r--morphic.js5
-rw-r--r--objects.js107
-rw-r--r--package.json8
-rwxr-xr-xsnap.html4
-rw-r--r--threads.js419
12 files changed, 1877 insertions, 3 deletions
diff --git a/binary.js b/binary.js
new file mode 100755
index 0000000..d1a9b21
--- /dev/null
+++ b/binary.js
@@ -0,0 +1,1099 @@
+/*
+
+ gui.js
+
+ a programming environment
+ based on morphic.js, blocks.js, threads.js and objects.js
+ inspired by Scratch
+
+ written by Jens Mönig
+ jens@moenig.org
+
+ Copyright (C) 2014 by Jens Mönig
+
+ This file is part of Snap!.
+
+ Snap! is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as
+ published by the Free Software Foundation, either version 3 of
+ the License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+ prerequisites:
+ --------------
+ needs blocks.js, threads.js, objects.js and morphic.js
+
+
+ toc
+ ---
+ the following list shows the order in which all constructors are
+ defined. Use this list to locate code in this document:
+
+ IDE_Morph
+
+
+ credits
+ -------
+ Nathan Dinsmore contributed saving and loading of projects,
+ ypr-Snap! project conversion and countless bugfixes
+ Ian Reynolds contributed handling and visualization of sounds
+
+*/
+
+/*global modules, Morph, SpriteMorph, Color, newCanvas,
+VariableFrame, Point, MenuMorph,
+morphicVersion, DialogBoxMorph, ToggleButtonMorph, contains,
+StageMorph, PushButtonMorph, Process, SnapSerializer, MorphicPreferences,
+SymbolMorph,
+BlockImportDialogMorph, SnapTranslator, localize, List */
+
+// Global stuff ////////////////////////////////////////////////////////
+
+modules.gui = '2014-July-30';
+
+// Declarations
+
+var IDE_Morph;
+
+// IDE_Morph ///////////////////////////////////////////////////////////
+
+// I am SNAP's top-level frame, the Editor window
+
+// IDE_Morph inherits from Morph:
+
+IDE_Morph.prototype = new Morph();
+IDE_Morph.prototype.constructor = IDE_Morph;
+IDE_Morph.uber = Morph.prototype;
+
+// IDE_Morph preferences settings and skins
+
+IDE_Morph.prototype.setDefaultDesign = function () {
+ MorphicPreferences.isFlat = false;
+ SpriteMorph.prototype.paletteColor = new Color(55, 55, 55);
+ SpriteMorph.prototype.paletteTextColor = new Color(230, 230, 230);
+ StageMorph.prototype.paletteTextColor
+ = SpriteMorph.prototype.paletteTextColor;
+ StageMorph.prototype.paletteColor = SpriteMorph.prototype.paletteColor;
+ SpriteMorph.prototype.sliderColor
+ = SpriteMorph.prototype.paletteColor.lighter(30);
+
+ IDE_Morph.prototype.buttonContrast = 30;
+ IDE_Morph.prototype.backgroundColor = new Color(40, 40, 40);
+ IDE_Morph.prototype.frameColor = SpriteMorph.prototype.paletteColor;
+
+ IDE_Morph.prototype.groupColor
+ = SpriteMorph.prototype.paletteColor.lighter(8);
+ IDE_Morph.prototype.sliderColor = SpriteMorph.prototype.sliderColor;
+ IDE_Morph.prototype.buttonLabelColor = new Color(255, 255, 255);
+ IDE_Morph.prototype.tabColors = [
+ IDE_Morph.prototype.groupColor.darker(40),
+ IDE_Morph.prototype.groupColor.darker(60),
+ IDE_Morph.prototype.groupColor
+ ];
+ IDE_Morph.prototype.rotationStyleColors = IDE_Morph.prototype.tabColors;
+ IDE_Morph.prototype.appModeColor = new Color();
+ IDE_Morph.prototype.scriptsPaneTexture = 'scriptsPaneTexture.gif';
+ IDE_Morph.prototype.padding = 5;
+};
+
+IDE_Morph.prototype.setFlatDesign = function () {
+ MorphicPreferences.isFlat = true;
+ SpriteMorph.prototype.paletteColor = new Color(255, 255, 255);
+ SpriteMorph.prototype.paletteTextColor = new Color(70, 70, 70);
+ StageMorph.prototype.paletteTextColor
+ = SpriteMorph.prototype.paletteTextColor;
+ StageMorph.prototype.paletteColor = SpriteMorph.prototype.paletteColor;
+ SpriteMorph.prototype.sliderColor = SpriteMorph.prototype.paletteColor;
+
+ IDE_Morph.prototype.buttonContrast = 30;
+ IDE_Morph.prototype.backgroundColor = new Color(200, 200, 200);
+ IDE_Morph.prototype.frameColor = new Color(255, 255, 255);
+
+ IDE_Morph.prototype.groupColor = new Color(230, 230, 230);
+ IDE_Morph.prototype.sliderColor = SpriteMorph.prototype.sliderColor;
+ IDE_Morph.prototype.buttonLabelColor = new Color(70, 70, 70);
+ IDE_Morph.prototype.tabColors = [
+ IDE_Morph.prototype.groupColor.lighter(60),
+ IDE_Morph.prototype.groupColor.darker(10),
+ IDE_Morph.prototype.groupColor
+ ];
+ IDE_Morph.prototype.rotationStyleColors = [
+ IDE_Morph.prototype.groupColor,
+ IDE_Morph.prototype.groupColor.darker(10),
+ IDE_Morph.prototype.groupColor.darker(30)
+ ];
+ IDE_Morph.prototype.appModeColor = IDE_Morph.prototype.frameColor;
+ IDE_Morph.prototype.scriptsPaneTexture = null;
+ IDE_Morph.prototype.padding = 1;
+};
+
+IDE_Morph.prototype.setDefaultDesign();
+
+// IDE_Morph instance creation:
+
+function IDE_Morph(isAutoFill) {
+ this.init(isAutoFill);
+}
+
+IDE_Morph.prototype.init = function (isAutoFill) {
+ // global font setting
+ MorphicPreferences.globalFontFamily = 'Helvetica, Arial';
+
+ // restore saved user preferences
+ this.userLanguage = null;
+
+ // additional properties:
+ this.source = 'local';
+ this.serializer = new SnapSerializer();
+
+ this.globalVariables = new VariableFrame();
+ this.currentSprite = new SpriteMorph(this.globalVariables);
+ this.sprites = new List([this.currentSprite]);
+ this.currentCategory = 'motion';
+ this.currentTab = 'scripts';
+ this.projectName = '';
+ this.projectNotes = '';
+
+ this.logo = null;
+ this.controlBar = null;
+ this.stage = null;
+
+ this.isAutoFill = isAutoFill || true;
+
+ this.filePicker = null;
+ this.hasChangedMedia = false;
+
+ this.isAnimating = true;
+ this.stageRatio = 1; // for IDE animations, e.g. when zooming
+
+ this.loadNewProject = false; // flag when starting up translated
+ this.shield = null;
+
+ // initialize inherited properties:
+ IDE_Morph.uber.init.call(this);
+
+ // override inherited properites:
+ this.color = this.appModeColor;
+};
+
+IDE_Morph.prototype.openIn = function (world) {
+ var hash;
+
+ this.buildPanes();
+ world.add(this);
+
+ this.reactToWorldResize(world.bounds);
+
+ function getURL(url) {
+ try {
+ var request = new XMLHttpRequest();
+ request.open('GET', url, false);
+ request.send();
+ if (request.status === 200) {
+ return request.responseText;
+ }
+ throw new Error('unable to retrieve ' + url);
+ } catch (err) {
+ return;
+ }
+ }
+
+ function interpretUrlAnchors() {
+ if (location.hash.substr(0, 6) === '#open:') {
+ hash = location.hash.substr(6);
+ if (hash.charAt(0) === '%'
+ || hash.search(/\%(?:[0-9a-f]{2})/i) > -1) {
+ hash = decodeURIComponent(hash);
+ }
+ if (contains(
+ ['project', 'blocks', 'sprites', 'snapdata'].map(
+ function (each) {
+ return hash.substr(0, 8).indexOf(each);
+ }
+ ),
+ 1
+ )) {
+ this.droppedText(hash);
+ } else {
+ this.droppedText(getURL(hash));
+ }
+ }
+ }
+
+ if (this.userLanguage) {
+ this.setLanguage(this.userLanguage, interpretUrlAnchors);
+ } else {
+ interpretUrlAnchors.call(this);
+ }
+};
+
+// IDE_Morph construction
+
+IDE_Morph.prototype.buildPanes = function () {
+ this.createLogo();
+ this.createControlBar();
+ this.createStage();
+};
+
+IDE_Morph.prototype.createLogo = function () {
+ var myself = this;
+
+ if (this.logo) {
+ this.logo.destroy();
+ }
+
+ this.logo = new Morph();
+ this.logo.texture = 'snap_logo_sm.png';
+ this.logo.drawNew = function () {
+ this.image = newCanvas(this.extent());
+ var context = this.image.getContext('2d');
+ context.fillStyle = this.color;
+ context.fillRect(0, 0, this.width(), this.height());
+ if (this.texture) {
+ this.drawTexture(this.texture);
+ }
+ };
+
+ this.logo.drawCachedTexture = function () {
+ var context = this.image.getContext('2d');
+ context.drawImage(
+ this.cachedTexture,
+ 5,
+ Math.round((this.height() - this.cachedTexture.height) / 2)
+ );
+ this.changed();
+ };
+
+ this.logo.mouseClickLeft = function () {
+ myself.snapMenu();
+ };
+
+ this.logo.color = new Color();
+ this.logo.setExtent(new Point(200, 28)); // dimensions are fixed
+ this.add(this.logo);
+};
+
+IDE_Morph.prototype.createControlBar = function () {
+ // assumes the logo has already been created
+ var padding = 5,
+ button,
+ stopButton,
+ pauseButton,
+ startButton,
+ x,
+ colors = [
+ this.groupColor,
+ this.frameColor.darker(50),
+ this.frameColor.darker(50)
+ ],
+ myself = this;
+
+ if (this.controlBar) {
+ this.controlBar.destroy();
+ }
+
+ this.controlBar = new Morph();
+ this.controlBar.color = this.appModeColor;
+ this.controlBar.setHeight(this.logo.height()); // height is fixed
+ this.controlBar.mouseClickLeft = function () {
+ this.world().fillPage();
+ };
+ this.add(this.controlBar);
+
+ // stopButton
+ button = new PushButtonMorph(
+ this,
+ 'stopAllScripts',
+ new SymbolMorph('octagon', 14)
+ );
+ button.corner = 12;
+ button.color = colors[0];
+ button.highlightColor = colors[1];
+ button.pressColor = colors[2];
+ button.labelMinExtent = new Point(36, 18);
+ button.padding = 0;
+ button.labelShadowOffset = new Point(-1, -1);
+ button.labelShadowColor = colors[1];
+ button.labelColor = new Color(200, 0, 0);
+ button.contrast = this.buttonContrast;
+ button.drawNew();
+ // button.hint = 'stop\nevery-\nthing';
+ button.fixLayout();
+ stopButton = button;
+ this.controlBar.add(stopButton);
+
+ //pauseButton
+ button = new ToggleButtonMorph(
+ null, //colors,
+ myself, // the IDE is the target
+ 'togglePauseResume',
+ [
+ new SymbolMorph('pause', 12),
+ new SymbolMorph('pointRight', 14)
+ ],
+ function () { // query
+ return myself.isPaused();
+ }
+ );
+
+ button.corner = 12;
+ button.color = colors[0];
+ button.highlightColor = colors[1];
+ button.pressColor = colors[2];
+ button.labelMinExtent = new Point(36, 18);
+ button.padding = 0;
+ button.labelShadowOffset = new Point(-1, -1);
+ button.labelShadowColor = colors[1];
+ button.labelColor = new Color(255, 220, 0);
+ button.contrast = this.buttonContrast;
+ button.drawNew();
+ // button.hint = 'pause/resume\nall scripts';
+ button.fixLayout();
+ button.refresh();
+ pauseButton = button;
+ this.controlBar.add(pauseButton);
+ this.controlBar.pauseButton = pauseButton; // for refreshing
+
+ // startButton
+ button = new PushButtonMorph(
+ this,
+ 'pressStart',
+ new SymbolMorph('flag', 14)
+ );
+ button.corner = 12;
+ button.color = colors[0];
+ button.highlightColor = colors[1];
+ button.pressColor = colors[2];
+ button.labelMinExtent = new Point(36, 18);
+ button.padding = 0;
+ button.labelShadowOffset = new Point(-1, -1);
+ button.labelShadowColor = colors[1];
+ button.labelColor = new Color(0, 200, 0);
+ button.contrast = this.buttonContrast;
+ button.drawNew();
+ // button.hint = 'start green\nflag scripts';
+ button.fixLayout();
+ startButton = button;
+ this.controlBar.add(startButton);
+ this.controlBar.startButton = startButton;
+
+ this.controlBar.fixLayout = function () {
+ x = this.right() - padding;
+ [stopButton, pauseButton, startButton].forEach(
+ function (button) {
+ button.setCenter(myself.controlBar.center());
+ button.setRight(x);
+ x -= button.width();
+ x -= padding;
+ }
+ );
+
+ x = myself.right() - StageMorph.prototype.dimensions.x;
+
+ this.updateLabel();
+ };
+
+ this.controlBar.updateLabel = function () {
+ if (this.label) {
+ this.label.destroy();
+ }
+ };
+};
+
+IDE_Morph.prototype.createStage = function () {
+ // assumes that the logo pane has already been created
+ if (this.stage) {
+ this.stage.destroy();
+ }
+ StageMorph.prototype.frameRate = 0;
+ this.stage = new StageMorph(this.globalVariables);
+ this.stage.setExtent(this.stage.dimensions); // dimensions are fixed
+ if (this.currentSprite instanceof SpriteMorph) {
+ this.currentSprite.setPosition(
+ this.stage.center().subtract(
+ this.currentSprite.extent().divideBy(2)
+ )
+ );
+ this.stage.add(this.currentSprite);
+ }
+ this.add(this.stage);
+};
+
+IDE_Morph.prototype.createCorral = function () {
+};
+
+// IDE_Morph resizing
+
+IDE_Morph.prototype.setExtent = function (point) {
+ var minExt,
+ ext;
+
+ // determine the minimum dimensions making sense for the current mode
+ minExt = StageMorph.prototype.dimensions.add(
+ this.controlBar.height() + 10
+ );
+ ext = point.max(minExt);
+ IDE_Morph.uber.setExtent.call(this, ext);
+ this.fixLayout();
+};
+
+// IDE_Morph events
+
+IDE_Morph.prototype.reactToWorldResize = function (rect) {
+ if (this.isAutoFill) {
+ this.setPosition(rect.origin);
+ this.setExtent(rect.extent());
+ }
+ if (this.filePicker) {
+ document.body.removeChild(this.filePicker);
+ this.filePicker = null;
+ }
+};
+
+// IDE_Morph layout
+
+IDE_Morph.prototype.fixLayout = function () {
+ var padding = this.padding;
+
+ Morph.prototype.trackChanges = false;
+
+ // controlBar
+ this.controlBar.setPosition(this.logo.topRight());
+ this.controlBar.setWidth(this.right() - this.controlBar.left());
+ this.controlBar.fixLayout();
+
+ // stage
+ this.stage.setScale(Math.floor(Math.min(
+ (this.width() - padding * 2) / this.stage.dimensions.x,
+ (this.height() - this.controlBar.height() * 2 - padding * 2)
+ / this.stage.dimensions.y
+ ) * 10) / 10);
+ this.stage.setCenter(this.center());
+
+ Morph.prototype.trackChanges = true;
+ this.changed();
+};
+
+IDE_Morph.prototype.droppedText = function (aString, name) {
+ var lbl = name ? name.split('.')[0] : '';
+ if (aString.indexOf('<project') === 0) {
+ return this.openProjectString(aString);
+ }
+ if (aString.indexOf('<snapdata') === 0) {
+ return this.openCloudDataString(aString);
+ }
+ if (aString.indexOf('<blocks') === 0) {
+ return this.openBlocksString(aString, lbl, true);
+ }
+ if (aString.indexOf('<sprites') === 0) {
+ return this.openSpritesString(aString);
+ }
+ if (aString.indexOf('<media') === 0) {
+ return this.openMediaString(aString);
+ }
+};
+
+// IDE_Morph events
+
+IDE_Morph.prototype.pressStart = function () {
+ if (this.world().currentKey === 16) { // shiftClicked
+ this.toggleFastTracking();
+ } else {
+ this.runScripts();
+ }
+};
+
+IDE_Morph.prototype.toggleFastTracking = function () {
+ if (this.stage.isFastTracked) {
+ this.stopFastTracking();
+ } else {
+ this.startFastTracking();
+ }
+};
+
+IDE_Morph.prototype.toggleVariableFrameRate = function () {
+ if (StageMorph.prototype.frameRate) {
+ StageMorph.prototype.frameRate = 0;
+ this.stage.fps = 0;
+ } else {
+ StageMorph.prototype.frameRate = 30;
+ this.stage.fps = 30;
+ }
+};
+
+IDE_Morph.prototype.startFastTracking = function () {
+ this.stage.isFastTracked = true;
+ this.stage.fps = 0;
+ this.controlBar.startButton.labelString = new SymbolMorph('flash', 14);
+ this.controlBar.startButton.drawNew();
+ this.controlBar.startButton.fixLayout();
+};
+
+IDE_Morph.prototype.stopFastTracking = function () {
+ this.stage.isFastTracked = false;
+ this.stage.fps = this.stage.frameRate;
+ this.controlBar.startButton.labelString = new SymbolMorph('flag', 14);
+ this.controlBar.startButton.drawNew();
+ this.controlBar.startButton.fixLayout();
+};
+
+IDE_Morph.prototype.runScripts = function () {
+ this.stage.fireGreenFlagEvent();
+};
+
+IDE_Morph.prototype.togglePauseResume = function () {
+ if (this.stage.threads.isPaused()) {
+ this.stage.threads.resumeAll(this.stage);
+ } else {
+ this.stage.threads.pauseAll(this.stage);
+ }
+ this.controlBar.pauseButton.refresh();
+};
+
+IDE_Morph.prototype.isPaused = function () {
+ if (!this.stage) {return false; }
+ return this.stage.threads.isPaused();
+};
+
+IDE_Morph.prototype.stopAllScripts = function () {
+ this.stage.fireStopAllEvent();
+};
+
+IDE_Morph.prototype.selectSprite = function (sprite) {
+ this.currentSprite = sprite;
+ this.currentSprite.scripts.fixMultiArgs();
+};
+
+// IDE_Morph menus
+
+IDE_Morph.prototype.snapMenu = function () {
+ var menu,
+ world = this.world();
+
+ menu = new MenuMorph(this);
+ menu.addItem('About...', 'aboutSnap');
+ menu.addLine();
+ menu.addItem(
+ 'Reference manual',
+ function () {
+ window.open('help/SnapManual.pdf', 'SnapReferenceManual');
+ }
+ );
+ menu.addItem(
+ 'Snap! website',
+ function () {
+ window.open('http://snap.berkeley.edu/', 'SnapWebsite');
+ }
+ );
+ menu.addItem(
+ 'Download source',
+ function () {
+ window.open(
+ 'http://snap.berkeley.edu/snapsource/snap.zip',
+ 'SnapSource'
+ );
+ }
+ );
+ if (world.isDevMode) {
+ menu.addLine();
+ menu.addItem(
+ 'Switch back to user mode',
+ 'switchToUserMode',
+ 'disable deep-Morphic\ncontext menus'
+ + '\nand show user-friendly ones',
+ new Color(0, 100, 0)
+ );
+ } else if (world.currentKey === 16) { // shift-click
+ menu.addLine();
+ menu.addItem(
+ 'Switch to dev mode',
+ 'switchToDevMode',
+ 'enable Morphic\ncontext menus\nand inspectors,'
+ + '\nnot user-friendly!',
+ new Color(100, 0, 0)
+ );
+ }
+ menu.popup(world, this.logo.bottomLeft());
+};
+
+// IDE_Morph menu actions
+
+IDE_Morph.prototype.aboutSnap = function () {
+ var dlg, aboutTxt, noticeTxt, creditsTxt, versions = '', translations,
+ module, btn1, btn2, btn3, btn4, licenseBtn, translatorsBtn,
+ world = this.world();
+
+ aboutTxt = 'Snap! 4.0\nBuild Your Own Blocks\n\n--- beta ---\n\n'
+ + 'Copyright \u24B8 2014 Jens M\u00F6nig and '
+ + 'Brian Harvey\n'
+ + 'jens@moenig.org, bh@cs.berkeley.edu\n\n'
+
+ + 'Snap! is developed by the University of California, Berkeley\n'
+ + ' with support from the National Science Foundation '
+ + 'and MioSoft. \n'
+
+ + 'The design of Snap! is influenced and inspired by Scratch,\n'
+ + 'from the Lifelong Kindergarten group at the MIT Media Lab\n\n'
+
+ + 'for more information see http://snap.berkeley.edu\n'
+ + 'and http://scratch.mit.edu';
+
+ noticeTxt = localize('License')
+ + '\n\n'
+ + 'Snap! is free software: you can redistribute it and/or modify\n'
+ + 'it under the terms of the GNU Affero General Public License as\n'
+ + 'published by the Free Software Foundation, either version 3 of\n'
+ + 'the License, or (at your option) any later version.\n\n'
+
+ + 'This program is distributed in the hope that it will be useful,\n'
+ + 'but WITHOUT ANY WARRANTY; without even the implied warranty of\n'
+ + 'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n'
+ + 'GNU Affero General Public License for more details.\n\n'
+
+ + 'You should have received a copy of the\n'
+ + 'GNU Affero General Public License along with this program.\n'
+ + 'If not, see http://www.gnu.org/licenses/';
+
+ creditsTxt = localize('Contributors')
+ + '\n\nNathan Dinsmore: Saving/Loading, Snap-Logo Design, '
+ + 'countless bugfixes'
+ + '\nKartik Chandra: Paint Editor'
+ + '\nMichael Ball: Time/Date UI, many bugfixes'
+ + '\n"Ava" Yuan Yuan: Graphic Effects'
+ + '\nKyle Hotchkiss: Block search design'
+ + '\nIan Reynolds: UI Design, Event Bindings, '
+ + 'Sound primitives'
+ + '\nIvan Motyashov: Initial Squeak Porting'
+ + '\nDavide Della Casa: Morphic Optimizations'
+ + '\nAchal Dave: Web Audio'
+ + '\nJoe Otto: Morphic Testing and Debugging';
+
+ for (module in modules) {
+ if (Object.prototype.hasOwnProperty.call(modules, module)) {
+ versions += ('\n' + module + ' (' +
+ modules[module] + ')');
+ }
+ }
+ if (versions !== '') {
+ versions = localize('current module versions:') + ' \n\n' +
+ 'morphic (' + morphicVersion + ')' +
+ versions;
+ }
+ translations = localize('Translations') + '\n' + SnapTranslator.credits();
+
+ dlg = new DialogBoxMorph();
+ dlg.inform('About Snap', aboutTxt, world);
+ btn1 = dlg.buttons.children[0];
+ translatorsBtn = dlg.addButton(
+ function () {
+ dlg.body.text = translations;
+ dlg.body.drawNew();
+ btn1.show();
+ btn2.show();
+ btn3.hide();
+ btn4.hide();
+ licenseBtn.hide();
+ translatorsBtn.hide();
+ dlg.fixLayout();
+ dlg.drawNew();
+ dlg.setCenter(world.center());
+ },
+ 'Translators...'
+ );
+ btn2 = dlg.addButton(
+ function () {
+ dlg.body.text = aboutTxt;
+ dlg.body.drawNew();
+ btn1.show();
+ btn2.hide();
+ btn3.show();
+ btn4.show();
+ licenseBtn.show();
+ translatorsBtn.hide();
+ dlg.fixLayout();
+ dlg.drawNew();
+ dlg.setCenter(world.center());
+ },
+ 'Back...'
+ );
+ btn2.hide();
+ licenseBtn = dlg.addButton(
+ function () {
+ dlg.body.text = noticeTxt;
+ dlg.body.drawNew();
+ btn1.show();
+ btn2.show();
+ btn3.hide();
+ btn4.hide();
+ licenseBtn.hide();
+ translatorsBtn.hide();
+ dlg.fixLayout();
+ dlg.drawNew();
+ dlg.setCenter(world.center());
+ },
+ 'License...'
+ );
+ btn3 = dlg.addButton(
+ function () {
+ dlg.body.text = versions;
+ dlg.body.drawNew();
+ btn1.show();
+ btn2.show();
+ btn3.hide();
+ btn4.hide();
+ licenseBtn.hide();
+ translatorsBtn.hide();
+ dlg.fixLayout();
+ dlg.drawNew();
+ dlg.setCenter(world.center());
+ },
+ 'Modules...'
+ );
+ btn4 = dlg.addButton(
+ function () {
+ dlg.body.text = creditsTxt;
+ dlg.body.drawNew();
+ btn1.show();
+ btn2.show();
+ translatorsBtn.show();
+ btn3.hide();
+ btn4.hide();
+ licenseBtn.hide();
+ dlg.fixLayout();
+ dlg.drawNew();
+ dlg.setCenter(world.center());
+ },
+ 'Credits...'
+ );
+ translatorsBtn.hide();
+ dlg.fixLayout();
+ dlg.drawNew();
+};
+
+IDE_Morph.prototype.openProjectString = function (str) {
+ var msg,
+ myself = this;
+ this.nextSteps([
+ function () {
+ msg = myself.showMessage('Opening project...');
+ },
+ function () {
+ myself.rawOpenProjectString(str);
+ },
+ function () {
+ msg.destroy();
+ }
+ ]);
+};
+
+IDE_Morph.prototype.rawOpenProjectString = function (str) {
+ StageMorph.prototype.hiddenPrimitives = {};
+ StageMorph.prototype.codeMappings = {};
+ StageMorph.prototype.codeHeaders = {};
+ StageMorph.prototype.enableCodeMapping = false;
+ if (Process.prototype.isCatchingErrors) {
+ try {
+ this.serializer.openProject(this.serializer.load(str), this);
+ } catch (err) {
+ this.showMessage('Load failed: ' + err);
+ }
+ } else {
+ this.serializer.openProject(this.serializer.load(str), this);
+ }
+ this.stopFastTracking();
+};
+
+IDE_Morph.prototype.openBlocksString = function (str, name, silently) {
+ var msg,
+ myself = this;
+ this.nextSteps([
+ function () {
+ msg = myself.showMessage('Opening blocks...');
+ },
+ function () {
+ myself.rawOpenBlocksString(str, name, silently);
+ },
+ function () {
+ msg.destroy();
+ }
+ ]);
+};
+
+IDE_Morph.prototype.rawOpenBlocksString = function (str, name, silently) {
+ // name is optional (string), so is silently (bool)
+ var blocks,
+ myself = this;
+ if (Process.prototype.isCatchingErrors) {
+ try {
+ blocks = this.serializer.loadBlocks(str, myself.stage);
+ } catch (err) {
+ this.showMessage('Load failed: ' + err);
+ }
+ } else {
+ blocks = this.serializer.loadBlocks(str, myself.stage);
+ }
+ if (silently) {
+ blocks.forEach(function (def) {
+ def.receiver = myself.stage;
+ myself.stage.globalBlocks.push(def);
+ myself.stage.replaceDoubleDefinitionsFor(def);
+ });
+ this.showMessage(
+ 'Imported Blocks Module' + (name ? ': ' + name : '') + '.',
+ 2
+ );
+ } else {
+ new BlockImportDialogMorph(blocks, this.stage, name).popUp();
+ }
+};
+
+IDE_Morph.prototype.openSpritesString = function (str) {
+ var msg,
+ myself = this;
+ this.nextSteps([
+ function () {
+ msg = myself.showMessage('Opening sprite...');
+ },
+ function () {
+ myself.rawOpenSpritesString(str);
+ },
+ function () {
+ msg.destroy();
+ }
+ ]);
+};
+
+IDE_Morph.prototype.rawOpenSpritesString = function (str) {
+ if (Process.prototype.isCatchingErrors) {
+ try {
+ this.serializer.loadSprites(str, this);
+ } catch (err) {
+ this.showMessage('Load failed: ' + err);
+ }
+ } else {
+ this.serializer.loadSprites(str, this);
+ }
+};
+
+IDE_Morph.prototype.openMediaString = function (str) {
+ if (Process.prototype.isCatchingErrors) {
+ try {
+ this.serializer.loadMedia(str);
+ } catch (err) {
+ this.showMessage('Load failed: ' + err);
+ }
+ } else {
+ this.serializer.loadMedia(str);
+ }
+ this.showMessage('Imported Media Module.', 2);
+};
+
+IDE_Morph.prototype.openProject = function (name) {
+ var str;
+ if (name) {
+ this.showMessage('opening project\n' + name);
+ str = localStorage['-snap-project-' + name];
+ this.openProjectString(str);
+ location.hash = '#open:' + str;
+ }
+};
+
+IDE_Morph.prototype.switchToUserMode = function () {
+ var world = this.world();
+
+ world.isDevMode = false;
+ Process.prototype.isCatchingErrors = true;
+ this.controlBar.updateLabel();
+ this.isAutoFill = true;
+ this.isDraggable = false;
+ this.siblings().forEach(function (morph) {
+ if (morph instanceof DialogBoxMorph) {
+ world.add(morph); // bring to front
+ } else {
+ morph.destroy();
+ }
+ });
+ this.flushBlocksCache();
+ // prevent non-DialogBoxMorphs from being dropped
+ // onto the World in user-mode
+ world.reactToDropOf = function (morph) {
+ if (!(morph instanceof DialogBoxMorph)) {
+ world.hand.grab(morph);
+ }
+ };
+ this.showMessage('entering user mode', 1);
+
+};
+
+IDE_Morph.prototype.switchToDevMode = function () {
+ var world = this.world();
+
+ world.isDevMode = true;
+ Process.prototype.isCatchingErrors = false;
+ this.controlBar.updateLabel();
+ this.isAutoFill = false;
+ this.isDraggable = true;
+ this.setExtent(world.extent().subtract(100));
+ this.setPosition(world.position().add(20));
+ this.flushBlocksCache();
+ // enable non-DialogBoxMorphs to be dropped
+ // onto the World in dev-mode
+ delete world.reactToDropOf;
+ this.showMessage(
+ 'entering development mode.\n\n'
+ + 'error catching is turned off,\n'
+ + 'use the browser\'s web console\n'
+ + 'to see error messages.'
+ );
+};
+
+IDE_Morph.prototype.flushBlocksCache = function (category) {
+ // if no category is specified, the whole cache gets flushed
+ if (category) {
+ this.stage.blocksCache[category] = null;
+ this.stage.children.forEach(function (m) {
+ if (m instanceof SpriteMorph) {
+ m.blocksCache[category] = null;
+ }
+ });
+ } else {
+ this.stage.blocksCache = {};
+ this.stage.children.forEach(function (m) {
+ if (m instanceof SpriteMorph) {
+ m.blocksCache = {};
+ }
+ });
+ }
+};
+
+// IDE_Morph localization
+
+IDE_Morph.prototype.setLanguage = function (lang, callback) {
+ var translation = document.getElementById('language'),
+ src = 'lang-' + lang + '.js',
+ myself = this;
+ SnapTranslator.unload();
+ if (translation) {
+ document.head.removeChild(translation);
+ }
+ if (lang === 'en') {
+ return this.reflectLanguage('en', callback);
+ }
+ myself.userLanguage = lang;
+ translation = document.createElement('script');
+ translation.id = 'language';
+ translation.onload = function () {
+ myself.reflectLanguage(lang, callback);
+ };
+ document.head.appendChild(translation);
+ translation.src = src;
+};
+
+IDE_Morph.prototype.reflectLanguage = function (lang, callback) {
+ var projectData;
+ SnapTranslator.language = lang;
+ if (!this.loadNewProject) {
+ if (Process.prototype.isCatchingErrors) {
+ try {
+ projectData = this.serializer.serialize(this.stage);
+ } catch (err) {
+ this.showMessage('Serialization failed: ' + err);
+ }
+ } else {
+ projectData = this.serializer.serialize(this.stage);
+ }
+ }
+ SpriteMorph.prototype.initBlocks();
+ this.fixLayout();
+ if (this.loadNewProject) {
+ this.newProject();
+ } else {
+ this.openProjectString(projectData);
+ }
+ this.saveSetting('language', lang);
+ if (callback) {callback.call(this); }
+};
+
+// IDE_Morph synchronous Http data fetching
+
+IDE_Morph.prototype.getURL = function (url) {
+ var request = new XMLHttpRequest(),
+ myself = this;
+ try {
+ request.open('GET', url, false);
+ request.send();
+ if (request.status === 200) {
+ return request.responseText;
+ }
+ throw new Error('unable to retrieve ' + url);
+ } catch (err) {
+ myself.showMessage(err);
+ return;
+ }
+};
+
+IDE_Morph.prototype.getURLsbeOrRelative = function (url) {
+ var request = new XMLHttpRequest(),
+ myself = this;
+ try {
+ request.open('GET', 'http://snap.berkeley.edu/snapsource/' +
+ url, false);
+ request.send();
+ if (request.status === 200) {
+ return request.responseText;
+ }
+ return myself.getURL(url);
+ } catch (err) {
+ myself.showMessage(err);
+ return;
+ }
+};
+
+// IDE_Morph user dialog shortcuts
+
+IDE_Morph.prototype.showMessage = function (message, secs) {
+ var m = new MenuMorph(null, message),
+ intervalHandle;
+ m.popUpCenteredInWorld(this.world());
+ if (secs) {
+ intervalHandle = setInterval(function () {
+ m.destroy();
+ clearInterval(intervalHandle);
+ }, secs * 1000);
+ }
+ return m;
+};
+
+IDE_Morph.prototype.inform = function (title, message) {
+ new DialogBoxMorph().inform(
+ title,
+ localize(message),
+ this.world()
+ );
+};
+
+IDE_Morph.prototype.confirm = function (message, title, action) {
+ new DialogBoxMorph(null, action).askYesNo(
+ title,
+ localize(message),
+ this.world()
+ );
+};
+
+IDE_Morph.prototype.prompt = function (message, callback, choices, key) {
+ (new DialogBoxMorph(null, callback)).withKey(key).prompt(
+ message,
+ '',
+ this.world(),
+ null,
+ choices
+ );
+};
diff --git a/binary.sh b/binary.sh
new file mode 100755
index 0000000..4fb045a
--- /dev/null
+++ b/binary.sh
@@ -0,0 +1,101 @@
+#!/bin/bash
+
+if [[ "$snapsource" == "" ]]
+then
+ export snapsource="https://github.com/Gubolin/snap.git"
+fi
+
+if [[ $# < 2 ]]
+then
+ echo "Usage: binary.sh OPTION PLATFORM [FILE/URL]"
+ echo ""
+ echo "OPTIONS:"
+ echo " -m Mobile"
+ echo " -d Desktop"
+ echo ""
+ echo "PLATFORMS:"
+ echo " Mobile amazon-fireos android blackberry10 firefoxos ios ubuntu wp8 win8 tizen"
+ echo " Desktop win32 win64 osx linux32 linux64"
+ echo ""
+ echo "If FILE/URL is given, it will be #open-ed inside Snap\! immediately. URL will be loaded at runtime."
+ exit 0
+fi
+
+scriptdir=$(readlink -e ".")
+
+# Requirements:
+# git
+# UglifyJS2 (https://github.com/mishoo/UglifyJS2)
+
+ide=true
+url=false
+platform=$2
+
+# presentation mode
+if [[ "$3" != "" ]]
+then
+ ide=false
+fi
+
+if [ $ide == false ]
+then
+ if [ -f "$3" ]
+ then
+ content="'$(cat $3)'"
+ else
+ url=true
+ fi
+fi
+
+buildsource=$(mktemp -d)
+git clone $snapsource $buildsource
+cd "$buildsource"
+git checkout mobileapp
+
+rm -rf .git/
+
+if [ $ide == false ]
+then
+ # minimize everything
+ rm lang* ypr.js paint.js cloud.js gui.js
+ rm -r help/
+
+ sed -i '/paint\.js"/d' snap.html
+ sed -i '/cloud\.js"/d' snap.html
+ sed -i 's/gui\.js"/binary\.js"/' snap.html
+
+ # load custom project from file or url
+ if [ $url == false ]
+ then
+ sed -i '/sha512\.js"/a\
+ <script type="text/javascript" src="code.js"></script> ' snap.html
+
+ echo "var code =" > code.js
+ echo "$content" >> code.js
+ echo ";" >> code.js
+
+ sed -i "/ide\.openIn/a\
+ ide.droppedText(code); " snap.html
+ else
+ sed -i "/ide\.openIn/a\
+ ide.droppedText(ide.getURL('$3')); " snap.html
+ fi
+
+else
+ rm binary.js
+fi
+
+# compress all js files
+find . -name '*.js' | xargs -I {} uglifyjs {} -o {} -c
+
+# return to the directory where the script was called from
+cd "$scriptdir"
+
+# run helper scripts for building
+
+if [[ $1 == "-m" ]]
+then
+ ./mobile.sh "$2" "$buildsource"
+else
+ ./desktop.sh "$2" "$buildsource"
+fi
diff --git a/blocks.js b/blocks.js
index e56d0ed..35f31a4 100644
--- a/blocks.js
+++ b/blocks.js
@@ -866,6 +866,25 @@ SyntaxElementMorph.prototype.labelPart = function (spec) {
);
part.setContents(['date']);
break;
+ case '%locations':
+ part = new InputSlotMorph(
+ null, // text
+ false, // non-numeric
+ {
+ 'all' : ['all'],
+ 'country' : ['country'],
+ 'state' : ['state'],
+ 'state district' : ['state district'],
+ 'suburb' : ['suburb'],
+ 'city' : ['city'],
+ 'road' : ['road'],
+ 'house number' : ['house number'],
+ 'licence': ['licence']
+ },
+ true // read-only
+ );
+ part.setContents(['all']);
+ break;
case '%delim':
part = new InputSlotMorph(
null, // text
diff --git a/config.xml b/config.xml
new file mode 100644
index 0000000..e043129
--- /dev/null
+++ b/config.xml
@@ -0,0 +1,16 @@
+<?xml version='4.0' encoding='utf-8'?>
+<widget id="edu.berkeley.snap" version="0.0.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
+ <name>Snap!</name>
+ <description>
+ Snap! Build Your Own Blocks
+ A visual, blocks based programming language inspired by Scratch
+ </description>
+ <author email="jens@moenig.org" href="http://snap.berkeley.edu">
+ Jens Mönig and Brian Harvey
+ </author>
+ <content src="snap.html" />
+ <access origin="*" />
+ <preference name="Fullscreen" value="true" />
+ <preference name="Orientation" value="landscape" />
+ <icon src="www/snap_logo_sm.png" />
+</widget>
diff --git a/desktop.sh b/desktop.sh
new file mode 100755
index 0000000..72b0786
--- /dev/null
+++ b/desktop.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+if [[ "$snapsource" == "" ]]
+then
+ export snapsource="https://github.com/Gubolin/snap.git"
+fi
+
+if [[ $1 = "" ]]
+then
+ echo "Usage: desktop.sh PLATFORM [BUILDSOURCE]"
+ exit 0
+fi
+
+scriptdir=$(readlink -e ".")
+
+# Requirements:
+# git, zip, nodejs
+# node-webkit (https://github.com/rogerwang/node-webkit), node-webkit-builder (https://github.com/mllrsohn/node-webkit-builder)
+
+builddir=$(mktemp -d)
+
+if [[ $2 == "" ]]
+then
+ git clone "$snapsource" $builddir
+ cd $builddir/
+ git checkout mobileapp
+else
+ mv "$2" $builddir
+ cd $builddir/
+fi
+
+nwbuild -p "$1" .
+
+mv build/* $scriptdir/
diff --git a/gui.js b/gui.js
index 73fffe9..206b27a 100644
--- a/gui.js
+++ b/gui.js
@@ -201,7 +201,7 @@ IDE_Morph.prototype.init = function (isAutoFill) {
MorphicPreferences.globalFontFamily = 'Helvetica, Arial';
// restore saved user preferences
- this.userLanguage = null; // user language preference for startup
+ this.userLanguage = null;
this.applySavedSettings();
// additional properties:
@@ -3478,6 +3478,7 @@ IDE_Morph.prototype.setLanguage = function (lang, callback) {
if (lang === 'en') {
return this.reflectLanguage('en', callback);
}
+ myself.userLanguage = lang;
translation = document.createElement('script');
translation.id = 'language';
translation.onload = function () {
diff --git a/mobile.sh b/mobile.sh
new file mode 100755
index 0000000..6ec8a0e
--- /dev/null
+++ b/mobile.sh
@@ -0,0 +1,65 @@
+#!/bin/bash
+
+if [[ "$snapsource" == "" ]]
+then
+ export snapsource="https://github.com/Gubolin/snap.git"
+fi
+
+if [[ $1 = "" ]]
+then
+ echo "Usage: mobile.sh PLATFORM [BUILDSOURCE]"
+ exit 0
+fi
+
+scriptdir=$(readlink -e ".")
+
+# Requirements:
+# git, nodejs, android SDK / other platform(s)
+# cordova (https://cordova.apache.org/)
+
+if [[ $2 != "" ]]
+then
+ buildsource=$(readlink -e "$2")
+fi
+
+builddir=$(mktemp -d)
+
+cordova create $builddir edu.berkeley.snap "Snap\!"
+cd $builddir
+rm -rf www config.xml
+
+if [[ $2 == "" ]]
+then
+ git clone "$snapsource" www
+ cd www/
+ git checkout mobileapp
+else
+ mv "$buildsource" www
+ cd www
+fi
+
+# add mobile-specific library; it's made available at runtime
+sed -i '/link rel="shortcut icon"/a\
+ <script type="text/javascript" src="cordova.js"></script>' snap.html
+
+# add everything needed and build for $device
+cordova platform add "$1"
+cordova plugin add org.apache.cordova.plugin.softkeyboard
+cordova plugin add org.apache.cordova.vibration
+cordova plugin add org.apache.cordova.device-motion
+cordova plugin add org.apache.cordova.device-orientation
+cordova plugin add org.apache.cordova.geolocation
+cordova plugin add de.appplant.cordova.plugin.local-notification
+
+if [[ $1 == "android" ]]
+then
+ # Remove default icons
+ cd "$builddir/platforms/android"
+ find -name '*.png' | xargs rm
+fi
+
+cordova build "$1"
+
+cd $builddir
+# TODO other platforms
+find -name '*.apk' | xargs -I {} mv {} $scriptdir
diff --git a/morphic.js b/morphic.js
index 9616897..4a16597 100644
--- a/morphic.js
+++ b/morphic.js
@@ -10790,6 +10790,11 @@ WorldMorph.prototype.edit = function (aStringOrTextMorph) {
this.virtualKeyboard.style.top = this.cursor.top() + pos.y + "px";
this.virtualKeyboard.style.left = this.cursor.left() + pos.x + "px";
this.virtualKeyboard.focus();
+ if (cordova) {
+ if (cordova.plugins.SoftKeyboard) { // Android only
+ cordova.plugins.SoftKeyboard.show(); // Issue #81
+ }
+ }
}
if (MorphicPreferences.useSliderForInput) {
diff --git a/objects.js b/objects.js
index 981fb20..cb9ad4b 100644
--- a/objects.js
+++ b/objects.js
@@ -415,6 +415,11 @@ SpriteMorph.prototype.initBlocks = function () {
spec: 'go back %n layers',
defaults: [1]
},
+ doNotify: {
+ type: 'command',
+ category: 'looks',
+ spec: 'notification with title %s and content %s'
+ },
doScreenshot: {
type: 'command',
category: 'looks',
@@ -569,6 +574,16 @@ SpriteMorph.prototype.initBlocks = function () {
category: 'pen',
spec: 'stamp'
},
+ doStreamCamera: {
+ type: 'command',
+ category: 'pen',
+ spec: 'start streaming from the camera'
+ },
+ doStopCamera: {
+ type: 'command',
+ category: 'pen',
+ spec: 'stop streaming from the camera'
+ },
// Control
receiveGo: {
@@ -775,6 +790,23 @@ SpriteMorph.prototype.initBlocks = function () {
category: 'sensing',
spec: 'color %clr is touching %clr ?'
},
+ reportCameraMotion: {
+ only: SpriteMorph,
+ type: 'predicate',
+ category: 'sensing',
+ spec: 'camera motion at my position?'
+ },
+ reportCameraDirection: {
+ only: SpriteMorph,
+ type: 'reporter',
+ category: 'sensing',
+ spec: 'camera motion direction'
+ },
+ reportStreamingCamera: {
+ type: 'predicate',
+ category: 'sensing',
+ spec: 'streaming from the camera?'
+ },
colorFiltered: {
dev: true,
type: 'reporter',
@@ -884,6 +916,41 @@ SpriteMorph.prototype.initBlocks = function () {
category: 'sensing',
spec: 'current %dates'
},
+ doVibrate: {
+ type: 'command',
+ category: 'sensing',
+ spec: 'vibrate %n seconds'
+ },
+ reportCompassHeading: {
+ type: 'reporter',
+ category: 'sensing',
+ spec: 'current compass heading'
+ },
+ reportAccelerationX: {
+ type: 'reporter',
+ category: 'sensing',
+ spec: 'current acceleration along the x axes'
+ },
+ reportAccelerationY: {
+ type: 'reporter',
+ category: 'sensing',
+ spec: 'current acceleration along the y axes'
+ },
+ reportAccelerationZ: {
+ type: 'reporter',
+ category: 'sensing',
+ spec: 'current acceleration along the z axes'
+ },
+ reportLanguage: {
+ type: 'reporter',
+ category: 'sensing',
+ spec: 'language'
+ },
+ reportLocation: {
+ type: 'reporter',
+ category: 'sensing',
+ spec: 'location %locations'
+ },
// Operators
reifyScript: {
@@ -1755,6 +1822,8 @@ SpriteMorph.prototype.blockTemplates = function (category) {
blocks.push('-');
blocks.push(block('comeToFront'));
blocks.push(block('goBack'));
+ blocks.push('-');
+ blocks.push(block('doNotify'));
// for debugging: ///////////////
@@ -1886,6 +1955,10 @@ SpriteMorph.prototype.blockTemplates = function (category) {
blocks.push(block('reportTouchingColor'));
blocks.push(block('reportColorIsTouchingColor'));
blocks.push('-');
+ blocks.push(block('reportCameraMotion'));
+ blocks.push(block('reportCameraDirection'));
+ blocks.push(block('reportStreamingCamera'));
+ blocks.push('-');
blocks.push(block('doAsk'));
blocks.push(watcherToggle('getLastAnswer'));
blocks.push(block('getLastAnswer'));
@@ -1912,6 +1985,17 @@ SpriteMorph.prototype.blockTemplates = function (category) {
blocks.push(block('doSetFastTracking'));
blocks.push('-');
blocks.push(block('reportDate'));
+ blocks.push('-');
+ blocks.push(block('reportLanguage'));
+ blocks.push(block('reportLocation'));
+ blocks.push('-');
+ blocks.push(block('doVibrate'));
+ blocks.push('-');
+ blocks.push(block('reportCompassHeading'));
+ blocks.push('-');
+ blocks.push(block('reportAccelerationX'));
+ blocks.push(block('reportAccelerationY'));
+ blocks.push(block('reportAccelerationZ'));
// for debugging: ///////////////
@@ -4326,6 +4410,11 @@ StageMorph.prototype.init = function (globals) {
this.paletteCache = {}; // not to be serialized (!)
this.lastAnswer = ''; // last user input, do not persist
this.activeSounds = []; // do not persist
+ this.acceleration = null; // do not persist
+ this.compassHeading = null; // do not persist
+ this.streamingCamera = false;
+ this.lastCameraCanvas = null;
+ this.lastCameraMotion = new Point(0, 0);
this.trailsCanvas = null;
this.isThreadSafe = false;
@@ -4961,6 +5050,8 @@ StageMorph.prototype.blockTemplates = function (category) {
blocks.push('-');
blocks.push(block('show'));
blocks.push(block('hide'));
+ blocks.push('-');
+ blocks.push(block('doNotify'));
// for debugging: ///////////////
@@ -5015,6 +5106,9 @@ StageMorph.prototype.blockTemplates = function (category) {
} else if (cat === 'pen') {
blocks.push(block('clear'));
+ blocks.push('-');
+ blocks.push(block('doStreamCamera'));
+ blocks.push(block('doStopCamera'));
} else if (cat === 'control') {
@@ -5071,6 +5165,8 @@ StageMorph.prototype.blockTemplates = function (category) {
} else if (cat === 'sensing') {
+ blocks.push(block('reportStreamingCamera'));
+ blocks.push('-');
blocks.push(block('doAsk'));
blocks.push(watcherToggle('getLastAnswer'));
blocks.push(block('getLastAnswer'));
@@ -5095,6 +5191,17 @@ StageMorph.prototype.blockTemplates = function (category) {
blocks.push(block('doSetFastTracking'));
blocks.push('-');
blocks.push(block('reportDate'));
+ blocks.push('-');
+ blocks.push(block('reportLanguage'));
+ blocks.push(block('reportLocation'));
+ blocks.push('-');
+ blocks.push(block('doVibrate'));
+ blocks.push('-');
+ blocks.push(block('reportCompassHeading'));
+ blocks.push('-');
+ blocks.push(block('reportAccelerationX'));
+ blocks.push(block('reportAccelerationY'));
+ blocks.push(block('reportAccelerationZ'));
// for debugging: ///////////////
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..678a69c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "Snap!",
+ "version": "4.0",
+ "main": "snap.html",
+ "window": {
+ "toolbar": false
+ }
+}
diff --git a/snap.html b/snap.html
index 93b4c73..e3c54dc 100755
--- a/snap.html
+++ b/snap.html
@@ -18,12 +18,14 @@
<script type="text/javascript" src="locale.js"></script>
<script type="text/javascript" src="cloud.js"></script>
<script type="text/javascript" src="sha512.js"></script>
+
<script type="text/javascript">
var world;
window.onload = function () {
world = new WorldMorph(document.getElementById('world'));
world.worldCanvas.focus();
- new IDE_Morph().openIn(world);
+ var ide = new IDE_Morph()
+ ide.openIn(world);
setInterval(loop, 1);
};
function loop() {
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