diff options
| -rw-r--r-- | .gitmodules | 12 | ||||
| m--------- | Snapin8r | 0 | ||||
| -rwxr-xr-x | binary.js | 1099 | ||||
| -rwxr-xr-x | binary.sh | 95 | ||||
| -rw-r--r-- | blocks.js | 55 | ||||
| -rw-r--r-- | config.xml | 16 | ||||
| -rwxr-xr-x | desktop.sh | 34 | ||||
| -rw-r--r-- | github.js | 312 | ||||
| -rw-r--r-- | gui.js | 482 | ||||
| -rwxr-xr-x | mobile.sh | 90 | ||||
| -rw-r--r-- | morphic.js | 5 | ||||
| -rw-r--r-- | objects.js | 286 | ||||
| m--------- | octokit.js | 0 | ||||
| -rw-r--r-- | package.json | 8 | ||||
| -rw-r--r-- | paint.js | 7 | ||||
| -rw-r--r-- | promise-1.0.0.js | 684 | ||||
| -rwxr-xr-x | snap.html | 9 | ||||
| -rw-r--r-- | store.js | 6 | ||||
| -rw-r--r-- | threads.js | 477 | ||||
| -rw-r--r-- | tools.xml | 2 | ||||
| m--------- | vkBeautify | 0 |
21 files changed, 3649 insertions, 30 deletions
diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c1a2763 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,12 @@ +[submodule "Snapin8r"] + path = Snapin8r + url = https://github.com/Hardmath123/Snapin8r +[submodule "octokit.js"] + path = octokit.js + url = https://github.com/philschatz/octokit.js +[submodule "vkBeautify"] + path = vkBeautify + url = https://github.com/vkiryukhin/vkBeautify +[submodule "diff-merge"] + path = diff-merge + url = https://github.com/nighca/diff-merge diff --git a/Snapin8r b/Snapin8r new file mode 160000 +Subproject c9cebaa147c2c53520212a221efd4061a5dcb19 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..9de5fa0 --- /dev/null +++ b/binary.sh @@ -0,0 +1,95 @@ +#!/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." + echo "" + echo "" + echo "The following environment variables will be used, if available:" + echo " snapsource Path or URL to the Snap! git repository. This repository must contain the branch" + echo " mobileapp with the configuration files! Default: https://github.com/Gubolin/snap.git" + echo " crosswalk Path to a crosswalk-cordova directory. Download \"Cordova Android\" here:" + echo " https://crosswalk-project.org/documentation/downloads.html" + echo " If omitted, standard cordova will be used." + exit 0 +fi + +scriptdir=$(readlink -e ".") + +# Requirements: +# git +# UglifyJS2 (https://github.com/mishoo/UglifyJS2) + +ide=true +platform=$2 + +# presentation mode +if [[ "$3" != "" ]] +then + ide=false +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 *.sh *.pdf *.txt + rm -r help/ Costumes/ Backgrounds/ Sounds/ + + sed -i '/paint\.js"/d' snap.html + sed -i '/cloud\.js"/d' snap.html + sed -i 's/gui\.js"/binary\.js"/' snap.html + + # if a file was given, move it to "project.xml" + # it will be loaded like an URL then + if [ -f "$3" ] + then + cp "$3" "project.xml" + url="project.xml" + else + url=$3 + fi + + # load custom project from url + sed -i "/ide\.openIn/a\ + ide.droppedText(ide.getURL('$url')); " snap.html + +else + rm binary.js +fi + +# compress all js files +find . -name '*.js' | xargs -I {} uglifyjs {} -o {} -c 2> /dev/null + +# 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 @@ -887,6 +887,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 @@ -994,6 +1013,8 @@ SyntaxElementMorph.prototype.labelPart = function (spec) { null, false, { + 'any key': ['any key'], + 'number key': ['number key'], 'up arrow': ['up arrow'], 'down arrow': ['down arrow'], 'right arrow': ['right arrow'], @@ -7778,6 +7799,8 @@ SymbolMorph.prototype.names = [ 'pointRight', 'gears', 'file', + 'mutedSounds', + 'unmutedSounds', 'fullScreen', 'normalScreen', 'smallStage', @@ -7902,6 +7925,10 @@ SymbolMorph.prototype.symbolCanvasColored = function (aColor) { return this.drawSymbolGears(canvas, aColor); case 'file': return this.drawSymbolFile(canvas, aColor); + case 'mutedSounds': + return this.drawSymbolMutedSounds(canvas, aColor); + case 'unmutedSounds': + return this.drawSymbolUnmutedSounds(canvas, aColor); case 'fullScreen': return this.drawSymbolFullScreen(canvas, aColor); case 'normalScreen': @@ -8104,6 +8131,34 @@ SymbolMorph.prototype.drawSymbolFile = function (canvas, color) { return canvas; }; +SymbolMorph.prototype.drawSymbolMutedSounds = function (canvas, color) { + // answer a canvas showing a muted sounds toggling symbol + var ctx = canvas.getContext('2d'), + w = canvas.width, + h = canvas.height, + w2 = w / 2, + h2 = h / 2; + + ctx.fillStyle = color.darker(40).toString(); + ctx.fillRect(0, 0, w, h); + + return canvas; +}; + +SymbolMorph.prototype.drawSymbolUnmutedSounds = function (canvas, color) { + // answer a canvas showing a UNmuted sounds toggling symbol + var ctx = canvas.getContext('2d'), + w = canvas.width, + h = canvas.height, + w2 = w / 2, + h2 = h / 2; + + ctx.fillStyle = color.darker(60).toString(); + ctx.fillRect(0, 0, w, h); + + return canvas; +}; + SymbolMorph.prototype.drawSymbolFullScreen = function (canvas, color) { // answer a canvas showing two arrows pointing diagonally outwards var ctx = canvas.getContext('2d'), 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/github.js b/github.js new file mode 100644 index 0000000..5294037 --- /dev/null +++ b/github.js @@ -0,0 +1,312 @@ +/* + + github.js + + a GitHubBackend backend API for SNAP! + + written by Gubolin, based on cloud.js by Jens Mönig + + Copyright (C) 2014 by Jens Mönig, Gubolin + + 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/>. + +*/ + +// Global settings ///////////////////////////////////////////////////// + +/*global modules, localize*/ + +modules.github = '2014-July-31'; + +// Global stuff + +var GitHubBackend; + +var GitHub = new GitHubBackend(); + +// GitHubBackend ///////////////////////////////////////////////////////////// + +function GitHubBackend() { + this.gh = null; + this.username = null; + this.password = null; // TODO saved as plain text +} + +GitHubBackend.prototype.clear = function () { + this.gh = null; + this.username = null; + this.password = null; +}; + +// GitHubBackend: Snap! API + +GitHubBackend.prototype.getProject = function ( + userName, + projectName, + callBack, + errorCall, + commitSha +) { + var myself = this; + + if (myself.gh === null) { + myself.gh = new Octokit(); + } + + var repo = myself.gh.getRepo(userName, projectName); + var branch = repo.getBranch(); // master (default) + branch.getCommits({}).then( + function (commits) { + branch.read('snap.xml', false).then( + function (sourceContent) { + callBack.call( + null, + sourceContent.content, + commits[0].sha + ); + }, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); + }, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); +}; + +GitHubBackend.prototype.login = function ( + username, + password, + validateData, + callBack, + errorCall +) { + var myself = this; + var me; + + myself.gh = new Octokit({ + username: username, + password: password + }); + + if (validateData === true) { + me = myself.gh.getUser(); + if (me !== null) { + me.getInfo().then( + function() { + myself.username = username; + myself.password = password; + + callBack.call(myself); + }, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); + } else { + errorCall.call(myself, localize('Something went wrong :('), 'GitHub'); + } + } else { + myself.username = username; + myself.password = password; + + callBack.call(myself); + } +}; + +GitHubBackend.prototype.saveProject = function (commitMessage, parentCommitSha, lastCommit, ide, callBack, errorCall) { + var myself = this, + data; + var pdata, media; + var repoName = ide.projectName.replace(/[^\w-]/g, ''); // TODO validation of project name + + ide.stage.fireStopAllEvent(); + ide.serializer.isCollectingMedia = true; + pdata = ide.serializer.serialize(ide.stage); + media = ide.hasChangedMedia ? + ide.serializer.mediaXML(ide.projectName) : null; + data = '<snapdata>\n' + pdata + '\n' + media + '\n</snapdata>'; + + // check if serialized data can be parsed back again + try { + ide.serializer.parse(pdata); + } catch (err) { + ide.showMessage('Serialization of program data failed:\n' + err); + throw new Error('Serialization of program data failed:\n' + err); + } + if (media !== null) { + try { + ide.serializer.parse(media); + } catch (err) { + ide.showMessage('Serialization of media failed:\n' + err); + throw new Error('Serialization of media failed:\n' + err); + } + } + ide.serializer.isCollectingMedia = false; + ide.serializer.flushMedia(); + + myself.getProjectList( + function (projects) { + var exists = false; + + projects.forEach(function (project) { + if (project.ProjectName.indexOf(repoName) > -1) { + exists = true; + return; + } + }); + + pushChanges = function () { + if (myself.gh !== null) { + var repo = myself.gh.getRepo(myself.username, ide.projectName); + var branch = repo.getBranch(); // master (default) + var message = commitMessage; + + writeChanges = function (code, pcSha) { + if (pcSha !== parentCommitSha && data !== code) { + var compareResult = window.diff.compare(lastCommit, data, '\n'); // get diff from last push + data = window.diff.merge(code, compareResult); // apply it to the latest code + console.log(data); + } + + var contents = { + 'snap.xml': data, + 'README.md': ide.projectNotes + }; + + branch.writeMany(contents, message, pcSha).then( + function () { + callBack.call(); + }, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); + }; + + if (parentCommitSha !== null) { // repo was just created + myself.getProject(myself.username, ide.projectName, + writeChanges, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); + } else { + writeChanges(data, parentCommitSha); + } + } + }; + + if (exists === false){ + myself.gh.getUser().createRepo(repoName, { // these should be discussed + 'description': 'Snap! Project - http://gubolin.github.io/snap/index.html#github:Username=' + myself.username + '&projectName=' + repoName, + 'has_wiki': 'false', + 'has_downloads': 'false', + 'auto_init': true, + 'license_template': 'mit' // discuss + }).then( + pushChanges, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); + } else { + pushChanges(); + } + + }, + function (error) { + errorCall.call(null, error, 'GitHub'); + } + ); +}; + +GitHubBackend.prototype.getProjectList = function (callBack, errorCall) { + var myself = this; + + if (myself.gh !== null){ + var user = myself.gh.getUser(); + + if (user === null) { + myself.message('You are not logged in'); + return; + } + + user.getRepos().then( + function (repos) { + var snapProjects = []; + + var modCallBack = (function () { + var called = 0; + return function () { + if (++called == repos.length) { + callBack.call(myself, snapProjects); + } + }; + })(); + + if (repos.length === 0) { + callBack.call(myself, snapProjects); + } + + repos.forEach(function (repo) { + if (repo.description.indexOf('Snap! Project') > -1) { // TODO nicer detection + var project, ghrepo, branch; + + ghrepo = myself.gh.getRepo(repo.owner.login, repo.name); + branch = ghrepo.getBranch(); // master (default) + + branch.read('README.md', false).then( + function (notesContent) { + project = { + 'ProjectName': repo.name, + 'Notes': notesContent.content, + 'Updated': repo.updated_at.replace(/T/, ' ').replace(/Z/, '') // TODO this could be better + }; + + snapProjects.push(project); + modCallBack(); + }, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); + } else { + modCallBack(); + } + }); + }, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); + } else { + myself.message('You are not logged in'); + return; + } +}; + +GitHubBackend.prototype.logout = function (callBack) { + this.clear(); +}; + +// GitHub: user messages (to be overridden) + +GitHubBackend.prototype.message = function (string) { + alert(string); +}; @@ -63,7 +63,7 @@ Costume, CostumeEditorMorph, MorphicPreferences, touchScreenSettings, standardSettings, Sound, BlockMorph, ToggleMorph, InputSlotDialogMorph, ScriptsMorph, isNil, SymbolMorph, BlockExportDialogMorph, BlockImportDialogMorph, SnapTranslator, localize, List, InputSlotMorph, -SnapCloud, Uint8Array, HandleMorph, SVG_Costume, fontHeight, hex_sha512, +SnapCloud, GitHub, Uint8Array, HandleMorph, SVG_Costume, fontHeight, hex_sha512, sb, CommentMorph, CommandBlockMorph, BlockLabelPlaceHolderMorph, Audio, SpeechBubbleMorph*/ @@ -229,6 +229,7 @@ IDE_Morph.prototype.init = function (isAutoFill) { this.corral = null; this.isAutoFill = isAutoFill || true; + this.isMuted = false; this.isAppMode = false; this.isSmallStage = false; this.filePicker = null; @@ -238,6 +239,8 @@ IDE_Morph.prototype.init = function (isAutoFill) { this.stageRatio = 1; // for IDE animations, e.g. when zooming this.loadNewProject = false; // flag when starting up translated + this.parentCommitSha = null; // for GitHub + this.lastCommit = null; // for GitHub this.shield = null; // initialize inherited properties: @@ -246,6 +249,7 @@ IDE_Morph.prototype.init = function (isAutoFill) { // override inherited properites: this.color = this.backgroundColor; + setInterval(this.save, 1000 * 60 * 60 * 5); // every 5 minutes window.peers = []; }; @@ -255,6 +259,7 @@ IDE_Morph.prototype.openIn = function (world) { // get persistent user data, if any if (localStorage) { usr = localStorage['-snap-user']; + ghusr = localStorage['-snap-ghuser']; if (usr) { usr = SnapCloud.parseResponse(usr)[0]; if (usr) { @@ -265,6 +270,13 @@ IDE_Morph.prototype.openIn = function (world) { } } } + if (ghusr) { + ghusr = SnapCloud.parseResponse(ghusr)[0]; + if (ghusr) { + GitHub.login(ghusr.username, ghusr.password, false, + function() {}, myself.githubError()); + } + } } this.buildPanes(); @@ -282,6 +294,16 @@ IDE_Morph.prototype.openIn = function (world) { }, 2000); }; + GitHub.message = function (string) { + var m = new MenuMorph(null, string), + intervalHandle; + m.popUpCenteredInWorld(world); + intervalHandle = setInterval(function () { + m.destroy(); + clearInterval(intervalHandle); + }, 2000); + }; + // prevent non-DialogBoxMorphs from being dropped // onto the World in user-mode world.reactToDropOf = function (morph) { @@ -306,7 +328,8 @@ IDE_Morph.prototype.openIn = function (world) { } throw new Error('unable to retrieve ' + url); } catch (err) { - return; + myself.showMessage('unable to retrieve project'); + return ''; } } @@ -393,6 +416,41 @@ IDE_Morph.prototype.openIn = function (world) { }, this.cloudError() ); + } else if (location.hash.substr(0, 8) === '#github:') { + this.shield = new Morph(); + this.shield.color = this.color; + this.shield.setExtent(this.parent.extent()); + this.parent.add(this.shield); + myself.showMessage('Fetching project\nfrom GitHub...'); + + dict = SnapCloud.parseDict(location.hash.substr(8)); + + GitHub.getProject( + dict.Username, + dict.projectName, + function (code, pcSha) { + var msg; + myself.nextSteps([ + function () { + msg = myself.showMessage('Opening GitHub project...'); + }, + function () { + myself.parentCommitSha = pcSha; + myself.lastCommit = code; + myself.rawOpenCloudDataString(code); + myself.hasChangedMedia = true; + }, + function () { + myself.shield.destroy(); + myself.shield = null; + msg.destroy(); + myself.toggleAppMode(true); + myself.runScripts(); + } + ]); + }, + this.githubError() + ); } else if (location.hash.substr(0, 7) === '#cloud:') { this.shield = new Morph(); this.shield.alpha = 0; @@ -517,6 +575,7 @@ IDE_Morph.prototype.createControlBar = function () { stopButton, pauseButton, startButton, + muteSoundsButton, projectButton, settingsButton, stageSizeButton, @@ -606,6 +665,38 @@ IDE_Morph.prototype.createControlBar = function () { this.controlBar.add(appModeButton); this.controlBar.appModeButton = appModeButton; // for refreshing + //muteSoundsButton + button = new ToggleButtonMorph( + null, //colors, + myself, // the IDE is the target + 'toggleMuteSounds', + [ + new SymbolMorph('mutedSounds', 14), + new SymbolMorph('unmutedSounds', 14) + ], + function () { // query + return myself.isMuted; + } + ); + + 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 = this.buttonLabelColor; + button.contrast = this.buttonContrast; + button.drawNew(); + // button.hint = 'sounds\nmuted & unmuted'; + button.fixLayout(); + button.refresh(); + muteSoundsButton = button; + this.controlBar.add(muteSoundsButton); + this.controlBar.muteSoundsButton = button; // for refreshing + // stopButton button = new PushButtonMorph( this, @@ -770,7 +861,7 @@ IDE_Morph.prototype.createControlBar = function () { myself.right() - StageMorph.prototype.dimensions.x * (myself.isSmallStage ? myself.stageRatio : 1) ); - [stageSizeButton, appModeButton].forEach( + [stageSizeButton, appModeButton, muteSoundsButton].forEach( function (button) { x += padding; button.setCenter(myself.controlBar.center()); @@ -1630,7 +1721,12 @@ IDE_Morph.prototype.droppedBinary = function (anArrayBuffer, name) { myself = this, suffix = name.substring(name.length - 3); - if (suffix.toLowerCase() !== 'ypr') {return; } + if (suffix.toLowerCase() !== 'ypr') { + var zip = new JSZip(anArrayBuffer); + myself.droppedText(Snapin8r(zip)); + + return; + } function loadYPR(buffer, lbl) { var reader = new sb.Reader(), @@ -2041,6 +2137,18 @@ IDE_Morph.prototype.cloudMenu = function () { 'changeCloudPassword' ); } + + if (!GitHub.username) { + menu.addItem( + 'Login via GitHub...', + 'initializeGitHub' + ); + } else { + menu.addItem( + localize('Logout') + ' ' + GitHub.username + ' ' + localize('from GitHub'), + 'logoutGitHub' + ); + } if (shiftClicked) { menu.addLine(); menu.addItem( @@ -2386,6 +2494,9 @@ IDE_Morph.prototype.projectMenu = function () { menu.addItem('New', 'createNewProject'); menu.addItem('Open...', 'openProjectsBrowser'); menu.addItem('Save', "save"); + if (GitHub.username) { + menu.addItem('Save with commit message', 'commitProjectToGitHub'); + } menu.addItem( 'Save to disk', 'saveProjectToDisk', @@ -2834,8 +2945,10 @@ IDE_Morph.prototype.save = function () { if (this.projectName) { if (this.source === 'local') { // as well as 'examples' this.saveProject(this.projectName); - } else { // 'cloud' + } else if (this.source === 'cloud') { // 'cloud' this.saveProjectToCloud(this.projectName); + } else { // 'github' + this.saveProjectToGitHub(this.projectName); } } else { this.saveProjectsBrowser(); @@ -3511,6 +3624,27 @@ IDE_Morph.prototype.toggleStageSize = function (isSmall) { } }; +IDE_Morph.prototype.toggleMuteSounds = function (isMuted) { + this.isMuted = isNil(isMuted) ? !this.isMuted : isMuted; + this.controlBar.muteSoundsButton.refresh(); + + /* stage.activeSounds holds all active sounds + * a sprite's .activeSounds holds just its own + * so you have to use the stage to mute + * and the sprite to unmute, because the stage's volume + * overrides the sprite's one. + */ + + if (this.isMuted === false) { + this.stage.unmuteAllSounds(); + this.sprites.asArray().forEach(function (sprt) { + sprt.unmuteAllSounds(); + }); + } else { + this.stage.muteAllSounds(); + } +}; + IDE_Morph.prototype.createNewProject = function () { var myself = this; this.confirm( @@ -3559,6 +3693,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 () { @@ -3571,6 +3706,12 @@ IDE_Morph.prototype.setLanguage = function (lang, callback) { IDE_Morph.prototype.reflectLanguage = function (lang, callback) { var projectData; SnapTranslator.language = lang; + this.world().children.forEach(function (morph) { + if (morph instanceof BlockEditorMorph) { + morph.updateDefinition(); // save custom blocks + // otherwise, initBlocks() will reset the definition + } + }); if (!this.loadNewProject) { if (Process.prototype.isCatchingErrors) { try { @@ -3803,6 +3944,48 @@ IDE_Morph.prototype.initializeCloud = function () { ); }; +IDE_Morph.prototype.initializeGitHub = function () { + var myself = this, + world = this.world(); + new DialogBoxMorph( + null, + function (user) { + var pw = user.password, + str; + GitHub.login( + user.username, + pw, + true, + function () { + if (user.choice) { + str = SnapCloud.encodeDict( + { + username: user.username, + password: pw + } + ); + localStorage['-snap-ghuser'] = str; + } + myself.source = 'github'; + myself.showMessage('now connected.', 2); + }, + myself.githubError() + ); + } + ).withKey('cloudlogin').promptCredentials( + 'Sign in with your GitHub account', + 'login', + null, + null, + null, + null, + 'stay signed in on this computer\nuntil logging out', + world, + myself.cloudIcon(), + myself.cloudMsg + ); +}; + IDE_Morph.prototype.createCloudAccount = function () { var myself = this, world = this.world(); @@ -3930,6 +4113,19 @@ IDE_Morph.prototype.logout = function () { ); }; +IDE_Morph.prototype.logoutGitHub = function () { + var myself = this; + delete localStorage['-snap-ghuser']; + GitHub.logout( + function () { + myself.showMessage('disconnected.', 2); + }, + function () { + myself.showMessage('disconnected.', 2); + } + ); +}; + IDE_Morph.prototype.saveProjectToCloud = function (name) { var myself = this; if (name) { @@ -3943,6 +4139,90 @@ IDE_Morph.prototype.saveProjectToCloud = function (name) { } }; +IDE_Morph.prototype.saveProjectToGitHub = function (name, commitMessage) { + var myself = this; + if (name) { + this.showMessage('Comitting project\nto GitHub...'); + this.setProjectName(name); + GitHub.saveProject( + commitMessage, + this.parentCommitSha, + this.lastCommit, + this, + function () { + GitHub.getProject( + GitHub.username, + name, + function (code, pcSha) { + myself.source = 'github'; + myself.parentCommitSha = pcSha; + myself.lastCommit = code; + myself.droppedText(code); + }, + myself.githubError() + ); + }, + this.githubError() + ); + } +}; + +IDE_Morph.prototype.commitProjectToGitHub = function () { + var dialog = new DialogBoxMorph().withKey('commitMessage'), + frame = new ScrollFrameMorph(), + text = new TextMorph(''), + ok = dialog.ok, + myself = this, + size = 120, + world = this.world(); + + frame.padding = 6; + frame.setWidth(size); + frame.acceptsDrops = false; + frame.contents.acceptsDrops = false; + + text.setWidth(size - frame.padding * 2); + text.setPosition(frame.topLeft().add(frame.padding)); + text.enableSelecting(); + text.isEditable = true; + + frame.setHeight(size); + frame.fixLayout = nop; + frame.edge = InputFieldMorph.prototype.edge; + frame.fontSize = InputFieldMorph.prototype.fontSize; + frame.typeInPadding = InputFieldMorph.prototype.typeInPadding; + frame.contrast = InputFieldMorph.prototype.contrast; + frame.drawNew = InputFieldMorph.prototype.drawNew; + frame.drawRectBorder = InputFieldMorph.prototype.drawRectBorder; + + frame.addContents(text); + text.drawNew(); + + dialog.ok = function () { + myself.saveProjectToGitHub( + myself.projectName, + text.text + ); + + ok.call(this); + }; + + dialog.justDropped = function () { + text.edit(); + }; + + dialog.labelString = 'Commit message'; + dialog.createLabel(); + dialog.addBody(frame); + frame.drawNew(); + dialog.addButton('ok', 'OK'); + dialog.addButton('cancel', 'Cancel'); + dialog.fixLayout(); + dialog.drawNew(); + dialog.popUp(world); + dialog.setCenter(world.center()); + text.edit(); +}; IDE_Morph.prototype.exportProjectMedia = function (name) { var menu, media; this.serializer.isCollectingMedia = true; @@ -4144,6 +4424,42 @@ IDE_Morph.prototype.cloudError = function () { }; }; +IDE_Morph.prototype.githubError = function () { + var myself = this; + + function getURL(url) { + try { + var request = new XMLHttpRequest(); + request.open('GET', url, false); + request.send(); + if (request.status === 200) { + return request.responseText; + } + return null; + } catch (err) { + return null; + } + } + + return function (responseText, url) { + var response = responseText; + if (myself.shield) { + myself.shield.destroy(); + myself.shield = null; + } + if (response.length > 50) { + response = response.substring(0, 50) + '...'; + } + new DialogBoxMorph().inform( + 'GitHub', + (url ? url + '\n' : '') + + response, + myself.world(), + myself.cloudIcon(null, new Color(180, 0, 0)) + ); + }; +}; + IDE_Morph.prototype.cloudIcon = function (height, color) { var clr = color || DialogBoxMorph.prototype.titleBarColor, isFlat = MorphicPreferences.isFlat, @@ -4274,7 +4590,7 @@ ProjectDialogMorph.prototype.init = function (ide, task) { // additional properties: this.ide = ide; this.task = task || 'open'; // String describing what do do (open, save) - this.source = ide.source || 'local'; // or 'cloud' or 'examples' + this.source = ide.source || 'local'; // or 'cloud' or 'github' or 'examples' this.projectList = []; // [{name: , thumb: , notes:}] this.handle = null; @@ -4334,6 +4650,7 @@ ProjectDialogMorph.prototype.buildContents = function () { } this.addSourceButton('cloud', localize('Cloud'), 'cloud'); + this.addSourceButton('github', localize('GitHub'), 'github'); this.addSourceButton('local', localize('Browser'), 'storage'); if (this.task === 'open') { this.addSourceButton('examples', localize('Examples'), 'poster'); @@ -4582,6 +4899,20 @@ ProjectDialogMorph.prototype.setSource = function (source) { } ); return; + case 'github': + msg = myself.ide.showMessage('Updating\nproject list...'); + this.projectList = []; + GitHub.getProjectList( + function (projectList) { + myself.installGitHubProjectList(projectList); + msg.destroy(); + }, + function (err, lbl) { + msg.destroy(); + myself.ide.githubError().call(null, err, lbl); + } + ); + return; case 'examples': this.projectList = this.getExamplesProjectList(); break; @@ -4634,7 +4965,7 @@ ProjectDialogMorph.prototype.setSource = function (source) { } myself.edit(); }; - } else { // 'examples', 'cloud' is initialized elsewhere + } else { // 'examples', 'cloud' and 'github' is initialized elsewhere this.listField.action = function (item) { var src, xml; if (item === undefined) {return; } @@ -4800,6 +5131,69 @@ ProjectDialogMorph.prototype.installCloudProjectList = function (pl) { } }; +ProjectDialogMorph.prototype.installGitHubProjectList = function (pl) { + var myself = this; + this.projectList = pl || []; + this.projectList.sort(function (x, y) { + return x.ProjectName < y.ProjectName ? -1 : 1; + }); + + this.listField.destroy(); + this.listField = new ListMorph( + this.projectList, + this.projectList.length > 0 ? + function (element) { + return element.ProjectName; + } : null, + [], + function () {myself.ok(); } + ); + this.fixListFieldItemColors(); + this.listField.fixLayout = nop; + this.listField.edge = InputFieldMorph.prototype.edge; + this.listField.fontSize = InputFieldMorph.prototype.fontSize; + this.listField.typeInPadding = InputFieldMorph.prototype.typeInPadding; + this.listField.contrast = InputFieldMorph.prototype.contrast; + this.listField.drawNew = InputFieldMorph.prototype.drawNew; + this.listField.drawRectBorder = InputFieldMorph.prototype.drawRectBorder; + + this.listField.action = function (item) { + if (item === undefined) {return; } + if (myself.nameField) { + myself.nameField.setContents(item.ProjectName || ''); + } + if (myself.task === 'open') { + myself.notesText.text = item.Notes || ''; + myself.notesText.drawNew(); + myself.notesField.contents.adjustBounds(); + myself.preview.texture = item.Thumbnail || null; + myself.preview.cachedTexture = null; + myself.preview.drawNew(); + (new SpeechBubbleMorph(new TextMorph( + localize('last changed') + '\n' + item.Updated, + null, + null, + null, + null, + 'center' + ))).popUp( + myself.world(), + myself.preview.rightCenter().add(new Point(2, 0)) + ); + } + myself.buttons.fixLayout(); + myself.fixLayout(); + myself.edit(); + }; + this.body.add(this.listField); + this.deleteButton.show(); + this.buttons.fixLayout(); + this.fixLayout(); + if (this.task === 'open') { + this.clearDetails(); + } +}; + ProjectDialogMorph.prototype.clearDetails = function () { this.notesText.text = ''; this.notesText.drawNew(); @@ -4816,6 +5210,8 @@ ProjectDialogMorph.prototype.openProject = function () { this.ide.source = this.source; if (this.source === 'cloud') { this.openCloudProject(proj); + } else if (this.source === 'github') { + this.openGitHubProject(proj); } else if (this.source === 'examples') { src = this.ide.getURL( 'http://snap.berkeley.edu/snapsource/Examples/' + @@ -4841,6 +5237,23 @@ ProjectDialogMorph.prototype.openCloudProject = function (project) { ]); }; +ProjectDialogMorph.prototype.openGitHubProject = function (project, user) { + var myself = this; + + if (user == null) { // jshint ignore:line + user = GitHub.username; + } + + myself.ide.nextSteps([ + function () { + myself.ide.showMessage('Fetching project\nfrom GitHub...'); + }, + function () { + myself.rawOpenGitHubProject(project, user); + } + ]); +}; + ProjectDialogMorph.prototype.rawOpenCloudProject = function (proj) { var myself = this; SnapCloud.reconnect( @@ -4867,6 +5280,21 @@ ProjectDialogMorph.prototype.rawOpenCloudProject = function (proj) { this.destroy(); }; +ProjectDialogMorph.prototype.rawOpenGitHubProject = function (proj, user) { + var myself = this; + GitHub.getProject( + user, + proj.ProjectName, + function (code, pcSha) { + myself.ide.source = 'github'; + myself.ide.parentCommitSha = pcSha; + myself.ide.lastCommit = code; + myself.ide.droppedText(code); + }, + myself.ide.githubError() + ); + this.destroy(); +}; ProjectDialogMorph.prototype.saveProject = function () { var name = this.nameField.contents().text.text, notes = this.notesText.text, @@ -4893,6 +5321,25 @@ ProjectDialogMorph.prototype.saveProject = function () { this.ide.setProjectName(name); myself.saveCloudProject(); } + } else if (this.source === 'github') { + if (detect( + this.projectList, + function (item) {return item.ProjectName === name; } + )) { + this.ide.confirm( + localize( + 'Are you sure you want to replace' + ) + '\n"' + name + '"?', + 'Replace Project', + function () { + myself.ide.setProjectName(name); + myself.saveGitHubProject(); + } + ); + } else { + this.ide.setProjectName(name); + myself.saveGitHubProject(); + } } else { // 'local' if (detect( this.projectList, @@ -4934,13 +5381,32 @@ ProjectDialogMorph.prototype.saveCloudProject = function () { this.destroy(); }; +ProjectDialogMorph.prototype.saveGitHubProject = function () { + var myself = this; + this.ide.showMessage('Committing project\nto GitHub...'); + GitHub.saveProject( + null, + this.ide.parentCommitSha, + this.ide.lastCommit, + this.ide, + function () { + myself.ide.source = 'github'; + myself.ide.showMessage('saved.', 2); + }, + this.ide.githubError() + ); + this.destroy(); +}; + ProjectDialogMorph.prototype.deleteProject = function () { var myself = this, proj, idx, name; - if (this.source === 'cloud') { + if (this.source === 'github') { + // TODO: not implemented + } else if (this.source === 'cloud') { proj = this.listField.selected; if (proj) { this.ide.confirm( diff --git a/mobile.sh b/mobile.sh new file mode 100755 index 0000000..edbf020 --- /dev/null +++ b/mobile.sh @@ -0,0 +1,90 @@ +#!/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/) +# optional: crosswalk-cordova (https://github.com/crosswalk-project/crosswalk-cordova-android) + +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 + +echo "Adding cordova plugins" +# add everything needed and build for $device +cordova platform add "$1" > /dev/null +cordova plugin add org.apache.cordova.plugin.softkeyboard \ + org.apache.cordova.vibration \ + org.apache.cordova.device-motion \ + org.apache.cordova.device-orientation \ + org.apache.cordova.geolocation \ + de.appplant.cordova.plugin.local-notification 2> /dev/null + +if [[ $1 == "android" ]] +then + # Remove default icons + cd "$builddir/platforms/android" + find -name '*.png' | xargs rm + + if [[ $crosswalk != "" ]] + then + # adapted from https://crosswalk-project.org/documentation/cordova/migrate_an_application.html#migrate + echo "Preparing crosswalk" + rm -Rf "$builddir/platforms/android/CordovaLib/*" + cp -a $crosswalk/framework/* "$builddir/platforms/android/CordovaLib/" + cp -a "$crosswalk/VERSION" "$builddir/platforms/android/" + export ANDROID_HOME=$(dirname $(dirname $(which android))) + cd "$builddir/platforms/android/CordovaLib/" + android update project --subprojects --target android-21 --path . > /dev/null + ant debug > /dev/null + cd "$builddir" + # prepend permissions to end of manifest + sed -i "s,</manifest>,\ + <uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />\ + <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\ + <uses-permission android:name=\"android.permission.INTERACT_ACROSS_USERS\" />\ + </manifest>,g" \ + "$builddir/platforms/android/AndroidManifest.xml" + fi +fi + +echo "Building application" +cordova build "$1" > /dev/null + +cd $builddir +# TODO other platforms +find -name '*.apk' | xargs -I {} mv {} $scriptdir +echo "Finished." @@ -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) { @@ -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', @@ -459,6 +464,23 @@ SpriteMorph.prototype.initBlocks = function () { category: 'sound', spec: 'stop all sounds' }, + doSetVolume: { + type: 'command', + category: 'sound', + spec: 'set volume to %n %', + defaults: [100] + }, + doChangeVolume: { + type: 'command', + category: 'sound', + spec: 'change volume by %n', + defaults: [-10] + }, + reportVolume: { + type: 'reporter', + category: 'sound', + spec: 'volume' + }, doRest: { type: 'command', category: 'sound', @@ -569,6 +591,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: { @@ -786,6 +818,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', @@ -847,6 +896,11 @@ SpriteMorph.prototype.initBlocks = function () { category: 'sensing', spec: 'key %key pressed?' }, + getKeysPressed: { + type: 'reporter', + category: 'sensing', + spec: 'keys pressed' + }, reportDistanceTo: { type: 'reporter', category: 'sensing', @@ -895,6 +949,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: { @@ -1345,6 +1434,8 @@ SpriteMorph.prototype.init = function (globals) { this.version = Date.now(); // for observer optimization this.isClone = false; // indicate a "temporary" Scratch-style clone this.cloneOriginName = ''; + this.volume = 100; + this.activeSounds = []; // sprite nesting properties this.parts = []; // not serialized, only anchor (name) @@ -1448,8 +1539,11 @@ SpriteMorph.prototype.appearIn = function (ide) { // SpriteMorph versioning SpriteMorph.prototype.setName = function (string) { - this.name = string || this.name; - this.version = Date.now(); + if (string != 'mouse-pointer' && string != 'pen trails' + && string != 'edge') { // used by system + this.name = string || this.name; + this.version = Date.now(); + } }; // SpriteMorph rendering @@ -1794,6 +1888,8 @@ SpriteMorph.prototype.blockTemplates = function (category) { blocks.push('-'); blocks.push(block('comeToFront')); blocks.push(block('goBack')); + blocks.push('-'); + blocks.push(block('doNotify')); // for debugging: /////////////// @@ -1822,6 +1918,11 @@ SpriteMorph.prototype.blockTemplates = function (category) { blocks.push(block('doPlaySoundUntilDone')); blocks.push(block('doStopAllSounds')); blocks.push('-'); + blocks.push(block('doSetVolume')); + blocks.push(block('doChangeVolume')); + blocks.push(watcherToggle('reportVolume')); + blocks.push(block('reportVolume')); + blocks.push('-'); blocks.push(block('doRest')); blocks.push('-'); blocks.push(block('doPlayNote')); @@ -1925,6 +2026,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')); @@ -1936,6 +2041,7 @@ SpriteMorph.prototype.blockTemplates = function (category) { blocks.push(block('reportMouseDown')); blocks.push('-'); blocks.push(block('reportKeyPressed')); + blocks.push(block('getKeysPressed')); blocks.push('-'); blocks.push(block('reportDistanceTo')); blocks.push('-'); @@ -1951,6 +2057,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: /////////////// @@ -2650,7 +2767,8 @@ SpriteMorph.prototype.reportCostumes = function () { // SpriteMorph sound management SpriteMorph.prototype.addSound = function (audio, name) { - this.sounds.add(new Sound(audio, name)); + var volume = this.volume; + this.sounds.add(new Sound(audio, name, volume)); }; SpriteMorph.prototype.playSound = function (name) { @@ -2661,7 +2779,18 @@ SpriteMorph.prototype.playSound = function (name) { ), active; if (sound) { + sound.volume = this.volume; active = sound.play(); + + if (stage.muted === true) { + active.volume = 0; + } + + this.activeSounds.push(active); + this.activeSounds = this.activeSounds.filter(function (aud) { + return !aud.ended && !aud.terminated; + }); + if (stage) { stage.activeSounds.push(active); stage.activeSounds = stage.activeSounds.filter(function (aud) { @@ -2672,6 +2801,34 @@ SpriteMorph.prototype.playSound = function (name) { } }; +SpriteMorph.prototype.doSetVolume = function (val) { + var myself = this; + myself.volume = Math.min(Math.max(0, val), 100); + + if (myself.parentThatIsA(StageMorph).muted === true) { + return; + } + + myself.activeSounds.forEach(function (snd) { + snd.volume = myself.volume / 100; // 'audio' objects + }); +}; + +SpriteMorph.prototype.doChangeVolume = function (val) { + this.doSetVolume(this.volume + val); +}; + +SpriteMorph.prototype.reportVolume = function () { + return this.volume; +} + +SpriteMorph.prototype.unmuteAllSounds = function () { + var stage = this.parentThatIsA(StageMorph); + stage.muted = false; + + this.doSetVolume(this.volume); +}; + SpriteMorph.prototype.reportSounds = function () { return this.sounds; }; @@ -3324,9 +3481,7 @@ Morph.prototype.setPosition = function (aPoint, justMe) { // override the inherited default to make sure my parts follow // unless it's justMe var delta = aPoint.subtract(this.topLeft()); - if ((delta.x !== 0) || (delta.y !== 0)) { - this.moveBy(delta, justMe); - } + this.moveBy(delta, justMe); }; SpriteMorph.prototype.forward = function (steps) { @@ -3559,7 +3714,19 @@ SpriteMorph.prototype.allHatBlocksForKey = function (key) { return this.scripts.children.filter(function (morph) { if (morph.selector) { if (morph.selector === 'receiveKey') { - return morph.inputs()[0].evaluate()[0] === key; + var selectedOption = morph.inputs()[0].evaluate()[0]; + + if (selectedOption === 'any key') { + return true; + } + if (selectedOption === 'number key' && + (key >= '0' && key <= '9')) { + return true; + } + if (selectedOption === key) { + return true; + } + return false; } } return false; @@ -3628,6 +3795,16 @@ SpriteMorph.prototype.getTempo = function () { return 0; }; +// SpriteMorph last key + +SpriteMorph.prototype.getKeysPressed = function () { + var stage = this.parentThatIsA(StageMorph); + if (stage) { + return stage.getKeysPressed(); + } + return ''; +}; + // SpriteMorph last message SpriteMorph.prototype.getLastMessage = function () { @@ -4383,6 +4560,8 @@ StageMorph.prototype.init = function (globals) { this.version = Date.now(); // for observers this.isFastTracked = false; this.cloneCount = 0; + this.volume = 100; + this.muted = false; this.timerStart = Date.now(); this.tempo = 60; // bpm @@ -4398,6 +4577,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; @@ -4755,6 +4939,16 @@ StageMorph.prototype.getTempo = function () { return +this.tempo; }; +// StageMorph keys + +StageMorph.prototype.getKeysPressed = function () { + var keys = []; + for (var key in this.keysPressed) { + keys.push(key); + } + return new List(keys); +}; + // StageMorph messages StageMorph.prototype.getLastMessage = function () { @@ -4808,7 +5002,7 @@ StageMorph.prototype.step = function () { world.keyboardReceiver = this; } if (world.currentKey === null) { - this.keyPressed = null; + this.keysPressed = {}; } // manage threads @@ -4996,7 +5190,7 @@ StageMorph.prototype.fireGreenFlagEvent = function () { StageMorph.prototype.fireStopAllEvent = function () { var ide = this.parentThatIsA(IDE_Morph); - this.threads.resumeAll(this.stage); + //this.threads.resumeAll(this.stage); // leads to a strange Note bug this.keysPressed = {}; this.threads.stopAll(); this.stopAllActiveSounds(); @@ -5122,6 +5316,8 @@ StageMorph.prototype.blockTemplates = function (category) { blocks.push('-'); blocks.push(block('show')); blocks.push(block('hide')); + blocks.push('-'); + blocks.push(block('doNotify')); // for debugging: /////////////// @@ -5150,6 +5346,11 @@ StageMorph.prototype.blockTemplates = function (category) { blocks.push(block('doPlaySoundUntilDone')); blocks.push(block('doStopAllSounds')); blocks.push('-'); + blocks.push(block('doSetVolume')); + blocks.push(block('doChangeVolume')); + blocks.push(watcherToggle('reportVolume')); + blocks.push(block('reportVolume')); + blocks.push('-'); blocks.push(block('doRest')); blocks.push('-'); blocks.push(block('doPlayNote')); @@ -5176,6 +5377,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') { @@ -5232,6 +5436,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')); @@ -5243,6 +5449,7 @@ StageMorph.prototype.blockTemplates = function (category) { blocks.push(block('reportMouseDown')); blocks.push('-'); blocks.push(block('reportKeyPressed')); + blocks.push(block('getKeysPressed')); blocks.push('-'); blocks.push(block('doResetTimer')); blocks.push(watcherToggle('getTimer')); @@ -5256,6 +5463,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: /////////////// @@ -5691,6 +5909,15 @@ StageMorph.prototype.addSound StageMorph.prototype.playSound = SpriteMorph.prototype.playSound; +StageMorph.prototype.doSetVolume + = SpriteMorph.prototype.doSetVolume; + +StageMorph.prototype.doChangeVolume + = SpriteMorph.prototype.doChangeVolume; + +StageMorph.prototype.reportVolume + = SpriteMorph.prototype.reportVolume; + StageMorph.prototype.stopAllActiveSounds = function () { this.activeSounds.forEach(function (audio) { audio.pause(); @@ -5705,11 +5932,29 @@ StageMorph.prototype.pauseAllActiveSounds = function () { }; StageMorph.prototype.resumeAllActiveSounds = function () { + var newSounds = []; // remove Sounds that have been played so they do not resume + this.activeSounds.forEach(function (audio) { - audio.play(); + if (audio.ended === false) { + newSounds.push(audio); + audio.play(); + } }); + + this.activeSounds = newSounds; }; +StageMorph.prototype.muteAllSounds = function () { + this.muted = true; + + this.activeSounds.forEach(function (audio) { + audio.volume = 0; + }); +}; + +StageMorph.prototype.unmuteAllSounds + = SpriteMorph.prototype.unmuteAllSounds; + StageMorph.prototype.reportSounds = SpriteMorph.prototype.reportSounds; @@ -6479,9 +6724,10 @@ CostumeEditorMorph.prototype.mouseMove // Sound instance creation -function Sound(audio, name) { +function Sound(audio, name, volume) { this.audio = audio; // mandatory this.name = name || "Sound"; + this.volume = volume || 100; } Sound.prototype.play = function () { @@ -6489,6 +6735,7 @@ Sound.prototype.play = function () { // externally (i.e. by the stage) var aud = document.createElement('audio'); aud.src = this.audio.src; + aud.volume = Math.min(Math.max(0, this.volume), 100) / 100; aud.play(); return aud; }; @@ -6498,7 +6745,8 @@ Sound.prototype.copy = function () { cpy; snd.src = this.audio.src; - cpy = new Sound(snd, this.name ? copy(this.name) : null); + snd.volume = this.volume; + cpy = new Sound(snd, this.name ? copy(this.name) : null, this.volume ? copy(this.volume) : null); return cpy; }; @@ -6512,8 +6760,9 @@ Sound.prototype.toDataURL = function () { // Note instance creation -function Note(pitch) { +function Note(pitch, volume) { this.pitch = pitch === 0 ? 0 : pitch || 69; + this.volume = volume; this.setupContext(); this.oscillator = null; } @@ -6544,12 +6793,13 @@ Note.prototype.setupContext = function () { } Note.prototype.audioContext = new AudioContext(); Note.prototype.gainNode = Note.prototype.audioContext.createGain(); - Note.prototype.gainNode.gain.value = 0.25; // reduce volume by 1/4 }; // Note playing Note.prototype.play = function () { + this.gainNode.gain.value = 0.25 * this.volume / 100; // reduce volume by 1/4 + this.oscillator = this.audioContext.createOscillator(); if (!this.oscillator.start) { this.oscillator.start = this.oscillator.noteOn; @@ -6565,6 +6815,12 @@ Note.prototype.play = function () { this.oscillator.start(0); }; +Note.prototype.setVolume = function (volume) { + this.stop(); + this.volume = volume; + this.play(); +} + Note.prototype.stop = function () { if (this.oscillator) { this.oscillator.stop(0); @@ -6993,7 +7249,7 @@ WatcherMorph.prototype.object = function () { WatcherMorph.prototype.isGlobal = function (selector) { return contains( - ['getLastAnswer', 'getLastMessage', 'getTempo', 'getTimer', + ['getLastAnswer', 'getKeysPressed', 'getLastMessage', 'getTempo', 'getTimer', 'reportMouseX', 'reportMouseY', 'reportThreadCount'], selector ); diff --git a/octokit.js b/octokit.js new file mode 160000 +Subproject 579cef9893626da93ab8afbd601e4a3cf0d6d27 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 + } +} @@ -657,6 +657,13 @@ PaintCanvasMorph.prototype.drawcrosshair = function (context) { ctx.strokeStyle = 'black'; ctx.clearRect(0, 0, this.mask.width, this.mask.height); + //draw rotation center coordinates near crosshairs + ctx.globalAlpha = 1; + ctx.fillStyle = "blue"; + ctx.font = "bold 10px Arial"; + var coordinates = -Math.floor((this.mask.width/2 - rp.x)) + ', ' + Math.floor((this.mask.height/2 - rp.y)); + ctx.fillText(coordinates, rp.x + 20, rp.y - 20); + // draw crosshairs: ctx.globalAlpha = 0.5; diff --git a/promise-1.0.0.js b/promise-1.0.0.js new file mode 100644 index 0000000..5619cfa --- /dev/null +++ b/promise-1.0.0.js @@ -0,0 +1,684 @@ +(function() { +var define, requireModule, require, requirejs; + +(function() { + var registry = {}, seen = {}; + + define = function(name, deps, callback) { + registry[name] = { deps: deps, callback: callback }; + }; + + requirejs = require = requireModule = function(name) { + requirejs._eak_seen = registry; + + if (seen[name]) { return seen[name]; } + seen[name] = {}; + + if (!registry[name]) { + throw new Error("Could not find module " + name); + } + + var mod = registry[name], + deps = mod.deps, + callback = mod.callback, + reified = [], + exports; + + for (var i=0, l=deps.length; i<l; i++) { + if (deps[i] === 'exports') { + reified.push(exports = {}); + } else { + reified.push(requireModule(resolve(deps[i]))); + } + } + + var value = callback.apply(this, reified); + return seen[name] = exports || value; + + function resolve(child) { + if (child.charAt(0) !== '.') { return child; } + var parts = child.split("/"); + var parentBase = name.split("/").slice(0, -1); + + for (var i=0, l=parts.length; i<l; i++) { + var part = parts[i]; + + if (part === '..') { parentBase.pop(); } + else if (part === '.') { continue; } + else { parentBase.push(part); } + } + + return parentBase.join("/"); + } + }; +})(); + +define("promise/all", + ["./utils","exports"], + function(__dependency1__, __exports__) { + "use strict"; + /* global toString */ + + var isArray = __dependency1__.isArray; + var isFunction = __dependency1__.isFunction; + + /** + Returns a promise that is fulfilled when all the given promises have been + fulfilled, or rejected if any of them become rejected. The return promise + is fulfilled with an array that gives all the values in the order they were + passed in the `promises` array argument. + + Example: + + ```javascript + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.resolve(2); + var promise3 = RSVP.resolve(3); + var promises = [ promise1, promise2, promise3 ]; + + RSVP.all(promises).then(function(array){ + // The array here would be [ 1, 2, 3 ]; + }); + ``` + + If any of the `promises` given to `RSVP.all` are rejected, the first promise + that is rejected will be given as an argument to the returned promises's + rejection handler. For example: + + Example: + + ```javascript + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.reject(new Error("2")); + var promise3 = RSVP.reject(new Error("3")); + var promises = [ promise1, promise2, promise3 ]; + + RSVP.all(promises).then(function(array){ + // Code here never runs because there are rejected promises! + }, function(error) { + // error.message === "2" + }); + ``` + + @method all + @for RSVP + @param {Array} promises + @param {String} label + @return {Promise} promise that is fulfilled when all `promises` have been + fulfilled, or rejected if any of them become rejected. + */ + function all(promises) { + /*jshint validthis:true */ + var Promise = this; + + if (!isArray(promises)) { + throw new TypeError('You must pass an array to all.'); + } + + return new Promise(function(resolve, reject) { + var results = [], remaining = promises.length, + promise; + + if (remaining === 0) { + resolve([]); + } + + function resolver(index) { + return function(value) { + resolveAll(index, value); + }; + } + + function resolveAll(index, value) { + results[index] = value; + if (--remaining === 0) { + resolve(results); + } + } + + for (var i = 0; i < promises.length; i++) { + promise = promises[i]; + + if (promise && isFunction(promise.then)) { + promise.then(resolver(i), reject); + } else { + resolveAll(i, promise); + } + } + }); + } + + __exports__.all = all; + }); +define("promise/asap", + ["exports"], + function(__exports__) { + "use strict"; + var browserGlobal = (typeof window !== 'undefined') ? window : {}; + var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; + var local = (typeof global !== 'undefined') ? global : (this === undefined? window:this); + + // node + function useNextTick() { + return function() { + process.nextTick(flush); + }; + } + + function useMutationObserver() { + var iterations = 0; + var observer = new BrowserMutationObserver(flush); + var node = document.createTextNode(''); + observer.observe(node, { characterData: true }); + + return function() { + node.data = (iterations = ++iterations % 2); + }; + } + + function useSetTimeout() { + return function() { + local.setTimeout(flush, 1); + }; + } + + var queue = []; + function flush() { + for (var i = 0; i < queue.length; i++) { + var tuple = queue[i]; + var callback = tuple[0], arg = tuple[1]; + callback(arg); + } + queue = []; + } + + var scheduleFlush; + + // Decide what async method to use to triggering processing of queued callbacks: + if (typeof process !== 'undefined' && {}.toString.call(process) === '[object process]') { + scheduleFlush = useNextTick(); + } else if (BrowserMutationObserver) { + scheduleFlush = useMutationObserver(); + } else { + scheduleFlush = useSetTimeout(); + } + + function asap(callback, arg) { + var length = queue.push([callback, arg]); + if (length === 1) { + // If length is 1, that means that we need to schedule an async flush. + // If additional callbacks are queued before the queue is flushed, they + // will be processed by this flush that we are scheduling. + scheduleFlush(); + } + } + + __exports__.asap = asap; + }); +define("promise/config", + ["exports"], + function(__exports__) { + "use strict"; + var config = { + instrument: false + }; + + function configure(name, value) { + if (arguments.length === 2) { + config[name] = value; + } else { + return config[name]; + } + } + + __exports__.config = config; + __exports__.configure = configure; + }); +define("promise/polyfill", + ["./promise","./utils","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + /*global self*/ + var RSVPPromise = __dependency1__.Promise; + var isFunction = __dependency2__.isFunction; + + function polyfill() { + var local; + + if (typeof global !== 'undefined') { + local = global; + } else if (typeof window !== 'undefined' && window.document) { + local = window; + } else { + local = self; + } + + var es6PromiseSupport = + "Promise" in local && + // Some of these methods are missing from + // Firefox/Chrome experimental implementations + "resolve" in local.Promise && + "reject" in local.Promise && + "all" in local.Promise && + "race" in local.Promise && + // Older version of the spec had a resolver object + // as the arg rather than a function + (function() { + var resolve; + new local.Promise(function(r) { resolve = r; }); + return isFunction(resolve); + }()); + + if (!es6PromiseSupport) { + local.Promise = RSVPPromise; + } + } + + __exports__.polyfill = polyfill; + }); +define("promise/promise", + ["./config","./utils","./all","./race","./resolve","./reject","./asap","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __exports__) { + "use strict"; + var config = __dependency1__.config; + var configure = __dependency1__.configure; + var objectOrFunction = __dependency2__.objectOrFunction; + var isFunction = __dependency2__.isFunction; + var now = __dependency2__.now; + var all = __dependency3__.all; + var race = __dependency4__.race; + var staticResolve = __dependency5__.resolve; + var staticReject = __dependency6__.reject; + var asap = __dependency7__.asap; + + var counter = 0; + + config.async = asap; // default async is asap; + + function Promise(resolver) { + if (!isFunction(resolver)) { + throw new TypeError('You must pass a resolver function as the first argument to the promise constructor'); + } + + if (!(this instanceof Promise)) { + throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function."); + } + + this._subscribers = []; + + invokeResolver(resolver, this); + } + + function invokeResolver(resolver, promise) { + function resolvePromise(value) { + resolve(promise, value); + } + + function rejectPromise(reason) { + reject(promise, reason); + } + + try { + resolver(resolvePromise, rejectPromise); + } catch(e) { + rejectPromise(e); + } + } + + function invokeCallback(settled, promise, callback, detail) { + var hasCallback = isFunction(callback), + value, error, succeeded, failed; + + if (hasCallback) { + try { + value = callback(detail); + succeeded = true; + } catch(e) { + failed = true; + error = e; + } + } else { + value = detail; + succeeded = true; + } + + if (handleThenable(promise, value)) { + return; + } else if (hasCallback && succeeded) { + resolve(promise, value); + } else if (failed) { + reject(promise, error); + } else if (settled === FULFILLED) { + resolve(promise, value); + } else if (settled === REJECTED) { + reject(promise, value); + } + } + + var PENDING = void 0; + var SEALED = 0; + var FULFILLED = 1; + var REJECTED = 2; + + function subscribe(parent, child, onFulfillment, onRejection) { + var subscribers = parent._subscribers; + var length = subscribers.length; + + subscribers[length] = child; + subscribers[length + FULFILLED] = onFulfillment; + subscribers[length + REJECTED] = onRejection; + } + + function publish(promise, settled) { + var child, callback, subscribers = promise._subscribers, detail = promise._detail; + + for (var i = 0; i < subscribers.length; i += 3) { + child = subscribers[i]; + callback = subscribers[i + settled]; + + invokeCallback(settled, child, callback, detail); + } + + promise._subscribers = null; + } + + Promise.prototype = { + constructor: Promise, + + _state: undefined, + _detail: undefined, + _subscribers: undefined, + + then: function(onFulfillment, onRejection) { + var promise = this; + + var thenPromise = new this.constructor(function() {}); + + if (this._state) { + var callbacks = arguments; + config.async(function invokePromiseCallback() { + invokeCallback(promise._state, thenPromise, callbacks[promise._state - 1], promise._detail); + }); + } else { + subscribe(this, thenPromise, onFulfillment, onRejection); + } + + return thenPromise; + }, + + 'catch': function(onRejection) { + return this.then(null, onRejection); + } + }; + + Promise.all = all; + Promise.race = race; + Promise.resolve = staticResolve; + Promise.reject = staticReject; + + function handleThenable(promise, value) { + var then = null, + resolved; + + try { + if (promise === value) { + throw new TypeError("A promises callback cannot return that same promise."); + } + + if (objectOrFunction(value)) { + then = value.then; + + if (isFunction(then)) { + then.call(value, function(val) { + if (resolved) { return true; } + resolved = true; + + if (value !== val) { + resolve(promise, val); + } else { + fulfill(promise, val); + } + }, function(val) { + if (resolved) { return true; } + resolved = true; + + reject(promise, val); + }); + + return true; + } + } + } catch (error) { + if (resolved) { return true; } + reject(promise, error); + return true; + } + + return false; + } + + function resolve(promise, value) { + if (promise === value) { + fulfill(promise, value); + } else if (!handleThenable(promise, value)) { + fulfill(promise, value); + } + } + + function fulfill(promise, value) { + if (promise._state !== PENDING) { return; } + promise._state = SEALED; + promise._detail = value; + + config.async(publishFulfillment, promise); + } + + function reject(promise, reason) { + if (promise._state !== PENDING) { return; } + promise._state = SEALED; + promise._detail = reason; + + config.async(publishRejection, promise); + } + + function publishFulfillment(promise) { + publish(promise, promise._state = FULFILLED); + } + + function publishRejection(promise) { + publish(promise, promise._state = REJECTED); + } + + __exports__.Promise = Promise; + }); +define("promise/race", + ["./utils","exports"], + function(__dependency1__, __exports__) { + "use strict"; + /* global toString */ + var isArray = __dependency1__.isArray; + + /** + `RSVP.race` allows you to watch a series of promises and act as soon as the + first promise given to the `promises` argument fulfills or rejects. + + Example: + + ```javascript + var promise1 = new RSVP.Promise(function(resolve, reject){ + setTimeout(function(){ + resolve("promise 1"); + }, 200); + }); + + var promise2 = new RSVP.Promise(function(resolve, reject){ + setTimeout(function(){ + resolve("promise 2"); + }, 100); + }); + + RSVP.race([promise1, promise2]).then(function(result){ + // result === "promise 2" because it was resolved before promise1 + // was resolved. + }); + ``` + + `RSVP.race` is deterministic in that only the state of the first completed + promise matters. For example, even if other promises given to the `promises` + array argument are resolved, but the first completed promise has become + rejected before the other promises became fulfilled, the returned promise + will become rejected: + + ```javascript + var promise1 = new RSVP.Promise(function(resolve, reject){ + setTimeout(function(){ + resolve("promise 1"); + }, 200); + }); + + var promise2 = new RSVP.Promise(function(resolve, reject){ + setTimeout(function(){ + reject(new Error("promise 2")); + }, 100); + }); + + RSVP.race([promise1, promise2]).then(function(result){ + // Code here never runs because there are rejected promises! + }, function(reason){ + // reason.message === "promise2" because promise 2 became rejected before + // promise 1 became fulfilled + }); + ``` + + @method race + @for RSVP + @param {Array} promises array of promises to observe + @param {String} label optional string for describing the promise returned. + Useful for tooling. + @return {Promise} a promise that becomes fulfilled with the value the first + completed promises is resolved with if the first completed promise was + fulfilled, or rejected with the reason that the first completed promise + was rejected with. + */ + function race(promises) { + /*jshint validthis:true */ + var Promise = this; + + if (!isArray(promises)) { + throw new TypeError('You must pass an array to race.'); + } + return new Promise(function(resolve, reject) { + var results = [], promise; + + for (var i = 0; i < promises.length; i++) { + promise = promises[i]; + + if (promise && typeof promise.then === 'function') { + promise.then(resolve, reject); + } else { + resolve(promise); + } + } + }); + } + + __exports__.race = race; + }); +define("promise/reject", + ["exports"], + function(__exports__) { + "use strict"; + /** + `RSVP.reject` returns a promise that will become rejected with the passed + `reason`. `RSVP.reject` is essentially shorthand for the following: + + ```javascript + var promise = new RSVP.Promise(function(resolve, reject){ + reject(new Error('WHOOPS')); + }); + + promise.then(function(value){ + // Code here doesn't run because the promise is rejected! + }, function(reason){ + // reason.message === 'WHOOPS' + }); + ``` + + Instead of writing the above, your code now simply becomes the following: + + ```javascript + var promise = RSVP.reject(new Error('WHOOPS')); + + promise.then(function(value){ + // Code here doesn't run because the promise is rejected! + }, function(reason){ + // reason.message === 'WHOOPS' + }); + ``` + + @method reject + @for RSVP + @param {Any} reason value that the returned promise will be rejected with. + @param {String} label optional string for identifying the returned promise. + Useful for tooling. + @return {Promise} a promise that will become rejected with the given + `reason`. + */ + function reject(reason) { + /*jshint validthis:true */ + var Promise = this; + + return new Promise(function (resolve, reject) { + reject(reason); + }); + } + + __exports__.reject = reject; + }); +define("promise/resolve", + ["exports"], + function(__exports__) { + "use strict"; + function resolve(value) { + /*jshint validthis:true */ + if (value && typeof value === 'object' && value.constructor === this) { + return value; + } + + var Promise = this; + + return new Promise(function(resolve) { + resolve(value); + }); + } + + __exports__.resolve = resolve; + }); +define("promise/utils", + ["exports"], + function(__exports__) { + "use strict"; + function objectOrFunction(x) { + return isFunction(x) || (typeof x === "object" && x !== null); + } + + function isFunction(x) { + return typeof x === "function"; + } + + function isArray(x) { + return Object.prototype.toString.call(x) === "[object Array]"; + } + + // Date.now is not available in browsers < IE9 + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now#Compatibility + var now = Date.now || function() { return new Date().getTime(); }; + + + __exports__.objectOrFunction = objectOrFunction; + __exports__.isFunction = isFunction; + __exports__.isArray = isArray; + __exports__.now = now; + }); +requireModule('promise/polyfill').polyfill(); +}());
\ No newline at end of file @@ -17,7 +17,13 @@ <script type="text/javascript" src="store.js"></script> <script type="text/javascript" src="locale.js"></script> <script type="text/javascript" src="cloud.js"></script> + <script type="text/javascript" src="promise-1.0.0.js"></script> + <script type="text/javascript" src="octokit.js/octokit.js"></script> + <script type="text/javascript" src="github.js"></script> <script type="text/javascript" src="sha512.js"></script> + <script type="text/javascript" src="Snapin8r/snapin8r.min.js"></script> + <script type="text/javascript" src="vkBeautify/vkbeautify.js"></script> + <script type="text/javascript" src="diff-merge/dist/diff.js"></script> <script type="text/javascript" src="peer.js"></script> <script type="text/javascript"> var world; @@ -25,6 +31,9 @@ world = new WorldMorph(document.getElementById('world')); world.worldCanvas.focus(); new IDE_Morph().openIn(world); + setInterval(loop, 10); + var ide = new IDE_Morph() + ide.openIn(world); setInterval(loop, 1); }; function loop() { @@ -97,7 +97,7 @@ XML_Serializer.prototype.serialize = function (object) { this.flushMedia(); xml = this.store(object); this.flush(); - return xml; + return vkbeautify.xml(xml); }; XML_Serializer.prototype.store = function (object, mediaID) { @@ -135,7 +135,7 @@ XML_Serializer.prototype.mediaXML = function () { ); xml = xml + str; }); - return xml + '</media>'; + return vkbeautify.xml(xml + '</media>'); }; XML_Serializer.prototype.add = function (object) { @@ -1037,7 +1037,7 @@ SnapSerializer.prototype.obsoleteBlock = function (isReporter) { : new CommandBlockMorph(); block.selector = 'nop'; block.color = new Color(200, 0, 20); - block.setSpec('Obsolete!'); + block.setSpec(localize('Obsolete!')); block.isDraggable = true; return block; }; @@ -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(); @@ -1356,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(); } @@ -1379,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; } @@ -1393,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; } @@ -1407,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 ''; } @@ -1420,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); }; @@ -1479,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(); @@ -1682,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 @@ -1970,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'; } @@ -2286,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) { @@ -2527,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, @@ -2676,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 = { @@ -2705,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 /* @@ -2820,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) { @@ -2907,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, @@ -2933,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 @@ -1 +1 @@ -<blocks app="Snap! 4.0, http://snap.berkeley.edu" version="1"><block-definition s="map %'function' over %'lists'" type="reporter" category="lists"><header></header><code></code><inputs><input type="%repRing"></input><input type="%mult%l"></input></inputs><script><block s="doWarp"><script><block s="doDeclareVariables"><list><l>mapone</l><l>mapmany</l></list></block><block s="doSetVar"><l>mapone</l><block s="reifyScript"><script><block s="doIf"><custom-block s="empty? %l"><block var="data"/></custom-block><script><block s="doReport"><block s="reportNewList"><list></list></block></block></script></block><block s="doReport"><block s="reportCONS"><block s="evaluate"><block var="function"/><list><block s="reportListItem"><l>1</l><block var="data"/></block></list></block><block s="evaluate"><block var="mapone"/><list><block s="reportCDR"><block var="data"/></block></list></block></block></block></script><list><l>data</l></list></block></block><block s="doSetVar"><l>mapmany</l><block s="reifyScript"><script><block s="doIf"><custom-block s="empty? %l"><block s="reportListItem"><l>1</l><block var="data lists"/></block></custom-block><script><block s="doReport"><block s="reportNewList"><list></list></block></block></script></block><block s="doReport"><block s="reportCONS"><block s="evaluate"><block var="function"/><custom-block s="map %repRing over %mult%l"><block s="reifyReporter"><autolambda><block s="reportListItem"><l>1</l><l/></block></autolambda><list></list></block><list><block var="data lists"/></list></custom-block></block><block s="evaluate"><block var="mapmany"/><list><custom-block s="map %repRing over %mult%l"><block s="reifyReporter"><autolambda><block s="reportCDR"><l/></block></autolambda><list></list></block><list><block var="data lists"/></list></custom-block></list></block></block></block></script><list><l>data lists</l></list></block></block><block s="doIfElse"><custom-block s="empty? %l"><block s="reportCDR"><block var="lists"/></block></custom-block><script><block s="doReport"><block s="evaluate"><block var="mapone"/><list><block s="reportListItem"><l>1</l><block var="lists"/></block></list></block></block></script><script><block s="doReport"><block s="evaluate"><block var="mapmany"/><list><block var="lists"/></list></block></block></script></block></script></block></script></block-definition><block-definition s="empty? %'data'" type="predicate" category="lists"><header></header><code></code><inputs><input type="%l"></input></inputs><script><block s="doReport"><block s="reportEquals"><block var="data"/><block s="reportNewList"><list></list></block></block></block></script></block-definition><block-definition s="keep items such that %'pred' from %'data'" type="reporter" category="lists"><header></header><code></code><inputs><input type="%predRing"></input><input type="%l"></input></inputs><script><block s="doWarp"><script><block s="doIf"><custom-block s="empty? %l"><block var="data"/></custom-block><script><block s="doReport"><block s="reportNewList"><list></list></block></block></script></block><block s="doIfElse"><block s="evaluate"><block var="pred"/><list><block s="reportListItem"><l>1</l><block var="data"/></block></list></block><script><block s="doReport"><block s="reportCONS"><block s="reportListItem"><l>1</l><block var="data"/></block><custom-block s="keep items such that %predRing from %l"><block var="pred"/><block s="reportCDR"><block var="data"/></block></custom-block></block></block></script><script><block s="doReport"><custom-block s="keep items such that %predRing from %l"><block var="pred"/><block s="reportCDR"><block var="data"/></block></custom-block></block></script></block></script></block></script></block-definition><block-definition s="combine with %'function' items of %'data'" type="reporter" category="lists"><header></header><code></code><inputs><input type="%repRing"></input><input type="%l"></input></inputs><script><block s="doWarp"><script><block s="doIf"><custom-block s="empty? %l"><block s="reportCDR"><block var="data"/></block></custom-block><script><block s="doReport"><block s="reportListItem"><l>1</l><block var="data"/></block></block></script></block><block s="doReport"><block s="evaluate"><block var="function"/><list><block s="reportListItem"><l>1</l><block var="data"/></block><custom-block s="combine with %repRing items of %l"><block var="function"/><block s="reportCDR"><block var="data"/></block></custom-block></list></block></block></script></block></script></block-definition><block-definition s="if %'test' then %'true' else %'false'" type="reporter" category="control"><header></header><code></code><inputs><input type="%b"></input><input type="%anyUE"></input><input type="%anyUE"></input></inputs><script><block s="doIfElse"><block var="test"/><script><block s="doReport"><block s="evaluate"><block var="true"/><list></list></block></block></script><script><block s="doReport"><block s="evaluate"><block var="false"/><list></list></block></block></script></block></script></block-definition><block-definition s="for %'i' = %'start' to %'end' %'action'" type="command" category="control"><header></header><code></code><inputs><input type="%upvar"></input><input type="%n">1</input><input type="%n">10</input><input type="%cs"></input></inputs><script><block s="doDeclareVariables"><list><l>step</l><l>tester</l></list></block><block s="doIfElse"><block s="reportGreaterThan"><block var="start"/><block var="end"/></block><script><block s="doSetVar"><l>step</l><l>-1</l></block><block s="doSetVar"><l>tester</l><block s="reifyReporter"><autolambda><block s="reportLessThan"><block var="i"/><block var="end"/></block></autolambda><list></list></block></block></script><script><block s="doSetVar"><l>step</l><l>1</l></block><block s="doSetVar"><l>tester</l><block s="reifyReporter"><autolambda><block s="reportGreaterThan"><block var="i"/><block var="end"/></block></autolambda><list></list></block></block></script></block><block s="doSetVar"><l>i</l><block var="start"/></block><block s="doUntil"><block s="evaluate"><block var="tester"/><list></list></block><script><block s="doRun"><block var="action"/><list></list></block><block s="doChangeVar"><l>i</l><block var="step"/></block></script></block></script></block-definition><block-definition s="join words %'words'" type="reporter" category="operators"><header></header><code></code><inputs><input type="%mult%txt"></input></inputs><script><block s="doWarp"><script><block s="doIf"><custom-block s="empty? %l"><block s="reportCDR"><block var="words"/></block></custom-block><script><block s="doReport"><block s="reportListItem"><l>1</l><block var="words"/></block></block></script></block><block s="doReport"><block s="reportJoinWords"><list><block s="reportListItem"><l>1</l><block var="words"/></block><block s="reportJoinWords"><list><l> </l><block s="evaluate"><block s="reifyReporter"><autolambda><custom-block s="join words %mult%txt"><block s="reportCDR"><block var="words"/></block></custom-block></autolambda><list></list></block><list></list></block></list></block></list></block></block></script></block></script></block-definition><block-definition s="list $arrowRight sentence %'data'" type="reporter" category="operators"><header></header><code></code><inputs><input type="%l"></input></inputs><script><block s="doWarp"><script><block s="doReport"><custom-block s="combine with %repRing items of %l"><block s="reifyReporter"><autolambda><custom-block s="join words %mult%txt"><list><l></l><l></l></list></custom-block></autolambda><list></list></block><block var="data"/></custom-block></block></script></block></script></block-definition><block-definition s="sentence $arrowRight list %'text'" type="reporter" category="operators"><header></header><code></code><inputs><input type="%txt"></input></inputs><script><block s="doWarp"><script><block s="doReport"><block s="reportTextSplit"><block var="text"/><l><option>whitespace</option></l></block></block></script></block></script></block-definition><block-definition s="catch %'tag' %'action'" type="command" category="control"><header></header><code></code><inputs><input type="%upvar"></input><input type="%cs"></input></inputs><script><block s="doCallCC"><block s="reifyScript"><script><block s="doSetVar"><l>tag</l><block var="cont"/></block><block s="doRun"><block var="action"/><list></list></block></script><list><l>cont</l></list></block></block></script></block-definition><block-definition s="throw %'cont'" type="command" category="control"><header></header><code></code><inputs><input type="%s">catchtag</input></inputs><script><block s="doRun"><block var="cont"/><list></list></block></script></block-definition><block-definition s="catch %'tag' %'value'" type="reporter" category="control"><header></header><code></code><inputs><input type="%upvar"></input><input type="%anyUE"></input></inputs><script><block s="doCallCC"><block s="reifyScript"><script><block s="doSetVar"><l>tag</l><block var="cont"/></block><block s="doReport"><block s="evaluate"><block var="value"/><list></list></block></block></script><list><l>cont</l></list></block></block></script></block-definition><block-definition s="throw %'tag' %'value'" type="command" category="control"><header></header><code></code><inputs><input type="%s">catchtag</input><input type="%s"></input></inputs><script><block s="doRun"><block var="tag"/><list><block var="value"/></list></block></script></block-definition><block-definition s="for each %'item' of %'data' %'action'" type="command" category="lists"><header></header><code></code><inputs><input type="%upvar"></input><input type="%l"></input><input type="%cs"></input></inputs><script><block s="doUntil"><custom-block s="empty? %l"><block var="data"/></custom-block><script><block s="doSetVar"><l>item</l><block s="reportListItem"><l>1</l><block var="data"/></block></block><block s="doRun"><block var="action"/><list><block s="reportListItem"><l>1</l><block var="data"/></block></list></block><block s="doSetVar"><l>data</l><block s="reportCDR"><block var="data"/></block></block></script></block></script></block-definition><block-definition s="if %'test' do %'action' and pause all $pause-1-255-220-0" type="command" category="control"><header></header><code></code><inputs><input type="%boolUE"></input><input type="%cs"></input></inputs><script><block s="doDeclareVariables"><list><l>breakpoint</l></list></block><block s="doIf"><block s="evaluate"><block var="test"/><list></list></block><script><block s="doSetVar"><l>breakpoint</l><block var="test"/></block><block s="doShowVar"><l>breakpoint</l></block><block s="doRun"><block var="action"/><list></list></block><block s="doPauseAll"></block><block s="doHideVar"><l></l></block></script></block></script></block-definition><block-definition s="word $arrowRight list %'word'" type="reporter" category="operators"><header></header><code></code><inputs><input type="%txt"></input></inputs><script><block s="doWarp"><script><block s="doReport"><block s="reportTextSplit"><block var="word"/><l><option>letter</option></l></block></block></script></block></script></block-definition><block-definition s="ignore %'x'" type="command" category="control"><header></header><code></code><inputs><input type="%s"></input></inputs></block-definition></blocks>
\ No newline at end of file +<blocks app="Snap! 4.0, http://snap.berkeley.edu" version="1"><block-definition s="map %'function' over %'lists'" type="reporter" category="lists"><header></header><code></code><inputs><input type="%repRing"></input><input type="%mult%l"></input></inputs><script><block s="doWarp"><script><block s="doDeclareVariables"><list><l>mapone</l><l>mapmany</l></list></block><block s="doSetVar"><l>mapone</l><block s="reifyScript"><script><block s="doIf"><custom-block s="empty? %l"><block var="data"/></custom-block><script><block s="doReport"><block s="reportNewList"><list></list></block></block></script></block><block s="doReport"><block s="reportCONS"><block s="evaluate"><block var="function"/><list><block s="reportListItem"><l>1</l><block var="data"/></block></list></block><block s="evaluate"><block var="mapone"/><list><block s="reportCDR"><block var="data"/></block></list></block></block></block></script><list><l>data</l></list></block></block><block s="doSetVar"><l>mapmany</l><block s="reifyScript"><script><block s="doIf"><custom-block s="empty? %l"><block s="reportListItem"><l>1</l><block var="data lists"/></block></custom-block><script><block s="doReport"><block s="reportNewList"><list></list></block></block></script></block><block s="doReport"><block s="reportCONS"><block s="evaluate"><block var="function"/><custom-block s="map %repRing over %mult%l"><block s="reifyReporter"><autolambda><block s="reportListItem"><l>1</l><l/></block></autolambda><list></list></block><list><block var="data lists"/></list></custom-block></block><block s="evaluate"><block var="mapmany"/><list><custom-block s="map %repRing over %mult%l"><block s="reifyReporter"><autolambda><block s="reportCDR"><l/></block></autolambda><list></list></block><list><block var="data lists"/></list></custom-block></list></block></block></block></script><list><l>data lists</l></list></block></block><block s="doIfElse"><custom-block s="empty? %l"><block s="reportCDR"><block var="lists"/></block></custom-block><script><block s="doReport"><block s="evaluate"><block var="mapone"/><list><block s="reportListItem"><l>1</l><block var="lists"/></block></list></block></block></script><script><block s="doReport"><block s="evaluate"><block var="mapmany"/><list><block var="lists"/></list></block></block></script></block></script></block></script></block-definition><block-definition s="empty? %'data'" type="predicate" category="lists"><header></header><code></code><inputs><input type="%l"></input></inputs><script><block s="doReport"><block s="reportEquals"><block var="data"/><block s="reportNewList"><list></list></block></block></block></script></block-definition><block-definition s="keep items such that %'pred' from %'data'" type="reporter" category="lists"><header></header><code></code><inputs><input type="%predRing"></input><input type="%l"></input></inputs><script><block s="doWarp"><script><block s="doIf"><custom-block s="empty? %l"><block var="data"/></custom-block><script><block s="doReport"><block s="reportNewList"><list></list></block></block></script></block><block s="doIfElse"><block s="evaluate"><block var="pred"/><list><block s="reportListItem"><l>1</l><block var="data"/></block></list></block><script><block s="doReport"><block s="reportCONS"><block s="reportListItem"><l>1</l><block var="data"/></block><custom-block s="keep items such that %predRing from %l"><block var="pred"/><block s="reportCDR"><block var="data"/></block></custom-block></block></block></script><script><block s="doReport"><custom-block s="keep items such that %predRing from %l"><block var="pred"/><block s="reportCDR"><block var="data"/></block></custom-block></block></script></block></script></block></script></block-definition><block-definition s="combine with %'function' items of %'data'" type="reporter" category="lists"><header></header><code></code><inputs><input type="%repRing"></input><input type="%l"></input></inputs><script><block s="doWarp"><script><block s="doIf"><custom-block s="empty? %l"><block s="reportCDR"><block var="data"/></block></custom-block><script><block s="doReport"><block s="reportListItem"><l>1</l><block var="data"/></block></block></script></block><block s="doReport"><block s="evaluate"><block var="function"/><list><block s="reportListItem"><l>1</l><block var="data"/></block><custom-block s="combine with %repRing items of %l"><block var="function"/><block s="reportCDR"><block var="data"/></block></custom-block></list></block></block></script></block></script></block-definition><block-definition s="if %'test' then %'true' else %'false'" type="reporter" category="control"><header></header><code></code><inputs><input type="%b"></input><input type="%anyUE"></input><input type="%anyUE"></input></inputs><script><block s="doIfElse"><block var="test"/><script><block s="doReport"><block s="evaluate"><block var="true"/><list></list></block></block></script><script><block s="doReport"><block s="evaluate"><block var="false"/><list></list></block></block></script></block></script></block-definition><block-definition s="for %'i' = %'start' to %'end' %'action'" type="command" category="control"><header></header><code></code><inputs><input type="%upvar"></input><input type="%n">1</input><input type="%n">10</input><input type="%cs"></input></inputs><script><block s="doDeclareVariables"><list><l>step</l><l>tester</l></list></block><block s="doIfElse"><block s="reportGreaterThan"><block var="start"/><block var="end"/></block><script><block s="doSetVar"><l>step</l><l>-1</l></block><block s="doSetVar"><l>tester</l><block s="reifyReporter"><autolambda><block s="reportLessThan"><block var="i"/><block var="end"/></block></autolambda><list></list></block></block></script><script><block s="doSetVar"><l>step</l><l>1</l></block><block s="doSetVar"><l>tester</l><block s="reifyReporter"><autolambda><block s="reportGreaterThan"><block var="i"/><block var="end"/></block></autolambda><list></list></block></block></script></block><block s="doSetVar"><l>i</l><block var="start"/></block><block s="doUntil"><block s="evaluate"><block var="tester"/><list></list></block><script><block s="doRun"><block var="action"/><list></list></block><block s="doChangeVar"><l>i</l><block var="step"/></block></script></block></script></block-definition><block-definition s="join words %'words'" type="reporter" category="operators"><header></header><code></code><inputs><input type="%mult%txt"></input></inputs><script><block s="doWarp"><script><block s="doIf"><custom-block s="empty? %l"><block s="reportCDR"><block var="words"/></block></custom-block><script><block s="doReport"><block s="reportListItem"><l>1</l><block var="words"/></block></block></script></block><block s="doReport"><block s="reportJoinWords"><list><block s="reportListItem"><l>1</l><block var="words"/></block><block s="reportJoinWords"><list><l> </l><block s="evaluate"><block s="reifyReporter"><autolambda><custom-block s="join words %mult%txt"><block s="reportCDR"><block var="words"/></block></custom-block></autolambda><list></list></block><list></list></block></list></block></list></block></block></script></block></script></block-definition><block-definition s="list $arrowRight sentence %'data'" type="reporter" category="operators"><header></header><code></code><inputs><input type="%l"></input></inputs><script><block s="doWarp"><script><block s="doReport"><custom-block s="combine with %repRing items of %l"><block s="reifyReporter"><autolambda><custom-block s="join words %mult%txt"><list><l></l><l></l></list></custom-block></autolambda><list></list></block><block var="data"/></custom-block></block></script></block></script></block-definition><block-definition s="sentence $arrowRight list %'text'" type="reporter" category="operators"><header></header><code></code><inputs><input type="%txt"></input></inputs><script><block s="doWarp"><script><block s="doReport"><block s="reportTextSplit"><block var="text"/><l><option>whitespace</option></l></block></block></script></block></script></block-definition><block-definition s="catch %'tag' %'action'" type="command" category="control"><header></header><code></code><inputs><input type="%upvar"></input><input type="%cs"></input></inputs><script><block s="doCallCC"><block s="reifyScript"><script><block s="doSetVar"><l>tag</l><block var="cont"/></block><block s="doRun"><block var="action"/><list></list></block></script><list><l>cont</l></list></block></block></script></block-definition><block-definition s="throw %'cont'" type="command" category="control"><header></header><code></code><inputs><input type="%s">catchtag</input></inputs><script><block s="doRun"><block var="cont"/><list></list></block></script></block-definition><block-definition s="catch %'tag' %'value'" type="reporter" category="control"><header></header><code></code><inputs><input type="%upvar"></input><input type="%anyUE"></input></inputs><script><block s="doCallCC"><block s="reifyScript"><script><block s="doSetVar"><l>tag</l><block var="cont"/></block><block s="doReport"><block s="evaluate"><block var="value"/><list></list></block></block></script><list><l>cont</l></list></block></block></script></block-definition><block-definition s="throw %'tag' %'value'" type="command" category="control"><header></header><code></code><inputs><input type="%s">catchtag</input><input type="%s"></input></inputs><script><block s="doRun"><block var="tag"/><list><block var="value"/></list></block></script></block-definition><block-definition s="for each %'item' of %'data' %'action'" type="command" category="lists"><header></header><code></code><inputs><input type="%upvar"></input><input type="%l"></input><input type="%cs"></input></inputs><script><block s="doUntil"><custom-block s="empty? %l"><block var="data"/></custom-block><script><block s="doSetVar"><l>item</l><block s="reportListItem"><l>1</l><block var="data"/></block></block><block s="doRun"><block var="action"/><list><block s="reportListItem"><l>1</l><block var="data"/></block></list></block><block s="doSetVar"><l>data</l><block s="reportCDR"><block var="data"/></block></block></script></block></script></block-definition><block-definition s="if %'test' do %'action' and pause all $pause-1-255-220-0" type="command" category="control"><header></header><code></code><inputs><input type="%boolUE"></input><input type="%cs"></input></inputs><script><block s="doDeclareVariables"><list><l>breakpoint</l></list></block><block s="doIf"><block s="evaluate"><block var="test"/><list></list></block><script><block s="doSetVar"><l>breakpoint</l><block var="test"/></block><block s="doShowVar"><l>breakpoint</l></block><block s="doRun"><block var="action"/><list></list></block><block s="doPauseAll"></block><block s="doHideVar"><l></l></block></script></block></script></block-definition><block-definition s="word $arrowRight list %'word'" type="reporter" category="operators"><header></header><code></code><inputs><input type="%txt"></input></inputs><script><block s="doWarp"><script><block s="doReport"><block s="reportTextSplit"><block var="word"/><l><option>letter</option></l></block></block></script></block></script></block-definition><block-definition s="ignore %'x'" type="command" category="control"><header></header><code></code><inputs><input type="%s"></input></inputs></block-definition><block-definition s="ask for %'reporter' from %'sprite'" type="reporter" category="sensing"><header></header><code></code><inputs><input type="%repRing"></input><input type="%txt"></input></inputs><script><block s="doReport"><block s="evaluate"><block s="reportAttributeOf"><block var="reporter"/><block var="sprite"/></block><list></list></block></block></script></block-definition><block-definition s="tell %'sprite' to %'commands'" type="command" category="sensing"><header></header><code></code><inputs><input type="%txt"></input><input type="%cs"></input></inputs><script><block s="doRun"><block s="reportAttributeOf"><block var="commands"/><block var="sprite"/></block><list></list></block></script></block-definition></blocks>
\ No newline at end of file diff --git a/vkBeautify b/vkBeautify new file mode 160000 +Subproject ecfc3b9e2b911ad8ebd49600c4089aca30619f2 |
