summaryrefslogtreecommitdiff
path: root/objects.js
diff options
context:
space:
mode:
authorjmoenig <jens@moenig.org>2013-03-16 09:02:16 +0100
committerjmoenig <jens@moenig.org>2013-03-16 09:02:16 +0100
commit0b510366d230399a38f18a8db92dd396a0dad42e (patch)
treeffb841d64bca04799ebc470701b71e59e726017e /objects.js
downloadsnap-byow-0b510366d230399a38f18a8db92dd396a0dad42e.tar.gz
snap-byow-0b510366d230399a38f18a8db92dd396a0dad42e.zip
Creation
Diffstat (limited to 'objects.js')
-rw-r--r--objects.js5610
1 files changed, 5610 insertions, 0 deletions
diff --git a/objects.js b/objects.js
new file mode 100644
index 0000000..546106e
--- /dev/null
+++ b/objects.js
@@ -0,0 +1,5610 @@
+/*
+
+ objects.js
+
+ a scriptable microworld
+ based on morphic.js, blocks.js and threads.js
+ inspired by Scratch
+
+ written by Jens Mšnig
+ jens@moenig.org
+
+ Copyright (C) 2013 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, morphic.js and widgets.js
+
+
+ toc
+ ---
+ the following list shows the order in which all constructors are
+ defined. Use this list to locate code in this document:
+
+ SpriteMorph
+ StageMorph
+ Costume
+ SVG_Costume
+ CostumeEditorMorph
+ Sound
+ Note
+ CellMorph
+ WatcherMorph
+ StagePrompterMorph
+
+ SpeechBubbleMorph*
+ SpriteBubbleMorph
+
+ * defined in Morphic.js
+
+
+ credits
+ -------
+ Ian Reynolds contributed initial porting of primitives from Squeak and
+ sound handling
+ Achal Dave contributed research and prototyping for creating music
+ using the Web Audio API
+
+*/
+
+// gloabls from lists.js:
+
+/*global ListWatcherMorph*/
+
+// gloabls from widgets.js:
+
+/*global PushButtonMorph, ToggleMorph, DialogBoxMorph, InputFieldMorph*/
+
+// gloabls from gui.js:
+
+/*global WatcherMorph*/
+
+// globals from threads.js:
+
+/*global ArgMorph, BlockMorph, Process, StackFrame, ThreadManager,
+VariableFrame, detect, threadsVersion*/
+
+// globals from blocks.js:
+
+/*global ArgMorph, ArrowMorph, BlockHighlightMorph, BlockMorph,
+BooleanSlotMorph, BoxMorph, Color, ColorPaletteMorph, ColorSlotMorph,
+CommandBlockMorph, CommandSlotMorph, FrameMorph, HatBlockMorph,
+InputSlotMorph, MenuMorph, Morph, MultiArgMorph, Point,
+ReporterBlockMorph, ScriptsMorph, ShaAwMorph, StringMorph,
+SyntaxElementMorph, TextMorph, WorldMorph, blocksVersion, contains,
+degrees, detect, getDocumentPositionOf, newCanvas, nop, radians,
+useBlurredShadows*/
+
+// globals from morphic.js:
+
+/*global Array, BlinkerMorph, BouncerMorph, BoxMorph, CircleBoxMorph,
+Color, ColorPaletteMorph, ColorPickerMorph, CursorMorph, Date,
+FrameMorph, Function, GrayPaletteMorph, HandMorph, HandleMorph,
+InspectorMorph, ListMorph, Math, MenuItemMorph, MenuMorph, Morph,
+MorphicPreferences, MouseSensorMorph, Node, Object, PenMorph, Point,
+Rectangle, ScrollFrameMorph, ShadowMorph, SliderButtonMorph,
+SliderMorph, String, StringFieldMorph, StringMorph, TextMorph,
+TriggerMorph, WorldMorph, clone, contains, copy, degrees, detect,
+document, getDocumentPositionOf, isNaN, isObject, isString, newCanvas,
+nop, parseFloat, radians, standardSettings, touchScreenSettings,
+useBlurredShadows, version, window, modules, IDE_Morph, VariableDialogMorph,
+HTMLCanvasElement, Context, List, SpeechBubbleMorph, RingMorph, isNil,
+FileReader*/
+
+// globals from byob.js:
+
+/*global CustomBlockDefinition, BlockEditorMorph, BlockDialogMorph,
+PrototypeHatBlockMorph*/
+
+// globals from locale.js:
+
+/*global localize*/
+
+// temporary globals
+
+// Global stuff ////////////////////////////////////////////////////////
+
+modules.objects = '2013-March-12';
+
+var SpriteMorph;
+var StageMorph;
+var SpriteBubbleMorph;
+var Costume;
+var SVG_Costume;
+var CostumeEditorMorph;
+var Sound;
+var Note;
+var CellMorph;
+var WatcherMorph;
+var StagePrompterMorph;
+var Note;
+
+// SpriteMorph /////////////////////////////////////////////////////////
+
+// I am a scriptable object
+
+// SpriteMorph inherits from PenMorph:
+
+SpriteMorph.prototype = new PenMorph();
+SpriteMorph.prototype.constructor = SpriteMorph;
+SpriteMorph.uber = PenMorph.prototype;
+
+// SpriteMorph settings
+
+SpriteMorph.prototype.categories =
+ [
+ 'motion',
+ 'control',
+ 'looks',
+ 'sensing',
+ 'sound',
+ 'operators',
+ 'pen',
+ 'variables',
+ 'lists',
+ 'other'
+ ];
+
+SpriteMorph.prototype.blockColor = {
+ motion : new Color(74, 108, 212),
+ looks : new Color(143, 86, 227),
+ sound : new Color(207, 74, 217),
+ pen : new Color(0, 161, 120),
+ control : new Color(230, 168, 34),
+ sensing : new Color(4, 148, 220),
+ operators : new Color(98, 194, 19),
+ variables : new Color(243, 118, 29),
+ lists : new Color(217, 77, 17),
+ other: new Color(150, 150, 150)
+};
+
+SpriteMorph.prototype.paletteColor = new Color(55, 55, 55);
+SpriteMorph.prototype.sliderColor
+ = SpriteMorph.prototype.paletteColor.lighter(30);
+SpriteMorph.prototype.isCachingPrimitives = true;
+
+SpriteMorph.prototype.bubbleColor = new Color(255, 255, 255);
+SpriteMorph.prototype.bubbleFontSize = 14;
+SpriteMorph.prototype.bubbleFontIsBold = true;
+SpriteMorph.prototype.bubbleCorner = 10;
+SpriteMorph.prototype.bubbleBorder = 3;
+SpriteMorph.prototype.bubbleBorderColor = new Color(190, 190, 190);
+SpriteMorph.prototype.bubbleMaxTextWidth = 130;
+
+SpriteMorph.prototype.initBlocks = function () {
+ SpriteMorph.prototype.blocks = {
+
+ // Motion
+ forward: {
+ type: 'command',
+ category: 'motion',
+ spec: 'move %n steps',
+ defaults: [10]
+ },
+ turn: {
+ type: 'command',
+ category: 'motion',
+ spec: 'turn %clockwise %n degrees',
+ defaults: [15]
+ },
+ turnLeft: {
+ type: 'command',
+ category: 'motion',
+ spec: 'turn %counterclockwise %n degrees',
+ defaults: [15]
+ },
+ setHeading: {
+ type: 'command',
+ category: 'motion',
+ spec: 'point in direction %dir'
+ },
+ doFaceTowards: {
+ type: 'command',
+ category: 'motion',
+ spec: 'point towards %dst'
+ },
+ gotoXY: {
+ type: 'command',
+ category: 'motion',
+ spec: 'go to x: %n y: %n',
+ defaults: [0, 0]
+ },
+ doGotoObject: {
+ type: 'command',
+ category: 'motion',
+ spec: 'go to %dst'
+ },
+ doGlide: {
+ type: 'command',
+ category: 'motion',
+ spec: 'glide %n secs to x: %n y: %n',
+ defaults: [1, 0, 0]
+ },
+ changeXPosition: {
+ type: 'command',
+ category: 'motion',
+ spec: 'change x by %n',
+ defaults: [10]
+ },
+ setXPosition: {
+ type: 'command',
+ category: 'motion',
+ spec: 'set x to %n',
+ defaults: [0]
+ },
+ changeYPosition: {
+ type: 'command',
+ category: 'motion',
+ spec: 'change y by %n',
+ defaults: [10]
+ },
+ setYPosition: {
+ type: 'command',
+ category: 'motion',
+ spec: 'set y to %n',
+ defaults: [0]
+ },
+ bounceOffEdge: {
+ type: 'command',
+ category: 'motion',
+ spec: 'if on edge, bounce'
+ },
+ xPosition: {
+ type: 'reporter',
+ category: 'motion',
+ spec: 'x position'
+ },
+ yPosition: {
+ type: 'reporter',
+ category: 'motion',
+ spec: 'y position'
+ },
+ direction: {
+ type: 'reporter',
+ category: 'motion',
+ spec: 'direction'
+ },
+
+ // Looks
+ doSwitchToCostume: {
+ type: 'command',
+ category: 'looks',
+ spec: 'switch to costume %cst'
+ },
+ doWearNextCostume: {
+ type: 'command',
+ category: 'looks',
+ spec: 'next costume'
+ },
+ getCostumeIdx: {
+ type: 'reporter',
+ category: 'looks',
+ spec: 'costume #'
+ },
+ doSayFor: {
+ type: 'command',
+ category: 'looks',
+ spec: 'say %s for %n secs',
+ defaults: [localize('Hello!'), 2]
+ },
+ bubble: {
+ type: 'command',
+ category: 'looks',
+ spec: 'say %s',
+ defaults: [localize('Hello!')]
+ },
+ doThinkFor: {
+ type: 'command',
+ category: 'looks',
+ spec: 'think %s for %n secs',
+ defaults: [localize('Hmm...'), 2]
+ },
+ doThink: {
+ type: 'command',
+ category: 'looks',
+ spec: 'think %s',
+ defaults: [localize('Hmm...')]
+ },
+ changeEffect: {
+ type: 'command',
+ category: 'looks',
+ spec: 'change %eff effect by %n',
+ defaults: [null, 25]
+ },
+ setEffect: {
+ type: 'command',
+ category: 'looks',
+ spec: 'set %eff effect to %n',
+ defaults: [null, 0]
+ },
+ clearEffects: {
+ type: 'command',
+ category: 'looks',
+ spec: 'clear graphic effects'
+ },
+ changeScale: {
+ type: 'command',
+ category: 'looks',
+ spec: 'change size by %n',
+ defaults: [10]
+ },
+ setScale: {
+ type: 'command',
+ category: 'looks',
+ spec: 'set size to %n %',
+ defaults: [100]
+ },
+ getScale: {
+ type: 'reporter',
+ category: 'looks',
+ spec: 'size'
+ },
+ show: {
+ type: 'command',
+ category: 'looks',
+ spec: 'show'
+ },
+ hide: {
+ type: 'command',
+ category: 'looks',
+ spec: 'hide'
+ },
+ comeToFront: {
+ type: 'command',
+ category: 'looks',
+ spec: 'go to front'
+ },
+ goBack: {
+ type: 'command',
+ category: 'looks',
+ spec: 'go back %n layers',
+ defaults: [1]
+ },
+
+ // Looks - Debugging primitives for development mode
+ alert: {
+ type: 'command',
+ category: 'looks',
+ spec: 'alert %mult%s'
+ },
+ log: {
+ type: 'command',
+ category: 'looks',
+ spec: 'console log %mult%s'
+ },
+
+ // Sound
+ playSound: {
+ type: 'command',
+ category: 'sound',
+ spec: 'play sound %snd'
+ },
+ doPlaySoundUntilDone: {
+ type: 'command',
+ category: 'sound',
+ spec: 'play sound %snd until done'
+ },
+ doStopAllSounds: {
+ type: 'command',
+ category: 'sound',
+ spec: 'stop all sounds'
+ },
+ doRest: {
+ type: 'command',
+ category: 'sound',
+ spec: 'rest for %n beats',
+ defaults: [0.2]
+ },
+ doPlayNote: {
+ type: 'command',
+ category: 'sound',
+ spec: 'play note %n for %n beats',
+ defaults: [60, 0.5]
+ },
+ doChangeTempo: {
+ type: 'command',
+ category: 'sound',
+ spec: 'change tempo by %n',
+ defaults: [20]
+ },
+ doSetTempo: {
+ type: 'command',
+ category: 'sound',
+ spec: 'set tempo to %n bpm',
+ defaults: [60]
+ },
+ getTempo: {
+ type: 'reporter',
+ category: 'sound',
+ spec: 'tempo'
+ },
+
+ // Pen
+ clear: {
+ type: 'command',
+ category: 'pen',
+ spec: 'clear'
+ },
+ down: {
+ type: 'command',
+ category: 'pen',
+ spec: 'pen down'
+ },
+ up: {
+ type: 'command',
+ category: 'pen',
+ spec: 'pen up'
+ },
+ setColor: {
+ type: 'command',
+ category: 'pen',
+ spec: 'set pen color to %clr'
+ },
+ changeHue: {
+ type: 'command',
+ category: 'pen',
+ spec: 'change pen color by %n',
+ defaults: [10]
+ },
+ setHue: {
+ type: 'command',
+ category: 'pen',
+ spec: 'set pen color to %n',
+ defaults: [0]
+ },
+ changeBrightness: {
+ type: 'command',
+ category: 'pen',
+ spec: 'change pen shade by %n',
+ defaults: [10]
+ },
+ setBrightness: {
+ type: 'command',
+ category: 'pen',
+ spec: 'set pen shade to %n',
+ defaults: [100]
+ },
+ changeSize: {
+ type: 'command',
+ category: 'pen',
+ spec: 'change pen size by %n',
+ defaults: [1]
+ },
+ setSize: {
+ type: 'command',
+ category: 'pen',
+ spec: 'set pen size to %n',
+ defaults: [1]
+ },
+ doStamp: {
+ type: 'command',
+ category: 'pen',
+ spec: 'stamp'
+ },
+
+ // Control
+ receiveGo: {
+ type: 'hat',
+ category: 'control',
+ spec: 'when %greenflag clicked'
+ },
+ receiveKey: {
+ type: 'hat',
+ category: 'control',
+ spec: 'when %keyHat key pressed'
+ },
+ receiveClick: {
+ type: 'hat',
+ category: 'control',
+ spec: 'when I am clicked'
+ },
+ receiveMessage: {
+ type: 'hat',
+ category: 'control',
+ spec: 'when I receive %msgHat'
+ },
+ doBroadcast: {
+ type: 'command',
+ category: 'control',
+ spec: 'broadcast %msg'
+ },
+ doBroadcastAndWait: {
+ type: 'command',
+ category: 'control',
+ spec: 'broadcast %msg and wait'
+ },
+ doWait: {
+ type: 'command',
+ category: 'control',
+ spec: 'wait %n secs',
+ defaults: [1]
+ },
+ doWaitUntil: {
+ type: 'command',
+ category: 'control',
+ spec: 'wait until %b'
+ },
+ doForever: {
+ type: 'command',
+ category: 'control',
+ spec: 'forever %c'
+ },
+ doRepeat: {
+ type: 'command',
+ category: 'control',
+ spec: 'repeat %n %c',
+ defaults: [10]
+ },
+ doUntil: {
+ type: 'command',
+ category: 'control',
+ spec: 'repeat until %b %c'
+ },
+ doIf: {
+ type: 'command',
+ category: 'control',
+ spec: 'if %b %c'
+ },
+ doIfElse: {
+ type: 'command',
+ category: 'control',
+ spec: 'if %b %c else %c'
+ },
+ doStop: {
+ type: 'command',
+ category: 'control',
+ spec: 'stop script'
+ },
+ doStopAll: {
+ type: 'command',
+ category: 'control',
+ spec: 'stop all %stop'
+ },
+ doRun: {
+ type: 'command',
+ category: 'control',
+ spec: 'run %cmdRing %inputs'
+ },
+ fork: {
+ type: 'command',
+ category: 'control',
+ spec: 'launch %cmdRing %inputs'
+ },
+ evaluate: {
+ type: 'reporter',
+ category: 'control',
+ spec: 'call %repRing %inputs'
+ },
+ /*
+ doRunWithInputList: {
+ type: 'command',
+ category: 'control',
+ spec: 'run %cmd with input list %l'
+ },
+
+ forkWithInputList: {
+ type: 'command',
+ category: 'control',
+ spec: 'launch %cmd with input list %l'
+ },
+
+ evaluateWithInputList: {
+ type: 'reporter',
+ category: 'control',
+ spec: 'call %r with input list %l'
+ },
+ */
+ doReport: {
+ type: 'command',
+ category: 'control',
+ spec: 'report %s'
+ },
+ doStopBlock: {
+ type: 'command',
+ category: 'control',
+ spec: 'stop block'
+ },
+ doCallCC: {
+ type: 'command',
+ category: 'control',
+ spec: 'run %cmdRing w/continuation'
+ },
+ reportCallCC: {
+ type: 'reporter',
+ category: 'control',
+ spec: 'call %cmdRing w/continuation'
+ },
+ doWarp: {
+ type: 'command',
+ category: 'other',
+ spec: 'warp %c'
+ },
+
+ // Cloning - very experimental
+ receiveOnClone: {
+ type: 'hat',
+ category: 'control',
+ spec: 'when I start as a clone'
+ },
+ createClone: {
+ type: 'command',
+ category: 'control',
+ spec: 'create a clone of %cln'
+ },
+ removeClone: {
+ type: 'command',
+ category: 'control',
+ spec: 'delete this clone'
+ },
+
+ // Sensing
+
+ reportTouchingObject: {
+ type: 'predicate',
+ category: 'sensing',
+ spec: 'touching %col ?'
+ },
+ reportTouchingColor: {
+ type: 'predicate',
+ category: 'sensing',
+ spec: 'touching %clr ?'
+ },
+ reportColorIsTouchingColor: {
+ type: 'predicate',
+ category: 'sensing',
+ spec: 'color %clr is touching %clr ?'
+ },
+ colorFiltered: {
+ type: 'reporter',
+ category: 'sensing',
+ spec: 'filtered for %clr'
+ },
+ reportStackSize: {
+ type: 'reporter',
+ category: 'sensing',
+ spec: 'stack size'
+ },
+ reportFrameCount: {
+ type: 'reporter',
+ category: 'sensing',
+ spec: 'frames'
+ },
+ doAsk: {
+ type: 'command',
+ category: 'sensing',
+ spec: 'ask %s and wait',
+ defaults: [localize('what\'s your name?')]
+ },
+ reportLastAnswer: {
+ type: 'reporter',
+ category: 'sensing',
+ spec: 'answer'
+ },
+ getLastAnswer: { // variant for watcher
+ type: 'reporter',
+ category: 'sensing',
+ spec: 'answer'
+ },
+ reportMouseX: {
+ type: 'reporter',
+ category: 'sensing',
+ spec: 'mouse x'
+ },
+ reportMouseY: {
+ type: 'reporter',
+ category: 'sensing',
+ spec: 'mouse y'
+ },
+ reportMouseDown: {
+ type: 'predicate',
+ category: 'sensing',
+ spec: 'mouse down?'
+ },
+ reportKeyPressed: {
+ type: 'predicate',
+ category: 'sensing',
+ spec: 'key %key pressed?'
+ },
+ reportDistanceTo: {
+ type: 'reporter',
+ category: 'sensing',
+ spec: 'distance to %dst'
+ },
+ doResetTimer: {
+ type: 'command',
+ category: 'sensing',
+ spec: 'reset timer'
+ },
+ reportTimer: {
+ type: 'reporter',
+ category: 'sensing',
+ spec: 'timer'
+ },
+ getTimer: { // variant for watcher
+ type: 'reporter',
+ category: 'sensing',
+ spec: 'timer'
+ },
+ reportAttributeOf: {
+ type: 'reporter',
+ category: 'sensing',
+ spec: '%att of %spr',
+ defaults: [['costume #']]
+ },
+ reportURL: {
+ type: 'reporter',
+ category: 'sensing',
+ spec: 'http:// %s',
+ defaults: ['snap.berkeley.edu']
+ },
+ reportIsFastTracking: {
+ type: 'predicate',
+ category: 'sensing',
+ spec: 'turbo mode?'
+ },
+ doSetFastTracking: {
+ type: 'command',
+ category: 'sensing',
+ spec: 'set turbo mode to %b'
+ },
+
+ // Operators
+ reifyScript: {
+ type: 'ring',
+ category: 'other',
+ spec: '%rc %ringparms'
+ },
+ reifyReporter: {
+ type: 'ring',
+ category: 'other',
+ spec: '%rr %ringparms'
+ },
+ reifyPredicate: {
+ type: 'ring',
+ category: 'other',
+ spec: '%rp %ringparms'
+ },
+ reportSum: {
+ type: 'reporter',
+ category: 'operators',
+ spec: '%n + %n'
+ },
+ reportDifference: {
+ type: 'reporter',
+ category: 'operators',
+ spec: '%n \u2212 %n'
+ },
+ reportProduct: {
+ type: 'reporter',
+ category: 'operators',
+ spec: '%n \u00D7 %n'
+ },
+ reportQuotient: {
+ type: 'reporter',
+ category: 'operators',
+ spec: '%n / %n' // '%n \u00F7 %n'
+ },
+ reportRound: {
+ type: 'reporter',
+ category: 'operators',
+ spec: 'round %n'
+ },
+ reportMonadic: {
+ type: 'reporter',
+ category: 'operators',
+ spec: '%fun of %n',
+ defaults: [null, 10]
+ },
+ reportModulus: {
+ type: 'reporter',
+ category: 'operators',
+ spec: '%n mod %n'
+ },
+ reportRandom: {
+ type: 'reporter',
+ category: 'operators',
+ spec: 'pick random %n to %n',
+ defaults: [1, 10]
+ },
+ reportLessThan: {
+ type: 'predicate',
+ category: 'operators',
+ spec: '%s < %s'
+ },
+ reportEquals: {
+ type: 'predicate',
+ category: 'operators',
+ spec: '%s = %s'
+ },
+ reportGreaterThan: {
+ type: 'predicate',
+ category: 'operators',
+ spec: '%s > %s'
+ },
+ reportAnd: {
+ type: 'predicate',
+ category: 'operators',
+ spec: '%b and %b'
+ },
+ reportOr: {
+ type: 'predicate',
+ category: 'operators',
+ spec: '%b or %b'
+ },
+ reportNot: {
+ type: 'predicate',
+ category: 'operators',
+ spec: 'not %b'
+ },
+ reportTrue: {
+ type: 'predicate',
+ category: 'operators',
+ spec: 'true'
+ },
+ reportFalse: {
+ type: 'predicate',
+ category: 'operators',
+ spec: 'false'
+ },
+ reportJoinWords: {
+ type: 'reporter',
+ category: 'operators',
+ spec: 'join %words',
+ defaults: [localize('hello') + ' ', localize('world')]
+ },
+ reportLetter: {
+ type: 'reporter',
+ category: 'operators',
+ spec: 'letter %n of %s',
+ defaults: [1, localize('world')]
+ },
+ reportStringSize: {
+ type: 'reporter',
+ category: 'operators',
+ spec: 'length of %s',
+ defaults: [localize('world')]
+ },
+ reportUnicode: {
+ type: 'reporter',
+ category: 'operators',
+ spec: 'unicode of %s',
+ defaults: ['a']
+ },
+ reportUnicodeAsLetter: {
+ type: 'reporter',
+ category: 'operators',
+ spec: 'unicode %n as letter',
+ defaults: [65]
+ },
+ reportIsA: {
+ type: 'predicate',
+ category: 'operators',
+ spec: 'is %s a %typ ?',
+ defaults: [5]
+ },
+ reportIsIdentical: {
+ type: 'predicate',
+ category: 'operators',
+ spec: 'is %s identical to %s ?'
+ },
+ reportTypeOf: { // only in dev mode for debugging
+ type: 'reporter',
+ category: 'operators',
+ spec: 'type of %s',
+ defaults: [5]
+ },
+ /*
+ reportScript: {
+ type: 'reporter',
+ category: 'operators',
+ spec: 'the script %parms %c'
+ },
+ reify: {
+ type: 'reporter',
+ category: 'operators',
+ spec: 'the %f block %parms'
+ },
+ */
+
+ // Variables
+ doSetVar: {
+ type: 'command',
+ category: 'variables',
+ spec: 'set %var to %s',
+ defaults: [null, 0]
+ },
+ doChangeVar: {
+ type: 'command',
+ category: 'variables',
+ spec: 'change %var by %n',
+ defaults: [null, 1]
+ },
+ doShowVar: {
+ type: 'command',
+ category: 'variables',
+ spec: 'show variable %var'
+ },
+ doHideVar: {
+ type: 'command',
+ category: 'variables',
+ spec: 'hide variable %var'
+ },
+ doDeclareVariables: {
+ type: 'command',
+ category: 'other',
+ spec: 'script variables %scriptVars'
+ },
+
+ // Lists
+ reportNewList: {
+ type: 'reporter',
+ category: 'lists',
+ spec: 'list %exp'
+ },
+ reportCONS: {
+ type: 'reporter',
+ category: 'lists',
+ spec: '%s in front of %l'
+ },
+ reportListItem: {
+ type: 'reporter',
+ category: 'lists',
+ spec: 'item %idx of %l',
+ defaults: [1]
+ },
+ reportCDR: {
+ type: 'reporter',
+ category: 'lists',
+ spec: 'all but first of %l'
+ },
+ reportListLength: {
+ type: 'reporter',
+ category: 'lists',
+ spec: 'length of %l'
+ },
+ reportListContainsItem: {
+ type: 'predicate',
+ category: 'lists',
+ spec: '%l contains %s',
+ defaults: [null, localize('thing')]
+ },
+ doAddToList: {
+ type: 'command',
+ category: 'lists',
+ spec: 'add %s to %l',
+ defaults: [localize('thing')]
+ },
+ doDeleteFromList: {
+ type: 'command',
+ category: 'lists',
+ spec: 'delete %ida of %l',
+ defaults: [1]
+ },
+ doInsertInList: {
+ type: 'command',
+ category: 'lists',
+ spec: 'insert %s at %idx of %l',
+ defaults: [localize('thing'), 1]
+ },
+ doReplaceInList: {
+ type: 'command',
+ category: 'lists',
+ spec: 'replace item %idx of %l with %s',
+ defaults: [1, null, localize('thing')]
+ }
+ };
+};
+
+SpriteMorph.prototype.initBlocks();
+
+SpriteMorph.prototype.blockAlternatives = {
+ // motion:
+ turn: ['turnLeft'],
+ turnLeft: ['turn'],
+ changeXPosition: ['changeYPosition', 'setXPosition', 'setYPosition'],
+ setXPosition: ['setYPosition', 'changeXPosition', 'changeYPosition'],
+ changeYPosition: ['changeXPosition', 'setYPosition', 'setXPosition'],
+ setYPosition: ['setXPosition', 'changeYPosition', 'changeXPosition'],
+ xPosition: ['yPosition'],
+ yPosition: ['xPosition'],
+
+ // looks:
+ doSayFor: ['doThinkFor'],
+ doThinkFor: ['doSayFor'],
+ bubble: ['doThink'],
+ doThink: ['bubble'],
+ show: ['hide'],
+ hide: ['show'],
+ changeEffect: ['setEffect'],
+ setEffect: ['changeEffect'],
+ changeScale: ['setScale'],
+ setScale: ['changeScale'],
+
+ // sound:
+ playSound: ['doPlaySoundUntilDone'],
+ doPlaySoundUntilDone: ['playSound'],
+ doChangeTempo: ['doSetTempo'],
+ doSetTempo: ['doChangeTempo'],
+
+ // pen:
+ clear: ['down', 'up', 'doStamp'],
+ down: ['up', 'clear', 'doStamp'],
+ up: ['down', 'clear', 'doStamp'],
+ doStamp: ['clear', 'down', 'up'],
+ changeHue: ['setHue', 'changeBrightness', 'setBrightness'],
+ setHue: ['changeHue', 'changeBrightness', 'setBrightness'],
+ changeBrightness: ['setBrightness', 'setHue', 'changeHue'],
+ setBrightness: ['changeBrightness', 'setHue', 'changeHue'],
+ changeSize: ['setSize'],
+ setSize: ['changeSize'],
+
+ // control:
+ receiveGo: ['receiveClick'],
+ receiveClick: ['receiveGo'],
+ doBroadcast: ['doBroadcastAndWait'],
+ doBroadcastAndWait: ['doBroadcast'],
+ doStopBlock: ['doStop', 'doStopAll'],
+ doStop: ['doStopBlock', 'doStopAll'],
+ doStopAll: ['doStopBlock', 'doStop'],
+
+ // sensing:
+ reportLastAnswer: ['reportTimer'],
+ reportTimer: ['reportLastAnswer'],
+ reportMouseX: ['reportMouseY'],
+ reportMouseY: ['reportMouseX'],
+
+ // operators:
+ reportSum: ['reportDifference', 'reportProduct', 'reportQuotient'],
+ reportDifference: ['reportSum', 'reportProduct', 'reportQuotient'],
+ reportProduct: ['reportDifference', 'reportSum', 'reportQuotient'],
+ reportQuotient: ['reportDifference', 'reportProduct', 'reportSum'],
+ reportLessThan: ['reportEquals', 'reportGreaterThan'],
+ reportEquals: ['reportLessThan', 'reportGreaterThan'],
+ reportGreaterThan: ['reportEquals', 'reportLessThan'],
+ reportAnd: ['reportOr'],
+ reportOr: ['reportAnd'],
+ reportTrue: ['reportFalse'],
+ reportFalse: ['reportTrue'],
+
+ // variables
+ doSetVar: ['doChangeVar'],
+ doChangeVar: ['doSetVar'],
+ doShowVar: ['doHideVar'],
+ doHideVar: ['doShowVar']
+};
+
+// SpriteMorph instance creation
+
+function SpriteMorph(globals) {
+ this.init(globals);
+}
+
+SpriteMorph.prototype.init = function (globals) {
+ this.name = localize('Sprite');
+ this.variables = new VariableFrame(globals || null, this);
+ this.scripts = new ScriptsMorph(this);
+ this.customBlocks = [];
+ this.costumes = new List();
+ this.costume = null;
+ this.sounds = new List();
+ this.normalExtent = new Point(60, 60); // only for costume-less situation
+ this.scale = 1;
+ this.rotationStyle = 1; // 1 = full, 2 = left/right, 0 = off
+ this.version = Date.now(); // for observer optimization
+ this.isClone = false; // indicate a "temporary" Scratch-style clone
+ this.cloneOriginName = '';
+
+ this.blocksCache = {}; // not to be serialized (!)
+ this.paletteCache = {}; // not to be serialized (!)
+ this.rotationOffset = new Point(); // not to be serialized (!)
+ this.idx = 0; // not to be serialized (!) - used for de-serialization
+ this.wasWarped = false; // not to be serialized, used for fast-tracking
+
+ SpriteMorph.uber.init.call(this);
+
+ this.isDraggable = true;
+ this.isDown = false;
+
+ this.heading = 90;
+ this.changed();
+ this.drawNew();
+ this.changed();
+};
+
+// SpriteMorph duplicating (fullCopy)
+
+SpriteMorph.prototype.fullCopy = function () {
+ var c = SpriteMorph.uber.fullCopy.call(this),
+ arr = [],
+ cb;
+
+ c.stopTalking();
+ c.color = this.color.copy();
+ c.blocksCache = {};
+ c.paletteCache = {};
+ c.scripts = this.scripts.fullCopy();
+ c.scripts.owner = c;
+ c.variables = this.variables.copy();
+ c.variables.owner = c;
+
+ c.customBlocks = [];
+ this.customBlocks.forEach(function (def) {
+ cb = def.copyAndBindTo(c);
+ c.customBlocks.push(cb);
+ c.allBlockInstances(def).forEach(function (block) {
+ block.definition = cb;
+ });
+ });
+ this.costumes.asArray().forEach(function (costume) {
+ arr.push(costume.copy());
+ });
+ c.costumes = new List(arr);
+ arr = [];
+ this.sounds.asArray().forEach(function (sound) {
+ arr.push(sound);
+ });
+ c.sounds = new List(arr);
+
+ return c;
+};
+
+// SpriteMorph versioning
+
+SpriteMorph.prototype.setName = function (string) {
+ this.name = string || this.name;
+ this.version = Date.now();
+};
+
+// SpriteMorph rendering
+
+SpriteMorph.prototype.drawNew = function () {
+ var myself = this,
+ currentCenter = this.center(),
+ facing, // actual costume heading based on my rotation style
+ isFlipped,
+ pic, // (flipped copy of) actual costume based on my rotation style
+ stageScale = this.parent instanceof StageMorph ?
+ this.parent.scale : 1,
+ newX,
+ corners = [],
+ origin,
+ shift,
+ corner,
+ costumeExtent,
+ ctx;
+
+ if (this.isWarped) {
+ this.wantsRedraw = true;
+ return;
+ }
+ facing = this.rotationStyle ? this.heading : 90;
+ if (this.rotationStyle === 2) {
+ facing = 90;
+ if ((this.heading > 180 && (this.heading < 360))
+ || (this.heading < 0 && (this.heading > -180))) {
+ isFlipped = true;
+ }
+ }
+ if (this.costume) {
+ pic = isFlipped ? this.costume.flipped() : this.costume;
+
+ // determine the rotated costume's bounding box
+ corners = pic.bounds().corners().map(function (point) {
+ return point.rotateBy(
+ radians(facing - 90),
+ myself.costume.center()
+ );
+ });
+ origin = corners[0];
+ corner = corners[0];
+ corners.forEach(function (point) {
+ origin = origin.min(point);
+ corner = corner.max(point);
+ });
+ costumeExtent = origin.corner(corner)
+ .extent().multiplyBy(this.scale * stageScale);
+
+ // determine the new relative origin of the rotated shape
+ shift = new Point(0, 0).rotateBy(
+ radians(-(facing - 90)),
+ pic.center()
+ ).subtract(origin);
+
+ // create a new, adequately dimensioned canvas
+ // and draw the costume on it
+ this.image = newCanvas(costumeExtent);
+ this.silentSetExtent(costumeExtent);
+ ctx = this.image.getContext('2d');
+ ctx.scale(this.scale * stageScale, this.scale * stageScale);
+ ctx.translate(shift.x, shift.y);
+ ctx.rotate(radians(facing - 90));
+ ctx.drawImage(pic.contents, 0, 0);
+
+ // adjust my position to the rotation
+ this.setCenter(currentCenter);
+
+ // determine my rotation offset
+ this.rotationOffset = shift
+ .translateBy(pic.rotationCenter)
+ .rotateBy(radians(-(facing - 90)), shift)
+ .scaleBy(this.scale * stageScale);
+ } else {
+ facing = isFlipped ? -90 : facing;
+ newX = Math.min(
+ Math.max(
+ this.normalExtent.x * this.scale * stageScale,
+ 5
+ ),
+ 1000
+ );
+ this.silentSetExtent(new Point(newX, newX));
+ this.image = newCanvas(this.extent());
+ this.setCenter(currentCenter);
+ SpriteMorph.uber.drawNew.call(this, facing);
+ this.rotationOffset = this.extent().divideBy(2);
+ }
+ this.version = Date.now();
+};
+
+SpriteMorph.prototype.endWarp = function () {
+ this.isWarped = false;
+ if (this.wantsRedraw) {
+ var x = this.xPosition(),
+ y = this.yPosition();
+ this.drawNew();
+ this.silentGotoXY(x, y);
+ this.wantsRedraw = false;
+ }
+ this.parent.changed();
+};
+
+SpriteMorph.prototype.rotationCenter = function () {
+ return this.position().add(this.rotationOffset);
+};
+
+SpriteMorph.prototype.colorFiltered = function (aColor) {
+ // answer a new Morph containing my image filtered by aColor
+ // ignore transparency (alpha)
+ var morph = new Morph(),
+ ext = this.extent(),
+ ctx,
+ src,
+ clr,
+ i,
+ dta;
+
+ src = this.image.getContext('2d').getImageData(0, 0, ext.x, ext.y);
+ morph.image = newCanvas(ext);
+ morph.bounds = this.bounds.copy();
+ ctx = morph.image.getContext('2d');
+ dta = ctx.createImageData(ext.x, ext.y);
+ for (i = 0; i < ext.x * ext.y * 4; i += 4) {
+ clr = new Color(
+ src.data[i],
+ src.data[i + 1],
+ src.data[i + 2]
+ );
+ if (clr.eq(aColor)) {
+ dta.data[i] = src.data[i];
+ dta.data[i + 1] = src.data[i + 1];
+ dta.data[i + 2] = src.data[i + 2];
+ dta.data[i + 3] = 255;
+ }
+ }
+ ctx.putImageData(dta, 0, 0);
+ return morph;
+};
+
+// SpriteMorph block instantiation
+
+SpriteMorph.prototype.blockForSelector = function (selector, setDefaults) {
+ var info, block, defaults, inputs, i;
+ info = this.blocks[selector];
+ if (!info) {return null; }
+ block = info.type === 'command' ? new CommandBlockMorph()
+ : info.type === 'hat' ? new HatBlockMorph()
+ : info.type === 'ring' ? new RingMorph()
+ : new ReporterBlockMorph(info.type === 'predicate');
+ block.color = this.blockColor[info.category];
+ block.category = info.category;
+ block.selector = selector;
+ if (contains(['reifyReporter', 'reifyPredicate'], block.selector)) {
+ block.isStatic = true;
+ }
+ block.setSpec(localize(info.spec));
+ if (setDefaults && info.defaults) {
+ defaults = info.defaults;
+ block.defaults = defaults;
+ inputs = block.inputs();
+ if (inputs[0] instanceof MultiArgMorph) {
+ inputs[0].setContents(defaults);
+ inputs[0].defaults = defaults;
+ } else {
+ for (i = 0; i < defaults.length; i += 1) {
+ if (defaults[i] !== null) {
+ inputs[i].setContents(defaults[i]);
+ }
+ }
+ }
+ }
+ return block;
+};
+
+SpriteMorph.prototype.variableBlock = function (varName) {
+ var block = new ReporterBlockMorph(false);
+ block.selector = 'reportGetVar';
+ block.color = this.blockColor.variables;
+ block.category = 'variables';
+ block.setSpec(varName);
+ block.isDraggable = true;
+ return block;
+};
+
+// SpriteMorph block templates
+
+SpriteMorph.prototype.blockTemplates = function (category) {
+ var blocks = [], myself = this, varNames, button,
+ cat = category || 'motion', txt;
+
+ function block(selector) {
+ var newBlock = SpriteMorph.prototype.blockForSelector(selector, true);
+ newBlock.isTemplate = true;
+ return newBlock;
+ }
+
+ function variableBlock(varName) {
+ var newBlock = SpriteMorph.prototype.variableBlock(varName);
+ newBlock.isDraggable = false;
+ newBlock.isTemplate = true;
+ return newBlock;
+ }
+
+ function watcherToggle(selector) {
+ var info = SpriteMorph.prototype.blocks[selector];
+ return new ToggleMorph(
+ 'checkbox',
+ this,
+ function () {
+ myself.toggleWatcher(
+ selector,
+ localize(info.spec),
+ myself.blockColor[info.category]
+ );
+ },
+ null,
+ function () {
+ return myself.showingWatcher(selector);
+ },
+ null
+ );
+ }
+
+ function variableWatcherToggle(varName) {
+ return new ToggleMorph(
+ 'checkbox',
+ this,
+ function () {
+ myself.toggleVariableWatcher(varName);
+ },
+ null,
+ function () {
+ return myself.showingVariableWatcher(varName);
+ },
+ null
+ );
+ }
+
+ if (cat === 'motion') {
+
+ blocks.push(block('forward'));
+ blocks.push(block('turn'));
+ blocks.push(block('turnLeft'));
+ blocks.push('-');
+ blocks.push(block('setHeading'));
+ blocks.push(block('doFaceTowards'));
+ blocks.push('-');
+ blocks.push(block('gotoXY'));
+ blocks.push(block('doGotoObject'));
+ blocks.push(block('doGlide'));
+ blocks.push('-');
+ blocks.push(block('changeXPosition'));
+ blocks.push(block('setXPosition'));
+ blocks.push(block('changeYPosition'));
+ blocks.push(block('setYPosition'));
+ blocks.push('-');
+ blocks.push(block('bounceOffEdge'));
+ blocks.push('-');
+ blocks.push(watcherToggle('xPosition'));
+ blocks.push(block('xPosition'));
+ blocks.push(watcherToggle('yPosition'));
+ blocks.push(block('yPosition'));
+ blocks.push(watcherToggle('direction'));
+ blocks.push(block('direction'));
+
+ } else if (cat === 'looks') {
+
+ blocks.push(block('doSwitchToCostume'));
+ blocks.push(block('doWearNextCostume'));
+ blocks.push(watcherToggle('getCostumeIdx'));
+ blocks.push(block('getCostumeIdx'));
+ blocks.push('-');
+ blocks.push(block('doSayFor'));
+ blocks.push(block('bubble'));
+ blocks.push(block('doThinkFor'));
+ blocks.push(block('doThink'));
+ blocks.push('-');
+ blocks.push(block('changeEffect'));
+ blocks.push(block('setEffect'));
+ blocks.push(block('clearEffects'));
+ blocks.push('-');
+ blocks.push(block('changeScale'));
+ blocks.push(block('setScale'));
+ blocks.push(watcherToggle('getScale'));
+ blocks.push(block('getScale'));
+ blocks.push('-');
+ blocks.push(block('show'));
+ blocks.push(block('hide'));
+ blocks.push('-');
+ blocks.push(block('comeToFront'));
+ blocks.push(block('goBack'));
+
+ // for debugging: ///////////////
+
+ if (this.world().isDevMode) {
+ blocks.push('-');
+ txt = new TextMorph(localize(
+ 'development mode \ndebugging primitives:'
+ ));
+ txt.fontSize = 9;
+ txt.setColor(new Color(230, 230, 230));
+ blocks.push(txt);
+ blocks.push('-');
+ blocks.push(block('log'));
+ blocks.push(block('alert'));
+ }
+
+ /////////////////////////////////
+
+ } else if (cat === 'sound') {
+
+ blocks.push(block('playSound'));
+ blocks.push(block('doPlaySoundUntilDone'));
+ blocks.push(block('doStopAllSounds'));
+ blocks.push('-');
+ blocks.push(block('doRest'));
+ blocks.push('-');
+ blocks.push(block('doPlayNote'));
+ blocks.push('-');
+ blocks.push(block('doChangeTempo'));
+ blocks.push(block('doSetTempo'));
+ blocks.push(watcherToggle('getTempo'));
+ blocks.push(block('getTempo'));
+
+ } else if (cat === 'pen') {
+
+ blocks.push(block('clear'));
+ blocks.push('-');
+ blocks.push(block('down'));
+ blocks.push(block('up'));
+ blocks.push('-');
+ blocks.push(block('setColor'));
+ blocks.push(block('changeHue'));
+ blocks.push(block('setHue'));
+ blocks.push('-');
+ blocks.push(block('changeBrightness'));
+ blocks.push(block('setBrightness'));
+ blocks.push('-');
+ blocks.push(block('changeSize'));
+ blocks.push(block('setSize'));
+ blocks.push('-');
+ blocks.push(block('doStamp'));
+
+ } else if (cat === 'control') {
+
+ blocks.push(block('receiveGo'));
+ blocks.push(block('receiveKey'));
+ blocks.push(block('receiveClick'));
+ blocks.push(block('receiveMessage'));
+ blocks.push('-');
+ blocks.push(block('doBroadcast'));
+ blocks.push(block('doBroadcastAndWait'));
+ blocks.push('-');
+ blocks.push(block('doWarp'));
+ blocks.push('-');
+ blocks.push(block('doWait'));
+ blocks.push(block('doWaitUntil'));
+ blocks.push('-');
+ blocks.push(block('doForever'));
+ blocks.push(block('doRepeat'));
+ blocks.push(block('doUntil'));
+ blocks.push('-');
+ blocks.push(block('doIf'));
+ blocks.push(block('doIfElse'));
+ blocks.push('-');
+ blocks.push(block('doReport'));
+ blocks.push('-');
+ blocks.push(block('doStopBlock'));
+ blocks.push(block('doStop'));
+ blocks.push(block('doStopAll'));
+ blocks.push('-');
+ blocks.push(block('doRun'));
+ blocks.push(block('fork'));
+ blocks.push(block('evaluate'));
+ blocks.push('-');
+ /*
+ // list variants commented out for now (redundant)
+ blocks.push(block('doRunWithInputList'));
+ blocks.push(block('forkWithInputList'));
+ blocks.push(block('evaluateWithInputList'));
+ blocks.push('-');
+ */
+ blocks.push(block('doCallCC'));
+ blocks.push(block('reportCallCC'));
+ blocks.push('-');
+ blocks.push(block('receiveOnClone'));
+ blocks.push(block('createClone'));
+ blocks.push(block('removeClone'));
+
+ } else if (cat === 'sensing') {
+
+ blocks.push(block('reportTouchingObject'));
+ blocks.push(block('reportTouchingColor'));
+ blocks.push(block('reportColorIsTouchingColor'));
+ blocks.push('-');
+ blocks.push(block('doAsk'));
+ blocks.push(watcherToggle('getLastAnswer'));
+ blocks.push(block('reportLastAnswer'));
+ blocks.push('-');
+ blocks.push(block('reportMouseX'));
+ blocks.push(block('reportMouseY'));
+ blocks.push(block('reportMouseDown'));
+ blocks.push('-');
+ blocks.push(block('reportKeyPressed'));
+ blocks.push('-');
+ blocks.push(block('reportDistanceTo'));
+ blocks.push('-');
+ blocks.push(block('doResetTimer'));
+ blocks.push(watcherToggle('getTimer'));
+ blocks.push(block('reportTimer'));
+ blocks.push('-');
+ blocks.push(block('reportAttributeOf'));
+ blocks.push('-');
+ blocks.push(block('reportURL'));
+ blocks.push('-');
+ blocks.push(block('reportIsFastTracking'));
+ blocks.push(block('doSetFastTracking'));
+
+ // for debugging: ///////////////
+
+ if (this.world().isDevMode) {
+
+ blocks.push('-');
+ txt = new TextMorph(localize(
+ 'development mode \ndebugging primitives:'
+ ));
+ txt.fontSize = 9;
+ txt.setColor(new Color(230, 230, 230));
+ blocks.push(txt);
+ blocks.push('-');
+ blocks.push(block('colorFiltered'));
+ blocks.push(block('reportStackSize'));
+ blocks.push(block('reportFrameCount'));
+ }
+
+ } else if (cat === 'operators') {
+
+ blocks.push(block('reifyScript'));
+ blocks.push(block('reifyReporter'));
+ blocks.push(block('reifyPredicate'));
+ blocks.push('#');
+ blocks.push('-');
+ blocks.push(block('reportSum'));
+ blocks.push(block('reportDifference'));
+ blocks.push(block('reportProduct'));
+ blocks.push(block('reportQuotient'));
+ blocks.push('-');
+ blocks.push(block('reportModulus'));
+ blocks.push(block('reportRound'));
+ blocks.push(block('reportMonadic'));
+ blocks.push(block('reportRandom'));
+ blocks.push('-');
+ blocks.push(block('reportLessThan'));
+ blocks.push(block('reportEquals'));
+ blocks.push(block('reportGreaterThan'));
+ blocks.push('-');
+ blocks.push(block('reportAnd'));
+ blocks.push(block('reportOr'));
+ blocks.push(block('reportNot'));
+ blocks.push('-');
+ blocks.push(block('reportTrue'));
+ blocks.push(block('reportFalse'));
+ blocks.push('-');
+ blocks.push(block('reportJoinWords'));
+ blocks.push(block('reportLetter'));
+ blocks.push(block('reportStringSize'));
+ blocks.push('-');
+ blocks.push(block('reportUnicode'));
+ blocks.push(block('reportUnicodeAsLetter'));
+ blocks.push('-');
+ blocks.push(block('reportIsA'));
+ blocks.push(block('reportIsIdentical'));
+
+ // for debugging: ///////////////
+
+ if (this.world().isDevMode) {
+ blocks.push('-');
+ txt = new TextMorph(
+ 'development mode \ndebugging primitives:'
+ );
+ txt.fontSize = 9;
+ txt.setColor(new Color(230, 230, 230));
+ blocks.push(txt);
+ blocks.push('-');
+ blocks.push(block('reportTypeOf'));
+ }
+
+ /////////////////////////////////
+
+ } else if (cat === 'variables') {
+
+ button = new PushButtonMorph(
+ null,
+ function () {
+ new VariableDialogMorph(
+ null,
+ function (pair) {
+ if (pair) {
+ myself.addVariable(pair[0], pair[1]);
+ myself.toggleVariableWatcher(pair[0], pair[1]);
+ myself.blocksCache[cat] = null;
+ myself.paletteCache[cat] = null;
+ myself.parentThatIsA(IDE_Morph).refreshPalette();
+ }
+ },
+ myself
+ ).prompt(
+ 'Variable name',
+ null,
+ myself.world()
+ );
+ },
+ 'Make a variable'
+ );
+ blocks.push(button);
+
+ if (this.variables.allNames().length > 0) {
+ button = new PushButtonMorph(
+ null,
+ function () {
+ var menu = new MenuMorph(
+ myself.deleteVariable,
+ null,
+ myself
+ );
+ myself.variables.allNames().forEach(function (name) {
+ menu.addItem(name, name);
+ });
+ menu.popUpAtHand(myself.world());
+ },
+ 'Delete a variable'
+ );
+ blocks.push(button);
+ }
+
+ blocks.push('-');
+
+ varNames = this.variables.allNames();
+ if (varNames.length > 0) {
+ varNames.forEach(function (name) {
+ blocks.push(variableWatcherToggle(name));
+ blocks.push(variableBlock(name));
+ });
+ blocks.push('-');
+ }
+
+ blocks.push(block('doSetVar'));
+ blocks.push(block('doChangeVar'));
+ blocks.push(block('doShowVar'));
+ blocks.push(block('doHideVar'));
+ blocks.push(block('doDeclareVariables'));
+
+ blocks.push('=');
+
+ blocks.push(block('reportNewList'));
+ blocks.push('-');
+ blocks.push(block('reportCONS'));
+ blocks.push(block('reportListItem'));
+ blocks.push(block('reportCDR'));
+ blocks.push('-');
+ blocks.push(block('reportListLength'));
+ blocks.push(block('reportListContainsItem'));
+ blocks.push('-');
+ blocks.push(block('doAddToList'));
+ blocks.push(block('doDeleteFromList'));
+ blocks.push(block('doInsertInList'));
+ blocks.push(block('doReplaceInList'));
+
+ blocks.push('=');
+
+ button = new PushButtonMorph(
+ null,
+ function () {
+ var ide = myself.parentThatIsA(IDE_Morph),
+ stage = myself.parentThatIsA(StageMorph);
+ new BlockDialogMorph(
+ null,
+ function (definition) {
+ if (definition.spec !== '') {
+ if (definition.isGlobal) {
+ stage.globalBlocks.push(definition);
+ } else {
+ myself.customBlocks.push(definition);
+ }
+ ide.flushPaletteCache();
+ ide.refreshPalette();
+ new BlockEditorMorph(definition, myself).popUp();
+ }
+ },
+ myself
+ ).prompt(
+ 'Make a block',
+ null,
+ myself.world()
+ );
+ },
+ 'Make a block'
+ );
+ blocks.push(button);
+ }
+ return blocks;
+};
+
+SpriteMorph.prototype.palette = function (category) {
+ if (!this.paletteCache[category]) {
+ this.paletteCache[category] = this.freshPalette(category);
+ }
+ return this.paletteCache[category];
+};
+
+SpriteMorph.prototype.freshPalette = function (category) {
+ var palette = new ScrollFrameMorph(null, null, this.sliderColor),
+ unit = SyntaxElementMorph.prototype.fontSize,
+ x = 0,
+ y = 5,
+ ry = 0,
+ blocks,
+ myself = this,
+ stage = this.parentThatIsA(StageMorph),
+ oldFlag = Morph.prototype.trackChanges;
+
+ Morph.prototype.trackChanges = false;
+
+ palette.owner = this;
+ palette.padding = unit / 2;
+ palette.color = this.paletteColor;
+
+ // primitives:
+
+ blocks = this.blocksCache[category];
+ if (!blocks) {
+ blocks = myself.blockTemplates(category);
+ if (this.isCachingPrimitives) {
+ myself.blocksCache[category] = blocks;
+ }
+ }
+
+ blocks.forEach(function (block) {
+ if (block === '-') {
+ y += unit * 0.8;
+ } else if (block === '=') {
+ y += unit * 1.6;
+ } else if (block === '#') {
+ x = 0;
+ y = ry;
+ } else {
+ if (x === 0) {
+ y += unit * 0.3;
+ }
+ block.setPosition(new Point(x, y));
+ palette.addContents(block);
+ if (block instanceof ToggleMorph
+ || (block instanceof RingMorph)) {
+ x = block.right() + unit / 2;
+ ry = block.bottom();
+ } else {
+ if (block.fixLayout) {block.fixLayout(); }
+ x = 0;
+ y += block.height();
+ }
+ }
+ });
+
+ // global custom blocks:
+
+ if (stage) {
+ y += unit * 1.6;
+
+ stage.globalBlocks.forEach(function (definition) {
+ var block;
+ if (definition.category === category ||
+ (category === 'variables'
+ && contains(
+ ['lists', 'other'],
+ definition.category
+ ))) {
+ block = definition.templateInstance();
+ y += unit * 0.3;
+ block.setPosition(new Point(x, y));
+ palette.addContents(block);
+ x = 0;
+ y += block.height();
+ }
+ });
+ }
+
+ // local custom blocks:
+
+ y += unit * 1.6;
+ this.customBlocks.forEach(function (definition) {
+ var block;
+ if (definition.category === category ||
+ (category === 'variables'
+ && contains(
+ ['lists', 'other'],
+ definition.category
+ ))) {
+ block = definition.templateInstance();
+ y += unit * 0.3;
+ block.setPosition(new Point(x, y));
+ palette.addContents(block);
+ x = 0;
+ y += block.height();
+ }
+ });
+
+ Morph.prototype.trackChanges = oldFlag;
+ return palette;
+};
+
+// SpriteMorph variable management
+
+SpriteMorph.prototype.addVariable = function (name, isGlobal) {
+ var ide = this.parentThatIsA(IDE_Morph);
+ if (isGlobal) {
+ this.variables.parentFrame.addVar(name);
+ if (ide) {
+ ide.flushBlocksCache('variables');
+ }
+ } else {
+ this.variables.addVar(name);
+ this.blocksCache.variables = null;
+ }
+};
+
+SpriteMorph.prototype.deleteVariable = function (varName) {
+ var ide = this.parentThatIsA(IDE_Morph);
+ this.deleteVariableWatcher(varName);
+ this.variables.deleteVar(varName);
+ if (ide) {
+ ide.flushBlocksCache('variables'); // b/c the var could be global
+ ide.refreshPalette();
+ }
+};
+
+// SpriteMorph costume management
+
+SpriteMorph.prototype.addCostume = function (costume) {
+ if (!costume.name) {
+ costume.name = 'costume' + (this.costumes.length() + 1);
+ }
+ this.costumes.add(costume);
+};
+
+SpriteMorph.prototype.wearCostume = function (costume) {
+ var x = this.xPosition ? this.xPosition() : null,
+ y = this.yPosition ? this.yPosition() : null,
+ isWarped = this.isWarped;
+ if (isWarped) {
+ this.endWarp();
+ }
+ this.changed();
+ this.costume = costume;
+ this.drawNew();
+ this.changed();
+ if (isWarped) {
+ this.startWarp();
+ }
+ if (x !== null) {
+ this.silentGotoXY(x, y);
+ }
+ if (this.positionTalkBubble) { // the stage doesn't talk
+ this.positionTalkBubble();
+ }
+ this.version = Date.now();
+};
+
+SpriteMorph.prototype.getCostumeIdx = function () {
+ return this.costumes.asArray().indexOf(this.costume) + 1;
+};
+
+SpriteMorph.prototype.doWearNextCostume = function () {
+ var arr = this.costumes.asArray(),
+ idx;
+ if (arr.length > 1) {
+ idx = arr.indexOf(this.costume);
+ if (idx > -1) {
+ idx += 1;
+ if (idx > (arr.length - 1)) {
+ idx = 0;
+ }
+ this.wearCostume(arr[idx]);
+ }
+ }
+};
+
+SpriteMorph.prototype.doWearPreviousCostume = function () {
+ var arr = this.costumes.asArray(),
+ idx;
+ if (arr.length > 1) {
+ idx = arr.indexOf(this.costume);
+ if (idx > -1) {
+ idx -= 1;
+ if (idx < 0) {
+ idx = arr.length - 1;
+ }
+ this.wearCostume(arr[idx]);
+ }
+ }
+};
+
+SpriteMorph.prototype.doSwitchToCostume = function (id) {
+ var num,
+ arr = this.costumes.asArray(),
+ costume;
+ if (
+ contains(
+ [localize('Turtle'), localize('Empty')],
+ (id instanceof Array ? id[0] : null)
+ )
+ ) {
+ costume = null;
+ } else {
+ if (id === -1) {
+ this.doWearPreviousCostume();
+ return;
+ }
+ costume = detect(arr, function (cst) {
+ return cst.name === id;
+ });
+ if (costume === null) {
+ num = parseFloat(id);
+ if (num === 0) {
+ costume = null;
+ } else {
+ costume = arr[num - 1] || null;
+ }
+ }
+ }
+ this.wearCostume(costume);
+};
+
+// SpriteMorph sound management
+
+SpriteMorph.prototype.addSound = function (audio, name) {
+ this.sounds.add(new Sound(audio, name));
+};
+
+SpriteMorph.prototype.playSound = function (name) {
+ var stage = this.parentThatIsA(StageMorph),
+ sound = detect(
+ this.sounds.asArray(),
+ function (s) {return s.name === name; }
+ ),
+ active;
+ if (sound) {
+ active = sound.play();
+ if (stage) {
+ stage.activeSounds.push(active);
+ stage.activeSounds = stage.activeSounds.filter(function (aud) {
+ return !aud.ended && !aud.terminated;
+ });
+ }
+ return active;
+ }
+};
+
+// SpriteMorph user menu
+
+SpriteMorph.prototype.userMenu = function () {
+ var ide = this.parentThatIsA(IDE_Morph),
+ menu = new MenuMorph(this);
+
+ if (ide && ide.isAppMode) {
+ menu.addItem('help', 'nop');
+ return menu;
+ }
+ menu.addItem("duplicate", 'duplicate');
+ menu.addItem("delete", 'remove');
+ menu.addItem("edit", 'edit');
+ menu.addLine();
+ menu.addItem("export...", 'exportSprite');
+ return menu;
+};
+
+SpriteMorph.prototype.exportSprite = function () {
+ var ide = this.parentThatIsA(IDE_Morph);
+ if (ide) {
+ ide.exportSprite(this);
+ }
+};
+
+SpriteMorph.prototype.edit = function () {
+ var ide = this.parentThatIsA(IDE_Morph);
+ if (ide) {
+ ide.selectSprite(this);
+ }
+};
+
+SpriteMorph.prototype.showOnStage = function () {
+ var stage = this.parentThatIsA(StageMorph);
+ if (stage) {
+ this.keepWithin(stage);
+ stage.add(this);
+ }
+ this.show();
+};
+
+SpriteMorph.prototype.duplicate = function () {
+ var ide = this.parentThatIsA(IDE_Morph);
+ if (ide) {
+ ide.duplicateSprite(this);
+ }
+};
+
+SpriteMorph.prototype.remove = function () {
+ var ide = this.parentThatIsA(IDE_Morph);
+ if (ide) {
+ ide.removeSprite(this);
+ }
+};
+
+// SpriteMorph cloning (experimental)
+
+SpriteMorph.prototype.createClone = function () {
+ var clone,
+ hats,
+ stage = this.parentThatIsA(StageMorph);
+ if (stage) {
+ if (stage.cloneCount > 128) {return; }
+ stage.cloneCount += 1;
+ clone = this.fullCopy();
+ clone.isClone = true;
+ clone.name = '';
+ clone.cloneOriginName = this.isClone ?
+ this.cloneOriginName : this.name;
+ stage.add(clone);
+ hats = clone.allHatBlocksFor('__clone__init__');
+ hats.forEach(function (block) {
+ stage.threads.startProcess(block, stage.isThreadSafe);
+ });
+ }
+};
+
+SpriteMorph.prototype.removeClone = function () {
+ if (this.isClone) {
+ // this.stopTalking();
+ this.parent.threads.stopAllForReceiver(this);
+ this.destroy();
+ this.parent.cloneCount -= 1;
+ }
+};
+
+// SpriteMorph primitives
+
+// SpriteMorph pen color
+
+SpriteMorph.prototype.setColor = function (aColor) {
+ var x = this.xPosition(),
+ y = this.yPosition();
+ if (!this.color.eq(aColor)) {
+ this.color = aColor;
+ this.drawNew();
+ this.gotoXY(x, y);
+ }
+};
+
+SpriteMorph.prototype.getHue = function () {
+ return this.color.hsv()[0] * 100;
+};
+
+SpriteMorph.prototype.setHue = function (num) {
+ var hsv = this.color.hsv(),
+ x = this.xPosition(),
+ y = this.yPosition();
+
+ hsv[0] = Math.max(Math.min(parseFloat(num), 100), 0) / 100;
+ hsv[1] = 1; // we gotta fix this at some time
+ this.color.set_hsv.apply(this.color, hsv);
+ if (!this.costume) {
+ this.drawNew();
+ this.changed();
+ }
+ this.gotoXY(x, y);
+};
+
+SpriteMorph.prototype.changeHue = function (delta) {
+ this.setHue(this.getHue() + parseFloat(delta));
+};
+
+SpriteMorph.prototype.getBrightness = function () {
+ return this.color.hsv()[2] * 100;
+};
+
+SpriteMorph.prototype.setBrightness = function (num) {
+ var hsv = this.color.hsv(),
+ x = this.xPosition(),
+ y = this.yPosition();
+
+ hsv[1] = 1; // we gotta fix this at some time
+ hsv[2] = Math.max(Math.min(parseFloat(num), 100), 0) / 100;
+ this.color.set_hsv.apply(this.color, hsv);
+ if (!this.costume) {
+ this.drawNew();
+ this.changed();
+ }
+ this.gotoXY(x, y);
+};
+
+SpriteMorph.prototype.changeBrightness = function (delta) {
+ this.setBrightness(this.getBrightness() + parseFloat(delta));
+};
+
+// SpriteMorph layers
+
+SpriteMorph.prototype.comeToFront = function () {
+ if (this.parent) {
+ this.parent.add(this);
+ this.changed();
+ }
+};
+
+SpriteMorph.prototype.goBack = function (layers) {
+ var layer;
+
+ if (!this.parent) {return null; }
+ layer = this.parent.children.indexOf(this);
+ if (layer < parseFloat(layers)) {return null; }
+ this.parent.removeChild(this);
+ this.parent.children.splice(layer - parseFloat(layers), null, this);
+ this.parent.changed();
+};
+
+// SpriteMorph collision detection optimization
+
+SpriteMorph.prototype.overlappingImage = function (otherSprite) {
+ // overrides method from Morph because Sprites aren't nested Morphs
+ var oRect = this.bounds.intersect(otherSprite.bounds),
+ oImg = newCanvas(oRect.extent()),
+ ctx = oImg.getContext('2d');
+
+ if (oRect.width() < 1 || oRect.height() < 1) {
+ return newCanvas(new Point(1, 1));
+ }
+ ctx.drawImage(
+ this.image,
+ this.left() - oRect.left(),
+ this.top() - oRect.top()
+ );
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.drawImage(
+ otherSprite.image,
+ otherSprite.left() - oRect.left(),
+ otherSprite.top() - oRect.top()
+ );
+ return oImg;
+};
+
+// SpriteMorph stamping
+
+SpriteMorph.prototype.doStamp = function () {
+ var stage = this.parent,
+ context = stage.penTrails().getContext('2d'),
+ isWarped = this.isWarped;
+ if (isWarped) {
+ this.endWarp();
+ }
+ context.save();
+ context.scale(1 / stage.scale, 1 / stage.scale);
+ context.drawImage(
+ this.image,
+ (this.left() - stage.left()),
+ (this.top() - stage.top())
+ );
+ context.restore();
+ this.changed();
+ if (isWarped) {
+ this.startWarp();
+ }
+};
+
+SpriteMorph.prototype.clear = function () {
+ this.parent.clearPenTrails();
+};
+
+// SpriteMorph pen size
+
+SpriteMorph.prototype.setSize = function (size) {
+ // pen size
+ this.size = Math.min(Math.max(parseFloat(size), 0.0001), 1000);
+};
+
+SpriteMorph.prototype.changeSize = function (delta) {
+ this.setSize(this.size + parseFloat(delta));
+};
+
+// SpriteMorph scale
+
+SpriteMorph.prototype.getScale = function () {
+ // answer my scale in percent
+ return this.scale * 100;
+};
+
+SpriteMorph.prototype.setScale = function (percentage) {
+ // set my (absolute) scale in percent
+ var x = this.xPosition(),
+ y = this.yPosition(),
+ isWarped = this.isWarped;
+ if (isWarped) {
+ this.endWarp();
+ }
+ this.scale = Math.max(parseFloat((percentage || '0') / 100), 0.01);
+ this.changed();
+ this.drawNew();
+ this.changed();
+ if (isWarped) {
+ this.startWarp();
+ }
+ this.silentGotoXY(x, y);
+ this.positionTalkBubble();
+};
+
+SpriteMorph.prototype.changeScale = function (delta) {
+ this.setScale(this.getScale() + parseFloat(delta || '0'));
+};
+
+// SpriteMorph graphic effects
+
+SpriteMorph.prototype.setEffect = function (effect, value) {
+ var eff = effect instanceof Array ? effect[0] : null;
+ if (eff === 'ghost') {
+ this.alpha = 1 -
+ Math.min(Math.max(parseFloat(value), 0), 100) / 100;
+ this.changed();
+ }
+};
+
+SpriteMorph.prototype.getGhostEffect = function () {
+ return (1 - this.alpha) * 100;
+};
+
+SpriteMorph.prototype.changeEffect = function (effect, value) {
+ var eff = effect instanceof Array ? effect[0] : null;
+ if (eff === 'ghost') {
+ this.setEffect(effect, this.getGhostEffect() + parseFloat(value));
+ }
+};
+
+SpriteMorph.prototype.clearEffects = function () {
+ this.setEffect(['ghost'], 0);
+};
+
+// SpriteMorph talk bubble
+
+SpriteMorph.prototype.stopTalking = function () {
+ var bubble = this.talkBubble();
+ if (bubble) {bubble.destroy(); }
+};
+
+SpriteMorph.prototype.doThink = function (data) {
+ this.bubble(data, true);
+};
+
+SpriteMorph.prototype.bubble = function (data, isThought, isQuestion) {
+ var bubble,
+ stage = this.parentThatIsA(StageMorph);
+
+ this.stopTalking();
+ if (data === '' || isNil(data)) {return; }
+ bubble = new SpriteBubbleMorph(
+ data,
+ stage ? stage.scale : 1,
+ isThought,
+ isQuestion
+ );
+ this.add(bubble);
+ this.positionTalkBubble();
+};
+
+SpriteMorph.prototype.talkBubble = function () {
+ return detect(
+ this.children,
+ function (morph) {return morph instanceof SpeechBubbleMorph; }
+ );
+};
+
+SpriteMorph.prototype.positionTalkBubble = function () {
+ var stage = this.parentThatIsA(StageMorph),
+ bubble = this.talkBubble(),
+ middle = this.center().y;
+ if (!bubble) {return null; }
+ bubble.show();
+ if (!bubble.isPointingRight) {
+ bubble.isPointingRight = true;
+ bubble.drawNew();
+ bubble.changed();
+ }
+ bubble.setLeft(this.right());
+ bubble.setBottom(this.top());
+ while (!this.isTouching(bubble) && bubble.bottom() < middle) {
+ bubble.silentMoveBy(new Point(-1, 1).scaleBy(stage.scale));
+ }
+ if (!stage) {return null; }
+ if (bubble.right() > stage.right()) {
+ bubble.isPointingRight = false;
+ bubble.drawNew();
+ bubble.setRight(this.center().x);
+ }
+ bubble.keepWithin(stage);
+ bubble.changed();
+};
+
+// dragging and dropping adjustments b/c of talk bubbles
+
+SpriteMorph.prototype.prepareToBeGrabbed = function (hand) {
+ var bubble = this.talkBubble();
+ if (!bubble) {return null; }
+ this.removeShadow();
+ bubble.hide();
+ if (!this.bounds.containsPoint(hand.position())) {
+ this.setCenter(hand.position());
+ }
+ this.addShadow();
+};
+
+SpriteMorph.prototype.justDropped = function () {
+ this.positionTalkBubble();
+};
+
+// SpriteMorph drawing:
+
+SpriteMorph.prototype.drawLine = function (start, dest) {
+ var stagePos = this.parent.bounds.origin,
+ stageScale = this.parent.scale,
+ context = this.parent.penTrails().getContext('2d'),
+ from = start.subtract(stagePos).divideBy(stageScale),
+ to = dest.subtract(stagePos).divideBy(stageScale),
+ damagedFrom = from.multiplyBy(stageScale).add(stagePos),
+ damagedTo = to.multiplyBy(stageScale).add(stagePos),
+ damaged = damagedFrom.rectangle(damagedTo).expandBy(
+ Math.max(this.size * stageScale / 2, 1)
+ ).intersect(this.parent.visibleBounds()).spread();
+
+ if (this.isDown) {
+ context.lineWidth = this.size;
+ context.strokeStyle = this.color.toString();
+ context.lineCap = 'round';
+ context.lineJoin = 'round';
+ context.beginPath();
+ context.moveTo(from.x, from.y);
+ context.lineTo(to.x, to.y);
+ context.stroke();
+ if (this.isWarped === false) {
+ this.world().broken.push(damaged);
+ }
+ }
+};
+
+// SpriteMorph motion
+
+SpriteMorph.prototype.forward = function (steps) {
+ var start = this.rotationCenter(),
+ dest,
+ dist = parseFloat(steps * this.parent.scale);
+
+ if (dist >= 0) {
+ dest = this.position().distanceAngle(dist, this.heading);
+ } else {
+ dest = this.position().distanceAngle(
+ Math.abs(dist),
+ (this.heading - 180)
+ );
+ }
+ this.setPosition(dest);
+ this.drawLine(start, this.rotationCenter());
+ this.positionTalkBubble();
+};
+
+SpriteMorph.prototype.setHeading = function (degrees) {
+ var x = this.xPosition(),
+ y = this.yPosition();
+ this.changed();
+ SpriteMorph.uber.setHeading.call(this, degrees);
+ this.silentGotoXY(x, y);
+ this.positionTalkBubble();
+};
+
+SpriteMorph.prototype.faceToXY = function (x, y) {
+ var deltaX = (x - this.xPosition()) * this.parent.scale,
+ deltaY = (y - this.yPosition()) * this.parent.scale,
+ angle = Math.abs(deltaX) < 0.001 ? (deltaY < 0 ? 90 : 270)
+ : Math.round(
+ (deltaX >= 0 ? 0 : 180)
+ - (Math.atan(deltaY / deltaX) * 57.2957795131)
+ );
+ this.setHeading(angle + 90);
+};
+
+SpriteMorph.prototype.turn = function (degrees) {
+ this.setHeading(this.heading + parseFloat(degrees || '0'));
+};
+
+SpriteMorph.prototype.turnLeft = function (degrees) {
+ this.setHeading(this.heading - parseFloat(degrees || '0'));
+};
+
+SpriteMorph.prototype.xPosition = function () {
+ var stage = this.parentThatIsA(StageMorph);
+
+ if (!stage && this.parent.grabOrigin) { // I'm currently being dragged
+ stage = this.parent.grabOrigin.origin;
+ }
+ if (stage) {
+ return (this.rotationCenter().x - stage.center().x) / stage.scale;
+ }
+ return this.rotationCenter().x;
+};
+
+SpriteMorph.prototype.yPosition = function () {
+ var stage = this.parentThatIsA(StageMorph);
+
+ if (!stage && this.parent.grabOrigin) { // I'm currently being dragged
+ stage = this.parent.grabOrigin.origin;
+ }
+ if (stage) {
+ return (stage.center().y - this.rotationCenter().y) / stage.scale;
+ }
+ return this.rotationCenter().y;
+};
+
+SpriteMorph.prototype.direction = function () {
+ return this.heading;
+};
+
+SpriteMorph.prototype.penSize = function () {
+ return this.size;
+};
+
+SpriteMorph.prototype.gotoXY = function (x, y) {
+ var stage = this.parentThatIsA(StageMorph),
+ start = this.rotationCenter(),
+ newX,
+ newY,
+ dest;
+
+ newX = stage.center().x + (parseFloat(x || '0') * stage.scale);
+ newY = stage.center().y - (parseFloat(y || '0') * stage.scale);
+ if (this.costume) {
+ dest = new Point(newX, newY).subtract(this.rotationOffset);
+ } else {
+ dest = new Point(newX, newY).subtract(this.extent().divideBy(2));
+ }
+ this.setPosition(dest);
+ this.drawLine(start, this.rotationCenter());
+ this.positionTalkBubble();
+};
+
+SpriteMorph.prototype.silentGotoXY = function (x, y) {
+ // move without drawing
+ var penState = this.isDown;
+ this.isDown = false;
+ this.gotoXY(x, y);
+ this.isDown = penState;
+};
+
+SpriteMorph.prototype.setXPosition = function (num) {
+ this.gotoXY(parseFloat(num), this.yPosition());
+};
+
+SpriteMorph.prototype.changeXPosition = function (delta) {
+ this.setXPosition(this.xPosition() + parseFloat(delta));
+};
+
+SpriteMorph.prototype.setYPosition = function (num) {
+ this.gotoXY(this.xPosition(), parseFloat(num));
+};
+
+SpriteMorph.prototype.changeYPosition = function (delta) {
+ this.setYPosition(this.yPosition() + parseFloat(delta));
+};
+
+SpriteMorph.prototype.glide = function (
+ duration,
+ endX,
+ endY,
+ elapsed,
+ startPoint
+) {
+ var fraction, endPoint, rPos;
+ endPoint = new Point(endX, endY);
+ fraction = Math.max(Math.min(elapsed / duration, 1), 0);
+ rPos = startPoint.add(
+ endPoint.subtract(startPoint).multiplyBy(fraction)
+ );
+ this.gotoXY(rPos.x, rPos.y);
+};
+
+SpriteMorph.prototype.bounceOffEdge = function () {
+ var stage = this.parentThatIsA(StageMorph),
+ dirX,
+ dirY;
+
+ if (!stage) {return null; }
+ if (stage.bounds.containsRectangle(this.bounds)) {return null; }
+
+ dirX = Math.cos(radians(this.heading - 90));
+ dirY = -(Math.sin(radians(this.heading - 90)));
+
+ if (this.left() < stage.left()) {
+ dirX = Math.abs(dirX);
+ }
+ if (this.right() > stage.right()) {
+ dirX = -(Math.abs(dirX));
+ }
+ if (this.top() < stage.top()) {
+ dirY = -(Math.abs(dirY));
+ }
+ if (this.bottom() > stage.bottom()) {
+ dirY = Math.abs(dirY);
+ }
+
+ this.setHeading(degrees(Math.atan2(-dirY, dirX)) + 90);
+ this.setPosition(this.position().add(
+ this.bounds.amountToTranslateWithin(stage.bounds)
+ ));
+ this.positionTalkBubble();
+};
+
+// SpriteMorph message broadcasting
+
+SpriteMorph.prototype.allMessageNames = function () {
+ var msgs = [];
+ this.scripts.allChildren().forEach(function (morph) {
+ var txt;
+ if (morph.selector) {
+ if (contains(
+ ['receiveMessage', 'doBroadcast', 'doBroadcastAndWait'],
+ morph.selector
+ )) {
+ txt = morph.inputs()[0].evaluate();
+ if (txt !== '') {
+ if (!contains(msgs, txt)) {
+ msgs.push(txt);
+ }
+ }
+ }
+ }
+ });
+ return msgs;
+};
+
+SpriteMorph.prototype.allHatBlocksFor = function (message) {
+ return this.scripts.children.filter(function (morph) {
+ if (morph.selector) {
+ if (morph.selector === 'receiveMessage') {
+ return morph.inputs()[0].evaluate() === message;
+ }
+ if (morph.selector === 'receiveGo') {
+ return message === '__shout__go__';
+ }
+ if (morph.selector === 'receiveOnClone') {
+ return message === '__clone__init__';
+ }
+ if (morph.selector === 'receiveClick') {
+ return message === '__click__';
+ }
+ }
+ return false;
+ });
+};
+
+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;
+ }
+ }
+ return false;
+ });
+};
+
+// SpriteMorph events
+
+SpriteMorph.prototype.mouseClickLeft = function () {
+ var stage = this.parentThatIsA(StageMorph),
+ hats = this.allHatBlocksFor('__click__'),
+ procs = [];
+
+ hats.forEach(function (block) {
+ procs.push(stage.threads.startProcess(block, stage.isThreadSafe));
+ });
+ return procs;
+};
+
+// SpriteMorph timer
+
+SpriteMorph.prototype.getTimer = function () {
+ var stage = this.parentThatIsA(StageMorph);
+ if (stage) {
+ return stage.getTimer();
+ }
+ return 0;
+};
+
+// SpriteMorph tempo
+
+SpriteMorph.prototype.getTempo = function () {
+ var stage = this.parentThatIsA(StageMorph);
+ if (stage) {
+ return stage.getTempo();
+ }
+ return 0;
+};
+
+// SpriteMorph user prompting
+
+SpriteMorph.prototype.getLastAnswer = function () {
+ return this.parentThatIsA(StageMorph).lastAnswer;
+};
+
+// SpriteMorph variable watchers (for palette checkbox toggling)
+
+SpriteMorph.prototype.findVariableWatcher = function (varName) {
+ var stage = this.parentThatIsA(StageMorph),
+ myself = this;
+ if (stage === null) {
+ return null;
+ }
+ return detect(
+ stage.children,
+ function (morph) {
+ return morph instanceof WatcherMorph
+ && (morph.target === myself.variables
+ || morph.target === myself.variables.parentFrame)
+ && morph.getter === varName;
+ }
+ );
+};
+
+SpriteMorph.prototype.toggleVariableWatcher = function (varName, isGlobal) {
+ var stage = this.parentThatIsA(StageMorph),
+ watcher,
+ others;
+ if (stage === null) {
+ return null;
+ }
+ watcher = this.findVariableWatcher(varName);
+ if (watcher !== null) {
+ if (watcher.isVisible) {
+ watcher.hide();
+ } else {
+ watcher.show();
+ watcher.fixLayout(); // re-hide hidden parts
+ }
+ return;
+ }
+
+ // if no watcher exists, create a new one
+ watcher = new WatcherMorph(
+ varName,
+ this.blockColor.variables,
+ isGlobal ? this.variables.parentFrame : this.variables,
+ varName
+ );
+ watcher.setPosition(stage.position().add(10));
+ others = stage.watchers(watcher.left());
+ if (others.length > 0) {
+ watcher.setTop(others[others.length - 1].bottom());
+ }
+ stage.add(watcher);
+ watcher.fixLayout();
+};
+
+SpriteMorph.prototype.showingVariableWatcher = function (varName) {
+ var stage = this.parentThatIsA(StageMorph),
+ watcher;
+ if (stage === null) {
+ return false;
+ }
+ watcher = this.findVariableWatcher(varName);
+ if (watcher) {
+ return watcher.isVisible;
+ }
+ return false;
+};
+
+SpriteMorph.prototype.deleteVariableWatcher = function (varName) {
+ var stage = this.parentThatIsA(StageMorph),
+ watcher;
+ if (stage === null) {
+ return null;
+ }
+ watcher = this.findVariableWatcher(varName);
+ if (watcher !== null) {
+ watcher.destroy();
+ }
+};
+
+// SpriteMorph non-variable watchers
+
+SpriteMorph.prototype.toggleWatcher = function (selector, label, color) {
+ var stage = this.parentThatIsA(StageMorph),
+ watcher,
+ others;
+ if (!stage) { return; }
+ watcher = this.watcherFor(stage, selector);
+ if (watcher) {
+ if (watcher.isVisible) {
+ watcher.hide();
+ } else {
+ watcher.show();
+ watcher.fixLayout(); // re-hide hidden parts
+ }
+ return;
+ }
+
+ // if no watcher exists, create a new one
+ watcher = new WatcherMorph(
+ label,
+ color,
+ WatcherMorph.prototype.isGlobal(selector) ? stage : this,
+ selector
+ );
+ watcher.setPosition(stage.position().add(10));
+ others = stage.watchers(watcher.left());
+ if (others.length > 0) {
+ watcher.setTop(others[others.length - 1].bottom());
+ }
+ stage.add(watcher);
+ watcher.fixLayout();
+};
+
+SpriteMorph.prototype.showingWatcher = function (selector) {
+ var stage = this.parentThatIsA(StageMorph),
+ watcher;
+ if (stage === null) {
+ return false;
+ }
+ watcher = this.watcherFor(stage, selector);
+ if (watcher) {
+ return watcher.isVisible;
+ }
+ return false;
+};
+
+SpriteMorph.prototype.watcherFor = function (stage, selector) {
+ var myself = this;
+ return detect(stage.children, function (morph) {
+ return morph instanceof WatcherMorph &&
+ morph.getter === selector &&
+ morph.target === (morph.isGlobal(selector) ? stage : myself);
+ });
+};
+
+// SpriteMorph custom blocks
+
+SpriteMorph.prototype.deleteAllBlockInstances = function (definition) {
+ this.allBlockInstances(definition).forEach(function (each) {
+ each.deleteBlock();
+ });
+ this.customBlocks.forEach(function (def) {
+ if (def.body && def.body.expression.isCorpse) {
+ def.body = null;
+ }
+ });
+};
+
+SpriteMorph.prototype.allBlockInstances = function (definition) {
+ var stage, objects, blocks = [], inDefinitions;
+ if (definition.isGlobal) {
+ stage = this.parentThatIsA(StageMorph);
+ objects = stage.children.filter(function (morph) {
+ return morph instanceof SpriteMorph;
+ });
+ objects.push(stage);
+ objects.forEach(function (sprite) {
+ blocks = blocks.concat(sprite.allLocalBlockInstances(definition));
+ });
+ inDefinitions = [];
+ stage.globalBlocks.forEach(function (def) {
+ if (def.body) {
+ def.body.expression.allChildren().forEach(function (c) {
+ if (c.definition && (c.definition === definition)) {
+ inDefinitions.push(c);
+ }
+ });
+ }
+ });
+ return blocks.concat(inDefinitions);
+ }
+ return this.allLocalBlockInstances(definition);
+};
+
+SpriteMorph.prototype.allLocalBlockInstances = function (definition) {
+ var inScripts, inDefinitions, inBlockEditors, inPalette, result;
+
+ inScripts = this.scripts.allChildren().filter(function (c) {
+ return c.definition && (c.definition === definition);
+ });
+
+ inDefinitions = [];
+ this.customBlocks.forEach(function (def) {
+ if (def.body) {
+ def.body.expression.allChildren().forEach(function (c) {
+ if (c.definition && (c.definition === definition)) {
+ inDefinitions.push(c);
+ }
+ });
+ }
+ });
+
+ inBlockEditors = this.allEditorBlockInstances(definition);
+ inPalette = this.paletteBlockInstance(definition);
+
+ result = inScripts.concat(inDefinitions).concat(inBlockEditors);
+ if (inPalette) {
+ result.push(inPalette);
+ }
+ return result;
+};
+
+SpriteMorph.prototype.allEditorBlockInstances = function (definition) {
+ var inBlockEditors = [],
+ world = this.world();
+
+ if (!world) {return []; } // when copying a sprite
+
+ this.world().children.forEach(function (morph) {
+ if (morph instanceof BlockEditorMorph) {
+ morph.body.contents.allChildren().forEach(function (block) {
+ if (!block.isPrototype
+ && !(block instanceof PrototypeHatBlockMorph)
+ && (block.definition === definition)) {
+ inBlockEditors.push(block);
+ }
+ });
+ }
+ });
+ return inBlockEditors;
+};
+
+
+SpriteMorph.prototype.paletteBlockInstance = function (definition) {
+ var ide = this.parentThatIsA(IDE_Morph);
+ if (!ide) {return null; }
+ return detect(
+ ide.palette.contents.children,
+ function (block) {
+ return block.definition === definition;
+ }
+ );
+};
+
+SpriteMorph.prototype.usesBlockInstance = function (definition) {
+ var inDefinitions,
+ inScripts = detect(
+ this.scripts.allChildren(),
+ function (c) {
+ return c.definition && (c.definition === definition);
+ }
+ );
+
+ if (inScripts) {return true; }
+
+ inDefinitions = [];
+ this.customBlocks.forEach(function (def) {
+ if (def.body) {
+ def.body.expression.allChildren().forEach(function (c) {
+ if (c.definition && (c.definition === definition)) {
+ inDefinitions.push(c);
+ }
+ });
+ }
+ });
+ return (inDefinitions.length > 0);
+};
+
+SpriteMorph.prototype.doubleDefinitionsFor = function (definition) {
+ var spec = definition.blockSpec(),
+ blockList,
+ idx,
+ stage;
+
+ if (definition.isGlobal) {
+ stage = this.parentThatIsA(StageMorph);
+ if (!stage) {return []; }
+ blockList = stage.globalBlocks;
+ } else {
+ blockList = this.customBlocks;
+ }
+ idx = blockList.indexOf(definition);
+ if (idx === -1) {return []; }
+ return blockList.filter(function (def, i) {
+ return def.blockSpec() === spec && (i !== idx);
+ });
+};
+
+SpriteMorph.prototype.replaceDoubleDefinitionsFor = function (definition) {
+ var doubles = this.doubleDefinitionsFor(definition),
+ myself = this,
+ stage,
+ ide;
+ doubles.forEach(function (double) {
+ myself.allBlockInstances(double).forEach(function (block) {
+ block.definition = definition;
+ block.refresh();
+ });
+ });
+ if (definition.isGlobal) {
+ stage = this.parentThatIsA(StageMorph);
+ stage.globalBlocks = stage.globalBlocks.filter(function (def) {
+ return !contains(doubles, def);
+ });
+ } else {
+ this.customBlocks = this.customBlocks.filter(function (def) {
+ return !contains(doubles, def);
+ });
+ }
+ ide = this.parentThatIsA(IDE_Morph);
+ if (ide) {
+ ide.flushPaletteCache();
+ ide.refreshPalette();
+ }
+};
+
+// SpriteMorph thumbnail
+
+SpriteMorph.prototype.thumbnail = function (extentPoint) {
+/*
+ answer a new Canvas of extentPoint dimensions containing
+ my thumbnail representation keeping the originial aspect ratio
+*/
+ var src = this.image, // at this time sprites aren't composite morphs
+ scale = Math.min(
+ (extentPoint.x / src.width),
+ (extentPoint.y / src.height)
+ ),
+ xOffset = (extentPoint.x - (src.width * scale)) / 2,
+ yOffset = (extentPoint.y - (src.height * scale)) / 2,
+ trg = newCanvas(extentPoint),
+ ctx = trg.getContext('2d');
+
+ ctx.scale(scale, scale);
+ ctx.drawImage(
+ src,
+ Math.floor(xOffset / scale),
+ Math.floor(yOffset / scale)
+ );
+ return trg;
+};
+
+// SpriteMorph Boolean visual representation
+
+SpriteMorph.prototype.booleanMorph = function (bool) {
+ // answer a block which can be shown in watchers, speech bubbles etc.
+ var block = new ReporterBlockMorph(true);
+ block.color = SpriteMorph.prototype.blockColor.operators;
+ block.setSpec(bool.toString());
+ return block;
+};
+
+// StageMorph /////////////////////////////////////////////////////////
+
+/*
+ I inherit from FrameMorph and copy from SpriteMorph.
+*/
+
+// StageMorph inherits from FrameMorph:
+
+StageMorph.prototype = new FrameMorph();
+StageMorph.prototype.constructor = StageMorph;
+StageMorph.uber = FrameMorph.prototype;
+
+// StageMorph preferences settings
+
+StageMorph.prototype.dimensions = new Point(480, 360); // unscaled extent
+
+StageMorph.prototype.frameRate = 0; // unscheduled per default
+
+StageMorph.prototype.isCachingPrimitives
+ = SpriteMorph.prototype.isCachingPrimitives;
+
+StageMorph.prototype.sliderColor
+ = SpriteMorph.prototype.sliderColor;
+
+// StageMorph instance creation
+
+function StageMorph(globals) {
+ this.init(globals);
+}
+
+StageMorph.prototype.init = function (globals) {
+ this.name = localize('Stage');
+ this.threads = new ThreadManager();
+ this.variables = new VariableFrame(globals || null, this);
+ this.scripts = new ScriptsMorph(this);
+ this.customBlocks = [];
+ this.globalBlocks = [];
+ this.costumes = new List();
+ this.costume = null;
+ this.sounds = new List();
+ this.version = Date.now(); // for observers
+ this.isFastTracked = false;
+ this.cloneCount = 0;
+
+ this.timerStart = Date.now();
+ this.tempo = 60; // bpm
+
+ this.watcherUpdateFrequency = 2;
+ this.lastWatcherUpdate = Date.now();
+
+ this.scale = 1; // for display modes, do not persist
+
+ this.keysPressed = {}; // for handling keyboard events, do not persist
+ this.blocksCache = {}; // not to be serialized (!)
+ this.paletteCache = {}; // not to be serialized (!)
+ this.lastAnswer = null; // last user input, do not persist
+ this.activeSounds = []; // do not persist
+
+ this.trailsCanvas = null;
+ this.isThreadSafe = false;
+
+ StageMorph.uber.init.call(this);
+
+ this.acceptsDrops = false;
+ this.setColor(new Color(255, 255, 255));
+ this.fps = this.frameRate;
+};
+
+// StageMorph scaling
+
+StageMorph.prototype.setScale = function (number) {
+ var delta = number / this.scale,
+ pos = this.position(),
+ relativePos,
+ bubble,
+ oldFlag = Morph.prototype.trackChanges,
+ myself = this;
+
+ if (delta === 1) {return; }
+ Morph.prototype.trackChanges = false;
+ this.scale = number;
+ this.setExtent(this.dimensions.multiplyBy(number));
+
+ // now move and resize all children - sprites, bubbles, watchers etc..
+ this.children.forEach(function (morph) {
+ relativePos = morph.position().subtract(pos);
+ morph.drawNew();
+ morph.setPosition(relativePos.multiplyBy(delta).add(pos));
+ if (morph instanceof SpriteMorph) {
+ bubble = morph.talkBubble();
+ if (bubble) {
+ bubble.setScale(number);
+ morph.positionTalkBubble();
+ }
+ } else if (morph instanceof StagePrompterMorph) {
+ if (myself.scale < 1) {
+ morph.setWidth(myself.width() - 10);
+ } else {
+ morph.setWidth(myself.dimensions.x - 20);
+ }
+ morph.fixLayout();
+ morph.setCenter(myself.center());
+ morph.setBottom(myself.bottom());
+ }
+ });
+ Morph.prototype.trackChanges = oldFlag;
+ this.changed();
+};
+
+// StageMorph rendering
+
+StageMorph.prototype.drawNew = function () {
+ var ctx;
+ StageMorph.uber.drawNew.call(this);
+ if (this.costume) {
+ ctx = this.image.getContext('2d');
+ ctx.scale(this.scale, this.scale);
+ ctx.drawImage(
+ this.costume.contents,
+ (this.width() - this.costume.width() * this.scale) / 2,
+ (this.height() - this.costume.height() * this.scale) / 2
+ );
+ }
+};
+
+StageMorph.prototype.drawOn = function (aCanvas, aRect) {
+ // make sure to draw the pen trails canvas as well
+ var rectangle, area, delta, src, context, w, h, sl, st, ws, hs;
+ if (!this.isVisible) {
+ return null;
+ }
+ rectangle = aRect || this.bounds;
+ area = rectangle.intersect(this.bounds).round();
+ if (area.extent().gt(new Point(0, 0))) {
+ delta = this.position().neg();
+ src = area.copy().translateBy(delta).round();
+ context = aCanvas.getContext('2d');
+ context.globalAlpha = this.alpha;
+
+ sl = src.left();
+ st = src.top();
+ w = Math.min(src.width(), this.image.width - sl);
+ h = Math.min(src.height(), this.image.height - st);
+
+ if (w < 1 || h < 1) {
+ return null;
+ }
+ context.drawImage(
+ this.image,
+ src.left(),
+ src.top(),
+ w,
+ h,
+ area.left(),
+ area.top(),
+ w,
+ h
+ );
+
+ // pen trails
+ ws = w / this.scale;
+ hs = h / this.scale;
+ context.save();
+ context.scale(this.scale, this.scale);
+ context.drawImage(
+ this.penTrails(),
+ src.left() / this.scale,
+ src.top() / this.scale,
+ ws,
+ hs,
+ area.left() / this.scale,
+ area.top() / this.scale,
+ ws,
+ hs
+ );
+ context.restore();
+ }
+};
+
+StageMorph.prototype.clearPenTrails = function () {
+ this.trailsCanvas = newCanvas(this.dimensions);
+ this.changed();
+};
+
+StageMorph.prototype.penTrails = function () {
+ if (!this.trailsCanvas) {
+ this.trailsCanvas = newCanvas(this.dimensions);
+ }
+ return this.trailsCanvas;
+};
+
+StageMorph.prototype.penTrailsMorph = function () {
+ // for collision detection purposes
+ var morph = new Morph(),
+ trails = this.penTrails(),
+ ctx;
+ morph.bounds = this.bounds.copy();
+ morph.image = newCanvas(this.extent());
+ ctx = morph.image.getContext('2d');
+ ctx.drawImage(
+ trails,
+ 0,
+ 0,
+ trails.width,
+ trails.height,
+ 0,
+ 0,
+ this.image.width,
+ this.image.height
+ );
+ return morph;
+};
+
+StageMorph.prototype.colorFiltered = function (aColor, excludedSprite) {
+ // answer a new Morph containing my image filtered by aColor
+ // ignore the excludedSprite, because its collision is checked
+ // ignore transparency (alpha)
+ var morph = new Morph(),
+ ext = this.extent(),
+ img = this.thumbnail(ext, excludedSprite),
+ ctx,
+ src,
+ clr,
+ i,
+ dta;
+
+ src = img.getContext('2d').getImageData(0, 0, ext.x, ext.y);
+ morph.bounds = this.bounds.copy();
+ morph.image = newCanvas(ext);
+ ctx = morph.image.getContext('2d');
+ dta = ctx.createImageData(ext.x, ext.y);
+ for (i = 0; i < ext.x * ext.y * 4; i += 4) {
+ clr = new Color(
+ src.data[i],
+ src.data[i + 1],
+ src.data[i + 2]
+ );
+ if (clr.eq(aColor)) {
+ dta.data[i] = src.data[i];
+ dta.data[i + 1] = src.data[i + 1];
+ dta.data[i + 2] = src.data[i + 2];
+ dta.data[i + 3] = 255;
+ }
+ }
+ ctx.putImageData(dta, 0, 0);
+ return morph;
+};
+
+// StageMorph accessing
+
+StageMorph.prototype.watchers = function (leftPos) {
+/*
+ answer an array of all currently visible watchers.
+ If leftPos is specified, filter the list for all
+ shown or hidden watchers whose left side equals
+ the given border (for automatic positioning)
+*/
+ return this.children.filter(function (morph) {
+ if (morph instanceof WatcherMorph) {
+ if (leftPos) {
+ return morph.left() === leftPos;
+ }
+ return morph.isVisible;
+ }
+ return false;
+ });
+};
+
+// StageMorph timer
+
+StageMorph.prototype.resetTimer = function () {
+ this.timerStart = Date.now();
+};
+
+StageMorph.prototype.getTimer = function () {
+ var elapsed = Math.floor((Date.now() - this.timerStart) / 100);
+ return elapsed / 10;
+};
+
+// StageMorph tempo
+
+StageMorph.prototype.setTempo = function (bpm) {
+ this.tempo = Math.max(20, parseFloat(bpm || '0'));
+};
+
+StageMorph.prototype.changeTempo = function (delta) {
+ this.setTempo(this.getTempo() + parseFloat(delta || '0'));
+};
+
+StageMorph.prototype.getTempo = function () {
+ return +this.tempo;
+};
+
+// StageMorph drag & drop
+
+StageMorph.prototype.wantsDropOf = function (aMorph) {
+ return aMorph instanceof SpriteMorph ||
+ aMorph instanceof WatcherMorph ||
+ aMorph instanceof ListWatcherMorph;
+};
+
+// StageMorph stepping
+
+StageMorph.prototype.step = function () {
+ var current, elapsed, leftover, world = this.world();
+
+ // handle keyboard events
+ if (world.keyboardReceiver === null) {
+ world.keyboardReceiver = this;
+ }
+ if (world.currentKey === null) {
+ this.keyPressed = null;
+ }
+
+ // manage threads
+ if (this.isFastTracked && this.threads.processes.length) {
+ this.children.forEach(function (morph) {
+ if (morph instanceof SpriteMorph) {
+ morph.wasWarped = morph.isWarped;
+ if (!morph.isWarped) {
+ morph.startWarp();
+ }
+ }
+ });
+ while ((Date.now() - this.lastTime) < 100) {
+ this.threads.step();
+ }
+ this.children.forEach(function (morph) {
+ if (morph instanceof SpriteMorph) {
+ if (!morph.wasWarped) {
+ morph.endWarp();
+ }
+ }
+ });
+ this.changed();
+ } else {
+ this.threads.step();
+ }
+
+ // update watchers
+ current = Date.now();
+ elapsed = current - this.lastWatcherUpdate;
+ leftover = (1000 / this.watcherUpdateFrequency) - elapsed;
+ if (leftover < 1) {
+ this.watchers().forEach(function (w) {
+ w.update();
+ });
+ this.lastWatcherUpdate = Date.now();
+ }
+};
+
+StageMorph.prototype.developersMenu = function () {
+ var myself = this,
+ menu = StageMorph.uber.developersMenu.call(this);
+ menu.addItem(
+ "stop",
+ function () {
+ myself.threads.stopAll();
+ },
+ 'terminate all running threads'
+ );
+ return menu;
+};
+
+// StageMorph keyboard events
+
+StageMorph.prototype.processKeyDown = function (event) {
+ this.processKeyEvent(
+ event,
+ this.fireKeyEvent
+ );
+};
+
+StageMorph.prototype.processKeyUp = function (event) {
+ this.processKeyEvent(
+ event,
+ this.removePressedKey
+ );
+};
+
+StageMorph.prototype.processKeyEvent = function (event, action) {
+ var keyName;
+
+ // this.inspectKeyEvent(event);
+ switch (event.keyCode) {
+ case 13:
+ keyName = 'enter';
+ break;
+ case 27:
+ keyName = 'esc';
+ break;
+ case 32:
+ keyName = 'space';
+ break;
+ case 37:
+ keyName = 'left arrow';
+ break;
+ case 39:
+ keyName = 'right arrow';
+ break;
+ case 38:
+ keyName = 'up arrow';
+ break;
+ case 40:
+ keyName = 'down arrow';
+ break;
+ default:
+ keyName = String.fromCharCode(event.keyCode || event.charCode);
+ }
+ action.call(this, keyName);
+};
+
+StageMorph.prototype.fireKeyEvent = function (key) {
+ var evt = key.toLowerCase(),
+ hats = [],
+ procs = [],
+ myself = this;
+
+ this.keysPressed[evt] = true;
+ if (evt === 'enter') {
+ return this.fireGreenFlagEvent();
+ }
+ if (evt === 'esc') {
+ return this.fireStopAllEvent();
+ }
+ this.children.concat(this).forEach(function (morph) {
+ if (morph instanceof SpriteMorph || morph instanceof StageMorph) {
+ hats = hats.concat(morph.allHatBlocksForKey(evt));
+ }
+ });
+ hats.forEach(function (block) {
+ procs.push(myself.threads.startProcess(block, myself.isThreadSafe));
+ });
+ return procs;
+};
+
+StageMorph.prototype.removePressedKey = function (key) {
+ delete this.keysPressed[key.toLowerCase()];
+};
+
+StageMorph.prototype.processKeyPress = function (event) {
+ nop(event);
+};
+
+StageMorph.prototype.inspectKeyEvent
+ = CursorMorph.prototype.inspectKeyEvent;
+
+StageMorph.prototype.fireGreenFlagEvent = function () {
+ var procs = [],
+ hats = [],
+ ide = this.parentThatIsA(IDE_Morph),
+ myself = this;
+
+ this.children.concat(this).forEach(function (morph) {
+ if (morph instanceof SpriteMorph || morph instanceof StageMorph) {
+ hats = hats.concat(morph.allHatBlocksFor('__shout__go__'));
+ }
+ });
+ hats.forEach(function (block) {
+ procs.push(myself.threads.startProcess(
+ block,
+ myself.isThreadSafe
+ ));
+ });
+ if (ide) {
+ ide.controlBar.pauseButton.refresh();
+ }
+ return procs;
+};
+
+StageMorph.prototype.fireStopAllEvent = function () {
+ var ide = this.parentThatIsA(IDE_Morph);
+ this.threads.resumeAll(this.stage);
+ this.keysPressed = {};
+ this.threads.stopAll();
+ this.stopAllActiveSounds();
+ this.children.forEach(function (morph) {
+ if (morph.stopTalking) {
+ morph.stopTalking();
+ }
+ });
+ this.removeAllClones();
+ if (ide) {
+ ide.controlBar.pauseButton.refresh();
+ }
+};
+
+StageMorph.prototype.removeAllClones = function () {
+ var myself = this,
+ clones = this.children.filter(
+ function (morph) {return morph.isClone; }
+ );
+ clones.forEach(function (clone) {
+ myself.threads.stopAllForReceiver(clone);
+ clone.destroy();
+ });
+ this.cloneCount = 0;
+};
+
+// StageMorph block templates
+
+StageMorph.prototype.blockTemplates = function (category) {
+ var blocks = [], myself = this, varNames, button,
+ cat = category || 'motion', txt;
+
+ function block(selector) {
+ var newBlock = SpriteMorph.prototype.blockForSelector(selector, true);
+ newBlock.isTemplate = true;
+ return newBlock;
+ }
+
+ function variableBlock(varName) {
+ var newBlock = SpriteMorph.prototype.variableBlock(varName);
+ newBlock.isDraggable = false;
+ newBlock.isTemplate = true;
+ return newBlock;
+ }
+
+ function watcherToggle(selector) {
+ var info = SpriteMorph.prototype.blocks[selector];
+ return new ToggleMorph(
+ 'checkbox',
+ this,
+ function () {
+ myself.toggleWatcher(
+ selector,
+ localize(info.spec),
+ myself.blockColor[info.category]
+ );
+ },
+ null,
+ function () {
+ return myself.showingWatcher(selector);
+ },
+ null
+ );
+ }
+
+ function variableWatcherToggle(varName) {
+ return new ToggleMorph(
+ 'checkbox',
+ this,
+ function () {
+ myself.toggleVariableWatcher(varName);
+ },
+ null,
+ function () {
+ return myself.showingVariableWatcher(varName);
+ },
+ null
+ );
+ }
+
+ if (cat === 'motion') {
+
+ txt = new TextMorph(localize(
+ 'Stage selected:\nno motion primitives'
+ ));
+ txt.fontSize = 9;
+ txt.setColor(new Color(230, 230, 230));
+ blocks.push(txt);
+
+ } else if (cat === 'looks') {
+
+ blocks.push(block('doSwitchToCostume'));
+ blocks.push(block('doWearNextCostume'));
+ blocks.push(watcherToggle('getCostumeIdx'));
+ blocks.push(block('getCostumeIdx'));
+ blocks.push('-');
+ blocks.push(block('changeEffect'));
+ blocks.push(block('setEffect'));
+ blocks.push(block('clearEffects'));
+
+ // for debugging: ///////////////
+
+ if (this.world().isDevMode) {
+ blocks.push('-');
+ txt = new TextMorph(localize(
+ 'development mode \ndebugging primitives:'
+ ));
+ txt.fontSize = 9;
+ txt.setColor(new Color(230, 230, 230));
+ blocks.push(txt);
+ blocks.push('-');
+ blocks.push(block('log'));
+ blocks.push(block('alert'));
+ }
+
+ /////////////////////////////////
+
+ } else if (cat === 'sound') {
+
+ blocks.push(block('playSound'));
+ blocks.push(block('doPlaySoundUntilDone'));
+ blocks.push(block('doStopAllSounds'));
+ blocks.push('-');
+ blocks.push(block('doRest'));
+ blocks.push('-');
+ blocks.push(block('doPlayNote'));
+ blocks.push('-');
+ blocks.push(block('doChangeTempo'));
+ blocks.push(block('doSetTempo'));
+ blocks.push(watcherToggle('getTempo'));
+ blocks.push(block('getTempo'));
+
+ } else if (cat === 'pen') {
+
+ blocks.push(block('clear'));
+
+ } else if (cat === 'control') {
+
+ blocks.push(block('receiveGo'));
+ blocks.push(block('receiveKey'));
+ blocks.push(block('receiveClick'));
+ blocks.push(block('receiveMessage'));
+ blocks.push('-');
+ blocks.push(block('doBroadcast'));
+ blocks.push(block('doBroadcastAndWait'));
+ blocks.push('-');
+ blocks.push(block('doWarp'));
+ blocks.push('-');
+ blocks.push(block('doWait'));
+ blocks.push(block('doWaitUntil'));
+ blocks.push('-');
+ blocks.push(block('doForever'));
+ blocks.push(block('doRepeat'));
+ blocks.push(block('doUntil'));
+ blocks.push('-');
+ blocks.push(block('doIf'));
+ blocks.push(block('doIfElse'));
+ blocks.push('-');
+ blocks.push(block('doReport'));
+ blocks.push('-');
+ blocks.push(block('doStopBlock'));
+ blocks.push(block('doStop'));
+ blocks.push(block('doStopAll'));
+ blocks.push('-');
+ blocks.push(block('doRun'));
+ blocks.push(block('fork'));
+ blocks.push(block('evaluate'));
+ blocks.push('-');
+ /*
+ // list variants commented out for now (redundant)
+ blocks.push(block('doRunWithInputList'));
+ blocks.push(block('forkWithInputList'));
+ blocks.push(block('evaluateWithInputList'));
+ blocks.push('-');
+ */
+ blocks.push(block('doCallCC'));
+ blocks.push(block('reportCallCC'));
+ blocks.push('-');
+ blocks.push(block('createClone'));
+
+ } else if (cat === 'sensing') {
+
+ blocks.push(block('doAsk'));
+ blocks.push(watcherToggle('getLastAnswer'));
+ blocks.push(block('reportLastAnswer'));
+ blocks.push('-');
+ blocks.push(block('reportMouseX'));
+ blocks.push(block('reportMouseY'));
+ blocks.push(block('reportMouseDown'));
+ blocks.push('-');
+ blocks.push(block('reportKeyPressed'));
+ blocks.push('-');
+ blocks.push(block('doResetTimer'));
+ blocks.push(watcherToggle('getTimer'));
+ blocks.push(block('reportTimer'));
+ blocks.push('-');
+ blocks.push(block('reportAttributeOf'));
+ blocks.push('-');
+ blocks.push(block('reportURL'));
+ blocks.push('-');
+ blocks.push(block('reportIsFastTracking'));
+ blocks.push(block('doSetFastTracking'));
+
+ // for debugging: ///////////////
+
+ if (this.world().isDevMode) {
+
+ blocks.push('-');
+ txt = new TextMorph(localize(
+ 'development mode \ndebugging primitives:'
+ ));
+ txt.fontSize = 9;
+ txt.setColor(new Color(230, 230, 230));
+ blocks.push(txt);
+ blocks.push('-');
+ blocks.push(block('colorFiltered'));
+ blocks.push(block('reportStackSize'));
+ blocks.push(block('reportFrameCount'));
+ }
+
+ /////////////////////////////////
+
+ } else if (cat === 'operators') {
+
+ blocks.push(block('reifyScript'));
+ blocks.push(block('reifyReporter'));
+ blocks.push(block('reifyPredicate'));
+ blocks.push('#');
+ blocks.push('-');
+ blocks.push(block('reportSum'));
+ blocks.push(block('reportDifference'));
+ blocks.push(block('reportProduct'));
+ blocks.push(block('reportQuotient'));
+ blocks.push('-');
+ blocks.push(block('reportModulus'));
+ blocks.push(block('reportRound'));
+ blocks.push(block('reportMonadic'));
+ blocks.push(block('reportRandom'));
+ blocks.push('-');
+ blocks.push(block('reportLessThan'));
+ blocks.push(block('reportEquals'));
+ blocks.push(block('reportGreaterThan'));
+ blocks.push('-');
+ blocks.push(block('reportAnd'));
+ blocks.push(block('reportOr'));
+ blocks.push(block('reportNot'));
+ blocks.push('-');
+ blocks.push(block('reportTrue'));
+ blocks.push(block('reportFalse'));
+ blocks.push('-');
+ blocks.push(block('reportJoinWords'));
+ blocks.push(block('reportLetter'));
+ blocks.push(block('reportStringSize'));
+ blocks.push('-');
+ blocks.push(block('reportUnicode'));
+ blocks.push(block('reportUnicodeAsLetter'));
+ blocks.push('-');
+ blocks.push(block('reportIsA'));
+ blocks.push(block('reportIsIdentical'));
+
+ // for debugging: ///////////////
+
+ if (this.world().isDevMode) {
+ blocks.push('-');
+ txt = new TextMorph(
+ 'development mode \ndebugging primitives:'
+ );
+ txt.fontSize = 9;
+ txt.setColor(new Color(230, 230, 230));
+ blocks.push(txt);
+ blocks.push('-');
+ blocks.push(block('reportTypeOf'));
+ }
+
+ //////////////////////////////////
+
+ } else if (cat === 'variables') {
+
+ button = new PushButtonMorph(
+ null,
+ function () {
+ new VariableDialogMorph(
+ null,
+ function (pair) {
+ if (pair) {
+ myself.addVariable(pair[0], pair[1]);
+ myself.toggleVariableWatcher(pair[0], pair[1]);
+ myself.blocksCache[cat] = null;
+ myself.paletteCache[cat] = null;
+ myself.parentThatIsA(IDE_Morph).refreshPalette();
+ }
+ },
+ myself
+ ).prompt(
+ 'Variable name',
+ null,
+ myself.world()
+ );
+ },
+ 'Make a variable'
+ );
+ blocks.push(button);
+
+ if (this.variables.allNames().length > 0) {
+ button = new PushButtonMorph(
+ null,
+ function () {
+ var menu = new MenuMorph(
+ myself.deleteVariable,
+ null,
+ myself
+ );
+ myself.variables.allNames().forEach(function (name) {
+ menu.addItem(name, name);
+ });
+ menu.popUpAtHand(myself.world());
+ },
+ 'Delete a variable'
+ );
+ blocks.push(button);
+ }
+
+ blocks.push('-');
+
+ varNames = this.variables.allNames();
+ if (varNames.length > 0) {
+ varNames.forEach(function (name) {
+ blocks.push(variableWatcherToggle(name));
+ blocks.push(variableBlock(name));
+ });
+ blocks.push('-');
+ }
+
+ blocks.push(block('doSetVar'));
+ blocks.push(block('doChangeVar'));
+ blocks.push(block('doShowVar'));
+ blocks.push(block('doHideVar'));
+ blocks.push(block('doDeclareVariables'));
+
+ blocks.push('=');
+
+ blocks.push(block('reportNewList'));
+ blocks.push('-');
+ blocks.push(block('reportCONS'));
+ blocks.push(block('reportListItem'));
+ blocks.push(block('reportCDR'));
+ blocks.push('-');
+ blocks.push(block('reportListLength'));
+ blocks.push(block('reportListContainsItem'));
+ blocks.push('-');
+ blocks.push(block('doAddToList'));
+ blocks.push(block('doDeleteFromList'));
+ blocks.push(block('doInsertInList'));
+ blocks.push(block('doReplaceInList'));
+
+ blocks.push('=');
+
+ button = new PushButtonMorph(
+ null,
+ function () {
+ var ide = myself.parentThatIsA(IDE_Morph);
+ new BlockDialogMorph(
+ null,
+ function (definition) {
+ if (definition.spec !== '') {
+ if (definition.isGlobal) {
+ myself.globalBlocks.push(definition);
+ } else {
+ myself.customBlocks.push(definition);
+ }
+ ide.flushPaletteCache();
+ ide.refreshPalette();
+ new BlockEditorMorph(definition, myself).popUp();
+ }
+ },
+ myself
+ ).prompt(
+ 'Make a block',
+ null,
+ myself.world()
+ );
+ },
+ 'Make a block'
+ );
+ blocks.push(button);
+ }
+ return blocks;
+};
+
+// StageMorph primitives
+
+StageMorph.prototype.clear = function () {
+ this.clearPenTrails();
+};
+
+// StageMorph user menu
+
+StageMorph.prototype.userMenu = function () {
+ var ide = this.parentThatIsA(IDE_Morph),
+ menu = new MenuMorph(this);
+
+ if (ide && ide.isAppMode) {
+ menu.addItem('help', 'nop');
+ return menu;
+ }
+ menu.addItem("edit", 'edit');
+ menu.addItem("show all", 'showAll');
+ return menu;
+};
+
+StageMorph.prototype.showAll = function () {
+ var myself = this;
+ this.children.forEach(function (m) {
+ m.show();
+ m.keepWithin(myself);
+ if (m.fixLayout) {m.fixLayout(); }
+ });
+};
+
+StageMorph.prototype.edit = SpriteMorph.prototype.edit;
+
+// StageMorph thumbnail
+
+StageMorph.prototype.thumbnail = function (extentPoint, excludedSprite) {
+/*
+ answer a new Canvas of extentPoint dimensions containing
+ my thumbnail representation keeping the originial aspect ratio
+*/
+ var myself = this,
+ src = this.image,
+ scale = Math.min(
+ (extentPoint.x / src.width),
+ (extentPoint.y / src.height)
+ ),
+ trg = newCanvas(extentPoint),
+ ctx = trg.getContext('2d'),
+ fb;
+
+ ctx.scale(scale, scale);
+ ctx.drawImage(
+ src,
+ 0,
+ 0
+ );
+ ctx.drawImage(
+ this.penTrails(),
+ 0,
+ 0,
+ this.dimensions.x * this.scale,
+ this.dimensions.y * this.scale
+ );
+ this.children.forEach(function (morph) {
+ if (morph !== excludedSprite) {
+ fb = morph.fullBounds();
+ ctx.drawImage(
+ morph.fullImage(),
+ fb.origin.x - myself.bounds.origin.x,
+ fb.origin.y - myself.bounds.origin.y
+ );
+ }
+ });
+ return trg;
+};
+// StageMorph cloning overrice
+
+StageMorph.prototype.createClone = nop;
+
+// StageMorph pseudo-inherited behavior
+
+StageMorph.prototype.categories = SpriteMorph.prototype.categories;
+StageMorph.prototype.blockColor = SpriteMorph.prototype.blockColor;
+StageMorph.prototype.paletteColor = SpriteMorph.prototype.paletteColor;
+StageMorph.prototype.setName = SpriteMorph.prototype.setName;
+StageMorph.prototype.palette = SpriteMorph.prototype.palette;
+StageMorph.prototype.freshPalette = SpriteMorph.prototype.freshPalette;
+StageMorph.prototype.showingWatcher = SpriteMorph.prototype.showingWatcher;
+StageMorph.prototype.addVariable = SpriteMorph.prototype.addVariable;
+StageMorph.prototype.deleteVariable = SpriteMorph.prototype.deleteVariable;
+
+// StageMorph block rendering
+
+StageMorph.prototype.blockForSelector
+ = SpriteMorph.prototype.blockForSelector;
+
+// StageMorph variable watchers (for palette checkbox toggling)
+
+StageMorph.prototype.findVariableWatcher
+ = SpriteMorph.prototype.findVariableWatcher;
+
+StageMorph.prototype.toggleVariableWatcher
+ = SpriteMorph.prototype.toggleVariableWatcher;
+
+StageMorph.prototype.showingVariableWatcher
+ = SpriteMorph.prototype.showingVariableWatcher;
+
+StageMorph.prototype.deleteVariableWatcher
+ = SpriteMorph.prototype.deleteVariableWatcher;
+
+// StageMorph background management
+
+StageMorph.prototype.addCostume
+ = SpriteMorph.prototype.addCostume;
+
+StageMorph.prototype.wearCostume
+ = SpriteMorph.prototype.wearCostume;
+
+StageMorph.prototype.getCostumeIdx
+ = SpriteMorph.prototype.getCostumeIdx;
+
+StageMorph.prototype.doWearNextCostume
+ = SpriteMorph.prototype.doWearNextCostume;
+
+StageMorph.prototype.doWearPreviousCostume
+ = SpriteMorph.prototype.doWearPreviousCostume;
+
+StageMorph.prototype.doSwitchToCostume
+ = SpriteMorph.prototype.doSwitchToCostume;
+
+// StageMorph graphic effects
+
+StageMorph.prototype.setEffect
+ = SpriteMorph.prototype.setEffect;
+
+StageMorph.prototype.getGhostEffect
+ = SpriteMorph.prototype.getGhostEffect;
+
+StageMorph.prototype.changeEffect
+ = SpriteMorph.prototype.changeEffect;
+
+StageMorph.prototype.clearEffects
+ = SpriteMorph.prototype.clearEffects;
+
+// StageMorph sound management
+
+StageMorph.prototype.addSound
+ = SpriteMorph.prototype.addSound;
+
+StageMorph.prototype.playSound
+ = SpriteMorph.prototype.playSound;
+
+StageMorph.prototype.stopAllActiveSounds = function () {
+ this.activeSounds.forEach(function (audio) {
+ audio.pause();
+ });
+ this.activeSounds = [];
+};
+
+StageMorph.prototype.pauseAllActiveSounds = function () {
+ this.activeSounds.forEach(function (audio) {
+ audio.pause();
+ });
+};
+
+StageMorph.prototype.resumeAllActiveSounds = function () {
+ this.activeSounds.forEach(function (audio) {
+ audio.play();
+ });
+};
+
+// StageMorph non-variable watchers
+
+StageMorph.prototype.toggleWatcher
+ = SpriteMorph.prototype.toggleWatcher;
+
+StageMorph.prototype.showingWatcher
+ = SpriteMorph.prototype.showingWatcher;
+
+StageMorph.prototype.watcherFor =
+ SpriteMorph.prototype.watcherFor;
+
+StageMorph.prototype.getLastAnswer
+ = SpriteMorph.prototype.getLastAnswer;
+
+// StageMorph message broadcasting
+
+StageMorph.prototype.allMessageNames
+ = SpriteMorph.prototype.allMessageNames;
+
+StageMorph.prototype.allHatBlocksFor
+ = SpriteMorph.prototype.allHatBlocksFor;
+
+StageMorph.prototype.allHatBlocksForKey
+ = SpriteMorph.prototype.allHatBlocksForKey;
+
+// StageMorph events
+
+StageMorph.prototype.mouseClickLeft
+ = SpriteMorph.prototype.mouseClickLeft;
+
+// StageMorph custom blocks
+
+StageMorph.prototype.deleteAllBlockInstances
+ = SpriteMorph.prototype.deleteAllBlockInstances;
+
+StageMorph.prototype.allBlockInstances
+ = SpriteMorph.prototype.allBlockInstances;
+
+StageMorph.prototype.allLocalBlockInstances
+ = SpriteMorph.prototype.allLocalBlockInstances;
+
+StageMorph.prototype.allEditorBlockInstances
+ = SpriteMorph.prototype.allEditorBlockInstances;
+
+StageMorph.prototype.paletteBlockInstance
+ = SpriteMorph.prototype.paletteBlockInstance;
+
+StageMorph.prototype.usesBlockInstance
+ = SpriteMorph.prototype.usesBlockInstance;
+
+StageMorph.prototype.doubleDefinitionsFor
+ = SpriteMorph.prototype.doubleDefinitionsFor;
+
+StageMorph.prototype.replaceDoubleDefinitionsFor
+ = SpriteMorph.prototype.replaceDoubleDefinitionsFor;
+
+// SpriteBubbleMorph ////////////////////////////////////////////////////////
+
+/*
+ I am a sprite's scaleable speech bubble. I rely on SpriteMorph
+ for my preferences settings
+*/
+
+// SpriteBubbleMorph inherits from SpeechBubbleMorph:
+
+SpriteBubbleMorph.prototype = new SpeechBubbleMorph();
+SpriteBubbleMorph.prototype.constructor = SpriteBubbleMorph;
+SpriteBubbleMorph.uber = SpeechBubbleMorph.prototype;
+
+// SpriteBubbleMorph instance creation:
+
+function SpriteBubbleMorph(data, scale, isThought, isQuestion) {
+ this.init(data, scale, isThought, isQuestion);
+}
+
+SpriteBubbleMorph.prototype.init = function (
+ data,
+ scale,
+ isThought,
+ isQuestion
+) {
+ var sprite = SpriteMorph.prototype;
+ this.scale = scale || 1;
+ this.data = data;
+ this.isQuestion = isQuestion;
+
+ SpriteBubbleMorph.uber.init.call(
+ this,
+ this.dataAsMorph(data),
+ sprite.bubbleColor,
+ null,
+ null,
+ isQuestion ? sprite.blockColor.sensing : sprite.bubbleBorderColor,
+ null,
+ isThought
+ );
+};
+
+// SpriteBubbleMorph contents formatting
+
+SpriteBubbleMorph.prototype.dataAsMorph = function (data) {
+ var contents,
+ sprite = SpriteMorph.prototype,
+ isText,
+ img,
+ scaledImg,
+ width;
+
+ if (data instanceof Morph) {
+ contents = data;
+ } else if (isString(data)) {
+ isText = true;
+ contents = new TextMorph(
+ data,
+ sprite.bubbleFontSize * this.scale,
+ null, // fontStyle
+ sprite.bubbleFontIsBold,
+ false, // italic
+ 'center'
+ );
+ } else if (typeof data === 'boolean') {
+ img = sprite.booleanMorph(data).fullImage();
+ contents = new Morph();
+ contents.silentSetWidth(img.width);
+ contents.silentSetHeight(img.height);
+ contents.image = img;
+ } else if (data instanceof HTMLCanvasElement) {
+ contents = new Morph();
+ contents.silentSetWidth(data.width);
+ contents.silentSetHeight(data.height);
+ contents.image = data;
+ } else if (data instanceof List) {
+ contents = new ListWatcherMorph(data);
+ contents.isDraggable = false;
+ contents.update(true);
+ contents.step = contents.update;
+ } else if (data instanceof Context) {
+ img = data.image();
+ contents = new Morph();
+ contents.silentSetWidth(img.width);
+ contents.silentSetHeight(img.height);
+ contents.image = img;
+ } else {
+ contents = new TextMorph(
+ data.toString(),
+ sprite.bubbleFontSize * this.scale,
+ null, // fontStyle
+ sprite.bubbleFontIsBold,
+ false, // italic
+ 'center'
+ );
+ }
+ if (contents instanceof TextMorph) {
+ // reflow text boundaries
+ width = Math.max(
+ contents.width(),
+ sprite.bubbleCorner * 2 * this.scale
+ );
+ if (isText) {
+ width = Math.min(width, sprite.bubbleMaxTextWidth * this.scale);
+ }
+ contents.setWidth(width);
+ } else if (!(data instanceof List)) {
+ // scale contents image
+ scaledImg = newCanvas(contents.extent().multiplyBy(this.scale));
+ scaledImg.getContext('2d').drawImage(
+ contents.image,
+ 0,
+ 0,
+ scaledImg.width,
+ scaledImg.height
+ );
+ contents.image = scaledImg;
+ contents.bounds = contents.bounds.scaleBy(this.scale);
+ }
+ return contents;
+};
+
+// SpriteBubbleMorph scaling
+
+SpriteBubbleMorph.prototype.setScale = function (scale) {
+ this.scale = scale;
+ this.changed();
+ this.drawNew();
+ this.changed();
+};
+
+// SpriteBubbleMorph drawing:
+
+SpriteBubbleMorph.prototype.drawNew = function () {
+ var sprite = SpriteMorph.prototype;
+
+ // scale my settings
+ this.edge = sprite.bubbleCorner * this.scale;
+ this.border = sprite.bubbleBorder * this.scale;
+ this.padding = sprite.bubbleCorner / 2 * this.scale;
+
+ // re-build my contents
+ if (this.contentsMorph) {
+ this.contentsMorph.destroy();
+ }
+ this.contentsMorph = this.dataAsMorph(this.data);
+ this.add(this.contentsMorph);
+
+ // adjust my layout
+ this.silentSetWidth(this.contentsMorph.width()
+ + (this.padding ? this.padding * 2 : this.edge * 2));
+ this.silentSetHeight(this.contentsMorph.height()
+ + this.edge
+ + this.border * 2
+ + this.padding * 2
+ + 2);
+
+ // draw my outline
+ SpeechBubbleMorph.uber.drawNew.call(this);
+
+ // position my contents
+ this.contentsMorph.setPosition(this.position().add(
+ new Point(
+ this.padding || this.edge,
+ this.border + this.padding + 1
+ )
+ ));
+};
+
+// SpriteBubbleMorph resizing:
+
+SpriteBubbleMorph.prototype.fixLayout = function () {
+ // to be used when resizing list watchers
+ // otherwise use drawNew() to force re-layout
+
+ var sprite = SpriteMorph.prototype;
+
+ this.changed();
+ // scale my settings
+ this.edge = sprite.bubbleCorner * this.scale;
+ this.border = sprite.bubbleBorder * this.scale;
+ this.padding = sprite.bubbleCorner / 2 * this.scale;
+
+ // adjust my layout
+ this.silentSetWidth(this.contentsMorph.width()
+ + (this.padding ? this.padding * 2 : this.edge * 2));
+ this.silentSetHeight(this.contentsMorph.height()
+ + this.edge
+ + this.border * 2
+ + this.padding * 2
+ + 2);
+
+ // draw my outline
+ SpeechBubbleMorph.uber.drawNew.call(this);
+
+ // position my contents
+ this.contentsMorph.setPosition(this.position().add(
+ new Point(
+ this.padding || this.edge,
+ this.border + this.padding + 1
+ )
+ ));
+ this.changed();
+};
+
+// Costume /////////////////////////////////////////////////////////////
+
+/*
+ I am a picture that's "wearable" by a sprite. My rotationCenter is
+ relative to my contents position.
+*/
+
+// Costume instance creation
+
+function Costume(canvas, name, rotationCenter) {
+ this.contents = canvas || newCanvas();
+ this.shrinkToFit(this.maxExtent);
+ this.name = name || null;
+ this.rotationCenter = rotationCenter || this.center();
+ this.version = Date.now(); // for observer optimization
+ this.loaded = null; // for de-serialization only
+}
+
+Costume.prototype.maxExtent = StageMorph.prototype.dimensions;
+
+Costume.prototype.toString = function () {
+ return 'a Costume(' + this.name + ')';
+};
+
+// Costume dimensions - all relative
+
+Costume.prototype.extent = function () {
+ return new Point(this.contents.width, this.contents.height);
+};
+
+Costume.prototype.center = function () {
+ return this.extent().divideBy(2);
+};
+
+Costume.prototype.width = function () {
+ return this.contents.width;
+};
+
+Costume.prototype.height = function () {
+ return this.contents.height;
+};
+
+Costume.prototype.bounds = function () {
+ return new Rectangle(0, 0, this.width(), this.height());
+};
+
+// Costume duplication
+
+Costume.prototype.copy = function () {
+ var canvas = newCanvas(this.extent()),
+ cpy,
+ ctx;
+
+ ctx = canvas.getContext('2d');
+ ctx.drawImage(this.contents, 0, 0);
+ cpy = new Costume(canvas, this.name ? copy(this.name) : null);
+ cpy.rotationCenter = this.rotationCenter.copy();
+ return cpy;
+};
+
+// Costume flipping
+
+Costume.prototype.flipped = function () {
+/*
+ answer a copy of myself flipped horizontally
+ (mirrored along a vertical axis), used for
+ SpriteMorph's rotation style type 2
+*/
+ var canvas = newCanvas(this.extent()),
+ ctx = canvas.getContext('2d'),
+ flipped;
+
+ ctx.translate(this.width(), 0);
+ ctx.scale(-1, 1);
+ ctx.drawImage(this.contents, 0, 0);
+ flipped = new Costume(
+ canvas,
+ new Point(
+ this.width() - this.rotationCenter.x,
+ this.rotationCenter.y
+ )
+ );
+ return flipped;
+};
+
+// Costume actions
+
+Costume.prototype.edit = function (aWorld) {
+ var editor = new CostumeEditorMorph(this),
+ action,
+ dialog,
+ txt;
+
+ action = function () {editor.accept(); };
+ dialog = new DialogBoxMorph(this, action);
+ txt = new TextMorph(
+ localize('click or drag crosshairs to move the rotation center'),
+ dialog.fontSize,
+ dialog.fontStyle,
+ true,
+ false,
+ 'center',
+ null,
+ null,
+ new Point(1, 1),
+ new Color(255, 255, 255)
+ );
+
+ dialog.labelString = 'Costume Editor';
+ dialog.createLabel();
+ dialog.setPicture(editor);
+ dialog.addBody(txt);
+ dialog.addButton('ok', 'Ok');
+ dialog.addButton('cancel', 'Cancel');
+ dialog.fixLayout();
+ dialog.drawNew();
+ dialog.fixLayout();
+ if (aWorld) {
+ aWorld.add(dialog);
+ aWorld.keyboardReceiver = dialog;
+ dialog.setCenter(aWorld.center());
+ }
+};
+
+// Costume thumbnail
+
+Costume.prototype.shrinkToFit = function (extentPoint) {
+ if (extentPoint.x < this.width() || (extentPoint.y < this.height())) {
+ this.contents = this.thumbnail(extentPoint);
+ }
+};
+
+Costume.prototype.thumbnail = function (extentPoint) {
+/*
+ answer a new Canvas of extentPoint dimensions containing
+ my thumbnail representation keeping the originial aspect ratio
+*/
+ var src = this.contents, // at this time sprites aren't composite morphs
+ scale = Math.min(
+ (extentPoint.x / src.width),
+ (extentPoint.y / src.height)
+ ),
+ xOffset = (extentPoint.x - (src.width * scale)) / 2,
+ yOffset = (extentPoint.y - (src.height * scale)) / 2,
+ trg = newCanvas(extentPoint),
+ ctx = trg.getContext('2d');
+
+ ctx.scale(scale, scale);
+ ctx.drawImage(
+ src,
+ Math.floor(xOffset / scale),
+ Math.floor(yOffset / scale)
+ );
+ return trg;
+};
+
+// SVG_Costume /////////////////////////////////////////////////////////////
+
+/*
+ I am a costume containing an SVG image.
+*/
+
+// SVG_Costume inherits from Costume:
+
+SVG_Costume.prototype = new Costume();
+SVG_Costume.prototype.constructor = SVG_Costume;
+SVG_Costume.uber = Costume.prototype;
+
+// SVG_Costume instance creation
+
+function SVG_Costume(svgImage, name, rotationCenter) {
+ this.contents = svgImage;
+ this.shrinkToFit(this.maxExtent);
+ this.name = name || null;
+ this.rotationCenter = rotationCenter || this.center();
+ this.version = Date.now(); // for observer optimization
+ this.loaded = null; // for de-serialization only
+}
+
+SVG_Costume.prototype.toString = function () {
+ return 'an SVG_Costume(' + this.name + ')';
+};
+
+// SVG_Costume duplication
+
+SVG_Costume.prototype.copy = function () {
+ var img = new Image(),
+ cpy;
+ img.src = this.contents.src;
+ cpy = new SVG_Costume(img, this.name ? copy(this.name) : null);
+ cpy.rotationCenter = this.rotationCenter.copy();
+ return cpy;
+};
+
+// SVG_Costume flipping
+
+/*
+ flipping is currently inherited from Costume, which rasterizes it.
+ Therefore flipped SVG costumes may appear pixelated until we add
+ a method to either truly flip SVGs or change the Sprite's drawNew()
+ method to scale the costume before flipping it
+*/
+
+// SVG_Costume thumbnail
+
+SVG_Costume.prototype.shrinkToFit = function (extentPoint) {
+ // overridden for unrasterized SVGs
+ nop(extentPoint);
+ return;
+};
+
+// CostumeEditorMorph ////////////////////////////////////////////////////////
+
+// CostumeEditorMorph inherits from Morph:
+
+CostumeEditorMorph.prototype = new Morph();
+CostumeEditorMorph.prototype.constructor = CostumeEditorMorph;
+CostumeEditorMorph.uber = Morph.prototype;
+
+// CostumeEditorMorph preferences settings:
+CostumeEditorMorph.prototype.size = Costume.prototype.maxExtent;
+
+// CostumeEditorMorph instance creation
+
+function CostumeEditorMorph(costume) {
+ this.init(costume);
+}
+
+CostumeEditorMorph.prototype.init = function (costume) {
+ this.costume = costume || new Costume();
+ this.rotationCenter = this.costume.rotationCenter.copy();
+ this.margin = new Point(0, 0);
+ CostumeEditorMorph.uber.init.call(this);
+ this.noticesTransparentClick = true;
+};
+
+// CostumeEditorMorph edit ops
+
+CostumeEditorMorph.prototype.accept = function () {
+ this.costume.rotationCenter = this.rotationCenter.copy();
+ this.costume.version = Date.now();
+};
+
+// CostumeEditorMorph displaying
+
+CostumeEditorMorph.prototype.drawNew = function () {
+ var rp, ctx;
+
+ this.margin = this.size.subtract(this.costume.extent()).divideBy(2);
+ rp = this.rotationCenter.add(this.margin);
+
+ this.silentSetExtent(this.size);
+
+ this.image = newCanvas(this.extent());
+
+ // draw the background
+ if (!this.cachedTexture) {
+ this.cachedTexture = this.createTexture();
+
+ }
+ this.drawCachedTexture();
+
+/*
+ pattern = ctx.createPattern(this.background, 'repeat');
+ ctx.fillStyle = pattern;
+ ctx.fillRect(0, 0, this.size.x, this.size.y);
+*/
+
+ ctx = this.image.getContext('2d');
+
+ // draw the costume
+ ctx.drawImage(this.costume.contents, this.margin.x, this.margin.y);
+
+ // draw crosshairs:
+ ctx.globalAlpha = 0.5;
+
+ // circle around center:
+ ctx.fillStyle = 'white';
+ ctx.beginPath();
+ ctx.arc(
+ rp.x,
+ rp.y,
+ 20,
+ radians(0),
+ radians(360),
+ false
+ );
+ ctx.closePath();
+ ctx.fill();
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.arc(
+ rp.x,
+ rp.y,
+ 10,
+ radians(0),
+ radians(360),
+ false
+ );
+ ctx.stroke();
+
+ // horizontal line:
+ ctx.beginPath();
+ ctx.moveTo(0, rp.y);
+ ctx.lineTo(this.costume.width() + this.margin.x * 2, rp.y);
+ ctx.stroke();
+
+ // vertical line:
+ ctx.beginPath();
+ ctx.moveTo(rp.x, 0);
+ ctx.lineTo(rp.x, this.costume.height() + this.margin.y * 2);
+ ctx.stroke();
+};
+
+CostumeEditorMorph.prototype.createTexture = function () {
+ var size = 5,
+ texture = newCanvas(new Point(size * 2, size * 2)),
+ ctx = texture.getContext('2d'),
+ grey = new Color(230, 230, 230);
+
+ ctx.fillStyle = 'white';
+ ctx.fillRect(0, 0, size * 2, size * 2);
+ ctx.fillStyle = grey.toString();
+ ctx.fillRect(0, 0, size, size);
+ ctx.fillRect(size, size, size, size);
+ return texture;
+};
+
+
+// CostumeEditorMorph events
+
+CostumeEditorMorph.prototype.mouseDownLeft = function (pos) {
+ this.rotationCenter = pos.subtract(
+ this.position().add(this.margin)
+ );
+ this.drawNew();
+ this.changed();
+};
+
+CostumeEditorMorph.prototype.mouseMove
+ = CostumeEditorMorph.prototype.mouseDownLeft;
+
+// Sound /////////////////////////////////////////////////////////////
+
+// Sound instance creation
+
+function Sound(audio, name) {
+ this.audio = audio; // mandatory
+ this.name = name || "Sound";
+}
+
+Sound.prototype.play = function () {
+ // return an instance of an audio element which can be terminated
+ // externally (i.e. by the stage)
+ var aud = document.createElement('audio');
+ aud.src = this.audio.src;
+ aud.play();
+ return aud;
+};
+
+Sound.prototype.copy = function () {
+ var snd = document.createElement('audio'),
+ cpy;
+
+ snd.src = this.audio.src;
+ cpy = new Sound(snd, this.name ? copy(this.name) : null);
+ return cpy;
+};
+
+Sound.prototype.toDataURL = function () {
+ return this.audio.src;
+};
+
+// Note /////////////////////////////////////////////////////////
+
+// I am a single musical note
+
+// Note instance creation
+
+function Note(pitch) {
+ this.pitch = pitch === 0 ? 0 : pitch || 69;
+ this.setupContext();
+ this.oscillator = null;
+}
+
+// Note shared properties
+
+Note.prototype.audioContext = null;
+Note.prototype.gainNode = null;
+
+// Note audio context
+
+Note.prototype.setupContext = function () {
+ if (this.audioContext) { return; }
+ var AudioContext = (function () {
+ // cross browser some day?
+ return window.AudioContext ||
+ window.mozAudioContext ||
+ window.msAudioContext ||
+ window.oAudioContext ||
+ window.webkitAudioContext;
+ }());
+ if (!AudioContext) {
+ throw new Error('Web Audio API is not supported\nin this browser');
+ }
+ Note.prototype.audioContext = new AudioContext();
+ Note.prototype.gainNode = Note.prototype.audioContext.createGainNode();
+ Note.prototype.gainNode.gain.value = 0.25; // reduce volume by 1/4
+};
+
+// Note playing
+
+Note.prototype.play = function () {
+ this.oscillator = this.audioContext.createOscillator();
+ this.oscillator.type = 0;
+ this.oscillator.frequency.value =
+ Math.pow(2, (this.pitch - 69) / 12) * 440;
+ this.oscillator.connect(this.gainNode);
+ this.gainNode.connect(this.audioContext.destination);
+ this.oscillator.noteOn(0); // deprecated, renamed to start()
+};
+
+Note.prototype.stop = function () {
+ if (this.oscillator) {
+ this.oscillator.noteOff(0); // deprecated, renamed to stop()
+ this.oscillator = null;
+ }
+};
+
+// CellMorph //////////////////////////////////////////////////////////
+
+/*
+ I am a spreadsheet style cell that can display either a string,
+ a Morph, a Canvas or a toString() representation of anything else.
+ I can be used in variable watchers or list view element cells.
+*/
+
+// CellMorph inherits from BoxMorph:
+
+CellMorph.prototype = new BoxMorph();
+CellMorph.prototype.constructor = CellMorph;
+CellMorph.uber = BoxMorph.prototype;
+
+// CellMorph instance creation:
+
+function CellMorph(contents, color, idx) {
+ this.init(contents, color, idx);
+}
+
+CellMorph.prototype.init = function (contents, color, idx) {
+ this.contents = (contents === 0 ? 0
+ : contents === false ? false
+ : contents || '');
+ this.isEditable = isNil(idx) ? false : true;
+ this.idx = idx || null; // for list watchers
+ CellMorph.uber.init.call(
+ this,
+ SyntaxElementMorph.prototype.corner,
+ 1.000001, // shadow bug in Chrome,
+ new Color(255, 255, 255)
+ );
+ this.color = color || new Color(255, 140, 0);
+ this.isBig = false;
+ this.drawNew();
+};
+
+// CellMorph accessing:
+
+CellMorph.prototype.big = function () {
+ this.isBig = true;
+ this.changed();
+ this.drawNew();
+ this.changed();
+};
+
+CellMorph.prototype.normal = function () {
+ this.isBig = false;
+ this.changed();
+ this.drawNew();
+ this.changed();
+};
+
+// CellMorph layout:
+
+CellMorph.prototype.fixLayout = function () {
+ var listwatcher;
+ this.changed();
+ this.drawNew();
+ this.changed();
+ if (this.parent && this.parent.fixLayout) { // variable watcher
+ this.parent.fixLayout();
+ } else {
+ listwatcher = this.parentThatIsA(ListWatcherMorph);
+ if (listwatcher) {
+ listwatcher.fixLayout();
+ }
+ }
+};
+
+// CellMorph drawing:
+
+CellMorph.prototype.drawNew = function () {
+ var context,
+ txt,
+ img,
+ fontSize = SyntaxElementMorph.prototype.fontSize,
+ isSameList = this.contentsMorph instanceof ListWatcherMorph
+ && (this.contentsMorph.list === this.contents);
+
+ if (this.isBig) {
+ fontSize = fontSize * 1.5;
+ }
+
+ // re-build my contents
+ if (this.contentsMorph && !isSameList) {
+ this.contentsMorph.destroy();
+ }
+
+ if (!isSameList) {
+ if (this.contents instanceof Morph) {
+ this.contentsMorph = this.contents;
+ } else if (isString(this.contents)) {
+ txt = this.contents.length > 500 ?
+ this.contents.slice(0, 500) + '...' : this.contents;
+ this.contentsMorph = new TextMorph(
+ txt,
+ fontSize,
+ null,
+ true,
+ false,
+ 'center'
+ );
+ if (this.isEditable) {
+ this.contentsMorph.isEditable = true;
+ this.contentsMorph.enableSelecting();
+ }
+ this.contentsMorph.setColor(new Color(255, 255, 255));
+ } else if (typeof this.contents === 'boolean') {
+ this.contentsMorph = SpriteMorph.prototype.booleanMorph.call(
+ null,
+ this.contents
+ );
+ } else if (this.contents instanceof HTMLCanvasElement) {
+ this.contentsMorph = new Morph();
+ this.contentsMorph.silentSetWidth(this.contents.width);
+ this.contentsMorph.silentSetHeight(this.contents.height);
+ this.contentsMorph.image = this.contents;
+ } else if (this.contents instanceof Context) {
+ img = this.contents.image();
+ this.contentsMorph = new Morph();
+ this.contentsMorph.silentSetWidth(img.width);
+ this.contentsMorph.silentSetHeight(img.height);
+ this.contentsMorph.image = img;
+ } else if (this.contents instanceof List) {
+ this.contentsMorph = new ListWatcherMorph(this.contents);
+ this.contentsMorph.isDraggable = false;
+ } else {
+ this.contentsMorph = new TextMorph(
+ !isNil(this.contents) ? this.contents.toString() : '',
+ fontSize,
+ null,
+ true,
+ false,
+ 'center'
+ );
+ if (this.isEditable) {
+ this.contentsMorph.isEditable = true;
+ this.contentsMorph.enableSelecting();
+ }
+ this.contentsMorph.setColor(new Color(255, 255, 255));
+ }
+ this.add(this.contentsMorph);
+ }
+
+ // adjust my layout
+ this.silentSetHeight(this.contentsMorph.height()
+ + this.edge
+ + this.border * 2);
+ this.silentSetWidth(Math.max(
+ this.contentsMorph.width() + this.edge * 2,
+ (this.contents instanceof Context ||
+ this.contents instanceof List ? 0 : this.height() * 2)
+ ));
+
+ // draw my outline
+ this.image = newCanvas(this.extent());
+ context = this.image.getContext('2d');
+ if ((this.edge === 0) && (this.border === 0)) {
+ BoxMorph.uber.drawNew.call(this);
+ return null;
+ }
+ context.fillStyle = this.color.toString();
+ context.beginPath();
+ this.outlinePath(
+ context,
+ Math.max(this.edge - this.border, 0),
+ this.border
+ );
+ context.closePath();
+ context.fill();
+ if (this.border > 0) {
+ context.lineWidth = this.border;
+ context.strokeStyle = this.borderColor.toString();
+ context.beginPath();
+ this.outlinePath(context, this.edge, this.border / 2);
+ context.closePath();
+ context.stroke();
+
+ context.shadowOffsetX = this.border;
+ context.shadowOffsetY = this.border;
+ context.shadowBlur = this.border;
+ context.shadowColor = this.color.darker(80).toString();
+ this.drawShadow(context, this.edge, this.border / 2);
+ }
+
+ // position my contents
+ if (!isSameList) {
+ this.contentsMorph.setCenter(this.center());
+ }
+};
+
+CellMorph.prototype.drawShadow = function (context, radius, inset) {
+ var offset = radius + inset,
+ w = this.width(),
+ h = this.height();
+
+ // bottom left:
+ context.beginPath();
+ context.moveTo(0, h - offset);
+ context.lineTo(0, offset);
+ context.stroke();
+
+ // top left:
+ context.beginPath();
+ context.arc(
+ offset,
+ offset,
+ radius,
+ radians(-180),
+ radians(-90),
+ false
+ );
+ context.stroke();
+
+ // top right:
+ context.beginPath();
+ context.moveTo(offset, 0);
+ context.lineTo(w - offset, 0);
+ context.stroke();
+};
+
+// CellMorph editing (inside list watchers):
+
+CellMorph.prototype.layoutChanged = function () {
+ var context,
+ fontSize = SyntaxElementMorph.prototype.fontSize,
+ listWatcher = this.parentThatIsA(ListWatcherMorph);
+
+ if (this.isBig) {
+ fontSize = fontSize * 1.5;
+ }
+
+ // adjust my layout
+ this.silentSetHeight(this.contentsMorph.height()
+ + this.edge
+ + this.border * 2);
+ this.silentSetWidth(Math.max(
+ this.contentsMorph.width() + this.edge * 2,
+ (this.contents instanceof Context ||
+ this.contents instanceof List ? 0 : this.height() * 2)
+ ));
+
+
+ // draw my outline
+ this.image = newCanvas(this.extent());
+ context = this.image.getContext('2d');
+ if ((this.edge === 0) && (this.border === 0)) {
+ BoxMorph.uber.drawNew.call(this);
+ return null;
+ }
+ context.fillStyle = this.color.toString();
+ context.beginPath();
+ this.outlinePath(
+ context,
+ Math.max(this.edge - this.border, 0),
+ this.border
+ );
+ context.closePath();
+ context.fill();
+ if (this.border > 0) {
+ context.lineWidth = this.border;
+ context.strokeStyle = this.borderColor.toString();
+ context.beginPath();
+ this.outlinePath(context, this.edge, this.border / 2);
+ context.closePath();
+ context.stroke();
+
+ context.shadowOffsetX = this.border;
+ context.shadowOffsetY = this.border;
+ context.shadowBlur = this.border;
+ context.shadowColor = this.color.darker(80).toString();
+ this.drawShadow(context, this.edge, this.border / 2);
+ }
+
+ // position my contents
+ this.contentsMorph.setCenter(this.center());
+
+ if (listWatcher) {
+ listWatcher.fixLayout();
+ }
+};
+
+CellMorph.prototype.reactToEdit = function (textMorph) {
+ var listWatcher;
+ if (!isNil(this.idx)) {
+ listWatcher = this.parentThatIsA(ListWatcherMorph);
+ if (listWatcher) {
+ listWatcher.list.put(textMorph.text, this.idx);
+ }
+ }
+};
+
+CellMorph.prototype.mouseClickLeft = function (pos) {
+ if (this.isEditable && this.contentsMorph instanceof TextMorph) {
+ this.contentsMorph.selectAllAndEdit();
+ } else {
+ this.escalateEvent('mouseClickLeft', pos);
+ }
+};
+
+// WatcherMorph //////////////////////////////////////////////////////////
+
+/*
+ I am a little window which observes some value and continuously
+ updates itself accordingly.
+
+ My target can be either a SpriteMorph or a VariableFrame.
+*/
+
+// WatcherMorph inherits from BoxMorph:
+
+WatcherMorph.prototype = new BoxMorph();
+WatcherMorph.prototype.constructor = WatcherMorph;
+WatcherMorph.uber = BoxMorph.prototype;
+
+// WatcherMorph instance creation:
+
+function WatcherMorph(label, color, target, getter, isHidden) {
+ this.init(label, color, target, getter, isHidden);
+}
+
+WatcherMorph.prototype.init = function (
+ label,
+ color,
+ target,
+ getter,
+ isHidden
+) {
+ // additional properties
+ this.labelText = label || '';
+ this.version = null;
+ this.objName = '';
+
+ // initialize inherited properties
+ WatcherMorph.uber.init.call(
+ this,
+ SyntaxElementMorph.prototype.rounding,
+ 1.000001, // shadow bug in Chrome,
+ new Color(120, 120, 120)
+ );
+
+ // override inherited behavior
+ this.color = new Color(220, 220, 220);
+ this.readoutColor = color;
+ this.style = 'normal';
+ this.target = target || null; // target obj (Sprite) or VariableFrame
+ this.getter = getter || null; // callback or variable name (string)
+ this.currentValue = null;
+ this.labelMorph = null;
+ this.sliderMorph = null;
+ this.cellMorph = null;
+ this.isDraggable = true;
+ this.fixLayout();
+ this.update();
+ if (isHidden) { // for de-serializing
+ this.hide();
+ }
+};
+
+// WatcherMorph accessing:
+
+WatcherMorph.prototype.isTemporary = function () {
+ var stage = this.parentThatIsA(StageMorph);
+ if (this.target instanceof VariableFrame) {
+ if (stage) {
+ if (this.target === stage.variables.parentFrame) {
+ return false; // global
+ }
+ }
+ return this.target.owner === null;
+ }
+ return false;
+};
+
+WatcherMorph.prototype.object = function () {
+ // answer the actual sprite I refer to
+ return this.target instanceof VariableFrame ?
+ this.target.owner : this.target;
+};
+
+WatcherMorph.prototype.isGlobal = function (selector) {
+ return contains(['getTimer', 'getLastAnswer', 'getTempo'], selector);
+};
+
+// WatcherMorph slider accessing:
+
+WatcherMorph.prototype.setSliderMin = function (num) {
+ if (this.target instanceof VariableFrame) {
+ this.sliderMorph.setSize(1);
+ this.sliderMorph.setStart(num);
+ this.sliderMorph.setSize(this.sliderMorph.rangeSize() / 5);
+ }
+};
+
+WatcherMorph.prototype.setSliderMax = function (num) {
+ if (this.target instanceof VariableFrame) {
+ this.sliderMorph.setSize(1);
+ this.sliderMorph.setStop(num);
+ this.sliderMorph.setSize(this.sliderMorph.rangeSize() / 5);
+ }
+};
+
+// WatcherMorph updating:
+
+WatcherMorph.prototype.update = function () {
+ var newValue,
+ num;
+ if (this.target && this.getter) {
+ this.updateLabel();
+ if (this.target instanceof VariableFrame) {
+ newValue = this.target.vars[this.getter];
+ } else {
+ newValue = this.target[this.getter]();
+ }
+ if (newValue !== this.currentValue) {
+ this.changed();
+ this.cellMorph.contents = newValue;
+ this.cellMorph.drawNew();
+ num = parseFloat(newValue);
+ if (!isNaN(num)) {
+ this.sliderMorph.value = num;
+ this.sliderMorph.drawNew();
+ }
+ this.fixLayout();
+ this.currentValue = newValue;
+ }
+ }
+ if (this.cellMorph.contentsMorph instanceof ListWatcherMorph) {
+ this.cellMorph.contentsMorph.update();
+ }
+};
+
+WatcherMorph.prototype.updateLabel = function () {
+ // check whether the target object's name has been changed
+ var obj = this.object();
+
+ if (!obj || this.isGlobal(this.getter)) { return; }
+ if (obj.version !== this.version) {
+ this.objName = obj.name ? obj.name + ' ' : ' ';
+ if (this.labelMorph) {
+ this.labelMorph.destroy();
+ this.labelMorph = null;
+ this.fixLayout();
+ }
+ }
+};
+
+// WatcherMorph layout:
+
+WatcherMorph.prototype.fixLayout = function () {
+ var fontSize = SyntaxElementMorph.prototype.fontSize, isList,
+ myself = this;
+
+ this.changed();
+
+ // create my parts
+ if (this.labelMorph === null) {
+ this.labelMorph = new StringMorph(
+ this.objName + this.labelText,
+ fontSize,
+ null,
+ true,
+ false,
+ false,
+ new Point(1, 1),
+ new Color(255, 255, 255)
+ );
+ this.add(this.labelMorph);
+ }
+ if (this.cellMorph === null) {
+ this.cellMorph = new CellMorph('', this.readoutColor);
+ this.add(this.cellMorph);
+ }
+ if (this.sliderMorph === null) {
+ this.sliderMorph = new SliderMorph(
+ 0,
+ 100,
+ 0,
+ 20,
+ 'horizontal'
+ );
+ this.sliderMorph.alpha = 1;
+ this.sliderMorph.button.color = this.color.darker();
+ this.sliderMorph.color = this.color.lighter(60);
+ this.sliderMorph.button.highlightColor = this.color.darker();
+ this.sliderMorph.button.highlightColor.b += 50;
+ this.sliderMorph.button.pressColor = this.color.darker();
+ this.sliderMorph.button.pressColor.b += 100;
+ this.sliderMorph.setHeight(fontSize);
+ this.sliderMorph.action = function (num) {
+ myself.target.vars[myself.getter] = Math.round(num);
+ };
+ this.add(this.sliderMorph);
+ }
+
+ // adjust my layout
+ isList = this.cellMorph.contents instanceof List;
+ if (isList) { this.style = 'normal'; }
+
+ if (this.style === 'large') {
+ this.labelMorph.hide();
+ this.sliderMorph.hide();
+ this.cellMorph.big();
+ this.cellMorph.setPosition(this.position());
+ this.setExtent(this.cellMorph.extent().subtract(1));
+ return;
+ }
+
+ this.labelMorph.show();
+ this.sliderMorph.show();
+ this.cellMorph.normal();
+ this.labelMorph.setPosition(this.position().add(new Point(
+ this.edge,
+ this.border + SyntaxElementMorph.prototype.typeInPadding
+ )));
+
+ if (isList) {
+ this.cellMorph.setPosition(this.labelMorph.bottomLeft().add(
+ new Point(0, SyntaxElementMorph.prototype.typeInPadding)
+ ));
+ } else {
+ this.cellMorph.setPosition(this.labelMorph.topRight().add(new Point(
+ fontSize / 3,
+ 0
+ )));
+ this.labelMorph.setTop(
+ this.cellMorph.top()
+ + (this.cellMorph.height() - this.labelMorph.height()) / 2
+ );
+ }
+
+ if (this.style === 'slider') {
+ this.sliderMorph.silentSetPosition(new Point(
+ this.labelMorph.left(),
+ this.cellMorph.bottom()
+ + SyntaxElementMorph.prototype.typeInPadding
+ ));
+ this.sliderMorph.setWidth(this.cellMorph.right()
+ - this.labelMorph.left());
+ this.silentSetHeight(
+ this.cellMorph.height()
+ + this.sliderMorph.height()
+ + this.border * 2
+ + SyntaxElementMorph.prototype.typeInPadding * 3
+ );
+ } else {
+ this.sliderMorph.hide();
+ this.bounds.corner.y = this.cellMorph.bottom()
+ + this.border
+ + SyntaxElementMorph.prototype.typeInPadding;
+ }
+ this.bounds.corner.x = Math.max(
+ this.cellMorph.right(),
+ this.labelMorph.right()
+ ) + this.edge
+ + SyntaxElementMorph.prototype.typeInPadding;
+ this.drawNew();
+ this.changed();
+};
+
+// WatcherMorph events:
+
+/*
+// Scratch-like watcher-toggling, commented out b/c we have a drop-down menu
+
+WatcherMorph.prototype.mouseClickLeft = function () {
+ if (this.style === 'normal') {
+ if (this.target instanceof VariableFrame) {
+ this.style = 'slider';
+ } else {
+ this.style = 'large';
+ }
+ } else if (this.style === 'slider') {
+ this.style = 'large';
+ } else {
+ this.style = 'normal';
+ }
+ this.fixLayout();
+};
+*/
+
+// WatcherMorph user menu:
+
+WatcherMorph.prototype.userMenu = function () {
+ var myself = this,
+ menu = new MenuMorph(this),
+ on = '\u25CF',
+ off = '\u25CB';
+ menu.addItem(
+ (this.style === 'normal' ? on : off) + ' ' + localize('normal'),
+ 'styleNormal'
+ );
+ menu.addItem(
+ (this.style === 'large' ? on : off) + ' ' + localize('large'),
+ 'styleLarge'
+ );
+ if (this.target instanceof VariableFrame) {
+ menu.addItem(
+ (this.style === 'slider' ? on : off) + ' ' + localize('slider'),
+ 'styleSlider'
+ );
+ menu.addLine();
+ menu.addItem(
+ 'slider min...',
+ 'userSetSliderMin'
+ );
+ menu.addItem(
+ 'slider max...',
+ 'userSetSliderMax'
+ );
+ menu.addLine();
+ menu.addItem(
+ 'import...',
+ function () {
+ var inp = document.createElement('input'),
+ ide = myself.parentThatIsA(IDE_Morph);
+ if (ide.filePicker) {
+ document.body.removeChild(ide.filePicker);
+ ide.filePicker = null;
+ }
+ inp.type = 'file';
+ inp.style.color = "transparent";
+ inp.style.backgroundColor = "transparent";
+ inp.style.border = "none";
+ inp.style.outline = "none";
+ inp.style.position = "absolute";
+ inp.style.top = "0px";
+ inp.style.left = "0px";
+ inp.style.width = "0px";
+ inp.style.height = "0px";
+ inp.addEventListener(
+ "change",
+ function () {
+ var file, i;
+
+ function readText(aFile) {
+ var frd = new FileReader();
+ frd.onloadend = function (e) {
+ myself.target.setVar(
+ myself.getter,
+ e.target.result
+ );
+ };
+ frd.readAsText(aFile);
+ }
+
+ document.body.removeChild(inp);
+ ide.filePicker = null;
+ if (inp.files.length > 0) {
+ for (i = 0; i < inp.files.length; i += 1) {
+ file = inp.files[i];
+ if (file.type.indexOf("text") === 0) {
+ readText(file);
+ }
+ }
+ }
+ },
+ false
+ );
+ document.body.appendChild(inp);
+ ide.filePicker = inp;
+ inp.click();
+ }
+ );
+ if (this.currentValue &&
+ (isString(this.currentValue) || !isNaN(+this.currentValue))) {
+ menu.addItem(
+ 'export...',
+ function () {
+ window.open(
+ 'data:text/plain,' +
+ encodeURIComponent(this.currentValue.toString())
+ );
+ }
+ );
+ }
+ }
+ return menu;
+};
+
+WatcherMorph.prototype.setStyle = function (style) {
+ this.style = style;
+ this.fixLayout();
+};
+
+WatcherMorph.prototype.styleNormal = function () {
+ this.setStyle('normal');
+};
+
+WatcherMorph.prototype.styleLarge = function () {
+ this.setStyle('large');
+};
+
+WatcherMorph.prototype.styleSlider = function () {
+ this.setStyle('slider');
+};
+
+WatcherMorph.prototype.userSetSliderMin = function () {
+ new DialogBoxMorph(
+ this,
+ this.setSliderMin,
+ this
+ ).prompt(
+ "Slider minimum value",
+ this.sliderMorph.start.toString(),
+ this.world()
+ );
+};
+
+WatcherMorph.prototype.userSetSliderMax = function () {
+ new DialogBoxMorph(
+ this,
+ this.setSliderMax,
+ this
+ ).prompt(
+ "Slider maximum value",
+ this.sliderMorph.stop.toString(),
+ this.world()
+ );
+};
+
+// WatcherMorph drawing:
+
+WatcherMorph.prototype.drawNew = function () {
+ var context,
+ gradient;
+ this.image = newCanvas(this.extent());
+ context = this.image.getContext('2d');
+ if ((this.edge === 0) && (this.border === 0)) {
+ BoxMorph.uber.drawNew.call(this);
+ return null;
+ }
+ gradient = context.createLinearGradient(0, 0, 0, this.height());
+ gradient.addColorStop(0, this.color.lighter().toString());
+ gradient.addColorStop(1, this.color.darker().toString());
+ context.fillStyle = gradient;
+ context.beginPath();
+ this.outlinePath(
+ context,
+ Math.max(this.edge - this.border, 0),
+ this.border
+ );
+ context.closePath();
+ context.fill();
+ if (this.border > 0) {
+ gradient = context.createLinearGradient(0, 0, 0, this.height());
+ gradient.addColorStop(0, this.borderColor.lighter().toString());
+ gradient.addColorStop(1, this.borderColor.darker().toString());
+ context.lineWidth = this.border;
+ context.strokeStyle = gradient;
+ context.beginPath();
+ this.outlinePath(context, this.edge, this.border / 2);
+ context.closePath();
+ context.stroke();
+ }
+};
+
+// StagePrompterMorph ////////////////////////////////////////////////////////
+
+/*
+ I am a sensor-category-colored input box at the bottom of the stage
+ which lets the user answer to a question. If I am opened from within
+ the context of a sprite, my question can be anything that is displayable
+ in a SpeechBubble and will be, if I am opened from within the stage
+ my question will be shown as a single line of text within my label morph.
+*/
+
+// StagePrompterMorph inherits from BoxMorph:
+
+StagePrompterMorph.prototype = new BoxMorph();
+StagePrompterMorph.prototype.constructor = StagePrompterMorph;
+StagePrompterMorph.uber = BoxMorph.prototype;
+
+// StagePrompterMorph instance creation:
+
+function StagePrompterMorph(question) {
+ this.init(question);
+}
+
+StagePrompterMorph.prototype.init = function (question) {
+ // question is optional in case the Stage is asking
+ var myself = this;
+
+ // additional properties
+ this.isDone = false;
+ if (question) {
+ this.label = new StringMorph(
+ question,
+ SpriteMorph.prototype.bubbleFontSize,
+ null, // fontStyle
+ SpriteMorph.prototype.bubbleFontIsBold,
+ false, // italic
+ 'left'
+ );
+ } else {
+ this.label = null;
+ }
+ this.inputField = new InputFieldMorph();
+ this.button = new PushButtonMorph(
+ null,
+ function () {myself.accept(); },
+ '\u2713'
+ );
+
+ // initialize inherited properties
+ StagePrompterMorph.uber.init.call(
+ this,
+ SyntaxElementMorph.prototype.rounding,
+ SpriteMorph.prototype.bubbleBorder,
+ SpriteMorph.prototype.blockColor.sensing
+ );
+
+ // override inherited behavior
+ this.color = new Color(255, 255, 255);
+ if (this.label) {this.add(this.label); }
+ this.add(this.inputField);
+ this.add(this.button);
+ this.setWidth(480 - 20);
+ this.fixLayout();
+};
+
+// StagePrompterMorph layout:
+
+StagePrompterMorph.prototype.fixLayout = function () {
+ var y = 0;
+ if (this.label) {
+ this.label.setPosition(new Point(
+ this.left() + this.edge,
+ this.top() + this.edge
+ ));
+ y = this.label.bottom() - this.top();
+ }
+ this.inputField.setPosition(new Point(
+ this.left() + this.edge,
+ this.top() + y + this.edge
+ ));
+ this.inputField.setWidth(
+ this.width()
+ - this.edge * 2
+ - this.button.width()
+ - this.border
+ );
+ this.button.setCenter(this.inputField.center());
+ this.button.setLeft(this.inputField.right() + this.border);
+ this.setHeight(
+ this.inputField.bottom()
+ - this.top()
+ + this.edge
+ );
+};
+
+// StagePrompterMorph events:
+
+StagePrompterMorph.prototype.mouseClickLeft = function () {
+ this.inputField.edit();
+};
+
+StagePrompterMorph.prototype.accept = function () {
+ this.isDone = true;
+};