text: /*
  
      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) 2017 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
          SpriteHighlightMorph
          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
      Yuan Yuan and Dylan Servilla contributed graphic effects for costumes
  
  */
  
  // Global stuff ////////////////////////////////////////////////////////
  
  /*global PaintEditorMorph, ListWatcherMorph, PushButtonMorph, ToggleMorph,
  DialogBoxMorph, InputFieldMorph, SpriteIconMorph, BlockMorph,
  ThreadManager, VariableFrame, detect, BlockMorph, BoxMorph, Color,
  CommandBlockMorph, FrameMorph, HatBlockMorph, MenuMorph, Morph, MultiArgMorph,
  Point, ReporterBlockMorph, ScriptsMorph, StringMorph, SyntaxElementMorph,
  TextMorph, contains, degrees, detect, newCanvas, nop, radians, Array,
  CursorMorph, Date, FrameMorph, HandMorph, Math, MenuMorph, Morph,
  MorphicPreferences, Object, PenMorph, Point, Rectangle, ScrollFrameMorph,
  SliderMorph, String, StringMorph, TextMorph, contains, copy, degrees, detect,
  document, isNaN, isString, newCanvas, nop, parseFloat, radians, window,
  modules, IDE_Morph, VariableDialogMorph, HTMLCanvasElement, Context, List,
  SpeechBubbleMorph, RingMorph, isNil, FileReader, TableDialogMorph,
  BlockEditorMorph, BlockDialogMorph, PrototypeHatBlockMorph, localize,
  TableMorph, TableFrameMorph, normalizeCanvas, BooleanSlotMorph*/
  
  modules.objects = '2017-January-13';
  
  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;
  var SpriteHighlightMorph;
  
  function isSnapObject(thing) {
      return thing instanceof SpriteMorph || (thing instanceof StageMorph);
  }
  
  // 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.paletteTextColor = new Color(230, 230, 230);
  SpriteMorph.prototype.sliderColor
      = SpriteMorph.prototype.paletteColor.lighter(30);
  SpriteMorph.prototype.isCachingPrimitives = true;
  
  SpriteMorph.prototype.enableNesting = true;
  SpriteMorph.prototype.enableFirstClass = true;
  SpriteMorph.prototype.useFlatLineEnds = false;
  SpriteMorph.prototype.highlightColor = new Color(250, 200, 130);
  SpriteMorph.prototype.highlightBorder = 8;
  
  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: {
              only: SpriteMorph,
              type: 'command',
              category: 'motion',
              spec: 'move %n steps',
              defaults: [10]
          },
          turn: {
              only: SpriteMorph,
              type: 'command',
              category: 'motion',
              spec: 'turn %clockwise %n degrees',
              defaults: [15]
          },
          turnLeft: {
              only: SpriteMorph,
              type: 'command',
              category: 'motion',
              spec: 'turn %counterclockwise %n degrees',
              defaults: [15]
          },
          setHeading: {
              only: SpriteMorph,
              type: 'command',
              category: 'motion',
              spec: 'point in direction %dir'
          },
          doFaceTowards: {
              only: SpriteMorph,
              type: 'command',
              category: 'motion',
              spec: 'point towards %dst'
          },
          gotoXY: {
              only: SpriteMorph,
              type: 'command',
              category: 'motion',
              spec: 'go to x: %n y: %n',
              defaults: [0, 0]
          },
          doGotoObject: {
              only: SpriteMorph,
              type: 'command',
              category: 'motion',
              spec: 'go to %dst'
          },
          doGlide: {
              only: SpriteMorph,
              type: 'command',
              category: 'motion',
              spec: 'glide %n secs to x: %n y: %n',
              defaults: [1, 0, 0]
          },
          changeXPosition: {
              only: SpriteMorph,
              type: 'command',
              category: 'motion',
              spec: 'change x by %n',
              defaults: [10]
          },
          setXPosition: {
              only: SpriteMorph,
              type: 'command',
              category: 'motion',
              spec: 'set x to %n',
              defaults: [0]
          },
          changeYPosition: {
              only: SpriteMorph,
              type: 'command',
              category: 'motion',
              spec: 'change y by %n',
              defaults: [10]
          },
          setYPosition: {
              only: SpriteMorph,
              type: 'command',
              category: 'motion',
              spec: 'set y to %n',
              defaults: [0]
          },
          bounceOffEdge: {
              only: SpriteMorph,
              type: 'command',
              category: 'motion',
              spec: 'if on edge, bounce'
          },
          xPosition: {
              only: SpriteMorph,
              type: 'reporter',
              category: 'motion',
              spec: 'x position'
          },
          yPosition: {
              only: SpriteMorph,
              type: 'reporter',
              category: 'motion',
              spec: 'y position'
          },
          direction: {
              only: SpriteMorph,
              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: {
              only: SpriteMorph,
              type: 'command',
              category: 'looks',
              spec: 'say %s for %n secs',
              defaults: [localize('Hello!'), 2]
          },
          bubble: {
              only: SpriteMorph,
              type: 'command',
              category: 'looks',
              spec: 'say %s',
              defaults: [localize('Hello!')]
          },
          doThinkFor: {
              only: SpriteMorph,
              type: 'command',
              category: 'looks',
              spec: 'think %s for %n secs',
              defaults: [localize('Hmm...'), 2]
          },
          doThink: {
              only: SpriteMorph,
              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: {
              only: SpriteMorph,
              type: 'command',
              category: 'looks',
              spec: 'change size by %n',
              defaults: [10]
          },
          setScale: {
              only: SpriteMorph,
              type: 'command',
              category: 'looks',
              spec: 'set size to %n %',
              defaults: [100]
          },
          getScale: {
              only: SpriteMorph,
              type: 'reporter',
              category: 'looks',
              spec: 'size'
          },
          show: {
              only: SpriteMorph,
              type: 'command',
              category: 'looks',
              spec: 'show'
          },
          hide: {
              only: SpriteMorph,
              type: 'command',
              category: 'looks',
              spec: 'hide'
          },
          comeToFront: {
              only: SpriteMorph,
              type: 'command',
              category: 'looks',
              spec: 'go to front'
          },
          goBack: {
              only: SpriteMorph,
              type: 'command',
              category: 'looks',
              spec: 'go back %n layers',
              defaults: [1]
          },
          doScreenshot: {
              type: 'command',
              category: 'looks',
              spec: 'save %imgsource as costume named %s',
              defaults: [['pen trails'], localize('screenshot')]
          },
  
          // Looks - Debugging primitives for development mode
          reportCostumes: {
              dev: true,
              type: 'reporter',
              category: 'looks',
              spec: 'wardrobe'
          },
  
          alert: {
              dev: true,
              type: 'command',
              category: 'looks',
              spec: 'alert %mult%s'
          },
          log: {
              dev: true,
              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'
          },
  
          // Sound - Debugging primitives for development mode
          reportSounds: {
              dev: true,
              type: 'reporter',
              category: 'sound',
              spec: 'jukebox'
          },
  
          // Pen
          clear: {
              type: 'command',
              category: 'pen',
              spec: 'clear'
          },
          down: {
              only: SpriteMorph,
              type: 'command',
              category: 'pen',
              spec: 'pen down'
          },
          up: {
              only: SpriteMorph,
              type: 'command',
              category: 'pen',
              spec: 'pen up'
          },
          setColor: {
              only: SpriteMorph,
              type: 'command',
              category: 'pen',
              spec: 'set pen color to %clr'
          },
          changeHue: {
              only: SpriteMorph,
              type: 'command',
              category: 'pen',
              spec: 'change pen color by %n',
              defaults: [10]
          },
          setHue: {
              only: SpriteMorph,
              type: 'command',
              category: 'pen',
              spec: 'set pen color to %n',
              defaults: [0]
          },
          changeBrightness: {
              only: SpriteMorph,
              type: 'command',
              category: 'pen',
              spec: 'change pen shade by %n',
              defaults: [10]
          },
          setBrightness: {
              only: SpriteMorph,
              type: 'command',
              category: 'pen',
              spec: 'set pen shade to %n',
              defaults: [100]
          },
          changeSize: {
              only: SpriteMorph,
              type: 'command',
              category: 'pen',
              spec: 'change pen size by %n',
              defaults: [1]
          },
          setSize: {
              only: SpriteMorph,
              type: 'command',
              category: 'pen',
              spec: 'set pen size to %n',
              defaults: [1]
          },
          doStamp: {
              only: SpriteMorph,
              type: 'command',
              category: 'pen',
              spec: 'stamp'
          },
          floodFill: {
              only: SpriteMorph,
              type: 'command',
              category: 'pen',
              spec: 'fill'
          },
  
          // Control
          receiveGo: {
              type: 'hat',
              category: 'control',
              spec: 'when %greenflag clicked'
          },
          receiveKey: {
              type: 'hat',
              category: 'control',
              spec: 'when %keyHat key pressed'
          },
  
      /* migrated to a newer block version:
  
          receiveClick: {
              type: 'hat',
              category: 'control',
              spec: 'when I am clicked'
          },
      */
  
          receiveInteraction: {
              type: 'hat',
              category: 'control',
              spec: 'when I am %interaction',
              defaults: ['clicked']
          },
          receiveMessage: {
              type: 'hat',
              category: 'control',
              spec: 'when I receive %msgHat'
          },
          receiveCondition: {
              type: 'hat',
              category: 'control',
              spec: 'when %b'
          },
          doBroadcast: {
              type: 'command',
              category: 'control',
              spec: 'broadcast %msg'
          },
          doBroadcastAndWait: {
              type: 'command',
              category: 'control',
              spec: 'broadcast %msg and wait'
          },
          getLastMessage: {
              type: 'reporter',
              category: 'control',
              spec: 'message'
          },
          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'
          },
  
      /* migrated to a newer block version:
  
          doStop: {
              type: 'command',
              category: 'control',
              spec: 'stop script'
          },
          doStopAll: {
              type: 'command',
              category: 'control',
              spec: 'stop all %stop'
          },
      */
  
          doStopThis: {
              type: 'command',
              category: 'control',
              spec: 'stop %stopChoices'
          },
          doStopOthers: {
              type: 'command',
              category: 'control',
              spec: 'stop %stopOthersChoices'
          },
          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: { // migrated to a newer block version
              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'
          },
  
          // Debugging - pausing
  
          doPauseAll: {
              type: 'command',
              category: 'control',
              spec: 'pause all %pause'
          },
  
          // Sensing
  
          reportTouchingObject: {
              only: SpriteMorph,
              type: 'predicate',
              category: 'sensing',
              spec: 'touching %col ?'
          },
          reportTouchingColor: {
              only: SpriteMorph,
              type: 'predicate',
              category: 'sensing',
              spec: 'touching %clr ?'
          },
          reportColorIsTouchingColor: {
              only: SpriteMorph,
              type: 'predicate',
              category: 'sensing',
              spec: 'color %clr is touching %clr ?'
          },
          colorFiltered: {
              dev: true,
              type: 'reporter',
              category: 'sensing',
              spec: 'filtered for %clr'
          },
          reportStackSize: {
              dev: true,
              type: 'reporter',
              category: 'sensing',
              spec: 'stack size'
          },
          reportFrameCount: {
              dev: true,
              type: 'reporter',
              category: 'sensing',
              spec: 'frames'
          },
          reportThreadCount: {
              dev: true,
              type: 'reporter',
              category: 'sensing',
              spec: 'processes'
          },
          doAsk: {
              type: 'command',
              category: 'sensing',
              spec: 'ask %s and wait',
              defaults: [localize('what\'s your name?')]
          },
          reportLastAnswer: { // retained for legacy compatibility
              dev: true,
              type: 'reporter',
              category: 'sensing',
              spec: 'answer'
          },
          getLastAnswer: {
              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: { // retained for legacy compatibility
              dev: true,
              type: 'reporter',
              category: 'sensing',
              spec: 'timer'
          },
          getTimer: {
              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'
          },
          reportDate: {
              type: 'reporter',
              category: 'sensing',
              spec: 'current %dates'
          },
          reportGet: {
              type: 'reporter',
              category: 'sensing',
              spec: 'my %get',
              defaults: [['neighbors']]
          },
  
          // Operators
          reifyScript: {
              type: 'ring',
              category: 'other',
              spec: '%rc %ringparms',
              alias: 'command ring lambda'
          },
          reifyReporter: {
              type: 'ring',
              category: 'other',
              spec: '%rr %ringparms',
              alias: 'reporter ring lambda'
          },
          reifyPredicate: {
              type: 'ring',
              category: 'other',
              spec: '%rp %ringparms',
              alias: 'predicate ring lambda'
          },
          reportSum: {
              type: 'reporter',
              category: 'operators',
              spec: '%n + %n'
          },
          reportDifference: {
              type: 'reporter',
              category: 'operators',
              spec: '%n \u2212 %n',
              alias: '-'
          },
          reportProduct: {
              type: 'reporter',
              category: 'operators',
              spec: '%n \u00D7 %n',
              alias: '*'
          },
          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'
          },
          reportBoolean: {
              type: 'predicate',
              category: 'operators',
              spec: '%bool',
              alias: 'true boolean'
          },
          reportFalse: { // special case for keyboard entry and search
              type: 'predicate',
              category: 'operators',
              spec: '%bool',
              defaults: [false],
              alias: 'false boolean'
          },
          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 ?'
          },
          reportTextSplit: {
              type: 'reporter',
              category: 'operators',
              spec: 'split %s by %delim',
              defaults: [localize('hello') + ' ' + localize('world'), " "]
          },
          reportJSFunction: { // experimental
              type: 'reporter',
              category: 'operators',
              spec: 'JavaScript function ( %mult%s ) { %code }'
          },
          reportTypeOf: { // only in dev mode for debugging
              dev: true,
              type: 'reporter',
              category: 'operators',
              spec: 'type of %s',
              defaults: [5]
          },
          reportTextFunction: { // only in dev mode - experimental
              dev: true,
              type: 'reporter',
              category: 'operators',
              spec: '%txtfun of %s',
              defaults: [null, "Abelson & Sussman"]
          },
  
      /*
          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'
          },
  
          // inheritance - experimental
          doDeleteAttr: {
              type: 'command',
              category: 'variables',
              spec: 'delete %shd'
          },
  
          // 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')]
          },
  
          // MAP - experimental
          reportMap: {
              dev: true,
              type: 'reporter',
              category: 'lists',
              spec: 'map %repRing over %l'
          },
          doForEach: {
              dev: true,
              type: 'command',
              category: 'lists',
              spec: 'for %upvar in %l %cl',
              defaults: [localize('each item')]
          },
  
          // Tables - experimental
  
          doShowTable: {
              dev: true,
              type: 'command',
              category: 'lists',
              spec: 'show table %l'
          },
  
          // Code mapping - experimental
          doMapCodeOrHeader: { // experimental
              type: 'command',
              category: 'other',
              spec: 'map %cmdRing to %codeKind %code'
          },
          doMapStringCode: { // experimental
              type: 'command',
              category: 'other',
              spec: 'map String to code %code',
              defaults: ['<#1>']
          },
          doMapListCode: { // experimental
              type: 'command',
              category: 'other',
              spec: 'map %codeListPart of %codeListKind to code %code'
          },
          reportMappedCode: { // experimental
              type: 'reporter',
              category: 'other',
              spec: 'code of %cmdRing'
          }
      };
  };
  
  SpriteMorph.prototype.initBlocks();
  
  SpriteMorph.prototype.initBlockMigrations = function () {
      SpriteMorph.prototype.blockMigrations = {
          doStopAll: {
              selector: 'doStopThis',
              inputs: [['all']]
          },
          doStop: {
              selector: 'doStopThis',
              inputs: [['this script']]
          },
          doStopBlock: {
              selector: 'doStopThis',
              inputs: [['this block']]
          },
          receiveClick: {
              selector: 'receiveInteraction',
              inputs: [['clicked']]
          },
          reportTrue: {
              selector: 'reportBoolean',
              inputs: [true]
          },
          reportFalse: {
              selector: 'reportBoolean',
              inputs: [false]
          }
      };
  };
  
  SpriteMorph.prototype.initBlockMigrations();
  
  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', 'bubble', 'doThink', 'doAsk'],
      doThinkFor: ['doSayFor', 'doThink', 'bubble', 'doAsk'],
      bubble: ['doThink', 'doAsk', 'doSayFor', 'doThinkFor'],
      doThink: ['bubble', 'doAsk', 'doSayFor', 'doThinkFor'],
      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:
      doBroadcast: ['doBroadcastAndWait'],
      doBroadcastAndWait: ['doBroadcast'],
      doIf: ['doIfElse', 'doUntil'],
      doIfElse: ['doIf', 'doUntil'],
      doRepeat: ['doUntil'],
      doUntil: ['doRepeat', 'doIf'],
  
      // sensing:
      doAsk: ['bubble', 'doThink', 'doSayFor', 'doThinkFor'],
      getLastAnswer: ['getTimer'],
      getTimer: ['getLastAnswer'],
      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'],
  
      // 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.isCorpse = false; // indicate whether a sprite/clone has been deleted
      this.cloneOriginName = '';
  
      // sprite nesting properties
      this.parts = []; // not serialized, only anchor (name)
      this.anchor = null;
      this.nestingScale = 1;
      this.rotatesWithAnchor = true;
      this.layers = null; // cache for dragging nested sprites, don't serialize
  
      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
  
      this.graphicsValues = {
          'color': 0,
          'fisheye': 0,
          'whirl': 0,
          'pixelate': 0,
          'mosaic': 0,
          'duplicate': 0,
          'negative': 0,
          'comic': 0,
          'confetti': 0,
          'saturation': 0,
          'brightness': 0
      };
  
      // sprite inheritance
      this.exemplar = null;
  
      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 (forClone) {
      var c = SpriteMorph.uber.fullCopy.call(this),
          myself = this,
          arr = [],
          cb, effect;
  
      c.stopTalking();
      c.color = this.color.copy();
      c.blocksCache = {};
      c.paletteCache = {};
      c.scripts = this.scripts.fullCopy(forClone);
      c.scripts.owner = c;
      c.variables = this.variables.copy();
      c.variables.owner = c;
      c.customBlocks = [];
      if (!forClone) {
          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) {
          var cst = forClone ? costume : costume.copy();
          arr.push(cst);
          if (costume === myself.costume) {
              c.costume = cst;
          }
      });
      c.costumes = new List(arr);
      arr = [];
      this.sounds.asArray().forEach(function (sound) {
          var snd = forClone ? sound : sound.copy();
          arr.push(snd);
      });
      c.sounds = new List(arr);
      arr = [];
      c.nestingScale = 1;
      c.rotatesWithAnchor = true;
      c.anchor = null;
      c.parts = [];
      this.parts.forEach(function (part) {
          var dp = part.fullCopy(forClone);
          dp.nestingScale = part.nestingScale;
          dp.rotatesWithAnchor = part.rotatesWithAnchor;
          c.attachPart(dp);
      });
      c.graphicsValues = {};
      for (effect in this.graphicsValues) {
          if (this.graphicsValues.hasOwnProperty(effect)) {
              c.graphicsValues[effect] = this.graphicsValues[effect];
          }
      }
      return c;
  };
  
  SpriteMorph.prototype.appearIn = function (ide) {
      // private - used in IDE_Morph.duplicateSprite()
      if (!this.isClone) {
          this.name = ide.newSpriteName(this.name);
          ide.corral.addSprite(this);
          ide.sprites.add(this);
      }
      ide.stage.add(this);
      this.parts.forEach(function (part) {
          part.appearIn(ide);
      });
  };
  
  // 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,
          facing, // actual costume heading based on my rotation style
          isFlipped,
          isLoadingCostume,
          cst,
          pic, // (flipped copy of) actual costume based on my rotation style
          stageScale,
          newX,
          corners = [],
          origin,
          shift,
          corner,
          costumeExtent,
          ctx,
          handle;
  
      if (this.isWarped) {
          this.wantsRedraw = true;
          return;
      }
      currentCenter = this.center();
      isLoadingCostume = this.costume &&
          typeof this.costume.loaded === 'function';
      stageScale = this.parent instanceof StageMorph ?
              this.parent.scale : 1;
      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 && !isLoadingCostume) {
          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, true);
          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);
  
          // apply graphics effects to image
          this.image = this.applyGraphicsEffects(this.image);
  
          // adjust my position to the rotation
          this.setCenter(currentCenter, true); // just me
  
          // 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(), true);
          this.setCenter(currentCenter, true); // just me
          SpriteMorph.uber.drawNew.call(this, facing);
          this.rotationOffset = this.extent().divideBy(2);
          this.image = this.applyGraphicsEffects(this.image);
          if (isLoadingCostume) { // retry until costume is done loading
              cst = this.costume;
              handle = setInterval(
                  function () {
                      myself.wearCostume(cst);
                      clearInterval(handle);
                  },
                  100
              );
              return myself.wearCostume(null);
  
          }
      }
      this.version = Date.now(); // for observer optimization
  };
  
  SpriteMorph.prototype.endWarp = function () {
      this.isWarped = false;
      if (this.wantsRedraw) {
          var x = this.xPosition(),
              y = this.yPosition();
          this.drawNew();
          this.silentGotoXY(x, y, true); // just me
          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 = normalizeCanvas(this.image, true).getContext('2d').getImageData(
          0,
          0,
          ext.x,
          ext.y
      );
      morph.image = newCanvas(ext, true);
      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 migration, info, block, defaults, inputs, i;
      migration = this.blockMigrations[selector];
      info = this.blocks[migration ? migration.selector : 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 = migration ? migration.selector : selector;
      if (contains(['reifyReporter', 'reifyPredicate'], block.selector)) {
          block.isStatic = true;
      }
      block.setSpec(localize(info.spec));
      if ((setDefaults && info.defaults) || (migration && migration.inputs)) {
          defaults = migration ? migration.inputs : 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,
          inheritedVars = this.inheritedVariableNames();
  
      function block(selector) {
          if (StageMorph.prototype.hiddenPrimitives[selector]) {
              return null;
          }
          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;
          if (contains(inheritedVars, varName)) {
              newBlock.ghost();
          }
          return newBlock;
      }
  
      function watcherToggle(selector) {
          if (StageMorph.prototype.hiddenPrimitives[selector]) {
              return null;
          }
          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
          );
      }
  
      function helpMenu() {
          var menu = new MenuMorph(this);
          menu.addItem('help...', 'showHelp');
          return menu;
      }
  
      function addVar(pair) {
          var ide;
          if (pair) {
              if (myself.isVariableNameInUse(pair[0], pair[1])) {
                  myself.inform('that name is already in use');
              } else {
                  ide = myself.parentThatIsA(IDE_Morph);
                  myself.addVariable(pair[0], pair[1]);
                  if (!myself.showingVariableWatcher(pair[0])) {
                      myself.toggleVariableWatcher(pair[0], pair[1]);
                  }
                  ide.flushBlocksCache('variables'); // b/c of inheritance
                  ide.refreshPalette();
              }
          }
      }
  
      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(this.paletteTextColor);
              blocks.push(txt);
              blocks.push('-');
              blocks.push(block('reportCostumes'));
              blocks.push('-');
              blocks.push(block('log'));
              blocks.push(block('alert'));
              blocks.push('-');
              blocks.push(block('doScreenshot'));
          }
  
      /////////////////////////////////
  
      } 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'));
  
      // for debugging: ///////////////
  
          if (this.world().isDevMode) {
              blocks.push('-');
              txt = new TextMorph(localize(
                  'development mode \ndebugging primitives:'
              ));
              txt.fontSize = 9;
              txt.setColor(this.paletteTextColor);
              blocks.push(txt);
              blocks.push('-');
              blocks.push(block('reportSounds'));
          }
  
      } 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'));
          blocks.push(block('floodFill'));
  
      } else if (cat === 'control') {
  
          blocks.push(block('receiveGo'));
          blocks.push(block('receiveKey'));
          blocks.push(block('receiveInteraction'));
          blocks.push(block('receiveCondition'));
          blocks.push(block('receiveMessage'));
          blocks.push('-');
          blocks.push(block('doBroadcast'));
          blocks.push(block('doBroadcastAndWait'));
          blocks.push(watcherToggle('getLastMessage'));
          blocks.push(block('getLastMessage'));
          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('-');
      /*
      // old STOP variants, migrated to a newer version, now redundant
          blocks.push(block('doStopBlock'));
          blocks.push(block('doStop'));
          blocks.push(block('doStopAll'));
      */
          blocks.push(block('doStopThis'));
          blocks.push(block('doStopOthers'));
          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'));
          blocks.push('-');
          blocks.push(block('doPauseAll'));
  
      } 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('getLastAnswer'));
          blocks.push('-');
          blocks.push(watcherToggle('reportMouseX'));
          blocks.push(block('reportMouseX'));
          blocks.push(watcherToggle('reportMouseY'));
          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('getTimer'));
          blocks.push('-');
          blocks.push(block('reportAttributeOf'));
  
          if (SpriteMorph.prototype.enableFirstClass) {
              blocks.push(block('reportGet'));
          }
          blocks.push('-');
  
          blocks.push(block('reportURL'));
          blocks.push('-');
          blocks.push(block('reportIsFastTracking'));
          blocks.push(block('doSetFastTracking'));
          blocks.push('-');
          blocks.push(block('reportDate'));
  
      // for debugging: ///////////////
  
          if (this.world().isDevMode) {
  
              blocks.push('-');
              txt = new TextMorph(localize(
                  'development mode \ndebugging primitives:'
              ));
              txt.fontSize = 9;
              txt.setColor(this.paletteTextColor);
              blocks.push(txt);
              blocks.push('-');
              blocks.push(watcherToggle('reportThreadCount'));
              blocks.push(block('reportThreadCount'));
              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(block('reportBoolean'));
          blocks.push('-');
          blocks.push(block('reportJoinWords'));
          blocks.push(block('reportTextSplit'));
          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'));
  
          if (true) { // (Process.prototype.enableJS) {
              blocks.push('-');
              blocks.push(block('reportJSFunction'));
          }
  
      // for debugging: ///////////////
  
          if (this.world().isDevMode) {
              blocks.push('-');
              txt = new TextMorph(localize(
                  'development mode \ndebugging primitives:'
              ));
              txt.fontSize = 9;
              txt.setColor(this.paletteTextColor);
              blocks.push(txt);
              blocks.push('-');
              blocks.push(block('reportTypeOf'));
              blocks.push(block('reportTextFunction'));
          }
  
      /////////////////////////////////
  
      } else if (cat === 'variables') {
  
          button = new PushButtonMorph(
              null,
              function () {
                  new VariableDialogMorph(
                      null,
                      addVar,
                      myself
                  ).prompt(
                      'Variable name',
                      null,
                      myself.world()
                  );
              },
              'Make a variable'
          );
          button.userMenu = helpMenu;
          button.selector = 'addVariable';
          button.showHelp = BlockMorph.prototype.showHelp;
          blocks.push(button);
  
          if (this.deletableVariableNames().length > 0) {
              button = new PushButtonMorph(
                  null,
                  function () {
                      var menu = new MenuMorph(
                          myself.deleteVariable,
                          null,
                          myself
                      );
                      myself.deletableVariableNames().forEach(function (name) {
                          menu.addItem(name, name);
                      });
                      menu.popUpAtHand(myself.world());
                  },
                  'Delete a variable'
              );
              button.userMenu = helpMenu;
              button.selector = 'deleteVariable';
              button.showHelp = BlockMorph.prototype.showHelp;
              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'));
  
      // inheritance:
  
          if (StageMorph.prototype.enableInheritance) {
              blocks.push('-');
              blocks.push(block('doDeleteAttr'));
          }
  
      ///////////////////////////////
  
          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'));
  
      // for debugging: ///////////////
  
          if (this.world().isDevMode) {
              blocks.push('-');
              txt = new TextMorph(localize(
                  'development mode \ndebugging primitives:'
              ));
              txt.fontSize = 9;
              txt.setColor(this.paletteTextColor);
              blocks.push(txt);
              blocks.push('-');
              blocks.push(block('reportMap'));
              blocks.push('-');
              blocks.push(block('doForEach'));
              blocks.push(block('doShowTable'));
          }
  
      /////////////////////////////////
  
          blocks.push('=');
  
          if (StageMorph.prototype.enableCodeMapping) {
              blocks.push(block('doMapCodeOrHeader'));
              blocks.push(block('doMapStringCode'));
              blocks.push(block('doMapListCode'));
              blocks.push('-');
              blocks.push(block('reportMappedCode'));
              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'
          );
          button.userMenu = helpMenu;
          button.selector = 'addCustomBlock';
          button.showHelp = BlockMorph.prototype.showHelp;
          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,
          hideNextSpace = false,
          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;
      palette.growth = new Point(0, MorphicPreferences.scrollBarSize);
  
      // menu:
  
      palette.userMenu = function () {
          var menu = new MenuMorph(),
              ide = this.parentThatIsA(IDE_Morph),
              more = {
                  operators:
                      ['reifyScript', 'reifyReporter', 'reifyPredicate'],
                  control:
                      ['doWarp'],
                  variables:
                      [
                          'doDeclareVariables',
                          'reportNewList',
                          'reportCONS',
                          'reportListItem',
                          'reportCDR',
                          'reportListLength',
                          'reportListContainsItem',
                          'doAddToList',
                          'doDeleteFromList',
                          'doInsertInList',
                          'doReplaceInList'
                      ]
              };
  
          function hasHiddenPrimitives() {
              var defs = SpriteMorph.prototype.blocks,
                  hiddens = StageMorph.prototype.hiddenPrimitives;
              return Object.keys(hiddens).some(function (any) {
                  return !isNil(defs[any]) && (defs[any].category === category
                      || contains((more[category] || []), any));
              });
          }
  
          function canHidePrimitives() {
              return palette.contents.children.some(function (any) {
                  return contains(
                      Object.keys(SpriteMorph.prototype.blocks),
                      any.selector
                  );
              });
          }
  
          menu.addPair(
              'find blocks...',
              function () {myself.searchBlocks(); },
              '⌘F'
          );
          if (canHidePrimitives()) {
              menu.addItem(
                  'hide primitives',
                  function () {
                      var defs = SpriteMorph.prototype.blocks;
                      Object.keys(defs).forEach(function (sel) {
                          if (defs[sel].category === category) {
                              StageMorph.prototype.hiddenPrimitives[sel] = true;
                          }
                      });
                      (more[category] || []).forEach(function (sel) {
                          StageMorph.prototype.hiddenPrimitives[sel] = true;
                      });
                      ide.flushBlocksCache(category);
                      ide.refreshPalette();
                  }
              );
          }
          if (hasHiddenPrimitives()) {
              menu.addItem(
                  'show primitives',
                  function () {
                      var hiddens = StageMorph.prototype.hiddenPrimitives,
                          defs = SpriteMorph.prototype.blocks;
                      Object.keys(hiddens).forEach(function (sel) {
                          if (defs[sel] && (defs[sel].category === category)) {
                              delete StageMorph.prototype.hiddenPrimitives[sel];
                          }
                      });
                      (more[category] || []).forEach(function (sel) {
                          delete StageMorph.prototype.hiddenPrimitives[sel];
                      });
                      ide.flushBlocksCache(category);
                      ide.refreshPalette();
                  }
              );
          }
          return menu;
      };
  
      // primitives:
  
      blocks = this.blocksCache[category];
      if (!blocks) {
          blocks = myself.blockTemplates(category);
          if (this.isCachingPrimitives) {
              myself.blocksCache[category] = blocks;
          }
      }
  
      blocks.forEach(function (block) {
          if (block === null) {
              return;
          }
          if (block === '-') {
              if (hideNextSpace) {return; }
              y += unit * 0.8;
              hideNextSpace = true;
          } else if (block === '=') {
              if (hideNextSpace) {return; }
              y += unit * 1.6;
              hideNextSpace = true;
          } else if (block === '#') {
              x = 0;
              y = ry;
          } else {
              hideNextSpace = false;
              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();
          }
      });
  
      //layout
  
      palette.scrollX(palette.padding);
      palette.scrollY(palette.padding);
  
      Morph.prototype.trackChanges = oldFlag;
      return palette;
  };
  
  // SpriteMorph blocks searching
  
  SpriteMorph.prototype.blocksMatching = function (
      searchString,
      strictly,
      types, // optional, ['hat', 'command', 'reporter', 'predicate']
      varNames // optional, list of reachable unique variable names
  ) {
      // answer an array of block templates whose spec contains
      // the given search string, ordered by descending relevance
      // types is an optional array containing block types the search
      // is limited to, e.g. "command", "hat", "reporter", "predicate".
      // Note that "predicate" is not subsumed by "reporter" and has
      // to be specified explicitly.
      // if no types are specified all blocks are searched
      var blocks = [],
          blocksDict,
          myself = this,
          search = searchString.toLowerCase(),
          stage = this.parentThatIsA(StageMorph),
          reporterized;
  
      if (!types || !types.length) {
          types = ['hat', 'command', 'reporter', 'predicate', 'ring'];
      }
      if (!varNames) {varNames = []; }
  
      function labelOf(aBlockSpec) {
          var words = (BlockMorph.prototype.parseSpec(aBlockSpec)),
              filtered = words.filter(
                  function (each) {return (each.indexOf('%') !== 0); }
              );
          return filtered.join(' ');
      }
  
      function fillDigits(anInt, totalDigits, fillChar) {
          var ans = String(anInt);
          while (ans.length < totalDigits) {ans = fillChar + ans; }
          return ans;
      }
  
      function relevance(aBlockLabel, aSearchString) {
          var lbl = ' ' + aBlockLabel,
              idx = lbl.indexOf(aSearchString),
              atWord;
          if (idx === -1) {return -1; }
          atWord = (lbl.charAt(idx - 1) === ' ');
          if (strictly && !atWord) {return -1; }
          return (atWord ? '1' : '2') + fillDigits(idx, 4, '0');
      }
  
      function primitive(selector) {
          var newBlock = SpriteMorph.prototype.blockForSelector(selector, true);
          newBlock.isTemplate = true;
          return newBlock;
      }
  
      // variable getters
      varNames.forEach(function (vName) {
          var rel = relevance(labelOf(vName.toLowerCase()), search);
          if (rel !== -1) {
              blocks.push([myself.variableBlock(vName), rel + '1']);
          }
      });
      // custom blocks
      [this.customBlocks, stage.globalBlocks].forEach(function (blocksList) {
          blocksList.forEach(function (definition) {
              if (contains(types, definition.type)) {
                  var spec = localize(definition.blockSpec()).toLowerCase(),
                      rel = relevance(labelOf(spec), search);
                  if (rel !== -1) {
                      blocks.push([definition.templateInstance(), rel + '2']);
                  }
              }
          });
      });
      // primitives
      blocksDict = SpriteMorph.prototype.blocks;
      Object.keys(blocksDict).forEach(function (selector) {
          if (!StageMorph.prototype.hiddenPrimitives[selector] &&
                  contains(types, blocksDict[selector].type)) {
              var block = blocksDict[selector],
                  spec = localize(block.alias || block.spec).toLowerCase(),
                  rel = relevance(labelOf(spec), search);
              if (
                  (rel !== -1) &&
                      (!block.dev) &&
                      (!block.only || (block.only === myself.constructor))
              ) {
                  blocks.push([primitive(selector), rel + '3']);
              }
          }
      });
      // infix arithmetic expression
      if (contains(types, 'reporter')) {
          reporterized = this.reporterize(searchString);
          if (reporterized) {
              // reporterized.isTemplate = true;
              // reporterized.isDraggable = false;
              blocks.push([reporterized, '']);
          }
      }
      blocks.sort(function (x, y) {return x[1] < y[1] ? -1 : 1; });
      return blocks.map(function (each) {return each[0]; });
  };
  
  SpriteMorph.prototype.searchBlocks = function (
      searchString,
      types,
      varNames,
      scriptFocus
  ) {
      var myself = this,
          unit = SyntaxElementMorph.prototype.fontSize,
          ide = this.parentThatIsA(IDE_Morph),
          oldSearch = '',
          searchBar = new InputFieldMorph(searchString || ''),
          searchPane = ide.createPalette('forSearch'),
          blocksList = [],
          selection,
          focus;
  
      function showSelection() {
          if (focus) {focus.destroy(); }
          if (!selection || !scriptFocus) {return; }
          focus = selection.outline(
              MorphicPreferences.isFlat ? new Color(150, 200, 255)
                      : new Color(255, 255, 255),
              2
          );
          searchPane.contents.add(focus);
          focus.scrollIntoView();
      }
  
      function show(blocks) {
          var oldFlag = Morph.prototype.trackChanges,
              x = searchPane.contents.left() + 5,
              y = (searchBar.bottom() + unit);
          blocksList = blocks;
          selection = null;
          if (blocks.length && scriptFocus) {
              selection = blocks[0];
          }
          Morph.prototype.trackChanges = false;
          searchPane.contents.children = [searchPane.contents.children[0]];
          blocks.forEach(function (block) {
              block.setPosition(new Point(x, y));
              searchPane.addContents(block);
              y += block.height();
              y += unit * 0.3;
          });
          Morph.prototype.trackChanges = oldFlag;
          showSelection();
          searchPane.changed();
      }
  
      searchPane.owner = this;
      searchPane.color = myself.paletteColor;
      searchPane.contents.color = myself.paletteColor;
      searchPane.addContents(searchBar);
      searchBar.drawNew();
      searchBar.setWidth(ide.logo.width() - 30);
      searchBar.contrast = 90;
      searchBar.setPosition(
          searchPane.contents.topLeft().add(new Point(10, 10))
      );
      searchBar.drawNew();
  
      searchPane.accept = function () {
          var search;
          if (scriptFocus) {
              searchBar.cancel();
              if (selection) {
                  scriptFocus.insertBlock(selection);
              }
          } else {
              search = searchBar.getValue();
              if (search.length > 0) {
                  show(myself.blocksMatching(search));
              }
          }
      };
  
      searchPane.reactToKeystroke = function (evt) {
          var search, idx, code = evt ? evt.keyCode : 0;
          switch (code) {
          case 38: // up arrow
              if (!scriptFocus || !selection) {return; }
              idx = blocksList.indexOf(selection) - 1;
              if (idx < 0) {
                  idx = blocksList.length - 1;
              }
              selection = blocksList[idx];
              showSelection();
              return;
          case 40: // down arrow
              if (!scriptFocus || !selection) {return; }
              idx = blocksList.indexOf(selection) + 1;
              if (idx >= blocksList.length) {
                  idx = 0;
              }
              selection = blocksList[idx];
              showSelection();
              return;
          default:
              search = searchBar.getValue();
              if (search !== oldSearch) {
                  oldSearch = search;
                  show(myself.blocksMatching(
                      search,
                      search.length < 2,
                      types,
                      varNames
                  ));
              }
          }
      };
  
      searchBar.cancel = function () {
          ide.refreshPalette();
          ide.palette.adjustScrollBars();
      };
  
      ide.fixLayout('refreshPalette');
      searchBar.edit();
      if (searchString) {searchPane.reactToKeystroke(); }
  };
  
  // SpritMorph parsing simple arithmetic expressions to reporter blocks
  
  SpriteMorph.prototype.reporterize = function (expressionString) {
      // highly experimental Christmas Easter Egg 2016 :-)
      var ast;
  
      function parseInfix(expression, operator, already) {
          // very basic diadic infix parser for arithmetic expressions
          // with strict left-to-right operator precedence (as in Smalltalk)
          // which can be overriden by - nested - parentheses.
          // assumes well-formed expressions, no graceful error handling yet.
  
          var inputs = ['', ''],
              idx = 0,
              ch;
  
          function format(value) {
              return value instanceof Array || isNaN(+value) ? value : +value;
          }
  
          function nested() {
              var level = 1,
                  expr = '';
              while (idx < expression.length) {
                  ch = expression[idx];
                  idx += 1;
                  switch (ch) {
                  case '(':
                      level += 1;
                      break;
                  case ')':
                      level -= 1;
                      if (!level) {
                          return expr;
                      }
                      break;
                  }
                  expr += ch;
              }
          }
  
          while (idx < expression.length) {
              ch = expression[idx];
              idx += 1;
              switch (ch) {
              case ' ':
                  break;
              case '(':
                  if (inputs[operator ? 1 : 0].length) {
                      inputs[operator ? 1 : 0] = [
                          inputs[operator ? 1 : 0],
                          parseInfix(nested())
                      ];
                  } else {
                      inputs[operator ? 1 : 0] = parseInfix(nested());
                  }
                  break;
              case '-':
              case '+':
              case '*':
              case '/':
              case '%':
              case '=':
              case '<':
              case '>':
              case '&':
              case '|':
                  if (!operator && !inputs[0].length) {
                      inputs[0] = ch;
                  } else if (operator) {
                      if (!inputs[1].length) {
                          inputs[1] = ch;
                      } else {
                          return parseInfix(
                              expression.slice(idx),
                              ch,
                              [operator, already, format(inputs[1])]
                          );
                      }
                  } else {
                      operator = ch;
                      already = format(inputs[0]);
                  }
                  break;
              default:
                  inputs[operator ? 1 : 0] += ch;
              }
          }
          if (operator) {
              return [operator, already, format(inputs[1])];
          }
          return format(inputs[0]);
      }
  
      function blockFromAST(ast) {
          var block, selectors, monads, alias, key, sel, i, inps,
              off = 1;
          selectors = {
              '+': 'reportSum',
              '-': 'reportDifference',
              '*': 'reportProduct',
              '/': 'reportQuotient',
              '%': 'reportModulus',
              '=': 'reportEquals',
              '<': 'reportLessThan',
              '>': 'reportGreaterThan',
              '&': 'reportAnd',
              '|': 'reportOr',
              round: 'reportRound',
              not: 'reportNot'
          };
          monads = ['abs', 'ceiling', 'floor', 'sqrt', 'sin', 'cos', 'tan',
              'asin', 'acos', 'atan', 'ln', 'log', 'round', 'not'];
          alias = {
              ceil: 'ceiling',
              '!' : 'not'
          };
          key = alias[ast[0]] || ast[0];
          if (contains(monads, key)) { // monadic
              sel = selectors[key];
              if (sel) { // single input
                  block = SpriteMorph.prototype.blockForSelector(sel);
                  inps = block.inputs();
              } else { // two inputs, first is function name
                  block = SpriteMorph.prototype.blockForSelector('reportMonadic');
                  inps = block.inputs();
                  inps[0].setContents([key]);
                  off = 0;
              }
          } else { // diadic
              block = SpriteMorph.prototype.blockForSelector(selectors[key]);
              inps = block.inputs();
          }
          for (i = 1; i < ast.length; i += 1) {
              if (ast[i] instanceof Array) {
                  block.silentReplaceInput(inps[i - off], blockFromAST(ast[i]));
              } else if (isString(ast[i])) {
                  if (contains(['true', 'false'], ast[i])) {
                      block.silentReplaceInput(
                          inps[i - off],
                          SpriteMorph.prototype.blockForSelector(
                              ast[i] === 'true' ? 'reportTrue' : 'reportFalse'
                          )
                      );
                  } else if (ast[i] !== '_') {
                      block.silentReplaceInput(
                          inps[i - off],
                          SpriteMorph.prototype.variableBlock(ast[i])
                      );
                  }
              } else { // number
                  inps[i - off].setContents(ast[i]);
              }
          }
          block.isDraggable = true;
          block.fixLayout();
          block.fixBlockColor(null, true);
          return block;
      }
  
      if (expressionString.length > 100) {return null; }
      try {
          ast = parseInfix(expressionString);
          return ast instanceof Array ? blockFromAST(ast) : null;
      } catch (error) {
          return null;
      }
  };
  
  // SpriteMorph variable management
  
  SpriteMorph.prototype.addVariable = function (name, isGlobal) {
      var ide = this.parentThatIsA(IDE_Morph);
      if (isGlobal) {
          this.globalVariables().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);
      if (!contains(this.inheritedVariableNames(true), varName)) {
          // check only shadowed variables
          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, true); // just me
      }
      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) {
      if (id instanceof Costume) { // allow first-class costumes
          this.wearCostume(id);
          return;
      }
  
      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.prototype.reportCostumes = function () {
      return this.costumes;
  };
  
  // 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.prototype.reportSounds = function () {
      return this.sounds;
  };
  
  // 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;
      }
      if (!this.isClone) {
          menu.addItem("duplicate", 'duplicate');
      }
      menu.addItem("delete", 'remove');
      menu.addItem("move", 'moveCenter');
      if (!this.isClone) {
          menu.addItem("edit", 'edit');
      }
      menu.addLine();
      if (this.anchor) {
          menu.addItem(
              localize('detach from') + ' ' + this.anchor.name,
              'detachFromAnchor'
          );
      }
      if (this.parts.length) {
          menu.addItem('detach all parts', 'detachAllParts');
      }
      menu.addItem("export...", 'exportSprite');
      return menu;
  };
  
  SpriteMorph.prototype.exportSprite = function () {
      if (this.isClone) {return; }
      var ide = this.parentThatIsA(IDE_Morph);
      if (ide) {
          ide.exportSprite(this);
      }
  };
  
  SpriteMorph.prototype.edit = function () {
      var ide = this.parentThatIsA(IDE_Morph);
      if (ide && !ide.isAppMode) {
          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
  
  /*
      clones are temporary, partially shallow copies of sprites that don't
      appear as icons in the corral. Clones get deleted when the red stop button
      is pressed. Shallow-copying clones' scripts and costumes makes spawning
      very fast, so they can be used for particle system simulations.
      This speed-up, however, comes at the cost of some detrimental side
      effects: Changes to a costume or a script of the original sprite are
      in some cases shared with all of its clones, however such shared changes
      are hard to predict for users and not actively propagated, so they don't
      offer any reliable feature, and will not be supported as such.
      Changes to the original sprite's scripts affect all of its clones, unless
      the script contains any custom block whose definition contains one or more
      block variables (in which case the script does get deep-copied).
      The original sprite's scripting area, costumes wardrobe or sounds jukebox
      are also not shared. therefore adding or deleting a script, sound or
      costume in the original sprite has no effect on any of its clones.
  */
  
  SpriteMorph.prototype.createClone = function (immediately) {
      var stage = this.parentThatIsA(StageMorph);
      if (stage && stage.cloneCount <= 2000) {
          this.fullCopy(true).clonify(stage, immediately);
      }
  };
  
  SpriteMorph.prototype.clonify = function (stage, immediately) {
      var hats;
      this.parts.forEach(function (part) {
          part.clonify(stage);
      });
      stage.cloneCount += 1;
      this.cloneOriginName = this.isClone ?
              this.cloneOriginName : this.name;
      this.isClone = true;
      this.name = '';
      stage.add(this);
      hats = this.allHatBlocksFor('__clone__init__');
      hats.forEach(function (block) {
          stage.threads.startProcess(
              block,
              stage.isThreadSafe,
              null, // export result
              null, // callback
              null, // is clicked
              immediately // without yielding
              );
      });
      this.endWarp();
  };
  
  SpriteMorph.prototype.removeClone = function () {
      if (this.isClone) {
          // this.stopTalking();
          this.parent.threads.stopAllForReceiver(this);
          this.corpsify();
          this.destroy();
          this.parent.cloneCount -= 1;
      }
  };
  
  // SpriteMorph deleting
  
  SpriteMorph.prototype.corpsify = function () {
      this.isCorpse = true;
      this.version = Date.now();
  };
  
  // SpriteMorph primitives
  
  // SpriteMorph hiding and showing:
  
  /*
      override the inherited behavior to also hide/show all
      nested parts.
  */
  
  SpriteMorph.prototype.hide = function () {
      SpriteMorph.uber.hide.call(this);
      this.parts.forEach(function (part) {part.hide(); });
  };
  
  SpriteMorph.prototype.show = function () {
      SpriteMorph.uber.show.call(this);
      this.parts.forEach(function (part) {part.show(); });
  };
  
  // SpriteMorph pen color
  
  SpriteMorph.prototype.setColor = function (aColor) {
      var x = this.xPosition(),
          y = this.yPosition();
      if (!this.color.eq(aColor)) {
          this.color = aColor.copy();
          if (!this.costume) {
              this.drawNew();
              this.silentGotoXY(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(+num || 0, 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() + (+delta || 0));
  };
  
  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(+num || 0, 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() + (+delta || 0));
  };
  
  // SpriteMorph layers
  
  SpriteMorph.prototype.comeToFront = function () {
      if (this.parent) {
          this.parent.add(this);
          this.changed();
      }
  };
  
  SpriteMorph.prototype.goBack = function (layers) {
      var layer,
          newLayer = +layers,
          targetLayer;
  
      if (!this.parent) {return null; }
      layer = this.parent.children.indexOf(this);
      this.parent.removeChild(this);
      targetLayer = Math.max(layer - newLayer, 0);
      this.parent.children.splice(targetLayer, 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(), true),
          ctx = oImg.getContext('2d');
  
      if (oRect.width() < 1 || oRect.height() < 1) {
          return newCanvas(new Point(1, 1), true);
      }
      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,
          originalAlpha = context.globalAlpha;
  
      if (isWarped) {
          this.endWarp();
      }
      context.save();
      context.scale(1 / stage.scale, 1 / stage.scale);
      context.globalAlpha = this.alpha;
      context.drawImage(
          this.image,
          (this.left() - stage.left()),
          (this.top() - stage.top())
      );
      context.globalAlpha = originalAlpha;
      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
      if (!isNaN(size)) {
          this.size = Math.min(Math.max(+size, 0.0001), 1000);
      }
  };
  
  SpriteMorph.prototype.changeSize = function (delta) {
      this.setSize(this.size + (+delta || 0));
  };
  
  // 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,
          realScale,
          growth;
  
      if (isWarped) {
          this.endWarp();
      }
      realScale = (+percentage || 0) / 100;
      growth = realScale / this.nestingScale;
      this.nestingScale = realScale;
      this.scale = Math.max(realScale, 0.01);
  
      // apply to myself
      this.changed();
      this.drawNew();
      this.changed();
      if (isWarped) {
          this.startWarp();
      }
      this.silentGotoXY(x, y, true); // just me
      this.positionTalkBubble();
  
      // propagate to nested parts
      this.parts.forEach(function (part) {
          var xDist = part.xPosition() - x,
              yDist = part.yPosition() - y;
          part.setScale(part.scale * 100 * growth);
          part.silentGotoXY(
              x + (xDist * growth),
              y + (yDist * growth)
          );
      });
  };
  
  SpriteMorph.prototype.changeScale = function (delta) {
      this.setScale(this.getScale() + (+delta || 0));
  };
  
  // Spritemorph graphic effects
  
  SpriteMorph.prototype.graphicsChanged = function () {
      var myself = this;
      return Object.keys(this.graphicsValues).some(
          function (any) {
              return myself.graphicsValues[any] < 0 ||
                      myself.graphicsValues[any] > 0;
          }
      );
  };
  
  SpriteMorph.prototype.applyGraphicsEffects = function (canvas) {
    // For every effect: apply transform of that effect(canvas, stored value)
    // Graphic effects from Scratch are heavily based on ScratchPlugin.c
  
      var ctx, imagedata;
  
      function transform_fisheye(imagedata, value) {
          var pixels, newImageData, newPixels, centerX, centerY,
              w, h, x, y, dx, dy, r, angle, srcX, srcY, i, srcI;
  
          w = imagedata.width;
          h = imagedata.height;
          pixels = imagedata.data;
          newImageData = ctx.createImageData(w, h);
          newPixels = newImageData.data;
  
          centerX = w / 2;
          centerY = h / 2;
          value = Math.max(0, (value + 100) / 100);
          for (y = 0; y < h; y++) {
              for (x = 0; x < w; x++) {
                  dx = (x - centerX) / centerX;
                  dy = (y - centerY) / centerY;
                  r = Math.pow(Math.sqrt(dx * dx + dy * dy), value);
                  if (r <= 1) {
                      angle = Math.atan2(dy, dx);
                      srcX = Math.floor(
                          centerX + (r * Math.cos(angle) * centerX)
                      );
                      srcY = Math.floor(
                          centerY + (r * Math.sin(angle) * centerY)
                      );
                  } else {
                      srcX = x;
                      srcY = y;
                  }
                  i = (y * w + x) * 4;
                  srcI = (srcY * w + srcX) * 4;
                  newPixels[i] = pixels[srcI];
                  newPixels[i + 1] = pixels[srcI + 1];
                  newPixels[i + 2] = pixels[srcI + 2];
                  newPixels[i + 3] = pixels[srcI + 3];
              }
          }
          return newImageData;
      }
  
      function transform_whirl(imagedata, value) {
          var pixels, newImageData, newPixels, w, h, centerX, centerY,
              x, y, radius, scaleX, scaleY, whirlRadians, radiusSquared,
              dx, dy, d, factor, angle, srcX, srcY, i, srcI, sina, cosa;
  
          w = imagedata.width;
          h = imagedata.height;
          pixels = imagedata.data;
          newImageData = ctx.createImageData(w, h);
          newPixels = newImageData.data;
  
          centerX = w / 2;
          centerY = h / 2;
          radius = Math.min(centerX, centerY);
          if (w < h) {
              scaleX = h / w;
              scaleY = 1;
          } else {
              scaleX = 1;
              scaleY = w / h;
          }
          whirlRadians = -radians(value);
          radiusSquared = radius * radius;
          for (y = 0; y < h; y++) {
              for (x = 0; x < w; x++) {
                  dx = scaleX * (x - centerX);
                  dy = scaleY * (y - centerY);
                  d = dx * dx + dy * dy;
                  if (d < radiusSquared) {
                      factor = 1 - (Math.sqrt(d) / radius);
                      angle = whirlRadians * (factor * factor);
                      sina = Math.sin(angle);
                      cosa = Math.cos(angle);
                      srcX = Math.floor(
                          (cosa * dx - sina * dy) / scaleX + centerX
                      );
                      srcY = Math.floor(
                          (sina * dx + cosa * dy) / scaleY + centerY
                      );
                  } else {
                      srcX = x;
                      srcY = y;
                  }
                  i = (y * w + x) * 4;
                  srcI = (srcY * w + srcX) * 4;
                  newPixels[i] = pixels[srcI];
                  newPixels[i + 1] = pixels[srcI + 1];
                  newPixels[i + 2] = pixels[srcI + 2];
                  newPixels[i + 3] = pixels[srcI + 3];
              }
          }
          return newImageData;
      }
  
      function transform_pixelate(imagedata, value) {
          var pixels, newImageData, newPixels, w, h,
              x, y, srcX, srcY, i, srcI;
  
          w = imagedata.width;
          h = imagedata.height;
          pixels = imagedata.data;
          newImageData = ctx.createImageData(w, h);
          newPixels = newImageData.data;
  
          value = Math.floor(Math.abs(value / 10) + 1);
          for (y = 0; y < h; y++) {
              for (x = 0; x < w; x++) {
                  srcX = Math.floor(x / value) * value;
                  srcY = Math.floor(y / value) * value;
                  i = (y * w + x) * 4;
                  srcI = (srcY * w + srcX) * 4;
                  newPixels[i] = pixels[srcI];
                  newPixels[i + 1] = pixels[srcI + 1];
                  newPixels[i + 2] = pixels[srcI + 2];
                  newPixels[i + 3] = pixels[srcI + 3];
              }
          }
          return newImageData;
      }
  
      function transform_mosaic(imagedata, value) {
          var pixels, i, l, newImageData, newPixels, srcI;
          pixels = imagedata.data;
          newImageData = ctx.createImageData(imagedata.width, imagedata.height);
          newPixels = newImageData.data;
  
          value = Math.round((Math.abs(value) + 10) / 10);
          value = Math.max(
              0,
              Math.min(value, Math.min(imagedata.width, imagedata.height))
          );
          for (i = 0, l = pixels.length; i < l; i += 4) {
              srcI = i * value % l;
              newPixels[i] = pixels[srcI];
              newPixels[i + 1] = pixels[srcI + 1];
              newPixels[i + 2] = pixels[srcI + 2];
              newPixels[i + 3] = pixels[srcI + 3];
          }
          return newImageData;
      }
  
      function transform_duplicate(imagedata, value) {
          var pixels, i;
          pixels = imagedata.data;
          for (i = 0; i < pixels.length; i += 4) {
              pixels[i] = pixels[i * value];
              pixels[i + 1] = pixels[i * value + 1];
              pixels[i + 2] = pixels[i * value + 2];
              pixels[i + 3] = pixels[i * value + 3];
          }
          return imagedata;
      }
  
      function transform_HSV(
              imagedata,
              hueShift,
              saturationShift,
              brightnessShift
      ) {
          var pixels, index, l, r, g, b, max, min, span,
              h, s, v, i, f, p, q, t, newR, newG, newB;
          pixels = imagedata.data;
          for (index = 0, l = pixels.length; index < l; index += 4) {
              r = pixels[index];
              g = pixels[index + 1];
              b = pixels[index + 2];
  
              max = Math.max(r, g, b);
              min = Math.min(r, g, b);
              span = max - min;
              if (span === 0) {
                  h = s = 0;
              } else {
                  if (max === r) {
                      h = (60 * (g - b)) / span;
                  } else if (max === g) {
                      h = 120 + ((60 * (b - r)) / span);
                  } else if (max === b) {
                      h = 240 + ((60 * (r - g)) / span);
                  }
                  s = (max - min) / max;
              }
              if (h < 0) {
                  h += 360;
              }
              v = max / 255;
  
              h = (h + hueShift * 360 / 200) % 360;
              s = Math.max(0, Math.min(s + saturationShift / 100, 1));
              v = Math.max(0, Math.min(v + brightnessShift / 100, 1));
  
              i = Math.floor(h / 60);
              f = (h / 60) - i;
              p = v * (1 - s);
              q = v * (1 - (s * f));
              t = v * (1 - (s * (1 - f)));
  
              if (i === 0 || i === 6) {
                  newR = v;
                  newG = t;
                  newB = p;
              } else if (i === 1) {
                  newR = q;
                  newG = v;
                  newB = p;
              } else if (i === 2) {
                  newR = p;
                  newG = v;
                  newB = t;
              } else if (i === 3) {
                  newR = p;
                  newG = q;
                  newB = v;
              } else if (i === 4) {
                  newR = t;
                  newG = p;
                  newB = v;
              } else if (i === 5) {
                  newR = v;
                  newG = p;
                  newB = q;
              }
  
              pixels[index] = newR * 255;
              pixels[index + 1] = newG * 255;
              pixels[index + 2] = newB * 255;
          }
          return imagedata;
      }
  
      function transform_negative (imagedata, value) {
          var pixels, i, l, rcom, gcom, bcom;
          pixels = imagedata.data;
          for (i = 0, l = pixels.length; i < l; i += 4) {
              rcom = 255 - pixels[i];
              gcom = 255 - pixels[i + 1];
              bcom = 255 - pixels[i + 2];
  
              if (pixels[i] < rcom) { //compare to the complement
                  pixels[i] += value;
              } else if (pixels[i] > rcom) {
                  pixels[i] -= value;
              }
              if (pixels[i + 1] < gcom) {
                  pixels[i + 1] += value;
              } else if (pixels[i + 1] > gcom) {
                  pixels[i + 1] -= value;
              }
              if (pixels[i + 2] < bcom) {
                  pixels[i + 2] += value;
              } else if (pixels[i + 2] > bcom) {
                  pixels[i + 2] -= value;
              }
          }
          return imagedata;
      }
  
      function transform_comic (imagedata, value) {
          var pixels, i, l;
          pixels = imagedata.data;
          for (i = 0, l = pixels.length; i < l; i += 4) {
              pixels[i] += Math.sin(i * value) * 127 + 128;
              pixels[i + 1] += Math.sin(i * value) * 127 + 128;
              pixels[i + 2] += Math.sin(i * value) * 127 + 128;
          }
          return imagedata;
      }
  
      function transform_confetti (imagedata, value) {
          var pixels, i, l;
          pixels = imagedata.data;
          for (i = 0, l = pixels.length; i < l; i += 1) {
              pixels[i] = Math.sin(value * pixels[i]) * 127 + pixels[i];
          }
          return imagedata;
      }
  
      if (this.graphicsChanged()) {
          ctx = canvas.getContext("2d");
          imagedata = ctx.getImageData(0, 0, canvas.width, canvas.height);
  
          if (this.graphicsValues.fisheye) {
              imagedata = transform_fisheye(
                  imagedata,
                  this.graphicsValues.fisheye
              );
          }
          if (this.graphicsValues.whirl) {
              imagedata = transform_whirl(
                  imagedata,
                  this.graphicsValues.whirl
              );
          }
          if (this.graphicsValues.pixelate) {
              imagedata = transform_pixelate(
                  imagedata,
                  this.graphicsValues.pixelate
              );
          }
          if (this.graphicsValues.mosaic) {
              imagedata = transform_mosaic(
                  imagedata,
                  this.graphicsValues.mosaic
              );
          }
          if (this.graphicsValues.duplicate) {
              imagedata = transform_duplicate(
                  imagedata,
                  this.graphicsValues.duplicate
              );
          }
          if (this.graphicsValues.color ||
                  this.graphicsValues.saturation ||
                  this.graphicsValues.brightness) {
              imagedata = transform_HSV(
                  imagedata,
                  this.graphicsValues.color,
                  this.graphicsValues.saturation,
                  this.graphicsValues.brightness
              );
          }
          if (this.graphicsValues.negative) {
              imagedata = transform_negative(
                  imagedata,
                  this.graphicsValues.negative
              );
          }
          if (this.graphicsValues.comic) {
              imagedata = transform_comic(
                  imagedata,
                  this.graphicsValues.comic
              );
          }
          if (this.graphicsValues.confetti) {
              imagedata = transform_confetti(
                  imagedata,
                  this.graphicsValues.confetti
              );
          }
  
          ctx.putImageData(imagedata, 0, 0);
      }
  
      return canvas;
  };
  
  SpriteMorph.prototype.setEffect = function (effect, value) {
      var eff = effect instanceof Array ? effect[0] : null;
      if (eff === 'ghost') {
          this.alpha = 1 - Math.min(Math.max(+value || 0, 0), 100) / 100;
      } else {
          this.graphicsValues[eff] = +value;
      }
      this.drawNew();
      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() + (+value || 0));
      } else {
          this.setEffect(effect, +this.graphicsValues[eff] + (+value));
      }
  };
  
  SpriteMorph.prototype.clearEffects = function () {
      var effect;
      for (effect in this.graphicsValues) {
          if (this.graphicsValues.hasOwnProperty(effect)) {
              this.setEffect([effect], 0);
          }
      }
      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,
          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),
          stageScale = stage ? stage.scale : 1,
          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(stageScale));
      }
      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 and parts
  
  SpriteMorph.prototype.prepareToBeGrabbed = function (hand) {
      this.removeShadow();
      this.recordLayers();
      if (!this.bounds.containsPoint(hand.position()) &&
              this.isCorrectingOutsideDrag()) {
          this.setCenter(hand.position());
      }
      this.addShadow();
  };
  
  SpriteMorph.prototype.isCorrectingOutsideDrag = function () {
      // make sure I don't "trail behind" the hand when dragged
      // override for morphs that you want to be dragged outside
      // their full bounds
      return !this.parts.length;
  };
  
  SpriteMorph.prototype.justDropped = function () {
      var stage = this.parentThatIsA(StageMorph);
      if (stage) {
          stage.enableCustomHatBlocks = true;
      }
      this.restoreLayers();
      this.positionTalkBubble();
      this.receiveUserInteraction('dropped');
  };
  
  // 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();
          if (this.useFlatLineEnds) {
              context.lineCap = 'butt';
              context.lineJoin = 'miter';
          } else {
              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.prototype.floodFill = function () {
      if (!this.parent.bounds.containsPoint(this.rotationCenter())) {
          return;
      }
      var layer = normalizeCanvas(this.parent.penTrails()),
          width = layer.width,
          height = layer.height,
          ctx = layer.getContext('2d'),
          img = ctx.getImageData(0, 0, width, height),
          dta = img.data,
          stack = [
              ((height / 2) - Math.round(this.yPosition())) * width +
              Math.round(this.xPosition() + (width / 2))
          ],
          current,
          src;
  
      function read(p) {
          var d = p * 4;
          return [dta[d], dta[d + 1], dta[d + 2], dta[d + 3]];
      }
  
      function check(p) {
          return p[0] === src[0] &&
              p[1] === src[1] &&
              p[2] === src[2] &&
              p[3] === src[3];
      }
  
      src = read(stack[0]);
      if (src[0] === Math.round(this.color.r) &&
              src[1] === Math.round(this.color.g) &&
              src[2] === Math.round(this.color.b) &&
              src[3] === Math.round(this.color.a * 255)) {
          return;
      }
      while (stack.length > 0) {
          current = stack.pop();
          if (check(read(current))) {
              if (current % width > 1) {
                  stack.push(current + 1);
                  stack.push(current - 1);
              }
              if (current > 0 && current < height * width) {
                  stack.push(current + width);
                  stack.push(current - width);
              }
          }
          dta[current * 4] = Math.round(this.color.r);
          dta[current * 4 + 1] = Math.round(this.color.g);
          dta[current * 4 + 2] = Math.round(this.color.b);
          dta[current * 4 + 3] = Math.round(this.color.a * 255);
      }
      ctx.putImageData(img, 0, 0);
      this.parent.changed();
  };
  
  // SpriteMorph motion - adjustments due to nesting
  
  SpriteMorph.prototype.moveBy = function (delta, justMe) {
      // override the inherited default to make sure my parts follow
      // unless it's justMe (a correction)
      var start = this.isDown && !justMe && this.parent ?
              this.rotationCenter() : null;
      SpriteMorph.uber.moveBy.call(this, delta);
      if (start) {
          this.drawLine(start, this.rotationCenter());
      }
      if (!justMe) {
          this.parts.forEach(function (part) {
              part.moveBy(delta);
          });
      }
  };
  
  SpriteMorph.prototype.silentMoveBy = function (delta, justMe) {
      SpriteMorph.uber.silentMoveBy.call(this, delta);
      if (!justMe && this.parent instanceof HandMorph) {
          this.parts.forEach(function (part) {
              part.moveBy(delta);
          });
      }
  };
  
  SpriteMorph.prototype.rootForGrab = function () {
      if (this.anchor) {
          return this.anchor.rootForGrab();
      }
      return SpriteMorph.uber.rootForGrab.call(this);
  };
  
  SpriteMorph.prototype.setCenter = function (aPoint, justMe) {
      // override the inherited default to make sure my parts follow
      // unless it's justMe
      var delta = aPoint.subtract(this.center());
      this.moveBy(delta, justMe);
  };
  
  SpriteMorph.prototype.nestingBounds = function () {
      // same as fullBounds(), except that it uses "parts" instead of children
      // and special cases the costume-less "arrow" shape's bounding box
      var result = this.bounds;
      if (!this.costume && this.penBounds) {
          result = this.penBounds.translateBy(this.position());
      }
      this.parts.forEach(function (part) {
          if (part.isVisible) {
              result = result.merge(part.nestingBounds());
          }
      });
      return result;
  };
  
  // SpriteMorph motion primitives
  
  SpriteMorph.prototype.setPosition = function (aPoint, justMe) {
      // override the inherited default to make sure my parts follow
      // unless it's justMe
      var delta = aPoint.subtract(this.topLeft());
      if ((delta.x !== 0) || (delta.y !== 0)) {
          this.moveBy(delta, justMe);
      }
  };
  
  SpriteMorph.prototype.forward = function (steps) {
      var dest,
          dist = steps * this.parent.scale || 0;
  
      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.positionTalkBubble();
  };
  
  SpriteMorph.prototype.setHeading = function (degrees) {
      var x = this.xPosition(),
          y = this.yPosition(),
          dir = (+degrees || 0),
          turn = dir - this.heading;
  
      // apply to myself
      if (this.rotationStyle) { // optimization, only redraw if rotatable
          this.changed();
          SpriteMorph.uber.setHeading.call(this, dir);
          this.silentGotoXY(x, y, true); // just me
          this.positionTalkBubble();
      } else {
          this.heading = parseFloat(degrees) % 360;
      }
  
      // propagate to my parts
      this.parts.forEach(function (part) {
          var pos = new Point(part.xPosition(), part.yPosition()),
              trg = pos.rotateBy(radians(turn), new Point(x, y));
          if (part.rotatesWithAnchor) {
              part.turn(turn);
          }
          part.gotoXY(trg.x, trg.y);
      });
  };
  
  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 + (+degrees || 0));
  };
  
  SpriteMorph.prototype.turnLeft = function (degrees) {
      this.setHeading(this.heading - (+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, justMe) {
      var stage = this.parentThatIsA(StageMorph),
          newX,
          newY,
          dest;
  
      if (!stage) {return; }
      newX = stage.center().x + (+x || 0) * stage.scale;
      newY = stage.center().y - (+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, justMe);
      this.positionTalkBubble();
  };
  
  SpriteMorph.prototype.silentGotoXY = function (x, y, justMe) {
      // move without drawing
      var penState = this.isDown;
      this.isDown = false;
      this.gotoXY(x, y, justMe);
      this.isDown = penState;
  };
  
  SpriteMorph.prototype.setXPosition = function (num) {
      this.gotoXY(+num || 0, this.yPosition());
  };
  
  SpriteMorph.prototype.changeXPosition = function (delta) {
      this.setXPosition(this.xPosition() + (+delta || 0));
  };
  
  SpriteMorph.prototype.setYPosition = function (num) {
      this.gotoXY(this.xPosition(), +num || 0);
  };
  
  SpriteMorph.prototype.changeYPosition = function (delta) {
      this.setYPosition(this.yPosition() + (+delta || 0));
  };
  
  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 () {
      // taking nested parts into account
      var stage = this.parentThatIsA(StageMorph),
          fb = this.nestingBounds(),
          dirX,
          dirY;
  
      if (!stage) {return null; }
      if (stage.bounds.containsRectangle(fb)) {return null; }
  
      dirX = Math.cos(radians(this.heading - 90));
      dirY = -(Math.sin(radians(this.heading - 90)));
  
      if (fb.left() < stage.left()) {
          dirX = Math.abs(dirX);
      }
      if (fb.right() > stage.right()) {
          dirX = -(Math.abs(dirX));
      }
      if (fb.top() < stage.top()) {
          dirY = -(Math.abs(dirY));
      }
      if (fb.bottom() > stage.bottom()) {
          dirY = Math.abs(dirY);
      }
  
      this.setHeading(degrees(Math.atan2(-dirY, dirX)) + 90);
      this.setPosition(this.position().add(
          fb.amountToTranslateWithin(stage.bounds)
      ));
      this.positionTalkBubble();
  };
  
  // SpriteMorph rotation center / fixation point manipulation
  
  SpriteMorph.prototype.setRotationX = function (absoluteX) {
    this.setRotationCenter(new Point(absoluteX, this.yPosition()));
  };
  
  SpriteMorph.prototype.setRotationY = function (absoluteY) {
    this.setRotationCenter(new Point(this.xPosition(), absoluteY));
  };
  
  SpriteMorph.prototype.setRotationCenter = function (absoluteCoordinate) {
      var delta, normal;
      if (!this.costume) {
          throw new Error('setting the rotation center requires a costume');
      }
      delta = absoluteCoordinate.subtract(
          new Point(this.xPosition(), this.yPosition())
      ).divideBy(this.scale).rotateBy(radians(90 - this.heading));
      normal = this.costume.rotationCenter.add(new Point(delta.x, -delta.y));
      this.costume.rotationCenter = normal;
      this.drawNew();
  };
  
  SpriteMorph.prototype.xCenter = 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.center().x - stage.center().x) / stage.scale;
      }
      return this.center().x;
  };
  
  SpriteMorph.prototype.yCenter = 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.center().y) / stage.scale;
      }
      return this.center().y;
  };
  
  // SpriteMorph message broadcasting
  
  SpriteMorph.prototype.allMessageNames = function () {
      var msgs = [],
          all = this.scripts.children.slice();
      this.customBlocks.forEach(function (def) {
          if (def.body) {
              all.push(def.body.expression);
          }
          def.scripts.forEach(function (scr) {
              all.push(scr);
          });
      });
      if (this.globalBlocks) {
          this.globalBlocks.forEach(function (def) {
              if (def.body) {
                  all.push(def.body.expression);
              }
              def.scripts.forEach(function (scr) {
                  all.push(scr);
              });
          });
      }
      all.forEach(function (script) {
          script.allChildren().forEach(function (morph) {
              var txt;
              if (morph.selector && contains(
                  ['receiveMessage', 'doBroadcast', 'doBroadcastAndWait'],
                  morph.selector
              )) {
                  txt = morph.inputs()[0].evaluate();
                  if (isString(txt) && txt !== '') {
                      if (!contains(msgs, txt)) {
                          msgs.push(txt);
                      }
                  }
              }
          });
      });
      return msgs;
  };
  
  SpriteMorph.prototype.allHatBlocksFor = function (message) {
      if (typeof message === 'number') {message = message.toString(); }
      return this.scripts.children.filter(function (morph) {
          var event;
          if (morph.selector) {
              if (morph.selector === 'receiveMessage') {
                  event = morph.inputs()[0].evaluate();
                  return event === message
                      || (event instanceof Array
                          && message !== '__shout__go__'
                          && message !== '__clone__init__');
              }
              if (morph.selector === 'receiveGo') {
                  return message === '__shout__go__';
              }
              if (morph.selector === 'receiveOnClone') {
                  return message === '__clone__init__';
              }
          }
          return false;
      });
  };
  
  SpriteMorph.prototype.allHatBlocksForKey = function (key) {
      return this.scripts.children.filter(function (morph) {
          if (morph.selector) {
              if (morph.selector === 'receiveKey') {
                  var evt = morph.inputs()[0].evaluate()[0];
                  return evt === key || evt === 'any key';
              }
          }
          return false;
      });
  };
  
  SpriteMorph.prototype.allHatBlocksForInteraction = function (interaction) {
      return this.scripts.children.filter(function (morph) {
          if (morph.selector) {
              if (morph.selector === 'receiveInteraction') {
                  return morph.inputs()[0].evaluate()[0] === interaction;
              }
          }
          return false;
      });
  };
  
  SpriteMorph.prototype.allGenericHatBlocks = function () {
      return this.scripts.children.filter(function (morph) {
          if (morph.selector) {
              return morph.selector === 'receiveCondition';
          }
          return false;
      });
  };
  
  // SpriteMorph events
  
  SpriteMorph.prototype.mouseClickLeft = function () {
      return this.receiveUserInteraction('clicked');
  };
  
  SpriteMorph.prototype.mouseEnter = function () {
      return this.receiveUserInteraction('mouse-entered');
  };
  
  SpriteMorph.prototype.mouseDownLeft = function () {
      return this.receiveUserInteraction('pressed');
  };
  
  SpriteMorph.prototype.receiveUserInteraction = function (interaction) {
      var stage = this.parentThatIsA(StageMorph),
          procs = [],
          hats;
      if (!stage) {return; } // currently dragged
      hats = this.allHatBlocksForInteraction(interaction);
      hats.forEach(function (block) {
          procs.push(stage.threads.startProcess(block, stage.isThreadSafe));
      });
      return procs;
  };
  
  SpriteMorph.prototype.mouseDoubleClick = function () {
      if (this.isClone) {return; }
      this.edit();
  };
  
  // 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 last message
  
  SpriteMorph.prototype.getLastMessage = function () {
      var stage = this.parentThatIsA(StageMorph);
      if (stage) {
          return stage.getLastMessage();
      }
      return '';
  };
  
  // SpriteMorph user prompting
  
  SpriteMorph.prototype.getLastAnswer = function () {
      return this.parentThatIsA(StageMorph).lastAnswer;
  };
  
  // SpriteMorph mouse coordinates
  
  SpriteMorph.prototype.reportMouseX = function () {
      var stage = this.parentThatIsA(StageMorph);
      if (stage) {
          return stage.reportMouseX();
      }
      return 0;
  };
  
  SpriteMorph.prototype.reportMouseY = function () {
      var stage = this.parentThatIsA(StageMorph);
      if (stage) {
          return stage.reportMouseY();
      }
      return 0;
  };
  
  // SpriteMorph thread count (for debugging)
  
  SpriteMorph.prototype.reportThreadCount = function () {
      var stage = this.parentThatIsA(StageMorph);
      if (stage) {
          return stage.threads.processes.length;
      }
      return 0;
  };
  
  // SpriteMorph variable watchers (for palette checkbox toggling)
  
  SpriteMorph.prototype.findVariableWatcher = function (varName) {
      var stage = this.parentThatIsA(StageMorph),
          globals = this.globalVariables(),
          myself = this;
      if (stage === null) {
          return null;
      }
      return detect(
          stage.children,
          function (morph) {
              return morph instanceof WatcherMorph
                      && (morph.target === myself.variables
                              || morph.target === globals)
                      && morph.getter === varName;
          }
      );
  };
  
  SpriteMorph.prototype.toggleVariableWatcher = function (varName, isGlobal) {
      var stage = this.parentThatIsA(StageMorph),
          globals = this.globalVariables(),
          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
              watcher.keepWithin(stage);
          }
          return;
      }
  
      // if no watcher exists, create a new one
      if (isNil(isGlobal)) {
          isGlobal = contains(globals.names(), varName);
      }
      watcher = new WatcherMorph(
          varName,
          this.blockColor.variables,
          isGlobal ? globals : 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();
      watcher.keepWithin(stage);
      return watcher;
  };
  
  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
              watcher.keepWithin(stage);
          }
          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();
      watcher.keepWithin(stage);
  };
  
  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,
      forRemoval, // optional bool
      skipGlobals, // optional bool
      skipBlocks // optional array with ignorable definitions
  ) {
      var inDefinitions,
          inScripts = detect(
              this.scripts.allChildren(),
              function (c) {
                  return c.definition && (c.definition === definition);
              }
          );
  
      if (inScripts) {return true; }
  
      if (definition.isGlobal && !skipGlobals) {
          inDefinitions = [];
          this.parentThatIsA(StageMorph).globalBlocks.forEach(
              function (def) {
                  if (forRemoval && (definition === def)) {return; }
                  if (skipBlocks && contains(skipBlocks, def)) {return; }
                  if (def.body) {
                      def.body.expression.allChildren().forEach(function (c) {
                          if (c.definition && (c.definition === definition)) {
                              inDefinitions.push(c);
                          }
                      });
                  }
              }
          );
          if (inDefinitions.length > 0) {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 inheritance - general
  
  SpriteMorph.prototype.chooseExemplar = function () {
      var stage = this.parentThatIsA(StageMorph),
          myself = this,
          other = stage.children.filter(function (m) {
              return m instanceof SpriteMorph &&
                  (!contains(m.allExemplars(), myself));
          }),
          menu;
      menu = new MenuMorph(
          function (aSprite) {myself.setExemplar(aSprite); },
          localize('current parent') +
              ':\n' +
              (this.exemplar ? this.exemplar.name : localize('none'))
      );
      other.forEach(function (eachSprite) {
          menu.addItem(eachSprite.name, eachSprite);
      });
      menu.addLine();
      menu.addItem(localize('none'), null);
      menu.popUpAtHand(this.world());
  };
  
  SpriteMorph.prototype.setExemplar = function (another) {
      var ide = this.parentThatIsA(IDE_Morph);
      this.exemplar = another;
      if (isNil(another)) {
          this.variables.parentFrame = (this.globalVariables());
      } else {
          this.variables.parentFrame = (another.variables);
      }
      if (ide) {
          ide.flushBlocksCache('variables');
          ide.refreshPalette();
      }
  };
  
  SpriteMorph.prototype.allExemplars = function () {
      // including myself
      var all = [],
          current = this;
      while (!isNil(current)) {
          all.push(current);
          current = current.exemplar;
      }
      return all;
  };
  
  SpriteMorph.prototype.specimens = function () {
      // without myself
      var myself = this;
      return this.siblings().filter(function (m) {
          return m instanceof SpriteMorph && (m.exemplar === myself);
      });
  };
  
  SpriteMorph.prototype.allSpecimens = function () {
      // without myself
      var myself = this;
      return this.siblings().filter(function (m) {
          return m instanceof SpriteMorph && contains(m.allExemplars(), myself);
      });
  };
  
  // SpriteMorph inheritance - variables
  
  SpriteMorph.prototype.isVariableNameInUse = function (vName, isGlobal) {
      if (isGlobal) {
          return contains(this.variables.allNames(), vName);
      }
      if (contains(this.variables.names(), vName)) {return true; }
      return contains(this.globalVariables().names(), vName);
  };
  
  SpriteMorph.prototype.globalVariables = function () {
      var current = this.variables.parentFrame;
      while (current.owner) {
          current = current.parentFrame;
      }
      return current;
  };
  
  SpriteMorph.prototype.shadowVar = function (name, value) {
      var ide = this.parentThatIsA(IDE_Morph);
      this.variables.addVar(name, value);
      if (ide) {
          ide.flushBlocksCache('variables');
          ide.refreshPalette();
      }
  };
  
  SpriteMorph.prototype.inheritedVariableNames = function (shadowedOnly) {
      var names = [],
          own = this.variables.names(),
          current = this.variables.parentFrame;
  
      function test(each) {
          return shadowedOnly ? contains(own, each) : !contains(own, each);
      }
  
      while (current.owner instanceof SpriteMorph) {
          names.push.apply(
              names,
              current.names().filter(test)
          );
          current = current.parentFrame;
      }
      return names;
  };
  
  SpriteMorph.prototype.deletableVariableNames = function () {
      var locals = this.variables.names(),
          inherited = this.inheritedVariableNames();
      return locals.concat(
          this.globalVariables().names().filter(
              function (each) {
                  return !contains(locals, each) && !contains(inherited, each);
              }
          )
      );
  };
  
  SpriteMorph.prototype.hasSpriteVariable = function (varName) {
      return contains(this.variables.names(), varName);
  };
  
  // Variable refactoring
  
  SpriteMorph.prototype.refactorVariableInstances = function (
      oldName,
      newName,
      isGlobal
  ) {
      if (isGlobal && this.hasSpriteVariable(oldName)) {
          return;
      }
  
      this.scripts.children.forEach(function (child) {
          if (child instanceof BlockMorph) {
              child.refactorVarInStack(oldName, newName);
          }
      });
  
  };
  
  // SpriteMorph inheritance - custom blocks
  
  /*
  // under construction, commented out for now
  
  SpriteMorph.prototype.ownBlocks = function () {
      var dict = {};
      this.customBlocks.forEach(function (def) {
          dict[def.blockSpec()] = def;
      });
      return dict;
  };
  
  SpriteMorph.prototype.allBlocks = function (valuesOnly) {
      var dict = {};
      this.allExemplars().reverse().forEach(function (sprite) {
          sprite.customBlocks.forEach(function (def) {
              dict[def.blockSpec()] = def;
          });
      });
      if (valuesOnly) {
          return Object.keys(dict).map(function (key) {return dict[key]; });
      }
      return dict;
  };
  
  SpriteMorph.prototype.inheritedBlocks = function (valuesOnly) {
      var dict = {},
          own = Object.keys(this.ownBlocks()),
          others = this.allExemplars().reverse();
      others.pop();
      others.forEach(function (sprite) {
          sprite.customBlocks.forEach(function (def) {
              var spec = def.blockSpec();
              if (!contains(own, spec)) {
                  dict[spec] = def;
              }
          });
      });
      if (valuesOnly) {
          return Object.keys(dict).map(function (key) {return dict[key]; });
      }
      return dict;
  };
  
  */
  
  // 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');
  
      function xOut(style, alpha, width) {
          var inset = Math.min(extentPoint.x, extentPoint.y) / 10;
          ctx.strokeStyle = style;
          ctx.globalAlpha = alpha;
          ctx.compositeOperation = 'lighter';
          ctx.lineWidth = width || 1;
          ctx.moveTo(inset, inset);
          ctx.lineTo(trg.width - inset, trg.height - inset);
          ctx.moveTo(inset, trg.height - inset);
          ctx.lineTo(trg.width - inset, inset);
          ctx.stroke();
      }
  
      ctx.save();
      if (this.isCorpse) {
          ctx.globalAlpha = 0.3;
      }
      if (src.width && src.height) {
          ctx.scale(scale, scale);
          ctx.drawImage(
              src,
              Math.floor(xOffset / scale),
              Math.floor(yOffset / scale)
          );
      }
      if (this.isCorpse) {
          ctx.restore();
          xOut('white', 0.8, 6);
          xOut('black', 0.8, 1);
      }
      return trg;
  };
  
  SpriteMorph.prototype.fullThumbnail = function (extentPoint) {
      // containing parts and anchor symbols, if any
      var thumb = this.thumbnail(extentPoint),
          ctx = thumb.getContext('2d'),
          ext = extentPoint.divideBy(3),
          i = 0;
  
      ctx.restore();
      if (this.anchor) {
          ctx.drawImage(
              this.anchor.thumbnail(ext),
              0,
              0
          );
      }
      for (i = 0; i < 3; i += 1) {
          if (this.parts[i]) {
              ctx.drawImage(
                  this.parts[i].thumbnail(ext),
                  i * ext.x,
                  extentPoint.y - ext.y
              );
          }
      }
      return thumb;
  };
  
  // SpriteMorph Boolean visual representation
  
  SpriteMorph.prototype.booleanMorph = function (bool) {
      var sym = new BooleanSlotMorph(bool);
      sym.isStatic = true;
      sym.drawNew();
      return sym;
  };
  
  // SpriteMorph nesting
  /*
      simulate Morphic trees
  */
  
  SpriteMorph.prototype.attachPart = function (aSprite) {
      var v = Date.now();
      if (aSprite.anchor) {
          aSprite.anchor.detachPart(aSprite);
      }
      this.parts.push(aSprite);
      this.version = v;
      aSprite.anchor = this;
      this.allParts().forEach(function (part) {
          part.nestingScale = part.scale;
      });
      aSprite.version = v;
  };
  
  SpriteMorph.prototype.detachPart = function (aSprite) {
      var idx = this.parts.indexOf(aSprite),
          v;
      if (idx !== -1) {
          v = Date.now();
          this.parts.splice(idx, 1);
          this.version = v;
          aSprite.anchor = null;
          aSprite.version = v;
      }
  };
  
  SpriteMorph.prototype.detachAllParts = function () {
      var v = Date.now();
  
      this.parts.forEach(function (part) {
          part.anchor = null;
          part.version = v;
      });
      this.parts = [];
      this.version = v;
  };
  
  SpriteMorph.prototype.detachFromAnchor = function () {
      if (this.anchor) {
          this.anchor.detachPart(this);
      }
  };
  
  SpriteMorph.prototype.allParts = function () {
      // includes myself
      var result = [this];
      this.parts.forEach(function (part) {
          result = result.concat(part.allParts());
      });
      return result;
  };
  
  SpriteMorph.prototype.allAnchors = function () {
      // includes myself
      var result = [this];
      if (this.anchor !== null) {
          result = result.concat(this.anchor.allAnchors());
      }
      return result;
  };
  
  SpriteMorph.prototype.recordLayers = function () {
      var stage = this.parentThatIsA(StageMorph);
      if (!stage) {
          this.layerCache = null;
          return;
      }
      this.layers = this.allParts();
      this.layers.forEach(function (part) {
          var bubble = part.talkBubble();
          if (bubble) {bubble.hide(); }
      });
      this.layers.sort(function (x, y) {
          return stage.children.indexOf(x) < stage.children.indexOf(y) ?
                  -1 : 1;
      });
  };
  
  SpriteMorph.prototype.restoreLayers = function () {
      if (this.layers && this.layers.length > 1) {
          this.layers.forEach(function (sprite) {
              sprite.comeToFront();
              sprite.positionTalkBubble();
          });
      }
      this.layers = null;
  };
  
  // SpriteMorph highlighting
  
  SpriteMorph.prototype.addHighlight = function (oldHighlight) {
      var isHidden = !this.isVisible,
          highlight;
  
      if (isHidden) {this.show(); }
      highlight = this.highlight(
          oldHighlight ? oldHighlight.color : this.highlightColor,
          this.highlightBorder
      );
      this.addBack(highlight);
      this.fullChanged();
      if (isHidden) {this.hide(); }
      return highlight;
  };
  
  SpriteMorph.prototype.removeHighlight = function () {
      var highlight = this.getHighlight();
      if (highlight !== null) {
          this.fullChanged();
          this.removeChild(highlight);
      }
      return highlight;
  };
  
  SpriteMorph.prototype.toggleHighlight = function () {
      if (this.getHighlight()) {
          this.removeHighlight();
      } else {
          this.addHighlight();
      }
  };
  
  SpriteMorph.prototype.highlight = function (color, border) {
      var highlight = new SpriteHighlightMorph(),
          fb = this.bounds, // sprites are not nested in a Morphic way
          edge = border,
          ctx;
  
      highlight.setExtent(fb.extent().add(edge * 2));
      highlight.color = color;
      highlight.image = this.highlightImage(color, border);
      ctx = highlight.image.getContext('2d');
      ctx.drawImage(
          this.highlightImage(new Color(255, 255, 255), 4),
          border - 4,
          border - 4
      );
      ctx.drawImage(
          this.highlightImage(new Color(50, 50, 50), 2),
          border - 2,
          border - 2
      );
      ctx.drawImage(
          this.highlightImage(new Color(255, 255, 255), 1),
          border - 1,
          border - 1
      );
      highlight.setPosition(fb.origin.subtract(new Point(edge, edge)));
      return highlight;
  };
  
  SpriteMorph.prototype.highlightImage = function (color, border) {
      var fb, img, hi, ctx, out;
      fb = this.extent();
      img = this.image;
  
      hi = newCanvas(fb.add(border * 2));
      ctx = hi.getContext('2d');
  
      ctx.drawImage(img, 0, 0);
      ctx.drawImage(img, border, 0);
      ctx.drawImage(img, border * 2, 0);
      ctx.drawImage(img, border * 2, border);
      ctx.drawImage(img, border * 2, border * 2);
      ctx.drawImage(img, border, border * 2);
      ctx.drawImage(img, 0, border * 2);
      ctx.drawImage(img, 0, border);
  
      ctx.globalCompositeOperation = 'destination-out';
      ctx.drawImage(img, border, border);
  
      out = newCanvas(fb.add(border * 2));
      ctx = out.getContext('2d');
      ctx.drawImage(hi, 0, 0);
      ctx.globalCompositeOperation = 'source-atop';
      ctx.fillStyle = color.toString();
      ctx.fillRect(0, 0, out.width, out.height);
  
      return out;
  };
  
  SpriteMorph.prototype.getHighlight = function () {
      var highlights;
      highlights = this.children.slice(0).reverse().filter(
          function (child) {
              return child instanceof SpriteHighlightMorph;
          }
      );
      if (highlights.length !== 0) {
          return highlights[0];
      }
      return null;
  };
  
  // SpriteMorph nesting events
  
  SpriteMorph.prototype.mouseEnterDragging = function () {
      var obj;
      if (!this.enableNesting) {return; }
      obj = this.world().hand.children[0];
      if (this.wantsDropOf(obj)) {
          this.addHighlight();
      }
  };
  
  SpriteMorph.prototype.mouseLeave = function () {
      this.receiveUserInteraction('mouse-departed');
      if (!this.enableNesting) {return; }
      this.removeHighlight();
  };
  
  SpriteMorph.prototype.wantsDropOf = function (morph) {
      // allow myself to be the anchor of another sprite
      // by drag & drop
      return this.enableNesting
          && morph instanceof SpriteIconMorph
          && !contains(morph.object.allParts(), this);
  };
  
  SpriteMorph.prototype.reactToDropOf = function (morph, hand) {
      this.removeHighlight();
      this.attachPart(morph.object);
      this.world().add(morph);
      morph.slideBackTo(hand.grabOrigin);
  };
  
  // SpriteMorph screenshots
  
  SpriteMorph.prototype.newCostumeName = function (name, ignoredCostume) {
      var ix = name.indexOf('('),
          stem = (ix < 0) ? name : name.substring(0, ix),
          count = 1,
          newName = stem,
          all = this.costumes.asArray().filter(
              function (each) {return each !== ignoredCostume; }
          ).map(
              function (each) {return each.name; }
          );
      while (contains(all, newName)) {
          count += 1;
          newName = stem + '(' + count + ')';
      }
      return newName;
  };
  
  SpriteMorph.prototype.doScreenshot = function (imgSource, data) {
      var canvas,
          stage = this.parentThatIsA(StageMorph),
          costume;
      data = this.newCostumeName(data);
      if (imgSource[0] === undefined) {
          return;
      }
      if (imgSource[0] === "pen trails") {
          canvas = stage.trailsCanvas;
          costume = new Costume(canvas, data).copy(); // prevent mutation
      } else if (imgSource[0] === "stage image") {
          canvas = stage.fullImageClassic();
          costume = new Costume(canvas, data);
      }
      this.addCostume(costume);
  };
  
  // SpriteHighlightMorph /////////////////////////////////////////////////
  
  // SpriteHighlightMorph inherits from Morph:
  
  SpriteHighlightMorph.prototype = new Morph();
  SpriteHighlightMorph.prototype.constructor = SpriteHighlightMorph;
  SpriteHighlightMorph.uber = Morph.prototype;
  
  // SpriteHighlightMorph instance creation:
  
  function SpriteHighlightMorph() {
      this.init();
  }
  
  // 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.prototype.paletteTextColor
      = SpriteMorph.prototype.paletteTextColor;
  
  StageMorph.prototype.hiddenPrimitives = {};
  StageMorph.prototype.codeMappings = {};
  StageMorph.prototype.codeHeaders = {};
  StageMorph.prototype.enableCodeMapping = false;
  StageMorph.prototype.enableInheritance = false;
  StageMorph.prototype.enableSublistIDs = false;
  
  // 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.enableCustomHatBlocks = true;
      this.cloneCount = 0;
  
      this.timerStart = Date.now();
      this.tempo = 60; // bpm
      this.lastMessage = '';
  
      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 = ''; // last user input, do not persist
      this.activeSounds = []; // do not persist
  
      this.trailsCanvas = null;
      this.isThreadSafe = false;
  
      this.graphicsValues = {
          'color': 0,
          'fisheye': 0,
          'whirl': 0,
          'pixelate': 0,
          'mosaic': 0,
          'duplicate': 0,
          'negative': 0,
          'comic': 0,
          'confetti': 0,
          'saturation': 0,
          'brightness': 0
      };
  
      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),
              true // just me (for nested sprites)
          );
          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.scale - this.costume.width()) / 2,
              (this.height() / this.scale - this.costume.height()) / 2
          );
          this.image = this.applyGraphicsEffects(this.image);
      }
      this.version = Date.now(); // for observer optimization
  };
  
  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);
      if (area.extent().gt(new Point(0, 0))) {
          delta = this.position().neg();
          src = area.copy().translateBy(delta);
          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,
              sl,
              st,
              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);
          try {
              context.drawImage(
                  this.penTrails(),
                  sl / this.scale,
                  st / this.scale,
                  ws,
                  hs,
                  area.left() / this.scale,
                  area.top() / this.scale,
                  ws,
                  hs
              );
          } catch (err) { // sometimes triggered only by Firefox
              // console.log(err);
              context.restore();
              context.drawImage(
                  this.penTrails(),
                  0,
                  0,
                  this.dimensions.x,
                  this.dimensions.y,
                  this.left(),
                  this.top(),
                  this.dimensions.x * this.scale,
                  this.dimensions.y * this.scale
              );
          }
          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 = normalizeCanvas(img, true).getContext('2d').getImageData(
          0,
          0,
          ext.x,
          ext.y
      );
      morph.bounds = this.bounds.copy();
      morph.image = newCanvas(ext, true);
      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, (+bpm || 0));
  };
  
  StageMorph.prototype.changeTempo = function (delta) {
      this.setTempo(this.getTempo() + (+delta || 0));
  };
  
  StageMorph.prototype.getTempo = function () {
      return +this.tempo;
  };
  
  // StageMorph messages
  
  StageMorph.prototype.getLastMessage = function () {
      return this.lastMessage || '';
  };
  
  // StageMorph Mouse Coordinates
  
  StageMorph.prototype.reportMouseX = function () {
      var world = this.world();
      if (world) {
          return (world.hand.position().x - this.center().x) / this.scale;
      }
      return 0;
  };
  
  StageMorph.prototype.reportMouseY = function () {
      var world = this.world();
      if (world) {
          return (this.center().y - world.hand.position().y) / this.scale;
      }
      return 0;
  };
  
  // StageMorph drag & drop
  
  StageMorph.prototype.wantsDropOf = function (aMorph) {
      return aMorph instanceof SpriteMorph ||
          aMorph instanceof WatcherMorph ||
          aMorph instanceof ListWatcherMorph ||
          aMorph instanceof SpriteIconMorph;
  };
  
  StageMorph.prototype.reactToDropOf = function (morph, hand) {
      if (morph instanceof SpriteIconMorph) { // detach sprite from anchor
          if (morph.object.anchor) {
              morph.object.anchor.detachPart(morph.object);
          }
          this.world().add(morph);
          morph.slideBackTo(hand.grabOrigin);
      }
  };
  
  // StageMorph stepping
  
  StageMorph.prototype.step = function () {
      var current, elapsed, leftover, ide, world = this.world();
  
      // handle keyboard events
      if (world.keyboardReceiver === null) {
          world.keyboardReceiver = this;
      }
      if (world.currentKey === null) {
          this.keyPressed = null;
      }
  
      // manage threads
      if (this.enableCustomHatBlocks) {
          this.stepGenericConditions();
      }
      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();
  
          // single-stepping hook:
          if (this.threads.wantsToPause) {
              ide = this.parentThatIsA(IDE_Morph);
              if (ide) {
                  ide.controlBar.pauseButton.refresh();
              }
          }
      }
  
      // 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.stepGenericConditions = function (stopAll) {
      var hats = [],
          myself = this,
          ide;
      this.children.concat(this).forEach(function (morph) {
          if (morph instanceof SpriteMorph || morph instanceof StageMorph) {
              hats = hats.concat(morph.allGenericHatBlocks());
          }
      });
      if (!hats.length) {
          this.enableCustomHatBlocks = false;
          ide = this.parentThatIsA(IDE_Morph);
          if (ide) {
              ide.controlBar.stopButton.refresh();
          }
          return;
      }
      hats.forEach(function (block) {
          myself.threads.doWhen(block, stopAll);
      });
  };
  
  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';
          if (event.ctrlKey || event.metaKey) {
              keyName = 'ctrl enter';
          } else if (event.shiftKey) {
              keyName = 'shift 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);
          if (event.ctrlKey || event.metaKey) {
              keyName = 'ctrl ' + (event.shiftKey ? 'shift ' : '') + keyName;
          }
      }
      action.call(this, keyName);
  };
  
  StageMorph.prototype.fireKeyEvent = function (key) {
      var evt = key.toLowerCase(),
          hats = [],
          procs = [],
          ide = this.parentThatIsA(IDE_Morph),
          myself = this;
  
      this.keysPressed[evt] = true;
      if (evt === 'ctrl enter') {
          return this.fireGreenFlagEvent();
      }
      if (evt === 'shift enter') {
          return this.editScripts();
      }
      if (evt === 'ctrl f') {
          if (!ide.isAppMode) {ide.currentSprite.searchBlocks(); }
          return;
      }
      if (evt === 'ctrl z') {
          if (!ide.isAppMode) {ide.currentSprite.scripts.undrop(); }
           return;
      }
      if (evt === 'ctrl shift z' || (evt === 'ctrl y')) {
          if (!ide.isAppMode) {ide.currentSprite.scripts.redrop(); }
           return;
      }
      if (evt === 'ctrl n') {
          if (!ide.isAppMode) {ide.createNewProject(); }
          return;
      }
      if (evt === 'ctrl o') {
          if (!ide.isAppMode) {ide.openProjectsBrowser(); }
          return;
      }
      if (evt === 'ctrl s') {
          if (!ide.isAppMode) {ide.save(); }
          return;
      }
      if (evt === 'ctrl shift s') {
          if (!ide.isAppMode) {return ide.saveProjectsBrowser(); }
          return;
      }
      if (evt === 'esc') {
          return this.fireStopAllEvent();
      }
      this.children.concat(this).forEach(function (morph) {
          if (isSnapObject(morph)) {
              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 (isSnapObject(morph)) {
              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.nextSteps([
              nop,
              function () {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.detachFromAnchor();
          clone.corpsify();
          clone.destroy();
      });
      this.cloneCount = 0;
  };
  
  StageMorph.prototype.editScripts = function () {
      var ide = this.parentThatIsA(IDE_Morph),
          scripts,
          sorted;
      if (ide.isAppMode || !ScriptsMorph.prototype.enableKeyboard) {return; }
      scripts = this.parentThatIsA(IDE_Morph).currentSprite.scripts;
      scripts.edit(scripts.position());
      sorted = scripts.focus.sortedScripts();
      if (sorted.length) {
          scripts.focus.element = sorted[0];
          if (scripts.focus.element instanceof HatBlockMorph) {
              scripts.focus.nextCommand();
          }
      } else {
          scripts.focus.moveBy(new Point(50, 50));
      }
      scripts.focus.fixLayout();
  };
  
  // StageMorph block templates
  
  StageMorph.prototype.blockTemplates = function (category) {
      var blocks = [], myself = this, varNames, button,
          cat = category || 'motion', txt;
  
      function block(selector) {
          if (myself.hiddenPrimitives[selector]) {
              return null;
          }
          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) {
          if (myself.hiddenPrimitives[selector]) {
              return null;
          }
          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
          );
      }
  
      function addVar(pair) {
          if (pair) {
              if (myself.isVariableNameInUse(pair[0])) {
                  myself.inform('that name is already in use');
              } else {
                  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();
              }
          }
      }
  
      if (cat === 'motion') {
  
          txt = new TextMorph(localize(
              'Stage selected:\nno motion primitives'
          ));
          txt.fontSize = 9;
          txt.setColor(this.paletteTextColor);
          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'));
          blocks.push('-');
          blocks.push(block('show'));
          blocks.push(block('hide'));
  
      // for debugging: ///////////////
  
          if (this.world().isDevMode) {
              blocks.push('-');
              txt = new TextMorph(localize(
                  'development mode \ndebugging primitives:'
              ));
              txt.fontSize = 9;
              txt.setColor(this.paletteTextColor);
              blocks.push(txt);
              blocks.push('-');
              blocks.push(block('reportCostumes'));
              blocks.push('-');
              blocks.push(block('log'));
              blocks.push(block('alert'));
              blocks.push('-');
              blocks.push(block('doScreenshot'));
          }
  
      /////////////////////////////////
  
      } 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'));
  
      // for debugging: ///////////////
  
          if (this.world().isDevMode) {
              blocks.push('-');
              txt = new TextMorph(localize(
                  'development mode \ndebugging primitives:'
              ));
              txt.fontSize = 9;
              txt.setColor(this.paletteTextColor);
              blocks.push(txt);
              blocks.push('-');
              blocks.push(block('reportSounds'));
          }
  
      } else if (cat === 'pen') {
  
          blocks.push(block('clear'));
  
      } else if (cat === 'control') {
  
          blocks.push(block('receiveGo'));
          blocks.push(block('receiveKey'));
          blocks.push(block('receiveInteraction'));
          blocks.push(block('receiveCondition'));
          blocks.push(block('receiveMessage'));
          blocks.push('-');
          blocks.push(block('doBroadcast'));
          blocks.push(block('doBroadcastAndWait'));
          blocks.push(watcherToggle('getLastMessage'));
          blocks.push(block('getLastMessage'));
          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('-');
      /*
      // old STOP variants, migrated to a newer version, now redundant
          blocks.push(block('doStopBlock'));
          blocks.push(block('doStop'));
          blocks.push(block('doStopAll'));
      */
          blocks.push(block('doStopThis'));
          blocks.push(block('doStopOthers'));
          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'));
          blocks.push('-');
          blocks.push(block('doPauseAll'));
  
      } else if (cat === 'sensing') {
  
          blocks.push(block('doAsk'));
          blocks.push(watcherToggle('getLastAnswer'));
          blocks.push(block('getLastAnswer'));
          blocks.push('-');
          blocks.push(watcherToggle('reportMouseX'));
          blocks.push(block('reportMouseX'));
          blocks.push(watcherToggle('reportMouseY'));
          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('getTimer'));
          blocks.push('-');
          blocks.push(block('reportAttributeOf'));
  
          if (SpriteMorph.prototype.enableFirstClass) {
              blocks.push(block('reportGet'));
          }
          blocks.push('-');
  
          blocks.push(block('reportURL'));
          blocks.push('-');
          blocks.push(block('reportIsFastTracking'));
          blocks.push(block('doSetFastTracking'));
          blocks.push('-');
          blocks.push(block('reportDate'));
  
      // for debugging: ///////////////
  
          if (this.world().isDevMode) {
  
              blocks.push('-');
              txt = new TextMorph(localize(
                  'development mode \ndebugging primitives:'
              ));
              txt.fontSize = 9;
              txt.setColor(this.paletteTextColor);
              blocks.push(txt);
              blocks.push('-');
              blocks.push(watcherToggle('reportThreadCount'));
              blocks.push(block('reportThreadCount'));
              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(block('reportBoolean'));
          blocks.push('-');
          blocks.push(block('reportJoinWords'));
          blocks.push(block('reportTextSplit'));
          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'));
  
          if (true) { // (Process.prototype.enableJS) {
              blocks.push('-');
              blocks.push(block('reportJSFunction'));
          }
  
      // for debugging: ///////////////
  
          if (this.world().isDevMode) {
              blocks.push('-');
              txt = new TextMorph(
                  'development mode \ndebugging primitives:'
              );
              txt.fontSize = 9;
              txt.setColor(this.paletteTextColor);
              blocks.push(txt);
              blocks.push('-');
              blocks.push(block('reportTypeOf'));
              blocks.push(block('reportTextFunction'));
          }
  
      //////////////////////////////////
  
      } else if (cat === 'variables') {
  
          button = new PushButtonMorph(
              null,
              function () {
                  new VariableDialogMorph(
                      null,
                      addVar,
                      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'));
  
      // for debugging: ///////////////
  
          if (this.world().isDevMode) {
              blocks.push('-');
              txt = new TextMorph(localize(
                  'development mode \ndebugging primitives:'
              ));
              txt.fontSize = 9;
              txt.setColor(this.paletteTextColor);
              blocks.push(txt);
              blocks.push('-');
              blocks.push(block('reportMap'));
              blocks.push('-');
              blocks.push(block('doForEach'));
              blocks.push(block('doShowTable'));
          }
  
      /////////////////////////////////
  
          blocks.push('=');
  
          if (StageMorph.prototype.enableCodeMapping) {
              blocks.push(block('doMapCodeOrHeader'));
              blocks.push(block('doMapStringCode'));
              blocks.push(block('doMapListCode'));
              blocks.push('-');
              blocks.push(block('reportMappedCode'));
              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),
          shiftClicked = this.world().currentKey === 16,
          myself = this;
  
      if (ide && ide.isAppMode) {
          // menu.addItem('help', 'nop');
          return menu;
      }
      menu.addItem("edit", 'edit');
      menu.addItem("show all", 'showAll');
      menu.addItem(
          "pic...",
          function () {
              ide.saveCanvasAs(
                  myself.fullImageClassic(),
                  myself.name,
                  true // open as new window
              );
          },
          'open a new window\nwith a picture of the stage'
      );
      if (shiftClicked) {
          menu.addLine();
          menu.addItem(
              ide.currentSprite instanceof SpriteMorph ?
                  "turn pen trails into new costume..."
                      : "turn pen trails into new background...",
              function () {
                  var costume = new Costume(
                      myself.trailsCanvas,
                      Date.now().toString()
                  ).copy();
                  ide.currentSprite.addCostume(costume);
                  ide.currentSprite.wearCostume(costume);
                  ide.hasChangedMedia = true;
              },
              ide.currentSprite instanceof SpriteMorph ?
                  'turn all pen trails and stamps\n' +
                      'into a new costume for the\ncurrently selected sprite'
                          : 'turn all pen trails and stamps\n' +
                              'into a new background for the stage',
              new Color(100, 0, 0)
          );
      }
      return menu;
  };
  
  StageMorph.prototype.showAll = function () {
      var myself = this;
      this.children.forEach(function (m) {
          if (m instanceof SpriteMorph) {
              if (!m.anchor) {
                  m.show();
                  m.keepWithin(myself);
              }
          } else {
              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,
          fimg;
  
      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.isVisible && (morph !== excludedSprite)) {
              fb = morph.fullBounds();
              fimg = morph.fullImage();
              if (fimg.width && fimg.height) {
                  ctx.drawImage(
                      morph.fullImage(),
                      fb.origin.x - myself.bounds.origin.x,
                      fb.origin.y - myself.bounds.origin.y
                  );
              }
          }
      });
      return trg;
  };
  
  // StageMorph hiding and showing:
  
  /*
      override the inherited behavior to recursively hide/show all
      children.
  */
  
  StageMorph.prototype.hide = function () {
      this.isVisible = false;
      this.changed();
  };
  
  StageMorph.prototype.show = function () {
      this.isVisible = true;
      this.changed();
  };
  
  // StageMorph cloning override
  
  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.blocksMatching = SpriteMorph.prototype.blocksMatching;
  StageMorph.prototype.searchBlocks = SpriteMorph.prototype.searchBlocks;
  StageMorph.prototype.reporterize = SpriteMorph.prototype.reporterize;
  StageMorph.prototype.showingWatcher = SpriteMorph.prototype.showingWatcher;
  StageMorph.prototype.addVariable = SpriteMorph.prototype.addVariable;
  StageMorph.prototype.deleteVariable = SpriteMorph.prototype.deleteVariable;
  
  // StageMorph block rendering
  
  StageMorph.prototype.doScreenshot
      = SpriteMorph.prototype.doScreenshot;
  
  StageMorph.prototype.newCostumeName
      = SpriteMorph.prototype.newCostumeName;
  
  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.prototype.reportCostumes
      = SpriteMorph.prototype.reportCostumes;
  
  // StageMorph graphic effects
  
  StageMorph.prototype.graphicsChanged
      = SpriteMorph.prototype.graphicsChanged;
  
  StageMorph.prototype.applyGraphicsEffects
      = SpriteMorph.prototype.applyGraphicsEffects;
  
  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.prototype.reportSounds
      = SpriteMorph.prototype.reportSounds;
  
  // 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.prototype.reportThreadCount
      = SpriteMorph.prototype.reportThreadCount;
  
  // StageMorph message broadcasting
  
  StageMorph.prototype.allMessageNames
      = SpriteMorph.prototype.allMessageNames;
  
  StageMorph.prototype.allHatBlocksFor
      = SpriteMorph.prototype.allHatBlocksFor;
  
  StageMorph.prototype.allHatBlocksForKey
      = SpriteMorph.prototype.allHatBlocksForKey;
  
  StageMorph.prototype.allHatBlocksForInteraction
      = SpriteMorph.prototype.allHatBlocksForInteraction;
  
  StageMorph.prototype.allGenericHatBlocks
      = SpriteMorph.prototype.allGenericHatBlocks;
  
  // StageMorph events
  
  StageMorph.prototype.mouseClickLeft
      = SpriteMorph.prototype.mouseClickLeft;
  
  StageMorph.prototype.mouseEnter
      = SpriteMorph.prototype.mouseEnter;
  
  StageMorph.prototype.mouseLeave = function () {
      this.receiveUserInteraction('mouse-departed');
  };
  
  StageMorph.prototype.mouseDownLeft
      = SpriteMorph.prototype.mouseDownLeft;
  
  StageMorph.prototype.receiveUserInteraction
      = SpriteMorph.prototype.receiveUserInteraction;
  
  // 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;
  
  // StageMorph inheritance support - variables
  
  StageMorph.prototype.isVariableNameInUse
      = SpriteMorph.prototype.isVariableNameInUse;
  
  StageMorph.prototype.globalVariables
      = SpriteMorph.prototype.globalVariables;
  
  StageMorph.prototype.inheritedVariableNames = function () {
      return [];
  };
  
  // StageMorph variable refactoring
  
  StageMorph.prototype.hasSpriteVariable
      = SpriteMorph.prototype.hasSpriteVariable;
  
  StageMorph.prototype.refactorVariableInstances
      = SpriteMorph.prototype.refactorVariableInstances;
  
  // 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, stage, isThought, isQuestion) {
      this.init(data, stage, isThought, isQuestion);
  }
  
  SpriteBubbleMorph.prototype.init = function (
      data,
      stage,
      isThought,
      isQuestion
  ) {
      var sprite = SpriteMorph.prototype;
      this.stage = stage;
      this.scale = stage ? stage.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, toggle) {
      var contents,
          isTable,
          sprite = SpriteMorph.prototype,
          isText,
          img,
          scaledImg,
          width;
      if (data instanceof Morph) {
          if (isSnapObject(data)) {
              img = data.thumbnail(new Point(40, 40));
              contents = new Morph();
              contents.silentSetWidth(img.width);
              contents.silentSetHeight(img.height);
              contents.image = img;
              contents.version = data.version;
              contents.step = function () {
                  if (this.version !== data.version) {
                      img = data.thumbnail(new Point(40, 40));
                      this.image = img;
                      this.version = data.version;
                      this.changed();
                  }
              };
          } else {
              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 Costume) {
          img = data.thumbnail(new Point(40, 40));
          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) {
          if (toggle && this.contentsMorph) {
              isTable = (this.contentsMorph instanceof ListWatcherMorph);
          } else {
              isTable = data.isTable();
          }
  
          if (isTable) { // (!toggle && data.isTable()) {
              contents = new TableFrameMorph(new TableMorph(data, 10));
              if (this.stage) {
                  contents.expand(this.stage.extent().translateBy(
                      -2 * (this.edge + this.border + this.padding)
                  ));
              }
          } else {
              contents = new ListWatcherMorph(data);
              contents.update(true);
              contents.step = contents.update;
              if (this.stage) {
                  contents.expand(this.stage.extent().translateBy(
                      -2 * (this.edge + this.border + this.padding)
                  ));
              }
          }
          contents.isDraggable = false;
      } 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 (toggle) {
      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, toggle);
      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 ? normalizeCanvas(canvas, true)
              : newCanvas(null, true);
      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 = function () {
      return 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 shrink-wrapping
  
  Costume.prototype.shrinkWrap = function () {
      // adjust my contents'  bounds to my visible bounding box
      var bb = this.boundingBox(),
          ext = bb.extent(),
          pic = newCanvas(ext, true),
          ctx = pic.getContext('2d');
  
      ctx.drawImage(
          this.contents,
          bb.origin.x,
          bb.origin.y,
          ext.x,
          ext.y,
          0,
          0,
          ext.x,
          ext.y
      );
      this.rotationCenter = this.rotationCenter.subtract(bb.origin);
      this.contents = pic;
      this.version = Date.now();
  };
  
  Costume.prototype.canvasBoundingBox = function (pic) {
      // answer the rectangle surrounding my contents' non-transparent pixels
      var row,
          col,
          w = pic.width,
          h = pic.height,
          ctx = pic.getContext('2d'),
          dta = ctx.getImageData(0, 0, w, h);
  
      function getAlpha(x, y) {
          return dta.data[((y * w * 4) + (x * 4)) + 3];
      }
  
      function getLeft() {
          for (col = 0; col <= w; col += 1) {
              for (row = 0; row <= h; row += 1) {
                  if (getAlpha(col, row)) {
                      return col;
                  }
              }
          }
          return 0;
      }
  
      function getTop() {
          for (row = 0; row <= h; row += 1) {
              for (col = 0; col <= h; col += 1) {
                  if (getAlpha(col, row)) {
                      return row;
                  }
              }
          }
          return 0;
      }
  
      function getRight() {
          for (col = w; col >= 0; col -= 1) {
              for (row = h; row >= 0; row -= 1) {
                  if (getAlpha(col, row)) {
                      return Math.min(col + 1, w);
                  }
              }
          }
          return w;
      }
  
      function getBottom() {
          for (row = h; row >= 0; row -= 1) {
              for (col = w; col >= 0; col -= 1) {
                  if (getAlpha(col, row)) {
                      return Math.min(row + 1, h);
                  }
              }
          }
          return h;
      }
  
      return new Rectangle(getLeft(), getTop(), getRight(), getBottom());
  };
  
  Costume.prototype.boundingBox = function () {
      return this.canvasBoundingBox(this.contents);
  };
  
  // Costume duplication
  
  Costume.prototype.copy = function () {
      var canvas = newCanvas(this.extent(), true),
          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(), true),
          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, anIDE, isnew, oncancel, onsubmit) {
      var myself = this,
          editor = new PaintEditorMorph();
      editor.oncancel = oncancel || nop;
      editor.openIn(
          aWorld,
          isnew ?
                  newCanvas(StageMorph.prototype.dimensions, true) :
                  this.contents,
          isnew ?
                  null :
                  this.rotationCenter,
          function (img, rc) {
              myself.contents = img;
              myself.rotationCenter = rc;
              myself.version = Date.now();
              aWorld.changed();
              if (anIDE) {
                  if (anIDE.currentSprite instanceof SpriteMorph) {
                      // don't shrinkwrap stage costumes
                      myself.shrinkWrap();
                  }
                  anIDE.currentSprite.wearCostume(myself);
                  anIDE.hasChangedMedia = true;
              }
              (onsubmit || nop)();
          }
      );
  };
  
  Costume.prototype.editRotationPointOnly = 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();
      dialog.popUp(aWorld);
  };
  
  // 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');
  
      if (!src || src.width + src.height === 0) {return trg; }
      ctx.scale(scale, scale);
      ctx.drawImage(
          src,
          Math.floor(xOffset / scale),
          Math.floor(yOffset / scale)
      );
      return trg;
  };
  
  // Costume catching "tainted" canvases
  
  Costume.prototype.isTainted = function () {
      // find out whether the canvas has been tainted by cross-origin data
      // assumes that if reading image data throws an error it is tainted
      try {
          this.contents.getContext('2d').getImageData(
              0,
              0,
              this.contents.width,
              this.contents.height
          );
      } catch (err) {
          return true;
      }
      return false;
  };
  
  // 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?
          var ctx = window.AudioContext ||
              window.mozAudioContext ||
              window.msAudioContext ||
              window.oAudioContext ||
              window.webkitAudioContext;
          if (!ctx.prototype.createGain) {
              ctx.prototype.createGain = ctx.prototype.createGainNode;
          }
          return ctx;
      }());
      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.createGain();
      Note.prototype.gainNode.gain.value = 0.25; // reduce volume by 1/4
  };
  
  // Note playing
  
  Note.prototype.play = function () {
      this.oscillator = this.audioContext.createOscillator();
      if (!this.oscillator.start) {
          this.oscillator.start = this.oscillator.noteOn;
      }
      if (!this.oscillator.stop) {
          this.oscillator.stop = this.oscillator.noteOff;
      }
      this.oscillator.type = 'sine';
      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.start(0);
  };
  
  Note.prototype.stop = function () {
      if (this.oscillator) {
          this.oscillator.stop(0);
          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, parentCell) {
      this.init(contents, color, idx, parentCell);
  }
  
  CellMorph.prototype.init = function (contents, color, idx, parentCell) {
      this.contents = (contents === 0 ? 0
              : contents === false ? false
                      : contents || '');
      this.isEditable = isNil(idx) ? false : true;
      this.idx = idx || null; // for list watchers
      this.parentCell = parentCell || null; // for list circularity detection
      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.version = null; // only for observing sprites
      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 circularity testing:
  
  
  CellMorph.prototype.isCircular = function (list) {
      if (!this.parentCell) {return false; }
      if (list instanceof List) {
          return this.contents === list || this.parentCell.isCircular(list);
      }
      return this.parentCell.isCircular(this.contents);
  };
  
  // 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.update = function () {
      // special case for observing sprites
      if (!isSnapObject(this.contents)) {
          return;
      }
      if (this.version !== this.contents.version) {
          this.drawNew();
      }
  };
  
  CellMorph.prototype.drawNew = function (toggle, type) {
      var context,
          txt,
          img,
          fontSize = SyntaxElementMorph.prototype.fontSize,
          isSameList = this.contentsMorph instanceof ListWatcherMorph
                  && (this.contentsMorph.list === this.contents),
          isSameTable = this.contentsMorph instanceof TableFrameMorph
                  && (this.contentsMorph.tableMorph.table === this.contents);
  
      if (this.isBig) {
          fontSize = fontSize * 1.5;
      }
  
      // re-build my contents
      if (toggle || (this.contentsMorph && !isSameList && !isSameTable)) {
          this.contentsMorph.destroy();
          this.version = null;
      }
  
      if (toggle || (!isSameList && !isSameTable)) {
          if (this.contents instanceof Morph) {
              if (isSnapObject(this.contents)) {
                  img = this.contents.thumbnail(new Point(40, 40));
                  this.contentsMorph = new Morph();
                  this.contentsMorph.silentSetWidth(img.width);
                  this.contentsMorph.silentSetHeight(img.height);
                  this.contentsMorph.image = img;
                  this.version = this.contents.version;
              } else {
                  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,
                  'left' // was formerly 'center', reverted b/c of code-mapping
              );
              if (this.isEditable) {
                  this.contentsMorph.isEditable = true;
                  this.contentsMorph.enableSelecting();
              }
              this.contentsMorph.setColor(new Color(255, 255, 255));
          } else if (typeof this.contents === 'boolean') {
              img = SpriteMorph.prototype.booleanMorph.call(
                  null,
                  this.contents
              ).fullImage();
              this.contentsMorph = new Morph();
              this.contentsMorph.silentSetWidth(img.width);
              this.contentsMorph.silentSetHeight(img.height);
              this.contentsMorph.image = img;
          } 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 Costume) {
              img = this.contents.thumbnail(new Point(40, 40));
              this.contentsMorph = new Morph();
              this.contentsMorph.silentSetWidth(img.width);
              this.contentsMorph.silentSetHeight(img.height);
              this.contentsMorph.image = img;
          } else if (this.contents instanceof List) {
              if ('table' === type || (!toggle && this.contents.isTable())) {
                  this.contentsMorph = new TableFrameMorph(new TableMorph(
                      this.contents,
                      10
                  ));
                  this.contentsMorph.expand(new Point(200, 150));
              } else {
                  if (this.isCircular()) {
                      this.contentsMorph = new TextMorph(
                          '(...)',
                          fontSize,
                          null,
                          false, // bold
                          true, // italic
                          'center'
                      );
                      this.contentsMorph.setColor(new Color(255, 255, 255));
                  } else {
                      this.contentsMorph = new ListWatcherMorph(
                          this.contents,
                          this
                      );
                  }
              }
              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 :
                      SyntaxElementMorph.prototype.fontSize * 3.5)
      ));
  
      // 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 && !MorphicPreferences.isFlat) {
          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 (toggle || (!isSameList && !isSameTable)) {
          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 && !MorphicPreferences.isFlat) {
          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);
      }
  };
  
  CellMorph.prototype.mouseDoubleClick = function (pos) {
      if (List.prototype.enableTables &&
              this.currentValue instanceof List) {
          new TableDialogMorph(this.contents).popUp(this.world());
      } else {
          this.escalateEvent('mouseDoubleClick', 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(
          ['getLastAnswer', 'getLastMessage', 'getTempo', 'getTimer',
               'reportMouseX', 'reportMouseY', 'reportThreadCount'],
          selector
      );
  };
  
  // WatcherMorph slider accessing:
  
  WatcherMorph.prototype.setSliderMin = function (num, noUpdate) {
      if (this.target instanceof VariableFrame) {
          this.sliderMorph.setSize(1, noUpdate);
          this.sliderMorph.setStart(num, noUpdate);
          this.sliderMorph.setSize(this.sliderMorph.rangeSize() / 5, noUpdate);
      }
  };
  
  WatcherMorph.prototype.setSliderMax = function (num, noUpdate) {
      if (this.target instanceof VariableFrame) {
          this.sliderMorph.setSize(1, noUpdate);
          this.sliderMorph.setStop(num, noUpdate);
          this.sliderMorph.setSize(this.sliderMorph.rangeSize() / 5, noUpdate);
      }
  };
  
  // WatcherMorph updating:
  
  WatcherMorph.prototype.update = function () {
      var newValue, sprite, num;
  
      if (this.target && this.getter) {
          this.updateLabel();
          if (this.target instanceof VariableFrame) {
              newValue = this.target.vars[this.getter] ?
                      this.target.vars[this.getter].value : undefined;
              if (newValue === undefined && this.target.owner) {
                  sprite = this.target.owner;
                  if (contains(sprite.inheritedVariableNames(), this.getter)) {
                      newValue = this.target.getVar(this.getter);
                      // ghost cell color
                      this.cellMorph.setColor(
                          SpriteMorph.prototype.blockColor.variables
                              .lighter(35)
                      );
                  } else {
                      this.destroy();
                      return;
                  }
              } else {
                  // un-ghost the cell color
                  this.cellMorph.setColor(
                      SpriteMorph.prototype.blockColor.variables
                  );
              }
          } else {
              newValue = this.target[this.getter]();
          }
          if (newValue !== '' && !isNil(newValue)) {
              num = +newValue;
              if (typeof newValue !== 'boolean' && !isNaN(num)) {
                  newValue = Math.round(newValue * 1000000000) / 1000000000;
              }
          }
          if (newValue !== this.currentValue) {
              this.changed();
              this.cellMorph.contents = newValue;
              this.cellMorph.drawNew();
              if (!isNaN(newValue)) {
                  this.sliderMorph.value = newValue;
                  this.sliderMorph.drawNew();
              }
              this.fixLayout();
              this.currentValue = newValue;
          }
      }
      if (this.cellMorph.contentsMorph instanceof ListWatcherMorph) {
          this.cellMorph.contentsMorph.update();
      } else if (isSnapObject(this.cellMorph.contents)) {
          this.cellMorph.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,
              MorphicPreferences.isFlat ? new Point() : 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.setVar(
                  myself.getter,
                  Math.round(num),
                  myself.target.owner
              );
          };
          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:
  
  WatcherMorph.prototype.mouseDoubleClick = function (pos) {
      if (List.prototype.enableTables &&
              this.currentValue instanceof List) {
          new TableDialogMorph(this.currentValue).popUp(this.world());
      } else {
          this.escalateEvent('mouseDoubleClick', pos);
      }
  };
  
  /*
  // 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',
          vNames;
  
      function monitor(vName) {
          var stage = myself.parentThatIsA(StageMorph),
              varFrame = myself.currentValue.outerContext.variables;
          menu.addItem(
              vName + '...',
              function () {
                  var watcher = detect(
                      stage.children,
                      function (morph) {
                          return morph instanceof WatcherMorph
                              && morph.target === varFrame
                              && morph.getter === vName;
                      }
                  ),
                      others;
                  if (watcher !== null) {
                      watcher.show();
                      watcher.fixLayout(); // re-hide hidden parts
                      return;
                  }
                  watcher = new WatcherMorph(
                      vName + ' ' + localize('(temporary)'),
                      SpriteMorph.prototype.blockColor.variables,
                      varFrame,
                      vName
                  );
                  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();
              }
          );
      }
  
      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.style.display = "none";
                  inp.addEventListener(
                      "change",
                      function () {
                          var file;
  
                          function txtOnlyMsg(ftype) {
                              ide.inform(
                                  'Unable to import',
                                  'Snap! can only import "text" files.\n' +
                                      'You selected a file of type "' +
                                      ftype +
                                      '".'
                              );
                          }
  
                          function readText(aFile) {
                              var frd = new FileReader();
                              frd.onloadend = function (e) {
                                  myself.target.setVar(
                                      myself.getter,
                                      e.target.result
                                  );
                              };
  
                              if (aFile.type.indexOf("text") === 0) {
                                  frd.readAsText(aFile);
                              } else {
                                  txtOnlyMsg(aFile.type);
                              }
                          }
  
                          document.body.removeChild(inp);
                          ide.filePicker = null;
                          if (inp.files.length > 0) {
                              file = inp.files[inp.files.length - 1];
                              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 () {
                      var ide = myself.parentThatIsA(IDE_Morph);
                      ide.saveFileAs(
                          myself.currentValue.toString(),
                          'text/plain;charset=utf-8',
                          myself.getter // variable name
                      );
                  }
              );
          } else if (this.currentValue instanceof Context) {
              vNames = this.currentValue.outerContext.variables.names();
              if (vNames.length) {
                  menu.addLine();
                  vNames.forEach(function (vName) {
                      monitor(vName);
                  });
              }
          }
      }
      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(),
          null, // pic
          null, // choices
          null, // read only
          true // numeric
      );
  };
  
  WatcherMorph.prototype.userSetSliderMax = function () {
      new DialogBoxMorph(
          this,
          this.setSliderMax,
          this
      ).prompt(
          "Slider maximum value",
          this.sliderMorph.stop.toString(),
          this.world(),
          null, // pic
          null, // choices
          null, // read only
          true // numeric
      );
  };
  
  // WatcherMorph drawing:
  
  WatcherMorph.prototype.drawNew = function () {
      var context,
          gradient;
      this.image = newCanvas(this.extent());
      context = this.image.getContext('2d');
      if (MorphicPreferences.isFlat || (this.edge === 0 && this.border === 0)) {
          BoxMorph.uber.drawNew.call(this);
          return;
      }
      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(StageMorph.prototype.dimensions.x - 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;
  };
  