diff options
| author | jmoenig <jens@moenig.org> | 2013-03-16 09:02:16 +0100 |
|---|---|---|
| committer | jmoenig <jens@moenig.org> | 2013-03-16 09:02:16 +0100 |
| commit | 0b510366d230399a38f18a8db92dd396a0dad42e (patch) | |
| tree | ffb841d64bca04799ebc470701b71e59e726017e /objects.js | |
| download | snap-0b510366d230399a38f18a8db92dd396a0dad42e.tar.gz snap-0b510366d230399a38f18a8db92dd396a0dad42e.zip | |
Creation
Diffstat (limited to 'objects.js')
| -rw-r--r-- | objects.js | 5610 |
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; +}; |
