/* 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) 2022 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 . 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 Microphone 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, ZERO, DialogBoxMorph, InputFieldMorph, SpriteIconMorph, BlockMorph, SymbolMorph, nop, ThreadManager, VariableFrame, detect, BlockMorph, BoxMorph, Color, Animation, CommandBlockMorph, FrameMorph, HatBlockMorph, MenuMorph, Morph, MultiArgMorph, ReporterBlockMorph, ScriptsMorph, StringMorph, SyntaxElementMorph, XML_Element, TextMorph, contains, degrees, detect, newCanvas, radians, Array, CursorMorph, Date, FrameMorph, Math, MenuMorph, Morph, invoke, MorphicPreferences, WHITE, Object, PenMorph, Point, Rectangle, ScrollFrameMorph, SliderMorph, VideoMotion, StringMorph, TextMorph, contains, copy, degrees, detect, document, isNaN, Point, isString, newCanvas, nop, parseFloat, radians, window, modules, IDE_Morph, VariableDialogMorph, HTMLCanvasElement, Context, List, RingMorph, HandleMorph, SpeechBubbleMorph, InputSlotMorph, isNil, FileReader, TableDialogMorph, String, BlockEditorMorph, BlockDialogMorph, PrototypeHatBlockMorph, BooleanSlotMorph, localize, TableMorph, TableFrameMorph, normalizeCanvas, VectorPaintEditorMorph, AlignmentMorph, Process, WorldMap, copyCanvas, useBlurredShadows, BlockVisibilityDialogMorph, CostumeIconMorph, SoundIconMorph*/ /*jshint esversion: 6*/ modules.objects = '2022-February-07'; var SpriteMorph; var StageMorph; var SpriteBubbleMorph; var Costume; var SVG_Costume; var CostumeEditorMorph; var Sound; var Note; var Microphone; 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.attributes = [ 'x position', 'y position', 'direction', 'size', 'costumes', 'costume #', 'volume', 'balance', 'sounds', 'shown?', 'pen down?', 'scripts' ]; SpriteMorph.prototype.categories = [ 'motion', 'looks', 'sound', 'pen', 'control', 'sensing', 'operators', '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.customCategories = new Map(); // key: name, value: color SpriteMorph.prototype.allCategories = function () { return this.categories.concat( Array.from(this.customCategories.keys()).sort() ); }; SpriteMorph.prototype.blockColorFor = function (category) { return this.blockColor[category] || this.customCategories.get(category) || this.blockColor.other; }; 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.showingExtensions = false; SpriteMorph.prototype.useFlatLineEnds = false; SpriteMorph.prototype.penColorModel = 'hsv'; // or 'hsl' SpriteMorph.prototype.highlightColor = new Color(250, 200, 130); SpriteMorph.prototype.highlightBorder = 8; SpriteMorph.prototype.bubbleColor = WHITE; 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', defaults: [90] }, doFaceTowards: { only: SpriteMorph, type: 'command', category: 'motion', spec: 'point towards %dst', defaults: [['mouse-pointer']] }, 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', defaults: [['random position']] }, 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 #' }, reportGetImageAttribute: { type: 'reporter', category: 'looks', spec: '%img of costume %cst', defaults: [['width'], ['current']] }, reportNewCostume: { type: 'reporter', category: 'looks', spec: 'new costume %l width %dim height %dim' }, reportNewCostumeStretched: { type: 'reporter', category: 'looks', spec: 'stretch %cst x: %n y: %n %', defaults: [['current'], 100, 50] }, 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: [['ghost'], 25] }, setEffect: { type: 'command', category: 'looks', spec: 'set %eff effect to %n', defaults: [['ghost'], 0] }, getEffect: { type: 'reporter', category: 'looks', spec: '%eff effect', defaults: [['ghost']] }, 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: { type: 'command', category: 'looks', spec: 'show' }, hide: { type: 'command', category: 'looks', spec: 'hide' }, reportShown: { type: 'predicate', category: 'looks', spec: 'shown?' }, goToLayer: { only: SpriteMorph, type: 'command', category: 'looks', spec: 'go to %layer layer', defaults: [['front']] }, goBack: { only: SpriteMorph, type: 'command', category: 'looks', spec: 'go back %n layers', defaults: [1] }, // Looks - Debugging primitives for development mode doScreenshot: { dev: true, type: 'command', category: 'looks', spec: 'save %imgsource as costume named %s', defaults: [['pen trails'], localize('screenshot')] }, 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' }, doPlaySoundAtRate: { type: 'command', category: 'sound', spec: 'play sound %snd at %rate Hz', defaults: ['', 44100] }, doStopAllSounds: { type: 'command', category: 'sound', spec: 'stop all sounds' }, reportGetSoundAttribute: { type: 'reporter', category: 'sound', spec: '%aa of sound %snd', defaults: [['duration']] }, reportNewSoundFromSamples: { type: 'reporter', category: 'sound', spec: 'new sound %l rate %rate Hz', defaults: [null, 44100] }, doRest: { type: 'command', category: 'sound', spec: 'rest for %n beats', defaults: [0.2] }, doPlayNote: { type: 'command', category: 'sound', spec: 'play note %note for %n beats', defaults: [60, 0.5] }, doPlayFrequency: { // only in dev mode - experimental dev: true, type: 'command', category: 'sound', spec: 'play %n Hz for %n secs', defaults: [440, 2] }, doSetInstrument: { type: 'command', category: 'sound', spec: 'set instrument to %inst', defaults: [1] }, 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' }, changeVolume: { type: 'command', category: 'sound', spec: 'change volume by %n', defaults: [10] }, setVolume: { type: 'command', category: 'sound', spec: 'set volume to %n %', defaults: [100] }, getVolume: { type: 'reporter', category: 'sound', spec: 'volume' }, changePan: { type: 'command', category: 'sound', spec: 'change balance by %n', defaults: [10] }, setPan: { type: 'command', category: 'sound', spec: 'set balance to %n', defaults: [0] }, getPan: { type: 'reporter', category: 'sound', spec: 'balance' }, playFreq: { type: 'command', category: 'sound', spec: 'play frequency %n Hz', defaults: [440] }, stopFreq: { type: 'command', category: 'sound', spec: 'stop frequency' }, // 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' }, getPenDown: { only: SpriteMorph, type: 'predicate', category: 'pen', spec: 'pen down?' }, setColor: { only: SpriteMorph, type: 'command', category: 'pen', spec: 'set pen color to %clr' }, setPenColorDimension: { only: SpriteMorph, type: 'command', category: 'pen', spec: 'set pen %clrdim to %n', defaults: [['hue'], 50] }, changePenColorDimension: { only: SpriteMorph, type: 'command', category: 'pen', spec: 'change pen %clrdim by %n', defaults: [['hue'], 10] }, getPenAttribute: { type: 'reporter', category: 'pen', spec: 'pen %pen', defaults: [['hue']] }, setBackgroundColor: { only: StageMorph, type: 'command', category: 'pen', spec: 'set background color to %clr' }, setBackgroundColorDimension: { only: StageMorph, type: 'command', category: 'pen', spec: 'set background %clrdim to %n', defaults: [['hue'], 50] }, changeBackgroundColorDimension: { only: StageMorph, type: 'command', category: 'pen', spec: 'change background %clrdim by %n', defaults: [['hue'], 10] }, 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' }, write: { only: SpriteMorph, type: 'command', category: 'pen', spec: 'write %s size %n', defaults: [localize('Hello!'), 12] }, reportPenTrailsAsCostume: { type: 'reporter', category: 'pen', spec: 'pen trails' }, reportPentrailsAsSVG: { type: 'reporter', category: 'pen', spec: 'pen vectors' }, doPasteOn: { type: 'command', category: 'pen', spec: 'paste on %spr' }, doCutFrom: { type: 'command', category: 'pen', spec: 'cut from %spr' }, // Control receiveGo: { type: 'hat', category: 'control', spec: 'when %greenflag clicked' }, receiveKey: { type: 'hat', category: 'control', spec: 'when %keyHat key pressed %keyName', defaults: [['space']] }, receiveInteraction: { type: 'hat', category: 'control', spec: 'when I am %interaction', defaults: ['clicked'] }, receiveMessage: { type: 'hat', category: 'control', spec: 'when I receive %msgHat %message', defaults: [''] // trigger the "message" expansion to refresh }, receiveCondition: { type: 'hat', category: 'control', spec: 'when %b' }, getLastMessage: { // retained for legacy compatibility dev: true, type: 'reporter', category: 'control', spec: 'message' }, doBroadcast: { type: 'command', category: 'control', spec: 'broadcast %msg %receive' }, doBroadcastAndWait: { type: 'command', category: 'control', spec: 'broadcast %msg %receive and wait' }, doWait: { type: 'command', category: 'control', spec: 'wait %n secs', defaults: [1] }, doWaitUntil: { type: 'command', category: 'control', spec: 'wait until %b' }, doForever: { type: 'command', category: 'control', spec: 'forever %loop' }, doRepeat: { type: 'command', category: 'control', spec: 'repeat %n %loop', defaults: [10] }, doUntil: { type: 'command', category: 'control', spec: 'repeat until %b %loop' }, doFor: { type: 'command', category: 'control', spec: 'for %upvar = %n to %n %cla', defaults: ['i', 1, 10] }, doIf: { type: 'command', category: 'control', spec: 'if %b %c' }, doIfElse: { type: 'command', category: 'control', spec: 'if %b %c else %c' }, reportIfElse: { type: 'reporter', category: 'control', spec: 'if %b then %s else %s' }, doStopThis: { type: 'command', category: 'control', spec: 'stop %stopChoices', defaults: [['all']] }, 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' }, doReport: { type: 'command', category: 'control', spec: 'report %s' }, 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' }, // Message passing doTellTo: { type: 'command', category: 'control', // spec: 'tell %spr to %cl' // I liked this version better, -Jens spec: 'tell %spr to %cmdRing %inputs' }, reportAskFor: { type: 'reporter', category: 'control', spec: 'ask %spr for %repRing %inputs' }, // Cloning receiveOnClone: { type: 'hat', category: 'control', spec: 'when I start as a clone' }, createClone: { type: 'command', category: 'control', spec: 'create a clone of %cln', defaults: [['myself']] }, newClone: { type: 'reporter', category: 'control', spec: 'a new clone of %cln', defaults: [['myself']] }, removeClone: { type: 'command', category: 'control', spec: 'delete this clone' }, // Debugging - pausing doPauseAll: { type: 'command', category: 'control', spec: 'pause all %pause' }, // Scenes doSwitchToScene: { type: 'command', category: 'control', spec: 'switch to scene %scn %send', defaults: [['next']] }, // Sensing reportTouchingObject: { only: SpriteMorph, type: 'predicate', category: 'sensing', spec: 'touching %col ?', defaults: [['mouse-pointer']] }, reportTouchingColor: { only: SpriteMorph, type: 'predicate', category: 'sensing', spec: 'touching %clr ?' }, reportColorIsTouchingColor: { only: SpriteMorph, type: 'predicate', category: 'sensing', spec: 'color %clr is touching %clr ?' }, reportAspect: { type: 'reporter', category: 'sensing', spec: '%asp at %loc', defaults: [['hue'], ['mouse-pointer']] }, reportStackSize: { dev: true, type: 'reporter', category: 'sensing', spec: 'stack size' }, reportFrameCount: { dev: true, type: 'reporter', category: 'sensing', spec: 'frames' }, reportYieldCount: { dev: true, type: 'reporter', category: 'sensing', spec: 'yields' }, 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?', defaults: [['space']] }, reportRelationTo: { only: SpriteMorph, type: 'reporter', category: 'sensing', spec: '%rel to %dst', defaults: [['distance'], ['mouse-pointer']] }, 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 #']] }, reportObject: { type: 'reporter', category: 'sensing', spec: 'object %self', defaults: [['myself']] }, reportURL: { type: 'reporter', category: 'sensing', spec: 'url %s', defaults: ['snap.berkeley.edu'] }, doSetGlobalFlag: { type: 'command', category: 'sensing', spec: 'set %setting to %b', defaults: [['video capture']] }, reportGlobalFlag: { type: 'predicate', category: 'sensing', spec: 'is %setting on?', defaults: [['turbo mode']] }, reportDate: { type: 'reporter', category: 'sensing', spec: 'current %dates', defaults: [['date']] }, reportGet: { type: 'reporter', category: 'sensing', spec: 'my %get', defaults: [['neighbors']] }, reportAudio: { type: 'reporter', category: 'sensing', spec: 'microphone %audio', defaults: [['volume']] }, reportBlockAttribute: { type: 'reporter', category: 'sensing', spec: '%block of block %repRing', defaults: [['definition']] }, // 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: [['sqrt'], 10] }, reportPower: { type: 'reporter', category: 'operators', spec: '%n ^ %n' }, reportModulus: { type: 'reporter', category: 'operators', spec: '%n mod %n' }, reportAtan2: { type: 'reporter', category: 'operators', spec: 'atan2 %n ÷ %n' }, reportMin: { type: 'reporter', category: 'operators', spec: '%n min %n' }, reportMax: { type: 'reporter', category: 'operators', spec: '%n max %n' }, reportRandom: { type: 'reporter', category: 'operators', spec: 'pick random %n to %n', defaults: [1, 10] }, reportEquals: { type: 'predicate', category: 'operators', spec: '%s = %s' }, reportNotEquals: { type: 'predicate', category: 'operators', spec: '%s \u2260 %s' }, reportLessThan: { type: 'predicate', category: 'operators', spec: '%s < %s' }, reportLessThanOrEquals: { type: 'predicate', category: 'operators', spec: '%s \u2264 %s' }, reportGreaterThan: { type: 'predicate', category: 'operators', spec: '%s > %s' }, reportGreaterThanOrEquals: { type: 'predicate', category: 'operators', spec: '%s \u2265 %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', defaults: [true], 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 %idx 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, ['number']] }, 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: { 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: [['encode URI'], "Abelson & Sussman"] }, reportCompiled: { // experimental dev: true, type: 'reporter', category: 'operators', spec: 'compile %repRing for %n args', defaults: [null, 0] }, // 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 doDeleteAttr: { type: 'command', category: 'variables', spec: 'inherit %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: { // deprecated as of v6.6 dev: true, type: 'reporter', category: 'lists', spec: 'length of %l' }, reportListAttribute: { type: 'reporter', category: 'lists', spec: '%la of %l', defaults: [['length']] }, reportListContainsItem: { type: 'predicate', category: 'lists', spec: '%l contains %s', defaults: [null, localize('thing')] }, reportListIsEmpty: { type: 'predicate', category: 'lists', spec: 'is %l empty?' }, reportListIndex: { type: 'reporter', category: 'lists', spec: 'index of %s in %l', defaults: [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')] }, // numbers - (arrayed when hyper-blocks is on, otherwise linked) reportNumbers: { type: 'reporter', category: 'lists', spec: 'numbers from %n to %n', defaults: [1, 10] }, /* reportListCombination: { // currently not in use type: 'reporter', category: 'lists', spec: '%mlfunc %lists', defaults: [['append']] }, */ reportConcatenatedLists: { type: 'reporter', category: 'lists', spec: 'append %lists' }, reportTranspose: { // deprecated type: 'reporter', category: 'lists', spec: 'transpose %l' }, reportReshape: { type: 'reporter', category: 'lists', spec: 'reshape %l to %nums', defaults: [null, [4, 3]] }, /* reportSlice: { // currently not in use type: 'reporter', category: 'lists', spec: 'slice %l by %nums', defaults: [null, [2, -1]] }, */ // HOFs reportMap: { type: 'reporter', category: 'lists', spec: 'map %repRing over %l' }, reportAtomicMap: { dev: true, // not shown in palette, only accessible via relabelling type: 'reporter', category: 'lists', spec: '%blitz map %repRing over %l' }, reportKeep: { type: 'reporter', category: 'lists', spec: 'keep items %predRing from %l' }, reportAtomicKeep: { dev: true, // not shown in palette, only accessible via relabelling type: 'reporter', category: 'lists', spec: '%blitz keep items %predRing from %l' }, reportFindFirst: { type: 'reporter', category: 'lists', spec: 'find first item %predRing in %l' }, reportAtomicFindFirst: { dev: true, // not shown in palette, only accessible via relabelling type: 'reporter', category: 'lists', spec: '%blitz find first item %predRing in %l' }, reportCombine: { type: 'reporter', category: 'lists', spec: 'combine %l using %repRing' }, reportAtomicCombine: { dev: true, // not shown in palette, only accessible via relabelling type: 'reporter', category: 'lists', spec: '%blitz combine %l using %repRing' }, doForEach: { type: 'command', category: 'lists', spec: 'for each %upvar in %l %cla', defaults: [localize('item')] }, // Tables - experimental doShowTable: { dev: true, type: 'command', category: 'lists', spec: 'show table %l' }, // Code mapping doMapCodeOrHeader: { type: 'command', category: 'other', spec: 'map %cmdRing to %codeKind %code', defaults: [null, ['code']] }, doMapValueCode: { type: 'command', category: 'other', spec: 'map %mapValue to code %code', defaults: [['String'], '<#1>'] }, doMapListCode: { type: 'command', category: 'other', spec: 'map %codeListPart of %codeListKind to code %code' }, reportMappedCode: { type: 'reporter', category: 'other', spec: 'code of %cmdRing' }, // Extensions doApplyExtension: { type: 'command', category: 'other', spec: 'primitive %prim %mult%s' }, reportApplyExtension: { type: 'reporter', category: 'other', spec: 'primitive %prim %mult%s' }, // Video motion doSetVideoTransparency: { type: 'command', category: 'sensing', spec: 'set video transparency to %n', defaults: [50] }, reportVideo: { type: 'reporter', category: 'sensing', spec: 'video %vid on %self', defaults: [['motion'], ['myself']] } }; }; SpriteMorph.prototype.initBlocks(); SpriteMorph.prototype.initBlockMigrations = function () { // change blocks in existing projects to their updated version SpriteMorph.prototype.blockMigrations = { doStopAll: { selector: 'doStopThis', inputs: [['all']] }, doStop: { selector: 'doStopThis', inputs: [['this script']] }, doStopBlock: { selector: 'doStopThis', inputs: [['this block']] }, doStopOthers: { selector: 'doStopThis', inputs: [['all']], offset: 0 }, receiveClick: { selector: 'receiveInteraction', inputs: [['clicked']] }, reportTrue: { selector: 'reportBoolean', inputs: [true] }, reportFalse: { selector: 'reportBoolean', inputs: [false] }, reportCostumes: { selector: 'reportGet', inputs: [['costumes']] }, reportSounds: { selector: 'reportGet', inputs: [['sounds']] }, doMapStringCode: { selector: 'doMapValueCode', inputs: [['String'], '<#1>'], offset: 1 }, reportDistanceTo: { selector: 'reportRelationTo', inputs: [['distance']], offset: 1 }, comeToFront: { selector: 'goToLayer', inputs: [['front']] }, setHue: { selector: 'setPenColorDimension', inputs: [['hue']], offset: 1 }, setBrightness: { selector: 'setPenColorDimension', inputs: [['brightness']], offset: 1 }, setPenHSVA: { selector: 'setPenColorDimension' }, changeHue: { selector: 'changePenColorDimension', inputs: [['hue']], offset: 1 }, changeBrightness: { selector: 'changePenColorDimension', inputs: [['brightness']], offset: 1 }, changePenHSVA: { selector: 'changePenColorDimension' }, setBackgroundHSVA: { selector: 'setBackgroundColorDimension' }, changeBackgroundHSVA: { selector: 'changeBackgroundColorDimension' }, reportIsFastTracking: { selector: 'reportGlobalFlag', inputs: [['turbo mode']], offset: 1 }, doSetFastTracking: { selector: 'doSetGlobalFlag', inputs: [['turbo mode']], offset: 1 }, reportTableRotated: { selector: 'reportListAttribute', inputs: [['transpose']], offset: 1 }, reportTranspose: { selector: 'reportListAttribute', inputs: [['transpose']], offset: 1 }, reportListLength: { selector: 'reportListAttribute', inputs: [['length']], offset: 1 }, doSend: { selector: 'doBroadcast', expand: 1 } }; }; SpriteMorph.prototype.initBlockMigrations(); SpriteMorph.prototype.blockAlternatives = { // structure: // selector: [ersatz, ...] // ersatz can also be a 2-item array: [selector, input-offset] // motion: forward: ['changeXPosition', 'changeYPosition'], turn: ['turnLeft'], turnLeft: ['turn'], doFaceTowards: ['doGotoObject'], gotoXY: [['doGlide', 1]], doGotoObject: ['doFaceTowards'], doGlide: [['gotoXY', -1]], changeXPosition: ['changeYPosition', 'setXPosition', 'setYPosition', 'forward'], setXPosition: ['setYPosition', 'changeXPosition', 'changeYPosition'], changeYPosition: ['changeXPosition', 'setYPosition', 'setXPosition', 'forward'], 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', 'doPlaySoundAtRate'], doPlaySoundUntilDone: ['playSound', 'doPlaySoundAtRate'], doPlaySoundAtRate: ['playSound', 'doPlaySoundUntilDone'], doPlayNote: [['doRest', -1]], doRest: [['doPlayNote', 1]], doChangeTempo: ['doSetTempo'], doSetTempo: ['doChangeTempo'], setVolume: ['changeVolume'], changeVolume: ['setVolume'], setPan: ['changePan'], changePan: ['setPan'], getVolume: ['getTempo', 'getPan'], getTempo: ['getVolume', 'getPan'], getPan: ['getVolume', 'getTempo'], // pen: clear: ['down', 'up', 'doStamp'], down: ['up', 'clear', 'doStamp'], up: ['down', 'clear', 'doStamp'], doPasteOn: ['doCutFrom'], doCutFrom: ['doPasteOn'], doStamp: ['clear', 'down', 'up'], setPenColorDimension: ['changePenColorDimension'], changePenColorDimension: ['setPenColorDimension'], setBackgroundColorDimension: ['changeBackgroundColorDimension'], changeBackgroundColorDimension: ['setBackgroundColorDimension'], changeSize: ['setSize'], setSize: ['changeSize'], // control: doBroadcast: ['doBroadcastAndWait'], doBroadcastAndWait: ['doBroadcast'], doIf: ['doIfElse', 'doUntil'], doIfElse: ['doIf', 'doUntil'], doRepeat: ['doUntil', ['doForever', -1], ['doFor', 2], ['doForEach', 1]], doUntil: ['doRepeat', 'doIf', ['doForever', -1], ['doFor', 2], ['doForEach', 1]], doForever: [['doUntil', 1], ['doRepeat', 1], ['doFor', 3], ['doForEach', 2]], doFor: [['doForever', -3], ['doRepeat', -2], ['doUntil', -2], ['doForEach', -1]], // doRun: ['fork'], // fork: ['doRun'], // sensing: doAsk: ['bubble', 'doThink', 'doSayFor', 'doThinkFor'], getLastAnswer: ['getTimer'], getTimer: ['getLastAnswer'], reportMouseX: ['reportMouseY'], reportMouseY: ['reportMouseX'], // operators: reportSum: ['reportDifference', 'reportProduct', 'reportQuotient', 'reportPower', 'reportModulus', 'reportAtan2', 'reportMin', 'reportMax'], reportDifference: ['reportSum', 'reportProduct', 'reportQuotient', 'reportPower', 'reportModulus', 'reportAtan2', 'reportMin', 'reportMax'], reportProduct: ['reportDifference', 'reportSum', 'reportQuotient', 'reportPower', 'reportModulus', 'reportAtan2', 'reportMin', 'reportMax'], reportQuotient: ['reportDifference', 'reportProduct', 'reportSum', 'reportPower', 'reportModulus', 'reportAtan2', 'reportMin', 'reportMax'], reportPower: ['reportDifference', 'reportProduct', 'reportSum', 'reportQuotient', 'reportModulus', 'reportAtan2', 'reportMin', 'reportMax'], reportModulus: ['reportAtan2', 'reportDifference', 'reportProduct', 'reportSum','reportQuotient', 'reportPower', 'reportMin', 'reportMax'], reportAtan2: ['reportModulus', 'reportDifference', 'reportProduct', 'reportSum','reportQuotient', 'reportPower', 'reportMin', 'reportMax'], reportMin: ['reportMax', 'reportSum', 'reportDifference', 'reportProduct', 'reportQuotient', 'reportPower', 'reportModulus', 'reportAtan2'], reportMax: ['reportMin', 'reportSum', 'reportDifference', 'reportProduct', 'reportQuotient', 'reportPower', 'reportModulus', 'reportAtan2'], reportLessThan: ['reportLessThanOrEquals', 'reportEquals', 'reportIsIdentical', 'reportNotEquals', 'reportGreaterThan', 'reportGreaterThanOrEquals'], reportEquals: ['reportIsIdentical', 'reportNotEquals', 'reportLessThan', 'reportLessThanOrEquals', 'reportGreaterThan', 'reportGreaterThanOrEquals'], reportNotEquals: ['reportEquals', 'reportIsIdentical', 'reportLessThan', 'reportLessThanOrEquals', 'reportGreaterThan', 'reportGreaterThanOrEquals'], reportGreaterThan: ['reportGreaterThanOrEquals', 'reportEquals', 'reportIsIdentical', 'reportNotEquals', 'reportLessThan', 'reportLessThanOrEquals'], reportLessThanOrEquals: ['reportLessThan', 'reportEquals', 'reportIsIdentical', 'reportNotEquals', 'reportGreaterThan', 'reportGreaterThanOrEquals'], reportGreaterThanOrEquals: ['reportGreaterThan', 'reportEquals', 'reportIsIdentical', 'reportNotEquals', 'reportLessThan', 'reportLessThanOrEquals'], reportIsIdentical: ['reportEquals', 'reportNotEquals', 'reportLessThan', 'reportLessThanOrEquals', 'reportGreaterThan', 'reportGreaterThanOrEquals'], reportAnd: ['reportOr'], reportOr: ['reportAnd'], // variables doSetVar: ['doChangeVar'], doChangeVar: ['doSetVar'], doShowVar: ['doHideVar'], doHideVar: ['doShowVar'], // HOFs reportMap: ['reportKeep', 'reportFindFirst'], reportKeep: ['reportFindFirst', 'reportMap'], reportFindFirst: ['reportKeep', 'reportMap'], doForEach: [['doFor', 1], ['doForever', -2], ['doRepeat', -1], ['doUntil', -1]] }; // 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.customBlocks = []; this.costumes = new List(); this.costumes.type = 'costume'; this.costume = null; this.sounds = new List(); this.sounds.type = 'sound'; 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.instrument = null; this.version = Date.now(); // for observer optimization this.isTemporary = false; // indicate a temporary Scratch-style clone this.isCorpse = false; // indicate whether a sprite/clone has been deleted this.cloneOriginName = ''; // volume and stereo-pan support, experimental: this.volume = 100; this.gainNode = null; // must be lazily initialized in Chrome, sigh... this.pan = 0; this.pannerNode = null; // must be lazily initialized in Chrome, sigh... // frequency player, experimental this.freqPlayer = null; // Note, to be lazily initialized // pen color dimensions support this.cachedColorDimensions = [0, 0, 0]; // not serialized // only temporarily for serialization this.inheritedMethodsCache = []; // 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.primitivesCache = {}; // not to be serialized (!) this.paletteCache = {}; // not to be serialized (!) this.categoriesCache = null; // not to be serialized (!) this.rotationOffset = ZERO; // not to be serialized (!) this.idx = 0; // not to be serialized (!) - used for de-serialization 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; this.instances = []; this.cachedPropagation = false; // not to be persisted this.inheritedAttributes = []; // 'x position', 'direction', 'size' etc... // video- and rendering state this.imageExtent = ZERO; this.imageOffset = ZERO; this.imageData = {}; // version: date, pixels: Uint32Array this.motionAmount = 0; this.motionDirection = 0; this.frameNumber = 0; SpriteMorph.uber.init.call(this); this.isCachingImage = true; this.isFreeForm = true; this.cachedColorDimensions = this.color[this.penColorModel](); this.isDraggable = true; this.isDown = false; this.heading = 90; this.fixLayout(); this.rerender(); }; // SpriteMorph duplicating (fullCopy) SpriteMorph.prototype.fullCopy = function (forClone) { var c = SpriteMorph.uber.fullCopy.call(this), arr = [], cb, effect; // make sure the clone has its own canvas to recycle // needs to be copied instead of redrawn, because at // this time the clone is not yet onstage and therefore // has no access to the stage's scale c.cachedImage = copyCanvas(this.cachedImage); // un-share individual properties c.instances = []; c.stopTalking(); c.color = this.color.copy(); c.gainNode = null; c.pannerNode = null; c.freqPlayer = null; c.primitivesCache = {}; c.paletteCache = {}; c.categoriesCache = null; c.imageData = {}; c.cachedColorDimensions = c.color[this.penColorModel](); arr = []; this.inheritedAttributes.forEach(att => arr.push(att)); c.inheritedAttributes = arr; if (forClone) { c.exemplar = this; c.customBlocks = []; c.variables = new VariableFrame(null, c); c.variables.parentFrame = this.variables; c.inheritedVariableNames().forEach(name => c.shadowVar(name, c.variables.getVar(name)) ); this.addSpecimen(c); this.cachedPropagation = false; ['scripts', 'costumes', 'sounds'].forEach(att => { if (!contains(c.inheritedAttributes, att)) { c.inheritedAttributes.push(att); } }); } else { c.variables = this.variables.copy(); c.variables.owner = c; c.scripts = this.scripts.fullCopy(); c.customBlocks = []; this.customBlocks.forEach(def => { cb = def.copyAndBindTo(c); c.customBlocks.push(cb); c.allBlockInstances(def).forEach(block => block.definition = cb ); }); arr = []; this.costumes.asArray().forEach(costume => { var cst = forClone ? costume : costume.copy(); arr.push(cst); if (costume === this.costume) { c.costume = cst; } }); c.costumes = new List(arr); c.costumes.type = 'costume'; arr = []; this.sounds.asArray().forEach(sound => { var snd = forClone ? sound : sound.copy(); arr.push(snd); }); c.sounds = new List(arr); c.sounds.type = 'sound'; arr = []; } c.nestingScale = 1; c.rotatesWithAnchor = true; c.anchor = null; c.parts = []; this.parts.forEach(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.isTemporary) { this.name = ide.newSpriteName(this.name); ide.corral.addSprite(this); ide.sprites.add(this); } ide.stage.add(this); this.parts.forEach(part => part.appearIn(ide)); }; // SpriteMorph versioning SpriteMorph.prototype.setName = function (string) { this.name = string || this.name; this.version = Date.now(); }; // SpriteMorph rendering SpriteMorph.prototype.getImage = function () { // overrides inherited method to allow for an image exceeding my bounds // to accommodate rotation and to disable retina resolution to // optimize graphics performance if (this.shouldRerender || !this.cachedImage) { this.cachedImage = newCanvas( this.costume ? this.imageExtent : this.extent(), !isNil(this.costume), // retina this.cachedImage ); this.render(this.cachedImage.getContext('2d')); this.shouldRerender = false; } return this.cachedImage; }; SpriteMorph.prototype.fixLayout = function () { // determine my extent and the extent designated for my cached image var currentCenter, facing, // actual costume heading based on my rotation style isFlipped, isLoadingCostume, pic, // (flipped copy of) actual costume based on my rotation style imageSide, stageScale, newX, corners = [], origin, corner, costumeExtent; 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(point => point.rotateBy( radians(facing - 90), this.costume.center() ) ); origin = corners[0]; corner = corners[0]; corners.forEach(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 this.imageOffset = ZERO.rotateBy( radians(-(facing - 90)), pic.center() ).subtract(origin); // determine an adequately dimensioned image extent, so the // shape on the canvas ran be rotated without having to create // a new canvas each time if (this.rotationStyle === 1) { // rotate freely in all directions // create a canvas that is big enough, so the sprite's current // costume can be fully rotated inside, so we can recycle // the canvas for re-rendering until the sprite's or the stage's // scale changes. // note that the canvas will be too big for most situations, but // the sprite's bounds indicate the actually visible area. // recycling canvas elements instead of creating new ones whenever // we render a sprite boosts performance for recent browser // architectures that move canvas elements to the GPU for // rendering (which is a pain in the ass and an altogether // bad idea for any serious GUI but looks oh-so-nice for // some flashy graphic effects in the Apple store). imageSide = Math.sqrt( Math.pow(pic.width(), 2) + Math.pow(pic.height(), 2) ) * this.scale * stageScale; this.imageExtent = new Point(imageSide, imageSide); } else { // don't actually rotate this.imageExtent = costumeExtent; } this.bounds.setExtent(costumeExtent); // adjust my position to the rotation this.setCenter(currentCenter, true); // determine my rotation offset this.rotationOffset = this.imageOffset .translateBy(pic.rotationCenter) .rotateBy(radians(-(facing - 90)), this.imageOffset) .scaleBy(this.scale * stageScale); } else { facing = isFlipped ? -90 : facing; newX = Math.min( Math.max( this.normalExtent.x * this.scale * stageScale, 5 ), 1000 ); this.bounds.setWidth(newX); this.bounds.setHeight(newX); this.setCenter(currentCenter, true); // just me this.rotationOffset = this.extent().divideBy(2); } }; SpriteMorph.prototype.render = function (ctx) { var myself = this, facing, // actual costume heading based on my rotation style isFlipped, isLoadingCostume, cst, pic, // (flipped copy of) actual costume based on my rotation style stageScale, handle; 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; ctx.save(); ctx.scale(this.scale * stageScale, this.scale * stageScale); ctx.translate(this.imageOffset.x, this.imageOffset.y); ctx.rotate(radians(facing - 90)); ctx.drawImage(pic.contents, 0, 0); ctx.restore(); } else { facing = isFlipped ? -90 : facing; SpriteMorph.uber.render.call(this, ctx, facing); if (isLoadingCostume) { // retry until costume is done loading cst = this.costume; handle = setInterval( function () { myself.wearCostume(cst, true); clearInterval(handle); }, 100 ); return this.wearCostume(null, true); } } // apply graphics effects to image this.cachedImage = this.applyGraphicsEffects(this.cachedImage); this.version = Date.now(); }; SpriteMorph.prototype.rotationCenter = function () { return this.position().add(this.rotationOffset); }; SpriteMorph.prototype.getImageData = function () { // used for video motion detection. // Get sprite image data scaled to 1 an converted to ABGR array, // cache to reduce GC load if (this.version !== this.imageData.version) { var stage = this.parentThatIsA(StageMorph), ext = this.extent(), newExtent = new Point( Math.floor(ext.x / stage.scale), Math.floor(ext.y / stage.scale) ), canvas = newCanvas(newExtent, true), canvasContext, imageData; canvasContext = canvas.getContext("2d"); canvasContext.drawImage( this.getImage(), 0, 0, Math.floor(ext.x), Math.floor(ext.y), 0, 0, newExtent.x, newExtent.y ); imageData = canvasContext.getImageData( 0, 0, newExtent.x, newExtent.y ).data; this.imageData = { version : this.version, pixels : new Uint32Array(imageData.buffer.slice(0)) }; } return this.imageData.pixels; }; SpriteMorph.prototype.projectionSnap = function() { var stage = this.parentThatIsA(StageMorph), center = this.center().subtract(stage.position()) .divideBy(stage.scale), cst = this.costume || this.getImage(), w, h, rot, offset, snap, ctx; if (cst instanceof Costume) { rot = cst.rotationCenter.copy(); cst = cst.contents; w = cst.width; h = cst.height; } else { w = this.width(); h = this.height(); } offset = new Point( Math.floor(center.x - (w / 2)), Math.floor(center.y - (h / 2)) ); snap = newCanvas(new Point(w, h), true); ctx = snap.getContext('2d'); ctx.drawImage(cst, 0, 0); ctx.globalCompositeOperation = 'source-atop'; ctx.drawImage(stage.projectionLayer(), -offset.x, -offset.y); return new Costume(snap, this.newCostumeName(localize('snap')), rot); }; // 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.blockColorFor(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 (migration && migration.expand) { block.inputs()[migration.expand].addInput(); } 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]); if (inputs[i] instanceof MultiArgMorph) { inputs[i].defaults = defaults[i]; } } } } } return block; }; SpriteMorph.prototype.variableBlock = function (varName, isLocalTemplate) { var block = new ReporterBlockMorph(false); block.selector = 'reportGetVar'; block.color = this.blockColor.variables; block.category = 'variables'; block.isLocalVarTemplate = isLocalTemplate; block.setSpec(varName); block.isDraggable = true; return block; }; // SpriteMorph block templates SpriteMorph.prototype.blockTemplates = function ( category = 'motion', all = false // include hidden blocks ) { var blocks = [], myself = this, varNames, inheritedVars = this.inheritedVariableNames(), wrld = this.world(), devMode = wrld && wrld.isDevMode; function block(selector, isGhosted) { if (StageMorph.prototype.hiddenPrimitives[selector] && !all) { return null; } var newBlock = SpriteMorph.prototype.blockForSelector(selector, true); newBlock.isTemplate = true; if (isGhosted) {newBlock.ghost(); } return newBlock; } function variableBlock(varName, isLocal) { var newBlock = SpriteMorph.prototype.variableBlock(varName, isLocal); 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 ); } if (category === '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', this.inheritsAttribute('x position'))); blocks.push(watcherToggle('yPosition')); blocks.push(block('yPosition', this.inheritsAttribute('y position'))); blocks.push(watcherToggle('direction')); blocks.push(block('direction', this.inheritsAttribute('direction'))); } else if (category === 'looks') { blocks.push(block('doSwitchToCostume')); blocks.push(block('doWearNextCostume')); blocks.push(watcherToggle('getCostumeIdx')); blocks.push(block('getCostumeIdx', this.inheritsAttribute('costume #'))); blocks.push('-'); blocks.push(block('doSayFor')); blocks.push(block('bubble')); blocks.push(block('doThinkFor')); blocks.push(block('doThink')); blocks.push('-'); blocks.push(block('reportGetImageAttribute')); blocks.push(block('reportNewCostumeStretched')); blocks.push(block('reportNewCostume')); blocks.push('-'); blocks.push(block('changeEffect')); blocks.push(block('setEffect')); blocks.push(block('clearEffects')); blocks.push(block('getEffect')); blocks.push('-'); blocks.push(block('changeScale')); blocks.push(block('setScale')); blocks.push(watcherToggle('getScale')); blocks.push(block('getScale', this.inheritsAttribute('size'))); blocks.push('-'); blocks.push(block('show')); blocks.push(block('hide')); blocks.push(watcherToggle('reportShown')); blocks.push(block('reportShown', this.inheritsAttribute('shown?'))); blocks.push('-'); blocks.push(block('goToLayer')); blocks.push(block('goBack')); // for debugging: /////////////// if (devMode) { blocks.push('-'); blocks.push(this.devModeText()); blocks.push('-'); blocks.push(block('log')); blocks.push(block('alert')); blocks.push('-'); blocks.push(block('doScreenshot')); } } else if (category === 'sound') { blocks.push(block('playSound')); blocks.push(block('doPlaySoundUntilDone')); blocks.push(block('doStopAllSounds')); blocks.push('-'); blocks.push(block('doPlaySoundAtRate')); blocks.push(block('reportGetSoundAttribute')); blocks.push(block('reportNewSoundFromSamples')); blocks.push('-'); blocks.push(block('doRest')); blocks.push(block('doPlayNote')); blocks.push(block('doSetInstrument')); blocks.push('-'); blocks.push(block('doChangeTempo')); blocks.push(block('doSetTempo')); blocks.push(watcherToggle('getTempo')); blocks.push(block('getTempo')); blocks.push('-'); blocks.push(block('changeVolume')); blocks.push(block('setVolume')); blocks.push(watcherToggle('getVolume')); blocks.push(block('getVolume', this.inheritsAttribute('volume'))); blocks.push('-'); blocks.push(block('changePan')); blocks.push(block('setPan')); blocks.push(watcherToggle('getPan')); blocks.push(block('getPan', this.inheritsAttribute('balance'))); blocks.push('-'); blocks.push(block('playFreq')); blocks.push(block('stopFreq')); // for debugging: /////////////// if (devMode) { blocks.push('-'); blocks.push(this.devModeText()); blocks.push('-'); blocks.push(block('doPlayFrequency')); } } else if (category === 'pen') { blocks.push(block('clear')); blocks.push('-'); blocks.push(block('down')); blocks.push(block('up')); blocks.push(watcherToggle('getPenDown')); blocks.push(block('getPenDown', this.inheritsAttribute('pen down?'))); blocks.push('-'); blocks.push(block('setColor')); blocks.push(block('changePenColorDimension')); blocks.push(block('setPenColorDimension')); blocks.push(block('getPenAttribute')); blocks.push('-'); blocks.push(block('changeSize')); blocks.push(block('setSize')); blocks.push('-'); blocks.push(block('doStamp')); blocks.push(block('floodFill')); blocks.push(block('write')); blocks.push('-'); blocks.push(block('reportPenTrailsAsCostume')); blocks.push('-'); blocks.push(block('doPasteOn')); blocks.push(block('doCutFrom')); } else if (category === 'control') { blocks.push(block('receiveGo')); blocks.push(block('receiveKey')); blocks.push(block('receiveInteraction')); blocks.push(block('receiveCondition')); blocks.push('-'); blocks.push(block('receiveMessage')); blocks.push(block('doBroadcast')); blocks.push(block('doBroadcastAndWait')); blocks.push('-'); blocks.push(block('doWarp')); blocks.push('-'); blocks.push(block('doWait')); blocks.push(block('doWaitUntil')); blocks.push('-'); blocks.push(block('doForever')); blocks.push(block('doRepeat')); blocks.push(block('doUntil')); blocks.push(block('doFor')); blocks.push('-'); blocks.push(block('doIf')); blocks.push(block('doIfElse')); blocks.push(block('reportIfElse')); blocks.push('-'); blocks.push(block('doReport')); blocks.push(block('doStopThis')); blocks.push('-'); blocks.push(block('doRun')); blocks.push(block('fork')); blocks.push(block('evaluate')); blocks.push('-'); blocks.push(block('doTellTo')); blocks.push(block('reportAskFor')); blocks.push('-'); blocks.push(block('doCallCC')); blocks.push(block('reportCallCC')); blocks.push('-'); blocks.push(block('receiveOnClone')); blocks.push(block('createClone')); blocks.push(block('newClone')); blocks.push(block('removeClone')); blocks.push('-'); blocks.push(block('doPauseAll')); blocks.push(block('doSwitchToScene')); // for debugging: /////////////// if (devMode) { blocks.push('-'); blocks.push(this.devModeText()); blocks.push('-'); blocks.push(watcherToggle('getLastMessage')); blocks.push(block('getLastMessage')); } } else if (category === '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('reportRelationTo')); blocks.push(block('reportAspect')); 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(block('reportObject')); blocks.push('-'); blocks.push(block('reportURL')); blocks.push(block('reportAudio')); blocks.push(block('reportVideo')); blocks.push(block('doSetVideoTransparency')); blocks.push('-'); blocks.push(block('reportGlobalFlag')); blocks.push(block('doSetGlobalFlag')); blocks.push('-'); blocks.push(block('reportDate')); blocks.push(block('reportBlockAttribute')); // for debugging: /////////////// if (devMode) { blocks.push('-'); blocks.push(this.devModeText()); blocks.push('-'); blocks.push(watcherToggle('reportThreadCount')); blocks.push(block('reportThreadCount')); blocks.push(block('reportStackSize')); blocks.push(block('reportFrameCount')); blocks.push(block('reportYieldCount')); } } else if (category === '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(block('reportPower')); 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 (Process.prototype.enableJS) { blocks.push('-'); blocks.push(block('reportJSFunction')); if (Process.prototype.enableCompiling) { blocks.push(block('reportCompiled')); } } // for debugging: /////////////// if (devMode) { blocks.push('-'); blocks.push(this.devModeText()); blocks.push('-'); blocks.push(block('reportTypeOf')); blocks.push(block('reportTextFunction')); } } else if (category === 'variables') { blocks.push(this.makeVariableButton()); if (this.deletableVariableNames().length > 0) { blocks.push(this.deleteVariableButton()); } blocks.push('-'); varNames = this.reachableGlobalVariableNames(true, all); if (varNames.length > 0) { varNames.forEach(name => { blocks.push(variableWatcherToggle(name)); blocks.push(variableBlock(name)); }); blocks.push('-'); } varNames = this.allLocalVariableNames(true, all); if (varNames.length > 0) { varNames.forEach(name => { blocks.push(variableWatcherToggle(name)); blocks.push(variableBlock(name, true)); }); 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(block('reportNumbers')); blocks.push('-'); blocks.push(block('reportCONS')); blocks.push(block('reportListItem')); blocks.push(block('reportCDR')); blocks.push('-'); blocks.push(block('reportListAttribute')); blocks.push(block('reportListIndex')); blocks.push(block('reportListContainsItem')); blocks.push(block('reportListIsEmpty')); blocks.push('-'); blocks.push(block('reportMap')); blocks.push(block('reportKeep')); blocks.push(block('reportFindFirst')); blocks.push(block('reportCombine')); blocks.push('-'); blocks.push(block('doForEach')); blocks.push('-'); blocks.push(block('reportConcatenatedLists')); blocks.push(block('reportReshape')); blocks.push('-'); blocks.push(block('doAddToList')); blocks.push(block('doDeleteFromList')); blocks.push(block('doInsertInList')); blocks.push(block('doReplaceInList')); if (SpriteMorph.prototype.showingExtensions) { blocks.push('='); blocks.push(block('doApplyExtension')); blocks.push(block('reportApplyExtension')); } if (StageMorph.prototype.enableCodeMapping) { blocks.push('='); blocks.push(block('doMapCodeOrHeader')); blocks.push(block('doMapValueCode')); blocks.push(block('doMapListCode')); blocks.push('-'); blocks.push(block('reportMappedCode')); } // for debugging: /////////////// if (this.world().isDevMode) { blocks.push('-'); blocks.push(this.devModeText()); blocks.push('-'); blocks.push(block('doShowTable')); } } return blocks; }; // Utitlies displayed in the palette SpriteMorph.prototype.makeVariableButton = function () { var button, myself = this; 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]); myself.toggleVariableWatcher(pair[0], pair[1]); ide.flushBlocksCache('variables'); // b/c of inheritance ide.refreshPalette(); ide.recordUnsavedChanges(); } } } button = new PushButtonMorph( null, function () { new VariableDialogMorph( null, addVar, myself ).prompt( 'Variable name', null, myself.world() ); }, 'Make a variable' ); button.userMenu = this.helpMenu; button.selector = 'addVariable'; button.showHelp = BlockMorph.prototype.showHelp; return button; }; SpriteMorph.prototype.deleteVariableButton = function () { var button, myself = this; button = new PushButtonMorph( null, function () { var menu = new MenuMorph( myself.deleteVariable, null, myself ); myself.deletableVariableNames().forEach(name => menu.addItem( name, name, null, null, null, null, null, null, true // verbatim - don't translate ) ); menu.popUpAtHand(myself.world()); }, 'Delete a variable' ); button.userMenu = this.helpMenu; button.selector = 'deleteVariable'; button.showHelp = BlockMorph.prototype.showHelp; return button; }; SpriteMorph.prototype.categoryText = function (category) { var txt = new StringMorph( localize(category[0].toUpperCase().concat(category.slice(1))), 11, null, true ); txt.setColor(this.paletteTextColor); txt.category = category; return txt; }; SpriteMorph.prototype.devModeText = function () { var txt = new TextMorph( localize('development mode \ndebugging primitives:') ); txt.fontSize = 9; txt.setColor(this.paletteTextColor); return txt; }; SpriteMorph.prototype.helpMenu = function () { // return a 1 item context menu for anything that implements // a 'showHelp' method. var menu = new MenuMorph(this); menu.addItem('help...', 'showHelp'); return menu; }; SpriteMorph.prototype.customBlockTemplatesForCategory = function ( category, includeHidden ) { // returns an array of block templates for a selected category. var ide = this.parentThatIsA(IDE_Morph), blocks = [], isInherited = false, block, inheritedBlocks; function addCustomBlock(definition) { if ((!definition.isHelper || includeHidden) && definition.category === category) { block = definition.templateInstance(); if (isInherited) {block.ghost(); } blocks.push(block); } } // global custom blocks: if (ide && ide.stage) { ide.stage.globalBlocks.forEach(addCustomBlock); if (this.customBlocks.length) {blocks.push('='); } } // local custom blocks: this.customBlocks.forEach(addCustomBlock); // inherited custom blocks: if (this.exemplar) { inheritedBlocks = this.inheritedBlocks(true); if (this.customBlocks.length && inheritedBlocks.length) { blocks.push('='); } isInherited = true; inheritedBlocks.forEach(addCustomBlock); } return blocks; }; SpriteMorph.prototype.makeBlockButton = function (category) { // answer a button that prompts the user to make a new block var button = new PushButtonMorph( this, 'makeBlock', 'Make a block' ); button.userMenu = this.helpMenu; button.selector = 'addCustomBlock'; button.showHelp = BlockMorph.prototype.showHelp; return button; }; SpriteMorph.prototype.makeBlock = function () { // prompt the user to make a new block var ide = this.parentThatIsA(IDE_Morph), stage = this.parentThatIsA(StageMorph), category = ide.currentCategory === 'unified' ? ide.topVisibleCategoryInPalette() : ide.currentCategory, clr = SpriteMorph.prototype.blockColorFor(category), dlg; dlg = new BlockDialogMorph( null, definition => { if (definition.spec !== '') { if (definition.isGlobal) { stage.globalBlocks.push(definition); } else { this.customBlocks.push(definition); } ide.flushPaletteCache(); ide.categories.refreshEmpty(); ide.refreshPalette(); ide.recordUnsavedChanges(); new BlockEditorMorph(definition, this).popUp(); } }, this ); if (category !== 'variables' || category !== 'unified') { dlg.category = category; dlg.categories.refresh(); dlg.types.children.forEach(each => { each.setColor(clr); each.refresh(); }); } dlg.prompt( 'Make a block', null, this.world() ); }; SpriteMorph.prototype.getPrimitiveTemplates = function (category) { var blocks = this.primitivesCache[category]; if (!blocks) { blocks = this.blockTemplates(category); if (this.isCachingPrimitives) { this.primitivesCache[category] = blocks; } } 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 myself = this, palette = new ScrollFrameMorph(null, null, this.sliderColor), unit = SyntaxElementMorph.prototype.fontSize, ide, showCategories, showButtons, x = 0, y = 5, ry = 0, blocks, hideNextSpace = false, shade = new Color(140, 140, 140), searchButton, makeButton; palette.owner = this; palette.padding = unit / 2; palette.color = this.paletteColor; palette.growth = new Point(0, MorphicPreferences.scrollBarSize); // toolbar: palette.toolBar = new AlignmentMorph('column'); searchButton = new PushButtonMorph( this, "searchBlocks", new SymbolMorph("magnifierOutline", 16) ); searchButton.alpha = 0.2; searchButton.padding = 1; searchButton.hint = localize('find blocks') + '...'; searchButton.labelShadowColor = shade; searchButton.edge = 0; searchButton.padding = 3; searchButton.fixLayout(); palette.toolBar.add(searchButton); makeButton = new PushButtonMorph( this, "makeBlock", new SymbolMorph("cross", 16) ); makeButton.alpha = 0.2; makeButton.padding = 1; makeButton.hint = localize('Make a block') + '...'; makeButton.labelShadowColor = shade; makeButton.edge = 0; makeButton.padding = 3; makeButton.fixLayout(); palette.toolBar.add(makeButton); palette.toolBar.fixLayout(); palette.add(palette.toolBar); // menu: palette.userMenu = function () { var menu = new MenuMorph(); menu.addPair( [ new SymbolMorph( 'magnifyingGlass', MorphicPreferences.menuFontSize ), localize('find blocks') + '...' ], () => myself.searchBlocks(), '^F' ); menu.addItem( 'hide blocks...', () => new BlockVisibilityDialogMorph(myself).popUp(myself.world()) ); menu.addLine(); menu.addItem( 'make a category...', () => this.parentThatIsA(IDE_Morph).createNewCategory() ); if (SpriteMorph.prototype.customCategories.size) { menu.addItem( 'delete a category...', () => this.parentThatIsA(IDE_Morph).deleteUserCategory() ); } return menu; }; if (category === 'unified') { // In a Unified Palette custom blocks appear following each category, // but there is only 1 make a block button (at the end). ide = this.parentThatIsA(IDE_Morph); showCategories = ide.scene.showCategories; showButtons = ide.scene.showPaletteButtons; blocks = SpriteMorph.prototype.allCategories().reduce( (blocks, category) => { let header = [this.categoryText(category), '-'], primitives = this.getPrimitiveTemplates(category), customs = this.customBlockTemplatesForCategory(category), showHeader = showCategories && !['lists', 'other'].includes(category) && (primitives.some(item => item instanceof BlockMorph) || customs.length); // hide category names if (!showCategories && category !== 'variables') { primitives = primitives.filter(each => each !== '-' && each !== '='); } // hide "make / delete a variable" buttons if (!showButtons && category === 'variables') { primitives = primitives.filter(each => !(each instanceof PushButtonMorph && !(each instanceof ToggleMorph))); } return blocks.concat( showHeader ? header : [], primitives, showHeader ? '=' : null, customs, showHeader ? '=' : '-' ); }, [] ); } else { // ensure we do not modify the cached array blocks = this.getPrimitiveTemplates(category).slice(); } if (category !== 'unified' || showButtons) { blocks.push('='); blocks.push(this.makeBlockButton(category)); } if (category !== 'unified') { blocks.push('='); blocks.push(...this.customBlockTemplatesForCategory(category)); } if (category === 'variables') { blocks.push(...this.customBlockTemplatesForCategory('lists')); blocks.push(...this.customBlockTemplatesForCategory('other')); } blocks.forEach(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 === 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) { x = block.right() + unit / 2; } else if (block instanceof RingMorph) { x = block.right() + unit / 2; ry = block.bottom(); } else { x = 0; y += block.height(); } } }); palette.scrollX(palette.padding); palette.scrollY(palette.padding); return palette; }; // SpriteMorph utilities for showing & hiding blocks in the palette SpriteMorph.prototype.allPaletteBlocks = function () { // private - only to be used for showing & hiding blocks in the palette var blocks = SpriteMorph.prototype.allCategories().reduce( (blocks, category) => { let primitives = this.blockTemplates(category, true), customs = this.customBlockTemplatesForCategory(category, true); return blocks.concat( primitives, customs ); }, [] ); return blocks.filter(each => each instanceof BlockMorph); }; SpriteMorph.prototype.isHidingBlock = function (aBlock) { var frame; if (aBlock.isCustomBlock) { return ( aBlock.isGlobal ? aBlock.definition : this.getMethod(aBlock.semanticSpec) ).isHelper; } if (aBlock.selector === 'reportGetVar') { frame = this.variables.silentFind(aBlock.blockSpec); if (!frame) { return false; } return frame.vars[aBlock.blockSpec].isHidden; } return StageMorph.prototype.hiddenPrimitives[aBlock.selector] === true; }; SpriteMorph.prototype.isDisablingBlock = function (aBlock) { // show or hide certain kinds of blocks in search results only // if they are enabled var sel = aBlock.selector; if (sel === 'reportJSFunction') { return !Process.prototype.enableJS; } if ( sel === 'doApplyExtension' || sel === 'reportApplyExtension' ) { return !SpriteMorph.prototype.showingExtensions; } if ( sel === 'doMapCodeOrHeader' || sel === 'doMapValueCode' || sel === 'doMapListCode' || sel === 'reportMappedCode' ) { return !StageMorph.prototype.enableCodeMapping; } return false; }; SpriteMorph.prototype.changeBlockVisibility = function (aBlock, hideIt, quick) { var ide = this.parentThatIsA(IDE_Morph), dict, cat; if (aBlock.isCustomBlock) { (aBlock.isGlobal ? aBlock.definition : this.getMethod(aBlock.semanticSpec) ).isHelper = !!hideIt; } else if (aBlock.selector === 'reportGetVar') { this.variables.find( aBlock.blockSpec ).vars[aBlock.blockSpec].isHidden = !!hideIt; } else { if (hideIt) { StageMorph.prototype.hiddenPrimitives[aBlock.selector] = true; } else { delete StageMorph.prototype.hiddenPrimitives[aBlock.selector]; } } if (quick) {return; } dict = { doWarp: 'control', reifyScript: 'operators', reifyReporter: 'operators', reifyPredicate: 'operators', doDeclareVariables: 'variables' }; cat = dict[aBlock.selector] || aBlock.category; if (cat === 'lists') {cat = 'variables'; } ide.flushBlocksCache(cat); ide.refreshPalette(); }; SpriteMorph.prototype.emptyCategories = function () { // return a dictionary that indicates for each category whether // it has any shown blocks in it (true) or is empty (false) var hasBlocks = (any) => any instanceof BlockMorph && !this.isHidingBlock(any); if (this.categoriesCache === null) { this.categoriesCache = {}; SpriteMorph.prototype.allCategories().forEach(category => this.categoriesCache[category] = this.getPrimitiveTemplates(category).some(hasBlocks) || this.customBlockTemplatesForCategory(category).some(hasBlocks)); } return this.categoriesCache; }; // 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, 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(each => each.indexOf('%') !== 0 || each.length === 1 ), slots = words.filter(each => each.length > 1 && each.indexOf('%') === 0 ).map(spec => menuOf(spec)); return filtered.join(' ') + ' ' + slots.join(' '); } function menuOf(aSlotSpec) { var info = BlockMorph.prototype.labelParts[aSlotSpec] || {}, menu = info.menu; if (!menu) {return ''; } if (isString(menu)) { menu = InputSlotMorph.prototype[menu](true); } return Object.values(menu).map(entry => { if (isNil(entry)) {return ''; } if (entry instanceof Array) { return localize(entry[0]); } return entry.toString(); }).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.toLowerCase(), 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(vName => { var rel = relevance(vName, search); if (rel !== -1) { blocks.push([this.variableBlock(vName), rel + '1']); } }); // custom blocks [this.customBlocks, stage.globalBlocks].forEach(blocksList => blocksList.forEach(definition => { if (contains(types, definition.type)) { var spec = definition.localizedSpec(), rel = relevance(labelOf( spec) + ' ' + definition.menuSearchWords(), search ); if (rel !== -1) { blocks.push([definition.templateInstance(), rel + '2']); } } }) ); // primitives blocksDict = SpriteMorph.prototype.blocks; Object.keys(blocksDict).forEach(selector => { if (!StageMorph.prototype.hiddenPrimitives[selector] && contains(types, blocksDict[selector].type)) { var block = blocksDict[selector], spec = localize(block.alias || block.spec), rel = relevance(labelOf(spec), search); if ( (rel !== -1) && (!block.dev) && (!block.only || (block.only === this.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((x, y) => x[1] < y[1] ? -1 : 1); blocks = blocks.map(each => each[0]); return blocks.filter(each => !this.isHidingBlock(each) && !this.isDisablingBlock(each) ); }; SpriteMorph.prototype.searchBlocks = function ( searchString, types, varNames, scriptFocus ) { var myself = this, unit = SyntaxElementMorph.prototype.fontSize, ide = this.parentThatIsA(IDE_Morph), oldTop = ide.palette.contents.top(), 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) : WHITE, 2 ); searchPane.contents.add(focus); focus.scrollIntoView(); } function show(blocks) { var x = searchPane.contents.left() + 5, y = (searchBar.bottom() + unit); blocksList = blocks; selection = null; if (blocks.length && scriptFocus) { selection = blocks[0]; } searchPane.contents.children = [searchPane.contents.children[0]]; blocks.forEach(block => { block.setPosition(new Point(x, y)); searchPane.addContents(block); y += block.height(); y += unit * 0.3; }); showSelection(); searchPane.changed(); } searchPane.owner = this; searchPane.color = this.paletteColor; searchPane.contents.color = this.paletteColor; searchPane.addContents(searchBar); searchBar.setWidth(ide.logo.width() - 30); searchBar.contrast = 90; searchBar.setPosition( searchPane.contents.topLeft().add(new Point(10, 10)) ); searchBar.fixLayout(); searchPane.accept = function () { var search; if (scriptFocus) { searchBar.cancel(); if (selection) { scriptFocus.insertBlock(selection); } if (ide) { ide.recordUnsavedChanges(); } } else { search = searchBar.getValue(); if (search.length > 0) { show(myself.blocksMatching(search)); } } }; searchPane.reactToKeystroke = function (evt) { var 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: nop(); } }; searchPane.reactToInput = function (evt) { var search = searchBar.getValue(); if (search !== oldSearch) { oldSearch = search; show(myself.blocksMatching( search, search.length < 2, types, varNames )); } }; searchBar.cancel = function () { ide.refreshPalette(); ide.palette.contents.setTop(oldTop); 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 '&': 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, reverseDict = {}; selectors = { '+': 'reportSum', '-': 'reportDifference', '*': 'reportProduct', '/': 'reportQuotient', '%': 'reportModulus', '^': 'reportPower', '=': 'reportEquals', '<': 'reportLessThan', '>': 'reportGreaterThan', '&': 'reportAnd', '|': 'reportOr', round: 'reportRound', not: 'reportNot' }; monads = ['abs', 'neg', 'ceiling', 'floor', 'sqrt', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'ln', 'log', 'lg', 'id', 'round', 'not']; alias = { ceil: 'ceiling', '!' : 'not' }; monads.concat(['true', 'false']).forEach(word => reverseDict[localize(word).toLowerCase()] = word ); key = alias[ast[0]] || reverseDict[ast[0].toLowerCase()] || 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 { // dyadic block = SpriteMorph.prototype.blockForSelector(selectors[key]); inps = block.inputs(); } for (i = 1; i < ast.length; i += 1) { if (ast[i] instanceof Array) { block.replaceInput(inps[i - off], blockFromAST(ast[i])); } else if (isString(ast[i])) { if (contains( ['true', 'false'], reverseDict[ast[i]] || ast[i]) ) { block.replaceInput( inps[i - off], SpriteMorph.prototype.blockForSelector( (reverseDict[ast[i]] || ast[i]) === 'true' ? 'reportTrue' : 'reportFalse' ) ); } else if (ast[i] !== '_') { block.replaceInput( 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.primitivesCache.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(); ide.recordUnsavedChanges(); } }; // SpriteMorph costume management SpriteMorph.prototype.addCostume = function (costume) { if (!costume.name) { costume.name = 'costume' + (this.costumes.length() + 1); } this.shadowAttribute('costumes'); this.costumes.add(costume); }; SpriteMorph.prototype.wearCostume = function (costume, noShadow) { var x = this.xPosition ? this.xPosition() : null, y = this.yPosition ? this.yPosition() : null, idx = isNil(costume) ? null : this.costumes.asArray().indexOf(costume); this.changed(); this.costume = costume; this.fixLayout(); this.rerender(); if (x !== null) { this.silentGotoXY(x, y, true); // just me } if (this.positionTalkBubble) { // the stage doesn't talk this.positionTalkBubble(); } this.version = Date.now(); // propagate to children that inherit my costume # if (!noShadow) { this.shadowAttribute('costume #'); } this.specimens().forEach(instance => { if (instance.cachedPropagation) { if (instance.inheritsAttribute('costume #')) { if (idx === null) { instance.wearCostume(null, true); } else if (idx === -1) { instance.wearCostume(costume, true); } else { instance.doSwitchToCostume(idx + 1, true); } } } }); }; SpriteMorph.prototype.getCostumeIdx = function () { if (this.inheritsAttribute('costume #')) { return this.exemplar.getCostumeIdx(); } 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, noShadow) { var w = 0, h = 0, stage; if (id instanceof List) { // try to turn a list of pixels into a costume if (this.costume) { // recycle dimensions of current costume w = this.costume.width(); h = this.costume.height(); } if (w * h !== id.length()) { // assume stage's dimensions stage = this.parentThatIsA(StageMorph); w = stage.dimensions.x; h = stage.dimensions.y; } id = Process.prototype.reportNewCostume( id, w, h, this.newCostumeName(localize('snap')) ); } if (id instanceof Costume) { // allow first-class costumes this.wearCostume(id, noShadow); return; } if (id instanceof Array && (id[0] === 'current')) { 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, cst => cst.name === id); if (costume === null) { num = parseFloat(id); if (num === 0) { costume = null; } else { costume = arr[num - 1] || null; } } } this.wearCostume(costume, noShadow); }; SpriteMorph.prototype.reportCostumes = function () { return this.costumes; }; // SpriteMorph sound management SpriteMorph.prototype.addSound = function (audio, name) { var sound = new Sound(audio, name); this.shadowAttribute('sounds'); this.sounds.add(sound); return sound; }; SpriteMorph.prototype.doPlaySound = function (name) { var stage = this.parentThatIsA(StageMorph), sound = name instanceof Sound ? name : (typeof name === 'number' ? this.sounds.at(name) : detect( this.sounds.asArray(), s => s.name === name.toString() )), ctx = this.audioContext(), gain = this.getGainNode(), pan = this.getPannerNode(), aud, source; if (sound) { aud = document.createElement('audio'); aud.src = sound.audio.src; ctx.resume(); // needed to fix tainted context in case of autoplay source = ctx.createMediaElementSource(aud); source.connect(gain); if (pan) { gain.connect(pan); pan.connect(ctx.destination); // perhaps redundant this.setPan(this.getPan()); // yep, should be redundant } else { gain.connect(ctx.destination); } this.setVolume(this.getVolume()); // probably redundant as well aud.play(); if (stage) { stage.activeSounds.push(aud); stage.activeSounds = stage.activeSounds.filter(snd => !snd.ended && !snd.terminated ); } return aud; } }; SpriteMorph.prototype.reportSounds = function () { return this.sounds; }; // SpriteMorph volume SpriteMorph.prototype.setVolume = function (num, noShadow) { this.volume = Math.max(Math.min(+num || 0, 100), 0); this.getGainNode().gain.setValueAtTime( 1 / Math.pow(10, Math.log2(100 / this.volume)), this.audioContext().currentTime ); if (this instanceof StageMorph) { return; } // propagate to children that inherit my volume if (!noShadow) { this.shadowAttribute('volume'); } this.instances.forEach(instance => { if (instance.cachedPropagation) { if (instance.inheritsAttribute('volume')) { instance.setVolume(num, true); } } }); }; SpriteMorph.prototype.changeVolume = function (delta) { this.setVolume(this.getVolume() + (+delta || 0)); }; SpriteMorph.prototype.getVolume = function () { if (this.inheritsAttribute('volume')) { return this.exemplar.getVolume(); } return this.volume; }; SpriteMorph.prototype.getGainNode = function () { if (!this.gainNode) { this.gainNode = this.audioContext().createGain(); } return this.gainNode; }; SpriteMorph.prototype.audioContext = function () { return Note.prototype.getAudioContext(); }; // SpriteMorph stero panning SpriteMorph.prototype.setPan = function (num, noShadow) { var panner = this.getPannerNode(); if (!panner) {return; } this.pan = Math.max(Math.min((+num || 0), 100), -100); panner.pan.setValueAtTime( this.pan / 100, this.audioContext().currentTime ); if (this instanceof StageMorph) { return; } // propagate to children that inherit my balance if (!noShadow) { this.shadowAttribute('balance'); } this.instances.forEach(instance => { if (instance.cachedPropagation) { if (instance.inheritsAttribute('balance')) { instance.setPan(num, true); } } }); }; SpriteMorph.prototype.changePan = function (delta) { this.setPan(this.getPan() + (+delta || 0)); }; SpriteMorph.prototype.getPan = function () { if (this.inheritsAttribute('balance')) { return this.exemplar.getPan(); } return this.pan; }; SpriteMorph.prototype.getPannerNode = function () { var ctx; if (!this.pannerNode) { ctx = this.audioContext(); if (ctx.createStereoPanner) { this.pannerNode = this.audioContext().createStereoPanner(); } } return this.pannerNode; }; // SpriteMorph frequency player SpriteMorph.prototype.playFreq = function (hz) { // start playing the given frequency until stopped var note, ctx = this.audioContext(), gain = this.getGainNode(), pan = this.getPannerNode(), stage = this.parentThatIsA(StageMorph); if (!this.freqPlayer) { this.freqPlayer = new Note(); } note = this.freqPlayer; note.fader = ctx.createGain(); if (note.oscillator) { note.oscillator.frequency.value = hz; } else { note.oscillator = ctx.createOscillator(); if (!note.oscillator.start) { note.oscillator.start = note.oscillator.noteOn; } if (!note.oscillator.stop) { note.oscillator.stop = note.oscillator.noteOff; } note.setInstrument(this.instrument); note.oscillator.frequency.value = hz; this.setVolume(this.getVolume()); note.oscillator.connect(note.fader); note.fader.connect(gain); if (pan) { gain.connect(pan); pan.connect(ctx.destination); this.setPan(this.pan); } else { gain.connect(ctx.destination); } note.ended = false; if (stage) { stage.activeSounds.push(note); stage.activeSounds = stage.activeSounds.filter(snd => !snd.ended && !snd.terminated ); } note.fader.gain.setValueCurveAtTime( note.fadeIn, ctx.currentTime, note.fadeTime ); note.oscillator.start(0); } }; SpriteMorph.prototype.stopFreq = function () { if (this.freqPlayer) { this.freqPlayer.stop(); } }; // SpriteMorph user menu SpriteMorph.prototype.userMenu = function () { var ide = this.parentThatIsA(IDE_Morph), menu = new MenuMorph(this), allParts, anchors; if (ide && ide.isAppMode) { // menu.addItem('help', 'nop'); return menu; } if (!this.isTemporary) { menu.addItem("duplicate", 'duplicate'); if (StageMorph.prototype.enableInheritance) { menu.addItem("clone", 'instantiate'); menu.addLine(); } } menu.addItem("delete", 'remove'); menu.addItem("move", 'moveCenter'); menu.addItem("rotate", 'setRotation'); if (this.costume) { menu.addItem( "pivot", 'moveRotationCenter', 'edit the costume\'s\nrotation center' ); } if (this.isTemporary) { if (StageMorph.prototype.enableInheritance) { menu.addItem( "edit", 'perpetuateAndEdit', 'make permanent and\nshow in the sprite corral' ); } } else { menu.addItem("edit", 'edit'); } menu.addLine(); if (this.anchor) { menu.addItem( localize('detach from') + ' ' + this.anchor.name, 'detachFromAnchor' ); } else { allParts = this.allParts(); anchors = this.parent.children.filter(morph => morph instanceof SpriteMorph && !contains(allParts, morph) ); if (anchors.length) { menu.addMenu('stick to', this.anchorsMenu(anchors)); } } if (this.parts.length) { menu.addItem('detach all parts', 'detachAllParts'); } menu.addItem("export...", 'exportSprite'); return menu; }; SpriteMorph.prototype.anchorsMenu = function (targets) { var menu = new MenuMorph(this.attachTo, null, this); targets.forEach(sprite => menu.addItem( [ sprite.thumbnail(new Point(24, 24)), sprite.name, ], sprite ) ); return menu; }; SpriteMorph.prototype.exportSprite = function () { if (this.isTemporary) {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.instantiate = function () { var ide = this.parentThatIsA(IDE_Morph); if (ide) { ide.instantiateSprite(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), clone; if (stage && stage.cloneCount <= 5000) { clone = this.fullCopy(true); clone.clonify(stage, immediately); } return clone; }; SpriteMorph.prototype.newClone = function (immediately) { var clone = this.createClone(immediately); if (isNil(clone)) { throw new Error('exceeding maximum number of clones'); } return clone; }; SpriteMorph.prototype.clonify = function (stage, immediately) { var hats; this.parts.forEach(part => part.clonify(stage)); stage.cloneCount += 1; this.cloneOriginName = this.isTemporary ? this.cloneOriginName : this.name; this.isTemporary = true; this.name = ''; stage.add(this); hats = this.allHatBlocksFor('__clone__init__'); hats.forEach(block => stage.threads.startProcess( block, this, stage.isThreadSafe, null, // export result null, // callback null, // is clicked immediately // without yielding ) ); this.endWarp(); }; SpriteMorph.prototype.initClone = function (hats) { // used when manually instantiating a sprite in the IDE var stage = this.parentThatIsA(StageMorph); if (stage) { hats.forEach(block => stage.threads.startProcess( block, this, stage.isThreadSafe ) ); this.endWarp(); } }; SpriteMorph.prototype.removeClone = function () { var exemplar = this.exemplar; if (this.isTemporary) { // this.stopTalking(); this.parent.threads.stopAllForReceiver(this); this.parts.slice().forEach(part => { this.detachPart(part); part.removeClone(); }); this.corpsify(); this.instances.forEach(child => { if (child.isTemporary) { child.setExemplar(exemplar); } }); this.destroy(); this.parent.cloneCount -= 1; } }; SpriteMorph.prototype.perpetuate = function () { // make a temporary sprite (clone) permanent var stage = this.parentThatIsA(StageMorph), ide = this.parentThatIsA(IDE_Morph); // make sure my exemplar-chain is fully perpetuated if (this.exemplar) { this.exemplar.perpetuate(); } if (!this.isTemporary || !stage || !ide) { return; } this.isTemporary = false; this.name = ide.newSpriteName(this.cloneOriginName); this.cloneOriginName = ''; stage.cloneCount -= 1; ide.corral.addSprite(this); ide.sprites.add(this); this.parts.forEach(part => part.perpetuate()); }; SpriteMorph.prototype.perpetuateAndEdit = function () { var ide = this.parentThatIsA(IDE_Morph); if (ide) { this.perpetuate(); ide.selectSprite(this); ide.recordUnsavedChanges(); } }; SpriteMorph.prototype.release = function () { // turn a permenent sprite that's an instance of another one // into a temporary one (clone), that will vanish either when // the "delete this clone" operation is executed or when the user // hits the red stop sign button in the IDE var stage = this.parentThatIsA(StageMorph), ide = this.parentThatIsA(IDE_Morph), idx; if (this.isTemporary || !this.exemplar || !stage || !ide) { return; } // make sure all parts and instances are also released this.parts.forEach(part => part.release()); this.instances.forEach(inst => inst.release()); this.isTemporary = true; this.name = ''; this.cloneOriginName = this.exemplar.name; stage.cloneCount += 1; idx = ide.sprites.asArray().indexOf(this) + 1; stage.watchers().forEach(watcher => { if (watcher.object() === this) { watcher.destroy(); } }); if (idx > 0) { ide.sprites.remove(idx); } ide.createCorral(); ide.fixLayout(); if (ide.currentSprite === this) { ide.currentSprite = detect( stage.children, morph => morph instanceof SpriteMorph && !morph.isTemporary ) || this.stage; } ide.selectSprite(ide.currentSprite); if (ide.isAppMode) { ide.toggleAppMode(true); } }; // 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.show = function () { this.setVisibility(true); }; SpriteMorph.prototype.hide = function () { this.setVisibility(false); }; SpriteMorph.prototype.setVisibility = function (bool, noShadow) { var bubble = this.talkBubble(); if (bool) { SpriteMorph.uber.show.call(this); } else { SpriteMorph.uber.hide.call(this); } // propagate to speech bubble, if any if (bubble) { if (bool) { bubble.show(); } else { bubble.hide(); } } // progagate to parts this.parts.forEach(part => part.setVisibility(bool)); // propagate to children that inherit my visibility if (!noShadow) { this.shadowAttribute('shown?'); } this.instances.forEach(instance => { if (instance.cachedPropagation) { if (instance.inheritsAttribute('shown?')) { instance.setVisibility(bool, true); } } }); }; SpriteMorph.prototype.reportShown = function () { if (this.inheritsAttribute('shown?')) { return this.exemplar.reportShown(); } return this.isVisible; }; // SpriteMorph pen color SpriteMorph.prototype.setColorDimension = function (idx, num) { var x = this.xPosition(), y = this.yPosition(), n = +num; idx = +idx; if (idx < 0 || idx > 3) {return; } if (idx === 0) { if (n < 0 || n > 100) { // wrap the hue n = (n < 0 ? 100 : 0) + n % 100; } } else { n = Math.min(100, Math.max(0, n)); } if (idx === 3) { this.color.a = 1 - n / 100; } else { this.cachedColorDimensions[idx] = n / 100; this.color['set_' + this.penColorModel].apply( this.color, this.cachedColorDimensions ); } if (!this.costume) { this.rerender(); } this.gotoXY(x, y); }; SpriteMorph.prototype.getColorDimension = function (idx) { idx = +idx; if (idx === 3) { return (1 - this.color.a) * 100; } return (this.cachedColorDimensions[idx] || 0) * 100; }; SpriteMorph.prototype.changeColorDimension = function (idx, delta) { this.setColorDimension( idx, this.getColorDimension(idx) + (+delta || 0) ); }; SpriteMorph.prototype.setColorRGBA = function (dta) { // dta can be one of the following: // - a 4 item list representing r-g-b-a each on a scale of 0-255 // - a 3 item list representing r-g-b leaving a unchanged // - a 1 item list representing greyscale from 0-255 leaving alpha unchanged // - a 2 item list representing greyscale and alpha each from 0-255 // - a number representing greyscale from 0-255 leaving alpha unchanged var clr = this.color.copy(), num; if (dta instanceof List) { switch (dta.length()) { case 1: num = Math.max(0, Math.min(+(dta.at(1)), 255)); if (isNaN(num)) {return; } clr.r = num; clr.g = num; clr.b = num; break; case 2: num = Math.max(0, Math.min(+(dta.at(1)), 255)); if (isNaN(num)) {return; } clr.r = num; clr.g = num; clr.b = num; num = Math.max(0, Math.min(+(dta.at(2)), 255)); if (isNaN(num)) {return; } clr.a = num / 255; break; case 3: num = Math.max(0, Math.min(+(dta.at(1)), 255)); if (isNaN(num)) {return; } clr.r = num; num = Math.max(0, Math.min(+(dta.at(2)), 255)); if (isNaN(num)) {return; } clr.g = num; num = Math.max(0, Math.min(+(dta.at(3)), 255)); if (isNaN(num)) {return; } clr.b = num; break; case 4: num = Math.max(0, Math.min(+(dta.at(1)), 255)); if (isNaN(num)) {return; } clr.r = num; num = Math.max(0, Math.min(+(dta.at(2)), 255)); if (isNaN(num)) {return; } clr.g = num; num = Math.max(0, Math.min(+(dta.at(3)), 255)); if (isNaN(num)) {return; } clr.b = num; num = Math.max(0, Math.min(+(dta.at(4)), 255)); if (isNaN(num)) {return; } clr.a = num / 255; break; default: return; } } else { num = Math.max(0, Math.min(+dta, 255)); if (isNaN(num)) {return; } clr.r = num; clr.g = num; clr.b = num; } this.setColor(clr); }; SpriteMorph.prototype.changeColorRGBA = function (dta) { // dta can be one of the following: // - a 4 item list representing r-g-b-a each on a scale of 0-255 // - a 3 item list representing r-g-b leaving a unchanged // - a 1 item list representing greyscale from 0-255 leaving alpha unchanged // - a 2 item list representing greyscale and alpha each from 0-255 // - a number representing greyscale from 0-255 leaving alpha unchanged var clr = this.color.copy(), num; if (dta instanceof List) { switch (dta.length()) { case 1: num = +(dta.at(1)); if (isNaN(num)) {return; } clr.r = Math.max(0, Math.min(clr.r + num, 255)); clr.g = Math.max(0, Math.min(clr.g + num, 255)); clr.b = Math.max(0, Math.min(clr.b + num, 255)); break; case 2: num = +(dta.at(1)); if (isNaN(num)) {return; } clr.r = Math.max(0, Math.min(clr.r + num, 255)); clr.g = Math.max(0, Math.min(clr.g + num, 255)); clr.b = Math.max(0, Math.min(clr.b + num, 255)); num = +(dta.at(2)); if (isNaN(num)) {return; } clr.a = Math.max(0, Math.min((clr.a * 255) + num, 255)) / 255; break; case 3: num = +(dta.at(1)); if (isNaN(num)) {return; } clr.r = Math.max(0, Math.min(clr.r + num, 255)); num = +(dta.at(2)); if (isNaN(num)) {return; } clr.g = Math.max(0, Math.min(clr.g + num, 255)); num = +(dta.at(3)); if (isNaN(num)) {return; } clr.b = Math.max(0, Math.min(clr.b + num, 255)); break; case 4: num = +(dta.at(1)); if (isNaN(num)) {return; } clr.r = Math.max(0, Math.min(clr.r + num, 255)); num = +(dta.at(2)); if (isNaN(num)) {return; } clr.g = Math.max(0, Math.min(clr.g + num, 255)); num = +(dta.at(3)); if (isNaN(num)) {return; } clr.b = Math.max(0, Math.min(clr.b + num, 255)); num = +(dta.at(4)); if (isNaN(num)) {return; } clr.a = Math.max(0, Math.min((clr.a * 255) + num, 255)) / 255; break; default: return; } } else { num = +dta; if (isNaN(num)) {return; } clr.r = Math.max(0, Math.min(clr.r + num, 255)); clr.g = Math.max(0, Math.min(clr.g + num, 255)); clr.b = Math.max(0, Math.min(clr.b + num, 255)); } this.setColor(clr); }; SpriteMorph.prototype.setColor = function (aColor) { var x = this.xPosition(), y = this.yPosition(); if (!this.color.eq(aColor, true)) { // observeAlpha this.color = aColor.copy(); if (!this.costume) { this.rerender(); this.silentGotoXY(x, y); } this.cachedColorDimensions = this.color[this.penColorModel](); } }; SpriteMorph.prototype.setBackgroundColor = SpriteMorph.prototype.setColor; SpriteMorph.prototype.getPenAttribute = function (attrib) { var name = attrib instanceof Array ? attrib[0] : attrib.toString(), options = ['hue', 'saturation', 'brightness', 'transparency']; if (name === 'size') { return this.size || 0; } if (name === 'r-g-b-a') { return new List([ this.color.r, this.color.g, this.color.b, Math.round(this.color.a * 255) ]); } return this.getColorDimension(options.indexOf(name)); }; // SpriteMorph layers SpriteMorph.prototype.comeToFront = function () { if (this.parent) { this.parent.add(this); this.changed(); } }; SpriteMorph.prototype.goToBack = function () { if (this.parent) { this.parent.addBack(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 // attempted optimized collision detection using video buffer // turns out it's actually slower to partially enumerate 2 sets of pixels // than to create a new masked canvas. // commented out and kept for reference /* SpriteMorph.prototype.isTouching = function (other) { var stage = this.parentThatIsA(StageMorph), inter, off, src, trg, sw, tw, x, y; function isOpaque(imageData, width, x, y) { return (imageData[y * width + x] && 0x000000FF) > 0; // alpha } if (!(other instanceof SpriteMorph)) { return SpriteMorph.uber.isTouching.call(this, other); } // determine the intersection of both bounding boxes // unscaled and translated to local coordinates inter = this.bounds.intersect(other.bounds).translateBy( this.position().neg() ).scaleBy(1 / stage.scale); // .floor(); ? if (inter.width() < 1 || inter.height() < 1) { return false; } off = this.position().subtract( other.position() ).scaleBy(1 / stage.scale).floor(); src = this.getImageData(); sw = Math.floor(this.width() / stage.scale); trg = other.getImageData(); tw = Math.floor(other.width() / stage.scale); for (y = inter.origin.y; y <= inter.corner.y; y += 1) { for (x = inter.origin.x; x <= inter.corner.x; x += 1) { if (isOpaque(src, sw, x, y) && isOpaque(trg, tw, x + off.x, y + off.y)) { return true; } } } return false; }; */ SpriteMorph.prototype.reportTouchingColor = function (aColor) { var stage = this.parentThatIsA(StageMorph), data, len, i; if (stage) { data = this.overlappingPixels(stage); if (!data) {return false; } len = data[0].length; for (i = 3; i < len; i += 4) { if (data[0][i] && data[1][i]) { if ( data[1][i - 3] === aColor.r && data[1][i - 2] === aColor.g && data[1][i - 1] === aColor.b ) { return true; } } } } return false; }; SpriteMorph.prototype.reportColorIsTouchingColor = function ( thisColor, thatColor ) { var stage = this.parentThatIsA(StageMorph), data, len, i; if (stage) { data = this.overlappingPixels(stage); if (!data) {return false; } len = data[0].length; for (i = 3; i < len; i += 4) { if (data[0][i] && data[1][i]) { if ( data[0][i - 3] === thisColor.r && data[0][i - 2] === thisColor.g && data[0][i - 1] === thisColor.b && data[1][i - 3] === thatColor.r && data[1][i - 2] === thatColor.g && data[1][i - 1] === thatColor.b ) { return true; } } } } return false; }; SpriteMorph.prototype.overlappingPixels = function (otherSprite) { // overrides method from Morph because Sprites aren't nested Morphs // the same applies for speech balloons, where it's enough to // test the encompassing shape only var oRect = this.bounds.intersect(otherSprite.bounds), thisImg = this.getImage(), thatImg = otherSprite.getImage(); if (otherSprite instanceof StageMorph) { // only check for color collision thatImg = otherSprite.fancyThumbnail(otherSprite.extent(), this, true); } if (oRect.width() < 1 || oRect.height() < 1 || !thisImg || !thatImg || !thisImg.width || !thisImg.height || !thatImg.width || !thatImg.height ) { return false; } if (thisImg.isRetinaEnabled !== thatImg.isRetinaEnabled) { thisImg = normalizeCanvas(thisImg, true); thatImg = normalizeCanvas(thatImg, true); } return [ thisImg.getContext("2d").getImageData( oRect.left() - this.left(), oRect.top() - this.top(), oRect.width(), oRect.height() ).data, thatImg.getContext("2d").getImageData( oRect.left() - otherSprite.left(), oRect.top() - otherSprite.top(), oRect.width(), oRect.height() ).data ]; }; // SpriteMorph neighbor detection SpriteMorph.prototype.neighbors = function (aStage) { var stage = aStage || this.parentThatIsA(StageMorph), myPerimeter = this.perimeter(stage); return new List( stage.children.filter(each => { var eachPerimeter, distance; if (each instanceof SpriteMorph && each.isVisible && (each !== this) ) { eachPerimeter = each.perimeter(stage); distance = myPerimeter.center.distanceTo(eachPerimeter.center); return (distance - myPerimeter.radius - eachPerimeter.radius) < 0; } return false; }) ); }; SpriteMorph.prototype.perimeter = function (aStage) { var stage = aStage || this.parentThatIsA(StageMorph), stageScale = this instanceof StageMorph ? 1 : stage.scale, radius; if (this.costume) { radius = Math.max( this.costume.width(), this.costume.height() ) * this.scale * stageScale; } else { radius = Math.max(this.width(), this.height()); } return { center: this.center(), // geometric center rather than position radius: radius }; }; // SpriteMorph pen ops SpriteMorph.prototype.doStamp = function () { var stage = this.parent, ctx = stage.penTrails().getContext('2d'), img = this.getImage(); if (img.width < 1 || (img.height < 1)) { // too small to draw return; } ctx.save(); ctx.scale(1 / stage.scale, 1 / stage.scale); ctx.globalAlpha = this.alpha; ctx.drawImage( img, this.left() - stage.left(), this.top() - stage.top() ); ctx.restore(); this.changed(); stage.cachedPenTrailsMorph = null; }; SpriteMorph.prototype.clear = function () { this.parent.clearPenTrails(); }; SpriteMorph.prototype.write = function (text, size) { // thanks to Michael Ball for contributing this code! if (typeof text !== 'string' && typeof text !== 'number') { throw new Error( localize('can only write text or numbers, not a') + ' ' + typeof text ); } var stage = this.parentThatIsA(StageMorph), context = stage.penTrails().getContext('2d'), rotation = radians(this.direction() - 90), trans = new Point( this.rotationCenter().x - stage.left(), this.rotationCenter().y - stage.top() ), len, pos; context.save(); context.font = size + 'px monospace'; context.textAlign = 'left'; context.textBaseline = 'alphabetic'; context.fillStyle = this.color.toString(); len = context.measureText(text).width; trans = trans.multiplyBy(1 / stage.scale); context.translate(trans.x, trans.y); context.rotate(rotation); context.fillText(text, 0, 0); context.translate(-trans.x, -trans.y); context.restore(); pos = new Point( len * Math.sin(radians(this.direction())), len * Math.cos(radians(this.direction())) ); pos = pos.add(new Point(this.xPosition(), this.yPosition())); this.gotoXY(pos.x, pos.y, false); this.changed(); stage.changed(); }; 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 printing on another sprite: SpriteMorph.prototype.blitOn = function (target, mask = 'source-atop') { // draw my costume onto a copy of the target's costume scaled and rotated // so it appears as though I'm "stamped" onto it. var sourceHeading = (this.rotationStyle === 1) ? this.heading : 90, targetHeading = (target.rotationStyle === 1) ? target.heading : 90, sourceCostume, targetCostume, ctx, relRot, relScale, stageScale, centerDist, centerDelta, centerAngleRadians, center, originDist, originAngleRadians, spriteCenter, thisCenter, relPos, pos; // prevent pasting an object onto itself if (this === target) {return; } // check if both source and target have costumes, // rasterize copy of target costume if it's an SVG if (this.costume && target.costume) { sourceCostume = this.costume; if (sourceCostume instanceof SVG_Costume) { sourceCostume = sourceCostume.rasterized(); } if (target.costume instanceof SVG_Costume) { targetCostume = target.costume.rasterized(); } else { targetCostume = target.costume.copy(); } } else { return; } // do the math: if (target instanceof SpriteMorph) { if (this instanceof SpriteMorph) { // stamp a sprite on a sprite: relRot = sourceHeading - targetHeading; relScale = this.scale / target.scale; stageScale = this.parentThatIsA(StageMorph).scale; centerDist = target.center().distanceTo(this.center()); centerDelta = this.center().subtract(target.center()); centerAngleRadians = Math.atan2(centerDelta.y, centerDelta.x); center = new Point( sourceCostume.width(), sourceCostume.height() ).multiplyBy(0.5 * this.scale * stageScale); originDist = center.distanceTo(ZERO); originAngleRadians = Math.atan2(center.y, center.x); spriteCenter = new Point( target.costume.width(), target.costume.height() ).multiplyBy(0.5 * target.scale * stageScale) .rotateBy(radians(relRot)); thisCenter = spriteCenter.distanceAngle( centerDist, degrees(centerAngleRadians) - sourceHeading + 180 ); relPos = thisCenter.distanceAngle( originDist, degrees(originAngleRadians) -90 ); pos = relPos.divideBy(stageScale) .divideBy(relScale) .divideBy(target.scale); } else { // if the stage is the source // stamp the stage on a sprite: relRot = 90 - targetHeading; relScale = 1 / target.scale; centerDist = target.center().distanceTo(this.position()); centerDelta = this.position().subtract(target.center()); centerAngleRadians = Math.atan2(centerDelta.y, centerDelta.x); center = new Point( target.costume.width(), target.costume.height() ).multiplyBy(0.5 * target.scale) .rotateBy(radians(90 - targetHeading)); pos = center.distanceAngle( centerDist / this.scale, degrees(centerAngleRadians) + 90 ); } } else { // if the stage is the target // stamp a sprite on the stage: relRot = sourceHeading - 90; relScale = this.scale; center = this.center().subtract(target.position()) .divideBy(this.scale * target.scale) .rotateBy(radians(sourceHeading - 90)); pos = center.subtract( new Point( sourceCostume.width(), sourceCostume.height() ).multiplyBy(0.5) ); } // draw my costume onto the target's costume copy: ctx = targetCostume.contents.getContext('2d'); ctx.rotate(radians(relRot)); ctx.scale(relScale, relScale); ctx.globalCompositeOperation = mask; ctx.drawImage(sourceCostume.contents, pos.x, pos.y); // make the target wear the new costume target.doSwitchToCostume(targetCostume); }; // SpriteMorph pen up and down: SpriteMorph.prototype.down = function () { this.setPenDown(true); }; SpriteMorph.prototype.up = function () { this.setPenDown(false); }; SpriteMorph.prototype.setPenDown = function (bool, noShadow) { if (bool) { SpriteMorph.uber.down.call(this); } else { SpriteMorph.uber.up.call(this); } // propagate to children that inherit my visibility if (!noShadow) { this.shadowAttribute('pen down?'); } this.instances.forEach(instance => { if (instance.cachedPropagation) { if (instance.inheritsAttribute('pen down?')) { instance.setPenDown(bool, true); } } }); }; SpriteMorph.prototype.getPenDown = function () { if (this.inheritsAttribute('pen down?')) { return this.exemplar.getPenDown(); } return this.isDown; }; // SpriteMorph scale SpriteMorph.prototype.getScale = function () { // answer my scale in percent if (this.inheritsAttribute('size')) { return this.exemplar.getScale(); } return this.scale * 100; }; SpriteMorph.prototype.setScale = function (percentage, noShadow) { // set my (absolute) scale in percent var x = this.xPosition(), y = this.yPosition(), realScale, growth; realScale = (+percentage || 0) / 100; growth = realScale / this.nestingScale; this.nestingScale = realScale; this.scale = Math.max(realScale, 0.01); // apply to myself this.changed(); this.fixLayout(); this.rerender(); this.silentGotoXY(x, y, true); // just me this.positionTalkBubble(); // propagate to nested parts this.parts.forEach(part => { var xDist = part.xPosition() - x, yDist = part.yPosition() - y; part.setScale(part.scale * 100 * growth); part.silentGotoXY( x + (xDist * growth), y + (yDist * growth) ); }); // propagate to children that inherit my scale if (!noShadow) { this.shadowAttribute('size'); } this.instances.forEach(instance => { if (instance.cachedPropagation) { if (instance.inheritsAttribute('size')) { instance.setScale(percentage, true); } } }); }; SpriteMorph.prototype.changeScale = function (delta) { this.setScale(this.getScale() + (+delta || 0)); }; // Spritemorph graphic effects SpriteMorph.prototype.graphicsChanged = function () { return Object.keys(this.graphicsValues).some(any => this.graphicsValues[any] < 0 || this.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, w, h; 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_colorDimensions( imagedata, hueShift, saturationShift, brightnessShift ) { var pixels = imagedata.data, l = pixels.length, clr = new Color(), index, dim; for (index = 0; index < l; index += 4) { clr.r = pixels[index]; clr.g = pixels[index + 1]; clr.b = pixels[index + 2]; dim = clr[SpriteMorph.prototype.penColorModel](); dim[0] = dim[0] * 100 + hueShift; if (dim[0] < 0 || dim[0] > 100) { // wrap the hue dim[0] = (dim[0] < 0 ? 100 : 0) + dim[0] % 100; } dim[0] = dim[0] / 100; dim[1] = dim[1] + saturationShift / 100; dim[2] = dim[2] + brightnessShift / 100; clr['set_' + SpriteMorph.prototype.penColorModel].apply(clr, dim); pixels[index] = clr.r; pixels[index + 1] = clr.g; pixels[index + 2] = clr.b; } 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()) { w = Math.ceil(this.width()); h = Math.ceil(this.height()); if (!canvas.width || !canvas.height || !w || !h) { // too small to get image data, abort return canvas; } ctx = canvas.getContext("2d"); imagedata = ctx.getImageData(0, 0, w, h); 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_colorDimensions( 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] : effect.toString(); if (!contains( [ 'color', 'saturation', 'brightness', 'ghost', 'fisheye', 'whirl', 'pixelate', 'mosaic', 'negative', // depracated, but still supported in legacy projects: 'duplicate', 'comic', 'confetti' ], eff )) { throw new Error(localize('unsupported graphic effect') + ': "' + eff + '"'); } if (eff === 'ghost') { this.alpha = 1 - Math.min(Math.max(+value || 0, 0), 100) / 100; } else { this.graphicsValues[eff] = +value; } this.rerender(); }; SpriteMorph.prototype.getEffect = function (effect) { var eff = effect instanceof Array ? effect[0] : effect.toString(); if (eff === 'ghost') { return this.getGhostEffect(); } return this.graphicsValues[eff] || 0; }; SpriteMorph.prototype.getGhostEffect = function () { return (1 - this.alpha) * 100; }; SpriteMorph.prototype.changeEffect = function (effect, value) { var eff = effect instanceof Array ? effect[0] : effect.toString(); 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, morph => morph instanceof SpeechBubbleMorph ); }; SpriteMorph.prototype.positionTalkBubble = function () { var stage = this.parentThatIsA(StageMorph), stageScale = stage ? stage.scale : 1, bubble = this.talkBubble(), bottom = this.bottom(), step = this.extent().divideBy(10) .max(new Point(5, 5).scaleBy(stageScale)) .multiplyBy(new Point(-1, 1)); if (!bubble) {return null; } bubble.show(); if (!bubble.isPointingRight) { bubble.isPointingRight = true; bubble.fixLayout(); bubble.rerender(); } bubble.setLeft(this.right()); bubble.setBottom(this.top()); while (!this.isTouching(bubble) && bubble.bottom() < bottom) { bubble.moveBy(step); } bubble.moveBy(step.mirror()); if (!stage) {return null; } if (bubble.right() > stage.right()) { bubble.isPointingRight = false; bubble.fixLayout(); bubble.rerender(); bubble.setRight(this.center().x); } bubble.keepWithin(stage); }; // dragging and dropping adjustments b/c of talk bubbles and parts SpriteMorph.prototype.prepareToBeGrabbed = function (hand) { this.recordLayers(); this.shadowAttribute('x position'); this.shadowAttribute('y position'); if (!this.bounds.containsPoint(hand.position()) && this.isCorrectingOutsideDrag()) { this.setCenter(hand.position()); } }; 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; } if (this.exemplar) { this.inheritedAttributes.forEach(att => { if (contains(['direction', 'size', 'costume #'], att)) { // only refresh certain propagated attributes this.refreshInheritedAttribute(att); } }); } 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) { // record for later svg conversion if (StageMorph.prototype.enablePenLogging) { this.parent.trailsLog.push( [ this.snapPoint(start), this.snapPoint(dest), this.color.copy(), this.size, this.useFlatLineEnds ? 'butt' : 'round' ] ); } // draw on the pen-trails layer 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); } this.parent.cachedPenTrailsMorph = null; } }; SpriteMorph.prototype.floodFill = function () { if (!this.parent.bounds.containsPoint(this.rotationCenter())) { return; } this.parent.cachedPenTrailsMorph = null; if (this.color.a > 1) { // fix a legacy bug in Morphic color detection this.color.a = this.color.a / 255; } 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 = [ Math.floor((height / 2) - this.yPosition()) * width + Math.floor(this.xPosition() + (width / 2)) ], clr = new Color( Math.round(Math.min(Math.max(this.color.r, 0), 255)), Math.round(Math.min(Math.max(this.color.g, 0), 255)), Math.round(Math.min(Math.max(this.color.b, 0), 255)), this.color.a ), 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] === clr.r && src[1] === clr.g && src[2] === clr.b && src[3] === Math.round(clr.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] = clr.r; dta[current * 4 + 1] = clr.g; dta[current * 4 + 2] = clr.b; dta[current * 4 + 3] = Math.round(clr.a * 255); } ctx.putImageData(img, 0, 0); this.parent.changed(); }; // SpriteMorph pen trails as costume SpriteMorph.prototype.reportPenTrailsAsCostume = function () { var cst = new Costume( this.parentThatIsA(StageMorph).trailsCanvas, this.newCostumeName(localize('Costume')) ); cst.shrinkWrap(); cst.rotationCenter = cst.rotationCenter.translateBy( new Point(this.xPosition(), -this.yPosition()) ); return cst; }; // 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(part => part.moveBy(delta)); this.instances.forEach(instance => { if (instance.cachedPropagation) { var inheritsX = instance.inheritsAttribute('x position'), inheritsY = instance.inheritsAttribute('y position'); if (inheritsX && inheritsY) { instance.moveBy(delta); } else if (inheritsX) { instance.moveBy(new Point(delta.x, 0)); } else if (inheritsY) { instance.moveBy(new Point(0, delta.y)); } } }); } }; 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(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, dot = 0.1; if (dist === 0 && this.isDown) { // draw a dot // dot = Math.min(this.size, 1); this.isDown = false; this.forward(dot * -0.5); this.isDown = true; this.forward(dot); this.isDown = false; this.forward(dot * -0.5); this.isDown = true; return; } else if (dist >= 0) { dest = this.position().distanceAngle(dist, this.heading); } else { dest = this.position().distanceAngle( Math.abs(dist), (this.heading - 180) ); } this.shadowAttribute('x position'); this.shadowAttribute('y position'); this.setPosition(dest); this.positionTalkBubble(); }; SpriteMorph.prototype.setHeading = function (degrees, noShadow) { var x = this.xPosition(), y = this.yPosition(), dir = !isFinite(degrees) ? 0 : +degrees, 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 = ((+degrees % 360) + 360) % 360; } // propagate to my parts this.parts.forEach(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); }); // propagate to children that inherit my direction if (!noShadow) { this.shadowAttribute('direction'); } this.instances.forEach(instance => { if (instance.cachedPropagation) { if (instance.inheritsAttribute('direction')) { instance.setHeading(degrees, true); } } }); }; SpriteMorph.prototype.faceToXY = function (x, y) { this.setHeading(this.angleToXY(x, y)); }; SpriteMorph.prototype.angleToXY = 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) ); return 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 () { if (this.inheritsAttribute('x position')) { return this.exemplar.xPosition(); } 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 () { if (this.inheritsAttribute('y position')) { return this.exemplar.yPosition(); } 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 () { if (this.inheritsAttribute('direction')) { return this.exemplar.direction(); } return this.heading; }; SpriteMorph.prototype.penSize = function () { return this.size; }; SpriteMorph.prototype.gotoXY = function (x, y, justMe, noShadow) { var stage = this.parentThatIsA(StageMorph), newX, newY, dest; if (!stage) {return; } if (!noShadow) { this.shadowAttribute('x position'); this.shadowAttribute('y position'); } x = !isFinite(+x) ? 0 : +x; y = !isFinite(+y) ? 0 : +y; newX = stage.center().x + x * stage.scale; newY = stage.center().y - y * 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 // don't shadow coordinate attributes var penState = this.isDown; this.isDown = false; this.gotoXY(x, y, justMe, true); // don't shadow coordinates this.isDown = penState; }; SpriteMorph.prototype.setXPosition = function (num) { this.shadowAttribute('x position'); this.gotoXY(+num || 0, this.yPosition(), false, true); }; SpriteMorph.prototype.changeXPosition = function (delta) { this.setXPosition(this.xPosition() + (+delta || 0)); }; SpriteMorph.prototype.setYPosition = function (num) { this.shadowAttribute('y position'); this.gotoXY(this.xPosition(), +num || 0, false, true); }; 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.shadowAttribute('x position'); this.shadowAttribute('y position'); this.shadowAttribute('direction'); this.setHeading(degrees(Math.atan2(-dirY, dirX)) + 90); this.setPosition(this.position().add( fb.amountToTranslateWithin(stage.bounds) )); this.positionTalkBubble(); }; // SpriteMorph coordinate conversion SpriteMorph.prototype.snapPoint = function(aPoint) { var stage = this.parentThatIsA(StageMorph), origin = stage.center(); return new Point( (aPoint.x - origin.x) / stage.scale, (origin.y - aPoint.y) / stage.scale ); }; // SpriteMorph rotation center / fixation point manipulation SpriteMorph.prototype.setRotationX = function (absoluteX) { this.setRotationCenter(new Point(absoluteX, this.yPosition())); alert(absoluteX); }; SpriteMorph.prototype.setRotationY = function (absoluteY) { this.setRotationCenter(new Point(this.xPosition(), absoluteY)); alert(absolutexy); }; SpriteMorph.prototype.setRotationCenter = function (absoluteCoordinate) { var delta, normal; if (!this.costume) { throw new Error('setting the rotation center requires a costume'); } this.shadowAttribute('costumes'); 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.changed(); this.fixLayout(); this.changed(); }; SpriteMorph.prototype.moveRotationCenter = function () { // make this a method of Snap >> SpriteMorph this.world().activeHandle = new HandleMorph( this, null, null, null, null, 'movePivot' ); }; SpriteMorph.prototype.setPivot = function (worldCoordinate) { var stage = this.parentThatIsA(StageMorph), ide = this.parentThatIsA(IDE_Morph), cntr; if (stage) { cntr = stage.center(); this.setRotationCenter( new Point( (worldCoordinate.x - cntr.x) / stage.scale, (cntr.y - worldCoordinate.y) / stage.scale ) ); if (ide) { ide.recordUnsavedChanges(); } } }; // SpriteMorph dimension getters 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.prototype.xLeft = 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.left() - stage.center().x) / stage.scale; } return this.left(); }; SpriteMorph.prototype.xRight = 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.right() - stage.center().x) / stage.scale; } return this.right(); }; SpriteMorph.prototype.yTop = 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.top()) / stage.scale; } return this.top(); }; SpriteMorph.prototype.yBottom = 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.bottom()) / stage.scale; } return this.bottom(); }; // SpriteMorph message broadcasting SpriteMorph.prototype.allMessageNames = function () { var msgs = []; this.allScripts().forEach(script => { script.allChildren().forEach(morph => { var txt; if (morph instanceof InputSlotMorph && morph.choices && contains( ['messagesMenu', 'messagesReceivedMenu'], morph.choices )) { txt = morph.evaluate(); if (isString(txt) && txt !== '') { if (!contains(msgs, txt)) { msgs.push(txt); } } } }); }); return msgs; }; SpriteMorph.prototype.allSendersOf = function (message, receiverName, known) { return this.allScripts().filter(script => script.isSending && script.isSending(message, receiverName, known) ); }; SpriteMorph.prototype.allHatBlocksFor = function (message) { if (typeof message === 'number') { message = message.toString(); } return this.scripts.children.filter(morph => { var sel = morph.selector, event; if (sel) { if (sel === 'receiveMessage') { event = morph.inputs()[0].evaluate(); return event === message || (event instanceof Array && message !== '__shout__go__' && message !== '__clone__init__'); } if (sel === 'receiveGo') { return message === '__shout__go__'; } if (sel === 'receiveOnClone') { return message === '__clone__init__'; } } return false; }); }; SpriteMorph.prototype.allHatBlocksForKey = function (key) { return this.scripts.children.filter(morph => { if (morph.selector) { if (morph.selector === 'receiveKey') { var choice = morph.inputs()[0].evaluate(), evt = choice instanceof Array ? choice[0] : choice; return evt === key || evt === 'any key'; } } return false; }); }; SpriteMorph.prototype.allHatBlocksForInteraction = function (interaction) { return this.scripts.children.filter(morph => { if (morph.selector) { if (morph.selector === 'receiveInteraction') { return morph.inputs()[0].evaluate()[0] === interaction; } } return false; }); }; SpriteMorph.prototype.hasGenericHatBlocks = function () { return this.scripts.children.some(morph => morph.selector === 'receiveCondition' ); }; SpriteMorph.prototype.allGenericHatBlocks = function () { return this.scripts.children.filter(morph => { if (morph.selector) { return morph.selector === 'receiveCondition'; } return false; }); }; SpriteMorph.prototype.allScripts = function () { var all = this.scripts.children.slice(); this.customBlocks.forEach(def => { if (def.body) { all.push(def.body.expression); } def.scripts.forEach(scr => all.push(scr)); }); if (this.globalBlocks) { this.globalBlocks.forEach(def => { if (def.body) { all.push(def.body.expression); } def.scripts.forEach(scr => all.push(scr)); }); } return all; }; // 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.mouseScroll = function (y) { return this.receiveUserInteraction('scrolled-' + (y > 0 ? 'up' : 'down')); }; SpriteMorph.prototype.receiveUserInteraction = function ( interaction, rightAway, threadSafe ) { var stage = this.parentThatIsA(StageMorph), procs = [], hats; if (!stage) {return; } // currently dragged hats = this.allHatBlocksForInteraction(interaction); hats.forEach(block => procs.push(stage.threads.startProcess( block, this, threadSafe || stage.isThreadSafe, null, // export result null, // callback null, // is clicked rightAway, // immediately interaction === 'stopped' // atomic )) ); return procs; }; SpriteMorph.prototype.mouseDoubleClick = function () { if (this.isTemporary) {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 refactoring SpriteMorph.prototype.refactorVariableInstances = function ( oldName, newName, isGlobal ) { if (isGlobal && this.hasSpriteVariable(oldName)) { return; } this.scripts.children.forEach(child => { if (child instanceof BlockMorph) { child.refactorVarInStack(oldName, newName); } }); }; // SpriteMorph variable watchers (for palette checkbox toggling) SpriteMorph.prototype.findVariableWatcher = function (varName) { var stage = this.parentThatIsA(StageMorph), globals = this.globalVariables(); if (stage === null) { return null; } return detect( stage.children, morph => morph instanceof WatcherMorph && (morph.target === this.variables || morph.target === globals) && morph.getter === varName ); }; SpriteMorph.prototype.toggleVariableWatcher = function (varName, isGlobal) { var stage = this.parentThatIsA(StageMorph), ide = this.parentThatIsA(IDE_Morph), globals = this.globalVariables(), watcher, others; if (stage === null) { return null; } if (isNil(isGlobal)) { isGlobal = contains(globals.names(), varName); } watcher = this.findVariableWatcher(varName); if (watcher !== null) { if (watcher.isVisible) { watcher.hide(); } else { watcher.show(); watcher.fixLayout(); // re-hide hidden parts watcher.keepWithin(stage); } if (isGlobal) { ide.flushBlocksCache('variables'); ide.refreshPalette(); } return; } // if no watcher exists, create a new one 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()); } watcher.fixLayout(); watcher.keepWithin(stage); stage.add(watcher); watcher.changed(); 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), ide = this.parentThatIsA(IDE_Morph), 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); } if (watcher.isGlobal(selector)) { ide.flushBlocksCache(); ide.refreshPalette(); } 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); watcher.changed(); if (watcher.isGlobal(selector)) { ide.flushBlocksCache(); ide.refreshPalette(); } }; 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) { return detect( stage.children, morph => morph instanceof WatcherMorph && morph.getter === selector && morph.target === (morph.isGlobal(selector) ? stage : this) ); }; // SpriteMorph custom blocks SpriteMorph.prototype.deleteAllBlockInstances = function (definition) { var stage, blocks = definition.isGlobal ? this.allBlockInstances(definition) : this.allIndependentInvocationsOf(definition.blockSpec()); blocks.forEach(each => each.deleteBlock()); // purge custom block definitions of "corpses" // i.e. blocks that have been marked for deletion if (definition.isGlobal) { stage = this.parentThatIsA(StageMorph); if (stage) { stage.globalBlocks.forEach(def => def.purgeCorpses()); stage.children.concat(stage).forEach(sprite => { if (sprite.isSnapObject) { sprite.customBlocks.forEach(def => def.purgeCorpses()); } }); } } else { this.allSpecimens().concat(this).forEach(sprite => sprite.customBlocks.forEach(def => def.purgeCorpses()) ); } }; SpriteMorph.prototype.allBlockInstances = function (definition) { var stage, objects, blocks = [], inDefinitions; if (definition.isGlobal) { stage = this.parentThatIsA(StageMorph); objects = stage.children.filter(morph => morph instanceof SpriteMorph ); objects.push(stage); objects.forEach(sprite => blocks = blocks.concat( sprite.allLocalBlockInstances(definition) ) ); inDefinitions = []; stage.globalBlocks.forEach(def => { def.scripts.forEach(eachScript => eachScript.allChildren().forEach(c => { if (c.isCustomBlock && (c.definition === definition)) { inDefinitions.push(c); } }) ); if (def.body) { def.body.expression.allChildren().forEach(c => { if (c.isCustomBlock && (c.definition === definition)) { inDefinitions.push(c); } }); } }); return blocks.concat(inDefinitions); } return this.allLocalBlockInstances(definition); }; SpriteMorph.prototype.allIndependentInvocationsOf = function (aSpec) { var blocks; if (this.exemplar && this.exemplar.getMethod(aSpec)) { // shadows an inherited method, don't delete return []; } blocks = this.allInvocationsOf(aSpec); this.instances.forEach(sprite => sprite.addAllInvocationsOf(aSpec, blocks) ); return blocks; }; SpriteMorph.prototype.allDependentInvocationsOf = function (aSpec) { var blocks; blocks = this.allInvocationsOf(aSpec); this.instances.forEach(sprite => sprite.addAllInvocationsOf(aSpec, blocks) ); return blocks; }; SpriteMorph.prototype.allInvocationsOf = function (aSpec) { // only inside the receiver, without the inheritance branches var inScripts, inDefinitions, inBlockEditors, blocks; inScripts = this.scripts.allChildren().filter(c => c.isCustomBlock && !c.isGlobal && (c.blockSpec === aSpec) ); inDefinitions = []; this.customBlocks.forEach(def => { def.scripts.forEach(eachScript => eachScript.allChildren().forEach(c => { if (c.isCustomBlock && !c.isGlobal && (c.blockSpec === aSpec) ) { inDefinitions.push(c); } }) ); if (def.body) { def.body.expression.allChildren().forEach(c => { if (c.isCustomBlock && !c.isGlobal && (c.blockSpec === aSpec) ) { inDefinitions.push(c); } }); } }); inBlockEditors = this.allEditorBlockInstances(null, aSpec); blocks = inScripts.concat(inDefinitions).concat(inBlockEditors); return blocks; }; SpriteMorph.prototype.addAllInvocationsOf = function (aSpec, anArray) { if (!this.getLocalMethod(aSpec)) { this.allInvocationsOf(aSpec).forEach(block => anArray.push(block) ); this.instances.forEach(sprite => sprite.addAllInvocationsOf(aSpec, anArray) ); } }; SpriteMorph.prototype.allLocalBlockInstances = function (definition) { var inScripts, inDefinitions, inBlockEditors, inPalette, result; inScripts = this.scripts.allChildren().filter(c => c.isCustomBlock && (c.definition === definition) ); inDefinitions = []; this.customBlocks.forEach(def => { if (def.body) { def.body.expression.allChildren().forEach(c => { if (c.isCustomBlock && (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, spec) { // either pass a definition for global custom blocks // or a spec for local ones var inBlockEditors = [], world = this.world(); if (!world) {return []; } // when copying a sprite this.world().children.forEach(morph => { if (morph instanceof BlockEditorMorph) { morph.body.contents.allChildren().forEach(block => { if (definition) { // global if (!block.isPrototype && !(block instanceof PrototypeHatBlockMorph) && (block.definition === definition)) { inBlockEditors.push(block); } } else { // local if (block.isCustomBlock && !block.isGlobal && !block.isPrototype && !(block instanceof PrototypeHatBlockMorph) && (block.blockSpec === spec)) { 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, block => block.isCustomBlock && (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(), c => c.isCustomBlock && (c.definition === definition) ); if (inScripts) {return true; } if (definition.isGlobal && !skipGlobals) { inDefinitions = []; this.parentThatIsA(StageMorph).globalBlocks.forEach(def => { if (forRemoval && (definition === def)) {return; } if (skipBlocks && contains(skipBlocks, def)) {return; } if (def.body) { def.body.expression.allChildren().forEach(c => { if (c.isCustomBlock && (c.definition === definition)) { inDefinitions.push(c); } }); } }); if (inDefinitions.length > 0) {return true; } } inDefinitions = []; this.customBlocks.forEach(def => { if (def.body) { def.body.expression.allChildren().forEach(c => { if (c.isCustomBlock && (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((def, i) => def.blockSpec() === spec && (i !== idx) ); }; SpriteMorph.prototype.replaceDoubleDefinitionsFor = function (definition) { var doubles = this.doubleDefinitionsFor(definition), stage, ide; doubles.forEach(double => this.allBlockInstances(double).forEach(block => { block.definition = definition; block.refresh(); }) ); if (definition.isGlobal) { stage = this.parentThatIsA(StageMorph); stage.globalBlocks = stage.globalBlocks.filter(def => !contains(doubles, def) ); } else { this.customBlocks = this.customBlocks.filter(def => !contains(doubles, def) ); } ide = this.parentThatIsA(IDE_Morph); if (ide) { ide.flushPaletteCache(); ide.refreshPalette(); } }; // SpriteMorph controlling generic WHEN hats SpriteMorph.prototype.pauseGenericHatBlocks = function () { var stage = this.parentThatIsA(StageMorph), ide = this.parentThatIsA(IDE_Morph); if (this.hasGenericHatBlocks()) { stage.enableCustomHatBlocks = true; stage.threads.pauseCustomHatBlocks = true; ide.controlBar.stopButton.refresh(); } }; // SpriteMorph inheritance - general SpriteMorph.prototype.chooseExemplar = function () { var stage = this.parentThatIsA(StageMorph), other = stage.children.filter(m => m instanceof SpriteMorph && !m.isTemporary && (!contains(m.allExemplars(), this)) ), menu; menu = new MenuMorph( aSprite => this.setExemplar(aSprite), localize('current parent') + ':\n' + (this.exemplar ? this.exemplar.name : localize('none')) ); other.forEach(eachSprite => menu.addItem( eachSprite.name, eachSprite, null, // hint null, // color null, // bold null, // italic null, // doubleClickAction null, // shortcut true // verbatim ) ); menu.addLine(); menu.addItem(localize('none'), null); menu.popUpAtHand(this.world()); }; SpriteMorph.prototype.setExemplar = function (another, enableError) { var ide; // check for circularity if (another instanceof SpriteMorph && contains(another.allExemplars(), this)) { if (enableError) { throw new Error( localize('unable to inherit\n(disabled or circular?)') ); } return; // silently fail so stored projects can still be loaded } this.emancipate(); this.exemplar = another; if (another) { this.variables.parentFrame = another.variables; another.addSpecimen(this); } else { this.variables.parentFrame = this.globalVariables(); } if (this.isTemporary) { this.cloneOriginName = another.cloneOriginName || another.name; } else { ide = this.parentThatIsA(IDE_Morph); if (ide) { ide.flushBlocksCache(); ide.refreshPalette(); } } }; SpriteMorph.prototype.prune = function () { // sever ties with all my specimen, if any, this.instances.forEach(child => { child.shadowAllAttributes(); child.shadowAllMethods(); child.shadowAllVars(); child.exemplar = null; }); this.instances = []; }; SpriteMorph.prototype.emancipate = function () { // sever all relations with my exemplar, if any, // and make sure I am the root of my specimen if (this.exemplar) { if (!this.isTemporary) { this.shadowAllAttributes(); this.shadowAllMethods(); this.shadowAllVars(); } this.exemplar.removeSpecimen(this); // optimization this.exemplar = null; } }; 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 return this.instances; }; SpriteMorph.prototype.allSpecimens = function () { // without myself var all = this.instances.slice(); this.instances.forEach(child => all.push.apply(all, child.allSpecimens()) ); return all; }; SpriteMorph.prototype.addSpecimen = function (another) { // private - use setExemplar() to establish an inheritance relationship this.instances.push(another); }; SpriteMorph.prototype.removeSpecimen = function(another) { // private - use setExemplar(null) to cancel an inheritance relationship var idx = this.instances.indexOf(another); if (idx !== -1) { this.instances.splice(idx, 1); } }; // SpriteMorph inheritance - attributes SpriteMorph.prototype.inheritsAttribute = function (aName) { return !isNil(this.exemplar) && contains(this.inheritedAttributes, aName); }; SpriteMorph.prototype.updatePropagationCache = function () { // private - indicate whether one of my inherited attributes is technically // propagated down from my exemplar, instead of truly shared. // (only) needed for internal optimization caching this.cachedPropagation = !isNil(this.exemplar) && detect( [ 'x position', 'y position', 'direction', 'size', 'costume #', 'volume', 'balance', 'shown?', 'pen down?' ], att => contains(this.inheritedAttributes, att) ); }; SpriteMorph.prototype.shadowedAttributes = function () { // answer an array of attribute names that can be deleted/shared var inherited = this.inheritedAttributes; return this.attributes.filter(each => !contains(inherited, each)); }; SpriteMorph.prototype.shadowAllAttributes = function () { this.attributes.forEach(att => this.shadowAttribute(att) ); }; SpriteMorph.prototype.shadowAttribute = function (aName) { var ide, wardrobe, jukebox, pos; if (!this.inheritsAttribute(aName)) { return; } ide = this.parentThatIsA(IDE_Morph); this.inheritedAttributes = this.inheritedAttributes.filter(each => each !== aName ); if (aName === 'costumes') { wardrobe = new List(); this.costumes.asArray().forEach(costume => { var cst = costume.copy(); wardrobe.add(cst); if (costume === this.costume) { this.wearCostume(cst); } }); this.costumes = wardrobe; this.instances.forEach(obj => { if (obj.inheritsAttribute('costumes')) { obj.refreshInheritedAttribute('costumes'); } }); } else if (aName === 'sounds') { jukebox = new List(); this.sounds.asArray().forEach(sound => jukebox.add(sound.copy())); this.sounds = jukebox; this.instances.forEach(obj => { if (obj.inheritsAttribute('sounds')) { obj.refreshInheritedAttribute('sounds'); } }); } else if (aName === 'scripts') { ide.stage.threads.stopAllForReceiver(this); pos = this.scripts.position(); this.scripts = this.exemplar.scripts.fullCopy(); if (ide && (contains(ide.currentSprite.allExemplars(), this))) { ide.createSpriteEditor(); ide.fixLayout('selectSprite'); this.scripts.fixMultiArgs(); this.scripts.setPosition(pos); ide.spriteEditor.adjustScrollBars(); } this.instances.forEach(obj => { if (obj.inheritsAttribute('scripts')) { obj.refreshInheritedAttribute('scripts'); } }); } else { this.updatePropagationCache(); if (ide && !this.isTemporary) { ide.flushBlocksCache(); // optimization: specify category if known ide.refreshPalette(); } } }; SpriteMorph.prototype.inheritAttribute = function (aName) { var ide = this.parentThatIsA(IDE_Morph); if (!this.exemplar || !contains(this.attributes, aName)) { return; } if (!this.inheritsAttribute(aName)) { this.inheritedAttributes.push(aName); this.refreshInheritedAttribute(aName); if (ide) { ide.flushBlocksCache(); // optimization: specify category ide.refreshPalette(); } } }; SpriteMorph.prototype.refreshInheritedAttribute = function (aName) { var ide, idx; switch (aName) { case 'x position': case 'y position': this.cachedPropagation = true; this.gotoXY(this.xPosition(), this.yPosition(), false, true); break; case 'direction': this.cachedPropagation = true; this.setHeading(this.direction(), true); break; case 'size': this.cachedPropagation = true; this.setScale(this.getScale(), true); break; case 'costume #': this.cachedPropagation = true; if (this.inheritsAttribute('costumes')) { // if inheriting the whole wardrobe, // just switch to the exemplar's costume this.wearCostume(this.exemplar.costume, true); } else { // otherwise switch to the own costume of the // corresponing number this.doSwitchToCostume(this.getCostumeIdx(), true); } break; case 'volume': this.cachedPropagation = true; this.setVolume(this.getVolume(), true); break; case 'shown?': this.cachedPropagation = true; this.setVisibility(this.reportShown(), true); break; case 'pen down?': this.cachedPropagation = true; this.setPenDown(this.getPenDown(), true); break; case 'balance': this.cachedPropagation = true; this.setPan(this.getPan(), true); break; case 'costumes': idx = this.getCostumeIdx(); this.costumes = this.exemplar.costumes; this.doSwitchToCostume(idx, true); this.instances.forEach(sprite => { if (sprite.inheritsAttribute('costumes')) { sprite.refreshInheritedAttribute('costumes'); } }); break; case 'sounds': this.sounds = this.exemplar.sounds; this.instances.forEach(sprite => { if (sprite.inheritsAttribute('sounds')) { sprite.refreshInheritedAttribute('sounds'); } }); break; case 'scripts': this.scripts = this.exemplar.scripts; ide = this.parentThatIsA(IDE_Morph); if (ide) { ide.stage.threads.stopAllForReceiver(this); if (contains(ide.currentSprite.allExemplars(), this)) { ide.createSpriteEditor(); ide.fixLayout('selectSprite'); } } this.instances.forEach(sprite => { if (sprite.inheritsAttribute('scripts')) { sprite.refreshInheritedAttribute('scripts'); } }); break; default: nop(); } }; SpriteMorph.prototype.toggleInheritanceForAttribute = function (aName) { if (this.inheritsAttribute(aName)) { this.shadowAttribute(aName); } else { this.inheritAttribute(aName); } }; // 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.shadowAllVars = function () { this.inheritedVariableNames().forEach(name => this.shadowVar(name, this.variables.getVar(name)) ); }; SpriteMorph.prototype.shadowVar = function (name, value) { var ide; this.variables.addVar(name, value); if (!this.isTemporary) { ide = this.parentThatIsA(IDE_Morph); if (ide) { ide.flushBlocksCache('variables'); ide.refreshPalette(); } } }; SpriteMorph.prototype.toggleInheritedVariable = function (vName) { if (contains(this.inheritedVariableNames(true), vName)) { // is shadowed this.deleteVariable(vName); } else if (contains(this.inheritedVariableNames(), vName)) { // inherited this.shadowVar(vName, this.variables.getVar(vName)); } }; 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(each => !contains(locals, each) && !contains(inherited, each) ) ); }; SpriteMorph.prototype.hasSpriteVariable = function (varName) { return contains(this.variables.names(), varName); }; SpriteMorph.prototype.allLocalVariableNames = function (sorted, all) { // "all" includes hidden ones in the palette var exceptGlobals = this.globalVariables(), globalNames = exceptGlobals.names(all), data; function alphabetically(x, y) { return x.toLowerCase() < y.toLowerCase() ? -1 : 1; } data = this.variables.allNames(exceptGlobals, all).filter(each => !contains(globalNames, each) ); if (sorted) { data.sort(alphabetically); } return data; }; SpriteMorph.prototype.reachableGlobalVariableNames = function (sorted, all) { // "all" includes hidden ones in the palette var locals = this.allLocalVariableNames(null, all), data; function alphabetically(x, y) { return x.toLowerCase() < y.toLowerCase() ? -1 : 1; } data = this.globalVariables().names(all).filter(each => !contains(locals, each) ); if (sorted) { data.sort(alphabetically); } return data; }; // SpriteMorph inheritance - custom blocks SpriteMorph.prototype.getMethod = function (spec) { return this.allBlocks()[spec]; }; SpriteMorph.prototype.getLocalMethod = function (spec) { return this.ownBlocks()[spec]; }; SpriteMorph.prototype.ownBlocks = function () { var dict = {}; this.customBlocks.forEach(def => dict[def.blockSpec()] = def ); return dict; }; SpriteMorph.prototype.allBlocks = function (valuesOnly) { var dict = {}; this.allExemplars().reverse().forEach(sprite => sprite.customBlocks.forEach(def => dict[def.blockSpec()] = def ) ); if (valuesOnly) { return Object.keys(dict).map(key => dict[key]); } return dict; }; SpriteMorph.prototype.inheritedBlocks = function (valuesOnly) { var dict = {}, own = Object.keys(this.ownBlocks()), others = this.allExemplars().reverse(); others.pop(); others.forEach(sprite => sprite.customBlocks.forEach(def => { var spec = def.blockSpec(); if (!contains(own, spec)) { dict[spec] = def; } }) ); if (valuesOnly) { return Object.keys(dict).map(key => dict[key]); } return dict; }; SpriteMorph.prototype.shadowAllMethods = function () { var ide; this.inheritedMethods().forEach(dup => this.customBlocks.push(dup) ); if (!this.isTemporary) { ide = this.parentThatIsA(IDE_Morph); if (ide) { ide.flushPaletteCache(); ide.refreshPalette(); } } }; SpriteMorph.prototype.inheritedMethods = function () { // private - pre-serialization preparation return this.inheritedBlocks(true).map(def => def.copyAndBindTo(this, true) // header only ); }; // SpriteMorph thumbnail SpriteMorph.prototype.thumbnail = function (extentPoint, recycleMe, noCorpse) { // answer a new Canvas of extentPoint dimensions containing // my thumbnail representation keeping the originial aspect ratio // a "recycleMe canvas can be passed for re-use var src = this.getImage(), // at this time sprites aren't composite morphs w = this.width(), h = this.height(), scale = Math.min( (extentPoint.x / w), (extentPoint.y / h) ), xOffset = (extentPoint.x - (w * scale)) / 2, yOffset = (extentPoint.y - (h * scale)) / 2, trg = newCanvas(extentPoint, false, recycleMe), 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 && !noCorpse) { ctx.globalAlpha = 0.3; } if (w && h && src.width && src.height) { ctx.scale(scale, scale); ctx.drawImage( src, Math.floor(xOffset / scale), Math.floor(yOffset / scale) ); } if (this.isCorpse && !noCorpse) { ctx.restore(); xOut('white', 0.8, 6); xOut('black', 0.8, 1); } ctx.restore(); return trg; }; SpriteMorph.prototype.fullThumbnail = function (extentPoint, recycleMe) { // containing parts and anchor symbols, if any var thumb = this.thumbnail(extentPoint, recycleMe), 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.fixLayout(); return sym; }; // SpriteMorph nesting /* simulate Morphic trees */ SpriteMorph.prototype.attachTo = function (aSprite) { aSprite.attachPart(this); }; 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(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(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(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(part => { var bubble = part.talkBubble(); if (bubble) {bubble.hide(); } }); this.layers.sort((x, y) => stage.children.indexOf(x) < stage.children.indexOf(y) ? -1 : 1 ); }; SpriteMorph.prototype.restoreLayers = function () { if (this.layers && this.layers.length > 1) { this.layers.forEach(sprite => { sprite.comeToFront(); sprite.positionTalkBubble(); }); } this.layers = null; }; // SpriteMorph destroying SpriteMorph.prototype.destroy = function () { // make sure to sever all inheritance ties to other sprites if (this.anchor) { this.anchor.detachPart(this); } this.emancipate(); if (!this.isTemporary) { this.prune(); } SpriteMorph.uber.destroy.call(this); }; // SpriteMorph highlighting SpriteMorph.prototype.flash = function () { var world = this.world(); this.addHighlight(); world.animations.push(new Animation( nop, nop, 0, 800, nop, () => this.removeHighlight() )); }; 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.bounds.setExtent(fb.extent().add(edge * 2)); highlight.color = color; highlight.cachedImage = this.highlightImage(color, border); ctx = highlight.cachedImage.getContext('2d'); ctx.drawImage( this.highlightImage(WHITE, 4), border - 4, border - 4 ); ctx.drawImage( this.highlightImage(new Color(50, 50, 50), 2), border - 2, border - 2 ); ctx.drawImage( this.highlightImage(WHITE, 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.getImage(); 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 = this.children.slice(0).reverse().filter(child => 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(each => each !== ignoredCostume ).map(each => 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.fullImage(); costume = new Costume(canvas, data); } this.addCostume(costume); }; // SpriteMorph adding sounds SpriteMorph.prototype.newSoundName = function (name, ignoredSound) { var ix = name.indexOf('('), stem = (ix < 0) ? name : name.substring(0, ix), count = 1, newName = stem, all = this.sounds.asArray().filter(each => each !== ignoredSound ).map(each => each.name); while (contains(all, newName)) { count += 1; newName = stem + '(' + count + ')'; } return newName; }; // SpriteHighlightMorph ///////////////////////////////////////////////// // SpriteHighlightMorph inherits from Morph: SpriteHighlightMorph.prototype = new Morph(); SpriteHighlightMorph.prototype.constructor = SpriteHighlightMorph; SpriteHighlightMorph.uber = Morph.prototype; // SpriteHighlightMorph instance creation: function SpriteHighlightMorph() { this.init(); } SpriteHighlightMorph.prototype.init = function () { SpriteHighlightMorph.uber.init.call(this); this.isCachingImage = true; }; // 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); // fallback unscaled ext 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 = true; StageMorph.prototype.enableSublistIDs = false; StageMorph.prototype.enablePenLogging = false; // for SVG generation // StageMorph instance creation function StageMorph(globals) { this.init(globals); } StageMorph.prototype.init = function (globals) { this.name = localize('Stage'); this.dimensions = new Point(480, 360); // unscaled extent this.instrument = null; this.threads = new ThreadManager(); this.variables = new VariableFrame(globals || null, this); this.scripts = new ScriptsMorph(); this.customBlocks = []; this.globalBlocks = []; this.costumes = new List(); this.costumes.type = 'costume'; this.costume = null; this.sounds = new List(); this.sounds.type = 'sound'; 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 = ''; // volume and stereo-pan support, experimental: this.volume = 100; this.gainNode = null; // must be lazily initialized in Chrome, sigh... this.pan = 0; this.pannerNode = null; // must be lazily initialized in Chrome, sigh... // frequency player, experimental this.freqPlayer = null; // Note, to be lazily initialized this.watcherUpdateFrequency = 2; this.lastWatcherUpdate = Date.now(); this.scale = 1; // for display modes, do not persist this.cachedColorDimensions = [0, 0, 0]; // bg color support, not serialized this.keysPressed = {}; // for handling keyboard events, do not persist this.primitivesCache = {}; // not to be serialized (!) this.paletteCache = {}; // not to be serialized (!) this.categoriesCache = null; // not to be serialized (!) this.lastAnswer = ''; // last user input, do not persist this.activeSounds = []; // do not persist this.trailsCanvas = null; this.trailsLog = []; // each line being [p1, p2, color, width, cap] this.isThreadSafe = false; this.microphone = new Microphone(); // audio input, do not persist this.graphicsValues = { 'color': 0, 'fisheye': 0, 'whirl': 0, 'pixelate': 0, 'mosaic': 0, 'duplicate': 0, 'negative': 0, 'comic': 0, 'confetti': 0, 'saturation': 0, 'brightness': 0 }; this.cachedPenTrailsMorph = null; // optimization, do not persist this.remixID = null; // projection layer - for video, maps, 3D extensions etc., transient this.projectionSource = null; // offscreen DOM element for video, maps, 3D this.getProjectionImage = null; // function to return a blittable image this.stopProjectionSource = null; // function to turn off video stream etc. this.continuousProjection = false; // turn ON for video this.projectionCanvas = null; this.projectionTransparency = 50; // video motion detection, transient this.mirrorVideo = true; this.videoMotion = null; // world map client - experimental, transient this.worldMap = new WorldMap(); // Snap! API event listeners - experimental, transient this.messageCallbacks = {}; // name : [functions] StageMorph.uber.init.call(this); this.setExtent(this.dimensions); this.isCachingImage = true; this.cachedColorDimensions = this.color[ SpriteMorph.prototype.penColorModel ](); this.acceptsDrops = false; this.setColor(new Color(255, 255, 255)); }; // StageMorph scaling StageMorph.prototype.setScale = function (number) { var delta = number / this.scale, pos = this.position(), relativePos, bubble; if (delta === 1) {return; } this.cachedPenTrailsMorph = null; this.scale = number; this.setExtent(this.dimensions.multiplyBy(number)); // now move and resize all children - sprites, bubbles, watchers etc.. this.children.forEach(morph => { relativePos = morph.position().subtract(pos); morph.fixLayout(); morph.setPosition( relativePos.multiplyBy(delta).add(pos), true // just me (for nested sprites) ); if (morph instanceof SpriteMorph) { morph.rerender(); bubble = morph.talkBubble(); if (bubble) { bubble.setScale(number); morph.positionTalkBubble(); } } else if (morph instanceof StagePrompterMorph) { if (this.scale < 1) { morph.setWidth(this.width() - 10); } else { morph.setWidth(this.dimensions.x - 20); } morph.setCenter(this.center()); morph.setBottom(this.bottom()); } }); }; StageMorph.prototype.moveBy = function (delta) { // override the inherited method to skip attached sprite parts, // because they are also level-1 children of the stage and thus // will be moved individually var children = this.children, i = children.length; this.changed(); this.bounds = this.bounds.translateBy(delta); this.changed(); for (i; i > 0; i -= 1) { children[i - 1].moveBy(delta, true); // justMe - skip sprite parts } }; // StageMorph rendering StageMorph.prototype.render = function (ctx) { ctx.save(); ctx.fillStyle = this.color.toString(); ctx.fillRect(0, 0, this.width(), this.height()); if (this.costume && !(this.costume.loaded instanceof Function)) { 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.cachedImage = this.applyGraphicsEffects(this.cachedImage); } ctx.restore(); this.version = Date.now(); // for observer optimization }; StageMorph.prototype.drawOn = function (ctx, rect) { // draw pen trails and webcam layers var clipped = rect.intersect(this.bounds), pos, src, w, h, sl, st, ws, hs; if (!this.isVisible || !clipped.extent().gt(ZERO)) { return; } // costume, if any, and background color StageMorph.uber.drawOn.call(this, ctx, rect); pos = this.position(); src = clipped.translateBy(pos.neg()); sl = src.left(); st = src.top(); w = src.width(); h = src.height(); ws = w / this.scale; hs = h / this.scale; ctx.save(); ctx.scale(this.scale, this.scale); // projection layer (e.g. webcam) if (this.projectionSource) { ctx.globalAlpha = 1 - (this.projectionTransparency / 100); ctx.drawImage( this.projectionLayer(), sl / this.scale, st / this.scale, ws, hs, clipped.left() / this.scale, clipped.top() / this.scale, ws, hs ); this.version = Date.now(); // update watcher icons } // pen trails ctx.globalAlpha = 1; ctx.drawImage( this.penTrails(), sl / this.scale, st / this.scale, ws, hs, clipped.left() / this.scale, clipped.top() / this.scale, ws, hs ); ctx.restore(); }; StageMorph.prototype.clearPenTrails = function () { this.cachedPenTrailsMorph = null; this.trailsCanvas = newCanvas(this.dimensions, null, this.trailsCanvas); this.trailsLog = []; 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, trails, ctx; if (this.cachedPenTrailsMorph) { return this.cachedPenTrailsMorph; } morph = new Morph(); morph.isCachingImage = true; trails = this.penTrails(); morph.bounds = this.bounds.copy(); morph.cachedImage = newCanvas(this.extent(), true); ctx = morph.cachedImage.getContext('2d'); ctx.drawImage( trails, 0, 0, trails.width, trails.height, 0, 0, this.width(), this.height() ); this.cachedPenTrailsMorph = morph; return morph; }; StageMorph.prototype.projectionLayer = function () { if (!this.projectionCanvas) { this.projectionCanvas = newCanvas(this.dimensions, true); } return this.projectionCanvas; }; StageMorph.prototype.clearProjectionLayer = function () { this.projectionCanvas = null; this.changed(); }; // StageMorph video capture StageMorph.prototype.startVideo = function() { var myself = this; function noCameraSupport() { var dialog = new DialogBoxMorph(); dialog.inform( localize('Camera not supported'), localize('Please make sure your web browser is up to date\n' + 'and your camera is properly configured. \n\n' + 'Some browsers also require you to access Snap!\n' + 'through HTTPS to use the camera.\n\n' + 'Please replace the "http://" part of the address\n' + 'in your browser by "https://" and try again.'), this.world ); dialog.fixLayout(); if (myself.projectionSource) { myself.projectionSource.remove(); myself.projectionSource = null; } } if (this.projectionSource) { // video capture has already been started return; } this.projectionSource = document.createElement('video'); this.projectionSource.width = this.dimensions.x; this.projectionSource.height = this.dimensions.y; this.projectionSource.hidden = true; document.body.appendChild(this.projectionSource); if (!this.videoMotion) { this.videoMotion = new VideoMotion( this.dimensions.x, this.dimensions.y ); } if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { navigator.mediaDevices.getUserMedia({ video: true }) .then(function(stream) { myself.getProjectionImage = myself.getVideoImage; myself.stopProjectionSource = myself.stopVideo; myself.continuousProjection = true; myself.projectionSource.srcObject = stream; myself.projectionSource.play().catch(noCameraSupport); myself.projectionSource.stream = stream; }) .catch(noCameraSupport); } }; StageMorph.prototype.getVideoImage = function () { return this.projectionSource; }; StageMorph.prototype.stopVideo = function() { if (this.projectionSource && this.projectionSource.stream) { this.projectionSource.stream.getTracks().forEach(track => track.stop() ); } this.videoMotion = null; }; StageMorph.prototype.stopProjection = function() { if (this.projectionSource) { this.stopProjectionSource(); this.projectionSource.remove(); this.projectionSource = null; this.continuousProjection = false; } this.clearProjectionLayer(); }; StageMorph.prototype.projectionSnap = function() { var snap = newCanvas(this.dimensions, true), ctx = snap.getContext('2d'); ctx.drawImage(this.projectionLayer(), 0, 0); return new Costume(snap, this.newCostumeName(localize('snap'))); }; // StageMorph pixel access: StageMorph.prototype.getPixelColor = function (aPoint) { var point, context, data; if (this.trailsCanvas) { point = aPoint.subtract(this.bounds.origin); context = this.penTrailsMorph().getImage().getContext('2d'); data = context.getImageData(point.x, point.y, 1, 1); if (data.data[3] === 0) { if (this.projectionCanvas) { point = point.divideBy(this.scale); context = this.projectionCanvas.getContext('2d'); data = context.getImageData(point.x, point.y, 1, 1); return new Color( data.data[0], data.data[1], data.data[2], data.data[3] / 255 ); } return StageMorph.uber.getPixelColor.call(this, aPoint); } return new Color( data.data[0], data.data[1], data.data[2], data.data[3] / 255 ); } }; // 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(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.keyboardFocus === null) { world.keyboardFocus = this; } if (world.currentKey === null) { this.keyPressed = null; } // manage threads if (this.enableCustomHatBlocks) { this.stepGenericConditions(); } if (this.isFastTracked && this.threads.processes.length) { while (this.isFastTracked && (Date.now() - this.lastTime) < 15) { this.threads.step(); // approx. 67 fps } 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(w => w.update()); this.lastWatcherUpdate = Date.now(); } // projection layer update (e.g. video frame capture) if (this.continuousProjection && this.projectionSource) { this.updateProjection(); } }; StageMorph.prototype.updateProjection = function () { var context = this.projectionLayer().getContext('2d'); context.save(); if (this.mirrorVideo) { context.translate(this.dimensions.x, 0); context.scale(-1, 1); } context.drawImage( this.getProjectionImage(), 0, 0, this.projectionSource.width, this.projectionSource.height ); if (this.videoMotion) { this.videoMotion.addFrame( context.getImageData( 0, 0, this.projectionSource.width, this.projectionSource.height ).data ); } context.restore(); this.changed(); }; StageMorph.prototype.stepGenericConditions = function (stopAll) { var hatCount = 0, ide; this.children.concat(this).forEach(morph => { if (isSnapObject(morph)) { morph.allGenericHatBlocks().forEach(block => { hatCount += 1; this.threads.doWhen(block, morph, stopAll); }); } }); if (!hatCount) { this.enableCustomHatBlocks = false; ide = this.parentThatIsA(IDE_Morph); if (ide) { ide.controlBar.stopButton.refresh(); } } }; StageMorph.prototype.developersMenu = function () { var menu = StageMorph.uber.developersMenu.call(this); menu.addItem( "stop", () => this.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 = event.key || String.fromCharCode( event.keyCode || event.charCode ); if (event.ctrlKey || event.metaKey) { keyName = (keyName === 'Control' || keyName === 'Meta' ? '' : 'ctrl ') + (event.shiftKey ? 'shift ' : '') + keyName; } } action.call(this, keyName); }; StageMorph.prototype.fireKeyEvent = function (key) { var evt = key.toLowerCase(), procs = [], ide = this.parentThatIsA(IDE_Morph); this.keysPressed[evt] = true; if (evt === 'ctrl enter' && !ide.isAppMode) { 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' && !ide.isAppMode) { return this.fireStopAllEvent(); } this.children.concat(this).forEach(morph => { if (isSnapObject(morph)) { morph.allHatBlocksForKey(evt).forEach(block => { var varName = block.inputs()[1].evaluate()[0], varFrame; if (varName) { varFrame = new VariableFrame(); varFrame.addVar( varName, key === 'space' ? ' ' : key // not lowercased ); } procs.push(this.threads.startProcess( block, morph, true, // ignore running scripts, was: myself.isThreadSafe null, // exportResult (bool) null, // callback null, // isClicked null, // rightAway null, // atomic varFrame )); }); } }); 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.fireChangeOfSceneEvent = function (message) { var procs = []; // remove all clones when the green flag event is broadcast if (message === '__shout__go__') { this.removeAllClones(); } this.children.concat(this).forEach(morph => { if (isSnapObject(morph)) { morph.allHatBlocksFor(message).forEach(block => { var varName, varFrame; if (block.selector === 'receiveMessage') { varName = block.inputs()[1].evaluate()[0]; if (varName) { varFrame = new VariableFrame(); varFrame.addVar(varName, message); } procs.push(this.threads.startProcess( block, morph, this.isThreadSafe || // make "any msg" threadsafe block.inputs()[0].evaluate() instanceof Array, null, // exportResult (bool) null, // callback null, // isClicked null, // rightAway null, // atomic varFrame )); } else { procs.push(this.threads.startProcess( block, morph, this.isThreadSafe )); } }); } }); return procs; }; StageMorph.prototype.fireGreenFlagEvent = function () { var procs = [], ide = this.parentThatIsA(IDE_Morph); this.removeAllClones(); this.children.concat(this).forEach(morph => { if (isSnapObject(morph)) { morph.allHatBlocksFor('__shout__go__').forEach(block => procs.push(this.threads.startProcess( block, morph, this.isThreadSafe )) ); } }); if (ide) { ide.controlBar.pauseButton.refresh(); } return procs; }; StageMorph.prototype.fireStopAllEvent = function () { var ide = this.parentThatIsA(IDE_Morph); this.threads.resumeAll(this.stage); // experimental: run one step of a user-defined script this.runStopScripts(); this.keysPressed = {}; this.threads.stopAll(); this.stopAllActiveSounds(); this.children.forEach(morph => { if (morph.stopTalking) { morph.stopTalking(); } }); this.removeAllClones(); if (ide) { ide.nextSteps([ nop, () => this.stopAllActiveSounds(), // catch forever loops () => this.stopProjection(), () => ide.controlBar.pauseButton.refresh() ]); } }; StageMorph.prototype.runStopScripts = function () { // experimental: Allow each sprite to run one last step before termination // usage example: Stop a robot or device associated with the sprite this.receiveUserInteraction('stopped', true, true); this.children.forEach(morph => { if (morph instanceof SpriteMorph) { morph.receiveUserInteraction('stopped', true, true); } }); }; StageMorph.prototype.removeAllClones = function () { var clones = this.children.filter(morph => morph instanceof SpriteMorph && morph.isTemporary ); clones.forEach(clone => { this.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.selectForEdit(); // shadow on edit, if inherited 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 controlling generic WHEN hats StageMorph.prototype.pauseGenericHatBlocks = function () { var ide = this.parentThatIsA(IDE_Morph); if (this.hasGenericHatBlocks() || ide.sprites.asArray().some(any => any.hasGenericHatBlocks())) { this.enableCustomHatBlocks = true; this.threads.pauseCustomHatBlocks = true; ide.controlBar.stopButton.refresh(); } }; // StageMorph block templates StageMorph.prototype.blockTemplates = function ( category = 'motion', all = false // include hidden blocks ) { var blocks = [], myself = this, varNames, txt; function block(selector) { if (myself.hiddenPrimitives[selector] && !all) { return null; } var newBlock = SpriteMorph.prototype.blockForSelector(selector, true); newBlock.isTemplate = true; return newBlock; } function variableBlock(varName, isLocal) { var newBlock = SpriteMorph.prototype.variableBlock(varName, isLocal); 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 ); } if (category === 'motion') { txt = new TextMorph(localize('Stage selected:\nno motion primitives')); txt.fontSize = 9; txt.setColor(this.paletteTextColor); blocks.push(txt); } else if (category === 'looks') { blocks.push(block('doSwitchToCostume')); blocks.push(block('doWearNextCostume')); blocks.push(watcherToggle('getCostumeIdx')); blocks.push(block('getCostumeIdx')); blocks.push('-'); blocks.push(block('reportGetImageAttribute')); blocks.push(block('reportNewCostumeStretched')); blocks.push(block('reportNewCostume')); blocks.push('-'); blocks.push(block('changeEffect')); blocks.push(block('setEffect')); blocks.push(block('clearEffects')); blocks.push(block('getEffect')); blocks.push('-'); blocks.push(block('show')); blocks.push(block('hide')); blocks.push(watcherToggle('reportShown')); blocks.push(block('reportShown')); // for debugging: /////////////// if (this.world().isDevMode) { blocks.push('-'); blocks.push(this.devModeText()); blocks.push('-'); blocks.push(block('log')); blocks.push(block('alert')); blocks.push('-'); blocks.push(block('doScreenshot')); } } else if (category === 'sound') { blocks.push(block('playSound')); blocks.push(block('doPlaySoundUntilDone')); blocks.push(block('doStopAllSounds')); blocks.push('-'); blocks.push(block('doPlaySoundAtRate')); blocks.push(block('reportGetSoundAttribute')); blocks.push(block('reportNewSoundFromSamples')); blocks.push('-'); blocks.push(block('doRest')); blocks.push(block('doPlayNote')); blocks.push(block('doSetInstrument')); blocks.push('-'); blocks.push(block('doChangeTempo')); blocks.push(block('doSetTempo')); blocks.push(watcherToggle('getTempo')); blocks.push(block('getTempo')); blocks.push('-'); blocks.push(block('changeVolume')); blocks.push(block('setVolume')); blocks.push(watcherToggle('getVolume')); blocks.push(block('getVolume')); blocks.push('-'); blocks.push(block('changePan')); blocks.push(block('setPan')); blocks.push(watcherToggle('getPan')); blocks.push(block('getPan')); blocks.push('-'); blocks.push(block('playFreq')); blocks.push(block('stopFreq')); // for debugging: /////////////// if (this.world().isDevMode) { blocks.push('-'); blocks.push(this.devModeText()); blocks.push('-'); blocks.push(block('doPlayFrequency')); } } else if (category === 'pen') { blocks.push(block('clear')); blocks.push('-'); blocks.push(block('setBackgroundColor')); blocks.push(block('changeBackgroundColorDimension')); blocks.push(block('setBackgroundColorDimension')); blocks.push('-'); blocks.push(block('reportPenTrailsAsCostume')); blocks.push('-'); blocks.push(block('doPasteOn')); blocks.push(block('doCutFrom')); } else if (category === 'control') { blocks.push(block('receiveGo')); blocks.push(block('receiveKey')); blocks.push(block('receiveInteraction')); blocks.push(block('receiveCondition')); blocks.push('-'); blocks.push(block('receiveMessage')); blocks.push(block('doBroadcast')); blocks.push(block('doBroadcastAndWait')); blocks.push('-'); blocks.push(block('doWarp')); blocks.push('-'); blocks.push(block('doWait')); blocks.push(block('doWaitUntil')); blocks.push('-'); blocks.push(block('doForever')); blocks.push(block('doRepeat')); blocks.push(block('doUntil')); blocks.push(block('doFor')); blocks.push('-'); blocks.push(block('doIf')); blocks.push(block('doIfElse')); blocks.push(block('reportIfElse')); blocks.push('-'); blocks.push(block('doReport')); blocks.push(block('doStopThis')); blocks.push('-'); blocks.push(block('doRun')); blocks.push(block('fork')); blocks.push(block('evaluate')); blocks.push('-'); blocks.push(block('doTellTo')); blocks.push(block('reportAskFor')); blocks.push('-'); blocks.push(block('doCallCC')); blocks.push(block('reportCallCC')); blocks.push('-'); blocks.push(block('createClone')); blocks.push(block('newClone')); blocks.push('-'); blocks.push(block('doPauseAll')); blocks.push(block('doSwitchToScene')); // for debugging: /////////////// if (this.world().isDevMode) { blocks.push('-'); blocks.push(this.devModeText()); blocks.push('-'); blocks.push(watcherToggle('getLastMessage')); blocks.push(block('getLastMessage')); } } else if (category === '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('reportAspect')); 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(block('reportObject')); blocks.push('-'); blocks.push(block('reportURL')); blocks.push(block('reportAudio')); blocks.push(block('reportVideo')); blocks.push(block('doSetVideoTransparency')); blocks.push('-'); blocks.push(block('reportGlobalFlag')); blocks.push(block('doSetGlobalFlag')); blocks.push('-'); blocks.push(block('reportDate')); blocks.push(block('reportBlockAttribute')); // for debugging: /////////////// if (this.world().isDevMode) { blocks.push('-'); blocks.push(this.devModeText()); blocks.push('-'); blocks.push(watcherToggle('reportThreadCount')); blocks.push(block('reportThreadCount')); blocks.push(block('reportStackSize')); blocks.push(block('reportFrameCount')); blocks.push(block('reportYieldCount')); } } if (category === '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(block('reportPower')); 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 (Process.prototype.enableJS) { // (Process.prototype.enableJS) { blocks.push('-'); blocks.push(block('reportJSFunction')); if (Process.prototype.enableCompiling) { blocks.push(block('reportCompiled')); } } // for debugging: /////////////// if (this.world().isDevMode) { blocks.push('-'); blocks.push(this.devModeText()); blocks.push('-'); blocks.push(block('reportTypeOf')); blocks.push(block('reportTextFunction')); } } if (category === 'variables') { blocks.push(this.makeVariableButton()); if (this.variables.allNames().length > 0) { blocks.push(this.deleteVariableButton()); } blocks.push('-'); varNames = this.reachableGlobalVariableNames(true, all); if (varNames.length > 0) { varNames.forEach(name => { blocks.push(variableWatcherToggle(name)); blocks.push(variableBlock(name)); }); blocks.push('-'); } varNames = this.allLocalVariableNames(true, all); if (varNames.length > 0) { varNames.forEach(name => { blocks.push(variableWatcherToggle(name)); blocks.push(variableBlock(name, true)); }); 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(block('reportNumbers')); blocks.push('-'); blocks.push(block('reportCONS')); blocks.push(block('reportListItem')); blocks.push(block('reportCDR')); blocks.push('-'); blocks.push(block('reportListAttribute')); blocks.push(block('reportListIndex')); blocks.push(block('reportListContainsItem')); blocks.push(block('reportListIsEmpty')); blocks.push('-'); blocks.push(block('reportMap')); blocks.push(block('reportKeep')); blocks.push(block('reportFindFirst')); blocks.push(block('reportCombine')); blocks.push('-'); blocks.push(block('doForEach')); blocks.push('-'); blocks.push(block('reportConcatenatedLists')); blocks.push(block('reportReshape')); blocks.push('-'); blocks.push(block('doAddToList')); blocks.push(block('doDeleteFromList')); blocks.push(block('doInsertInList')); blocks.push(block('doReplaceInList')); if (SpriteMorph.prototype.showingExtensions) { blocks.push('='); blocks.push(block('doApplyExtension')); blocks.push(block('reportApplyExtension')); } if (StageMorph.prototype.enableCodeMapping) { blocks.push('='); blocks.push(block('doMapCodeOrHeader')); blocks.push(block('doMapValueCode')); blocks.push(block('doMapListCode')); blocks.push('-'); blocks.push(block('reportMappedCode')); } // for debugging: /////////////// if (this.world().isDevMode) { blocks.push('-'); blocks.push(this.devModeText()); blocks.push('-'); blocks.push(block('doShowTable')); } } return blocks; }; // StageMorph primitives StageMorph.prototype.clear = function () { this.clearPenTrails(); }; // StageMorph user menu StageMorph.prototype.userMenu = function () { var ide = this.parentThatIsA(IDE_Morph), menu = new MenuMorph(this); if (ide && ide.isAppMode) { // menu.addItem('help', 'nop'); return menu; } menu.addItem("edit", 'edit'); menu.addItem("show all", 'showAll'); menu.addItem( "pic...", () => ide.saveCanvasAs(this.fullImage(), this.name), 'save a picture\nof the stage' ); menu.addLine(); menu.addItem( 'pen trails', () => { var costume = ide.currentSprite.reportPenTrailsAsCostume().copy(); ide.currentSprite.addCostume(costume); ide.currentSprite.wearCostume(costume); ide.hasChangedMedia = true; ide.spriteBar.tabBar.tabTo('costumes'); }, 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' ); if (this.trailsLog.length) { menu.addItem( 'svg...', 'exportTrailsLogAsSVG', 'export pen trails\nline segments as SVG' ); } return menu; }; StageMorph.prototype.showAll = function () { this.children.forEach(m => { if (m instanceof SpriteMorph) { if (!m.anchor) { m.show(); m.keepWithin(this); } } else { m.show(); m.keepWithin(this); if (m.fixLayout) {m.fixLayout(); } } }); }; StageMorph.prototype.edit = SpriteMorph.prototype.edit; StageMorph.prototype.fullImage = Morph.prototype.fullImage; // StageMorph thumbnail StageMorph.prototype.thumbnail = function (extentPoint, recycleMe, noWatchers) { // answer a new Canvas of extentPoint dimensions containing // my thumbnail representation keeping the originial aspect ratio // a "recycleMe canvas can be passed for re-use return this.fancyThumbnail(extentPoint, null, false, recycleMe, noWatchers); }; StageMorph.prototype.fancyThumbnail = function ( extentPoint, excludedSprite, nonRetina, recycleMe, noWatchers ) { var src = this.getImage(), scale = Math.min( (extentPoint.x / src.width), (extentPoint.y / src.height) ), trg = newCanvas(extentPoint, nonRetina, recycleMe), ctx = trg.getContext('2d'), fb, fimg; ctx.save(); 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 ); if (this.projectionSource) { ctx.save(); ctx.globalAlpha = 1 - (this.projectionTransparency / 100); ctx.drawImage( this.projectionLayer(), 0, 0, this.dimensions.x * this.scale, this.dimensions.y * this.scale ); ctx.restore(); } this.children.forEach(morph => { if ((isSnapObject(morph) || !noWatchers) && morph.isVisible && (morph !== excludedSprite) ) { fb = morph.fullBounds(); fimg = morph.fullImage(); if (fimg.width && fimg.height) { ctx.drawImage( morph.fullImage(), fb.origin.x - this.bounds.origin.x, fb.origin.y - this.bounds.origin.y ); } } }); ctx.restore(); return trg; }; // StageMorph - exporting the pen trails as SVG StageMorph.prototype.exportTrailsLogAsSVG = function () { var ide = this.parentThatIsA(IDE_Morph); ide.saveFileAs( this.trailsLogAsSVG().src, 'image/svg', ide.projectName || this.name ); }; StageMorph.prototype.trailsLogAsSVG = function () { var bottomLeft = this.trailsLog[0][0], topRight = bottomLeft, maxWidth = this.trailsLog[0][3], shift, box, p1, p2, svg; // determine bounding box and max line width this.trailsLog.forEach(line => { bottomLeft = bottomLeft.min(line[0]); bottomLeft = bottomLeft.min(line[1]); topRight = topRight.max(line[0]); topRight = topRight.max(line[1]); maxWidth = Math.max(maxWidth, line[3]); }); box = bottomLeft.corner(topRight).expandBy(maxWidth / 2); shift = new Point(-bottomLeft.x, topRight.y).translateBy(maxWidth / 2); svg = ''; svg += ''; // for debugging the viewBox: // svg += '' this.trailsLog.forEach(line => { p1 = this.normalizePoint(line[0]).translateBy(shift); p2 = this.normalizePoint(line[1]).translateBy(shift); svg += ''; }); svg += ''; return { src : svg, rot : new Point(-box.origin.x, box.corner.y) }; }; StageMorph.prototype.normalizePoint = function (snapPoint) { return new Point(snapPoint.x, -snapPoint.y); }; // 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.prototype.reportShown = SpriteMorph.prototype.reportShown; // StageMorph cloning override StageMorph.prototype.createClone = nop; StageMorph.prototype.newClone = nop; // StageMorph background color setting StageMorph.prototype.setColorDimension = function (idx, num) { var n = +num; idx = +idx; if (idx < 0 || idx > 3) {return; } if (idx === 0) { if (n < 0 || n > 100) { // wrap the hue n = (n < 0 ? 100 : 0) + n % 100; } } else { n = Math.min(100, Math.max(0, n)); } if (idx === 3) { this.color.a = 1 - n / 100; } else { this.cachedColorDimensions[idx] = n / 100; this.color['set_' + SpriteMorph.prototype.penColorModel].apply( this.color, this.cachedColorDimensions ); } this.rerender(); }; StageMorph.prototype.getColorDimension = SpriteMorph.prototype.getColorDimension; StageMorph.prototype.changeColorDimension = SpriteMorph.prototype.changeColorDimension; StageMorph.prototype.setColorRGBA = SpriteMorph.prototype.setColorRGBA; StageMorph.prototype.changeColorRGBA = SpriteMorph.prototype.changeColorRGBA; StageMorph.prototype.setColor = function (aColor) { if (!this.color.eq(aColor, true)) { // observeAlpha this.color = aColor.copy(); this.rerender(); this.cachedColorDimensions = this.color[ SpriteMorph.prototype.penColorModel ](); } }; StageMorph.prototype.setBackgroundColor = StageMorph.prototype.setColor; StageMorph.prototype.getPenAttribute = SpriteMorph.prototype.getPenAttribute; // StageMorph printing on another sprite: StageMorph.prototype.blitOn = SpriteMorph.prototype.blitOn; // 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.reporterize = SpriteMorph.prototype.reporterize; StageMorph.prototype.variableBlock = SpriteMorph.prototype.variableBlock; StageMorph.prototype.showingWatcher = SpriteMorph.prototype.showingWatcher; StageMorph.prototype.addVariable = SpriteMorph.prototype.addVariable; StageMorph.prototype.deleteVariable = SpriteMorph.prototype.deleteVariable; // StageMorph Palette Utilities StageMorph.prototype.makeBlock = SpriteMorph.prototype.makeBlock; StageMorph.prototype.helpMenu = SpriteMorph.prototype.helpMenu; StageMorph.prototype.makeBlockButton = SpriteMorph.prototype.makeBlockButton; StageMorph.prototype.makeVariableButton = SpriteMorph.prototype.makeVariableButton; StageMorph.prototype.categoryText = SpriteMorph.prototype.categoryText; StageMorph.prototype.devModeText = SpriteMorph.prototype.devModeText; StageMorph.prototype.deleteVariableButton = SpriteMorph.prototype.deleteVariableButton; StageMorph.prototype.customBlockTemplatesForCategory = SpriteMorph.prototype.customBlockTemplatesForCategory; StageMorph.prototype.getPrimitiveTemplates = SpriteMorph.prototype.getPrimitiveTemplates; 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 utilities for showing & hiding blocks in the palette StageMorph.prototype.allPaletteBlocks = SpriteMorph.prototype.allPaletteBlocks; StageMorph.prototype.isHidingBlock = SpriteMorph.prototype.isHidingBlock; StageMorph.prototype.changeBlockVisibility = SpriteMorph.prototype.changeBlockVisibility; StageMorph.prototype.changePrimitiveVisibility = SpriteMorph.prototype.changePrimitiveVisibility; StageMorph.prototype.changeCustomBlockVisibility = SpriteMorph.prototype.changeCustomBlockVisibility; StageMorph.prototype.changeVarBlockVisibility = SpriteMorph.prototype.changeVarBlockVisibility; StageMorph.prototype.emptyCategories = SpriteMorph.prototype.emptyCategories; // StageMorph neighbor detection StageMorph.prototype.neighbors = SpriteMorph.prototype.neighbors; StageMorph.prototype.perimeter = SpriteMorph.prototype.perimeter; // 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.getEffect = SpriteMorph.prototype.getEffect; 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.doPlaySound = SpriteMorph.prototype.doPlaySound; StageMorph.prototype.stopAllActiveSounds = function () { this.activeSounds.forEach(audio => audio.pause()); this.activeSounds = []; if (this.microphone.modifier && this.microphone.isReady) { this.microphone.stop(); } }; StageMorph.prototype.pauseAllActiveSounds = function () { this.activeSounds.forEach(audio => audio.pause()); }; StageMorph.prototype.resumeAllActiveSounds = function () { this.activeSounds.forEach(audio => audio.play()); }; StageMorph.prototype.reportSounds = SpriteMorph.prototype.reportSounds; StageMorph.prototype.newSoundName = SpriteMorph.prototype.newSoundName; // StageMorph volume StageMorph.prototype.setVolume = SpriteMorph.prototype.setVolume; StageMorph.prototype.changeVolume = SpriteMorph.prototype.changeVolume; StageMorph.prototype.getVolume = SpriteMorph.prototype.getVolume; StageMorph.prototype.getGainNode = SpriteMorph.prototype.getGainNode; StageMorph.prototype.audioContext = SpriteMorph.prototype.audioContext; // StageMorph stereo panning StageMorph.prototype.setPan = SpriteMorph.prototype.setPan; StageMorph.prototype.changePan = SpriteMorph.prototype.changePan; StageMorph.prototype.getPan = SpriteMorph.prototype.getPan; StageMorph.prototype.getPannerNode = SpriteMorph.prototype.getPannerNode; // StageMorph frequency player StageMorph.prototype.playFreq = SpriteMorph.prototype.playFreq; StageMorph.prototype.stopFreq = SpriteMorph.prototype.stopFreq; // 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 coordinate conversion StageMorph.prototype.snapPoint = SpriteMorph.prototype.snapPoint; // StageMorph dimension getters StageMorph.prototype.xCenter = function () { return 0; }; StageMorph.prototype.yCenter = function () { return 0; }; StageMorph.prototype.xLeft = function () { return this.dimensions.x * -0.5; }; StageMorph.prototype.xRight = function () { return this.dimensions.x / 2; }; StageMorph.prototype.yTop = function () { return this.dimensions.y / 2; }; StageMorph.prototype.yBottom = function () { return this.dimensions.y * -0.5; }; // StageMorph message broadcasting StageMorph.prototype.allMessageNames = SpriteMorph.prototype.allMessageNames; StageMorph.prototype.allSendersOf = SpriteMorph.prototype.allSendersOf; StageMorph.prototype.allHatBlocksFor = SpriteMorph.prototype.allHatBlocksFor; StageMorph.prototype.allHatBlocksForKey = SpriteMorph.prototype.allHatBlocksForKey; StageMorph.prototype.allHatBlocksForInteraction = SpriteMorph.prototype.allHatBlocksForInteraction; StageMorph.prototype.hasGenericHatBlocks = SpriteMorph.prototype.hasGenericHatBlocks; StageMorph.prototype.allGenericHatBlocks = SpriteMorph.prototype.allGenericHatBlocks; StageMorph.prototype.allScripts = SpriteMorph.prototype.allScripts; // 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.mouseScroll = SpriteMorph.prototype.mouseScroll; 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.prototype.allInvocationsOf = SpriteMorph.prototype.allInvocationsOf; StageMorph.prototype.allIndependentInvocationsOf = SpriteMorph.prototype.allInvocationsOf; StageMorph.prototype.allDependentInvocationsOf = SpriteMorph.prototype.allInvocationsOf; // StageMorph inheritance support - general StageMorph.prototype.specimens = function () { return []; }; StageMorph.prototype.allSpecimens = function () { return []; }; StageMorph.prototype.shadowAttribute = nop; // StageMorph inheritance support - attributes StageMorph.prototype.inheritsAttribute = function () { return false; }; // StageMorph inheritance support - variables StageMorph.prototype.isVariableNameInUse = SpriteMorph.prototype.isVariableNameInUse; StageMorph.prototype.globalVariables = SpriteMorph.prototype.globalVariables; StageMorph.prototype.inheritedVariableNames = function () { return []; }; StageMorph.prototype.deletableVariableNames = function () { return this.variables.allNames(); }; StageMorph.prototype.allLocalVariableNames = SpriteMorph.prototype.allLocalVariableNames; StageMorph.prototype.reachableGlobalVariableNames = SpriteMorph.prototype.reachableGlobalVariableNames; // StageMorph inheritance - custom blocks StageMorph.prototype.getMethod = SpriteMorph.prototype.getMethod; StageMorph.prototype.getLocalMethod = SpriteMorph.prototype.getLocalMethod; StageMorph.prototype.ownBlocks = SpriteMorph.prototype.ownBlocks; StageMorph.prototype.allBlocks = function (valuesOnly) { var dict = this.ownBlocks(); if (valuesOnly) { return Object.keys(dict).map(key => dict[key]); } return dict; }; StageMorph.prototype.inheritedBlocks = function () { return []; }; // StageMorph variable refactoring StageMorph.prototype.hasSpriteVariable = SpriteMorph.prototype.hasSpriteVariable; StageMorph.prototype.refactorVariableInstances = SpriteMorph.prototype.refactorVariableInstances; // StageMorph pen trails as costume StageMorph.prototype.reportPenTrailsAsCostume = function () { return new Costume( this.trailsCanvas, this.newCostumeName(localize('Background')) ); }; // StageMorph scanning global custom blocks for message sends StageMorph.prototype.globalBlocksSending = function (message, receiverName) { // "transitive hull" var all = this.globalBlocks.filter( def => def.isSending(message, receiverName) ); this.globalBlocks.forEach(def => { if (def.collectDependencies().some(dep => contains(all, dep))) { all.push(def); } }); return all; }; // 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.data, sprite.bubbleColor, null, null, isQuestion ? sprite.blockColor.sensing : sprite.bubbleBorderColor, null, isThought, true // no shadow ); this.isCachingImage = true; this.rerender(); }; // SpriteBubbleMorph contents formatting SpriteBubbleMorph.prototype.dataAsMorph = function (data) { var contents, 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.isCachingImage = true; contents.bounds.setWidth(img.width); contents.bounds.setHeight(img.height); contents.cachedImage = img; contents.version = data.version; contents.step = function () { if (this.version !== data.version) { img = data.thumbnail(new Point(40, 40), this.cachedImage); this.cachedImage = 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' ); // support exporting text / numbers directly from speech balloons: contents.userMenu = function () { var menu = new MenuMorph(this), ide = this.parentThatIsA(IDE_Morph)|| this.world().childThatIsA(IDE_Morph); if (ide.isAppMode) {return; } menu.addItem( 'export', () => ide.saveFileAs( data, 'text/plain;charset=utf-8', localize('data') ) ); return menu; }; } else if (typeof data === 'boolean') { img = sprite.booleanMorph(data).fullImage(); contents = new Morph(); contents.isCachingImage = true; contents.bounds.setWidth(img.width); contents.bounds.setHeight(img.height); contents.cachedImage = img; } else if (data instanceof Costume) { img = data.thumbnail(new Point(40, 40)); contents = new Morph(); contents.isCachingImage = true; contents.bounds.setWidth(img.width); contents.bounds.setHeight(img.height); contents.cachedImage = img; // support costumes to be dragged out of speech balloons: contents.isDraggable = true; contents.selectForEdit = function () { var cst = data.copy(), icon, prepare, ide = this.parentThatIsA(IDE_Morph)|| this.world().childThatIsA(IDE_Morph); cst.name = ide.currentSprite.newCostumeName(cst.name); icon = new CostumeIconMorph(cst); prepare = icon.prepareToBeGrabbed; icon.prepareToBeGrabbed = function (hand) { hand.grabOrigin = { origin: ide.palette, position: ide.palette.center() }; this.prepareToBeGrabbed = prepare; }; if (ide.isAppMode) {return; } icon.setCenter(this.center()); return icon; }; // support exporting costumes directly from speech balloons: contents.userMenu = function () { var menu = new MenuMorph(this), ide = this.parentThatIsA(IDE_Morph)|| this.world().childThatIsA(IDE_Morph); if (ide.isAppMode) {return; } menu.addItem( 'export', () => { if (data instanceof SVG_Costume) { // don't show SVG costumes in a new tab (shows text) ide.saveFileAs( data.contents.src, 'text/svg', data.name ); } else { // rasterized Costume ide.saveCanvasAs(data.contents, data.name); } } ); return menu; }; } else if (data instanceof Sound) { contents = new SymbolMorph('notes', 30); // support sounds to be dragged out of speech balloons: contents.isDraggable = true; contents.selectForEdit = function () { var snd = data.copy(), icon, prepare, ide = this.parentThatIsA(IDE_Morph)|| this.world().childThatIsA(IDE_Morph); snd.name = ide.currentSprite.newSoundName(snd.name); icon = new SoundIconMorph(snd); prepare = icon.prepareToBeGrabbed; icon.prepareToBeGrabbed = function (hand) { hand.grabOrigin = { origin: ide.palette, position: ide.palette.center() }; this.prepareToBeGrabbed = prepare; }; if (ide.isAppMode) {return; } icon.setCenter(this.center()); return icon; }; // support exporting sounds directly from speech balloons: contents.userMenu = function () { var menu = new MenuMorph(this), ide = this.parentThatIsA(IDE_Morph)|| this.world().childThatIsA(IDE_Morph); if (ide.isAppMode) {return; } menu.addItem( 'export', () => ide.saveAudioAs(data.audio, data.name) ); return menu; }; } else if (data instanceof HTMLCanvasElement) { img = data; contents = new Morph(); contents.isCachingImage = true; contents.bounds.setWidth(img.width); contents.bounds.setHeight(img.height); contents.cachedImage = img; } else if (data instanceof List) { if (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.isCachingImage = true; contents.bounds.setWidth(img.width); contents.bounds.setHeight(img.height); contents.cachedImage = img; // support blocks to be dragged out of speech balloons: contents.isDraggable = true; contents.selectForEdit = function () { var script = data.toBlock(), prepare = script.prepareToBeGrabbed, ide = this.parentThatIsA(IDE_Morph)|| this.world().childThatIsA(IDE_Morph); script.prepareToBeGrabbed = function (hand) { prepare.call(this, hand); hand.grabOrigin = { origin: ide.palette, position: ide.palette.center() }; this.prepareToBeGrabbed = prepare; }; if (ide.isAppMode) {return; } script.setPosition(this.position()); return script; }; } else { contents = new TextMorph( data.toString(), sprite.bubbleFontSize * this.scale, null, // fontStyle sprite.bubbleFontIsBold, false, // italic 'center' ); // support exporting text / numbers directly from speech balloons: contents.userMenu = function () { var menu = new MenuMorph(this), ide = this.parentThatIsA(IDE_Morph)|| this.world().childThatIsA(IDE_Morph); if (ide.isAppMode) {return; } menu.addItem( 'export', () => ide.saveFileAs( data.toString(), 'text/plain;charset=utf-8', localize('data') ) ); return menu; }; } 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.getImage(), 0, 0, scaledImg.width, scaledImg.height ); contents.cachedImage = scaledImg; contents.bounds = contents.bounds.scaleBy(this.scale); } return contents; }; // SpriteBubbleMorph scaling SpriteBubbleMorph.prototype.setScale = function (scale) { this.scale = scale; this.changed(); this.fixLayout(); this.rerender(); }; // SpriteBubbleMorph layout: SpriteBubbleMorph.prototype.fixLayout = function () { var sprite = SpriteMorph.prototype; // rebuild my contents if (!(this.contentsMorph instanceof ListWatcherMorph || this.contentsMorph instanceof TableFrameMorph)) { this.contentsMorph.destroy(); this.contentsMorph = this.dataAsMorph(this.data); } this.add(this.contentsMorph); // scale my settings this.edge = sprite.bubbleCorner * this.scale; this.border = sprite.bubbleBorder * this.scale; this.padding = sprite.bubbleCorner / 2 * this.scale; // adjust my dimensions this.bounds.setWidth(this.contentsMorph.width() + (this.padding ? this.padding * 2 : this.edge * 2)); this.bounds.setHeight(this.contentsMorph.height() + this.edge + this.border * 2 + this.padding * 2 + 2); // position my contents this.contentsMorph.setPosition(this.position().add( new Point( this.padding || this.edge, this.border + this.padding + 1 ) )); }; // 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, noFit, maxExtent) { this.contents = canvas ? normalizeCanvas(canvas, true) : newCanvas(null, true); if (!noFit) {this.shrinkToFit(maxExtent || 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.maxDimensions = new Point(480, 360); Costume.prototype.maxExtent = function () { // return StageMorph.prototype.dimensions; return this.maxDimensions; }; 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 < w; col += 1) { if (getAlpha(col, row)) { return row; } } } return 0; } function getRight() { for (col = w - 1; col >= 0; col -= 1) { for (row = h - 1; row >= 0; row -= 1) { if (getAlpha(col, row)) { return Math.min(col + 1, w); } } } return w; } function getBottom() { for (row = h - 1; row >= 0; row -= 1) { for (col = w - 1; 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 & stretching 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, this.name, new Point( this.width() - this.rotationCenter.x, this.rotationCenter.y ), true // no shrink-wrap ); return flipped; }; Costume.prototype.stretched = function (w, h) { w = (Math.sign(w) || 1) * Math.max(1, Math.abs(w)); h = (Math.sign(h) || 1) * Math.max(1, Math.abs(h)); var canvas = newCanvas(new Point(Math.abs(w), Math.abs(h)), true), ctx = canvas.getContext('2d'), xRatio = w / this.width(), yRatio = h / this.height(), center = this.rotationCenter.multiplyBy(new Point(xRatio, yRatio)), stretched; if (xRatio < 0) { center.x = canvas.width - Math.abs(center.x); } if (yRatio < 0) { center.y = canvas.height - Math.abs(center.y); } ctx.translate(Math.abs(Math.min(w, 0)), Math.abs(Math.min(h, 0))); ctx.scale(xRatio, yRatio); // first rasterize in case it's an SVG and in case it's on Firefox // because Firefox prevents stretching of SVGs with locked aspect ratios ctx.drawImage(this.rasterized().contents, 0, 0); stretched = new Costume( canvas, this.name, center, true ); return stretched; }; // Costume actions Costume.prototype.edit = function (aWorld, anIDE, isnew, oncancel, onsubmit) { var editor = new PaintEditorMorph(); editor.oncancel = oncancel || nop; editor.openIn( aWorld, isnew ? newCanvas(anIDE.stage.dimensions, true) : this.contents, isnew ? null : this.rotationCenter, (img, rc) => { this.contents = img; this.rotationCenter = rc; this.version = Date.now(); aWorld.changed(); if (anIDE) { if (anIDE.currentSprite instanceof SpriteMorph) { // don't shrinkwrap stage costumes this.shrinkWrap(); } anIDE.currentSprite.wearCostume(this, true); // don't shadow anIDE.hasChangedMedia = true; } (onsubmit || nop)(); }, anIDE ); }; Costume.prototype.editRotationPointOnly = function (aWorld, anIDE) { var editor = new CostumeEditorMorph(this), action, dialog, txt; editor.fixLayout(); action = () => { editor.accept(); anIDE.currentSprite.wearCostume(this, true); // don't shadow }; 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), WHITE ); dialog.labelString = 'Costume Editor'; dialog.createLabel(); dialog.setPicture(editor); dialog.addBody(txt); dialog.addButton('ok', 'Ok'); dialog.addButton('cancel', 'Cancel'); 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, null, true); } }; Costume.prototype.thumbnail = function (extentPoint, recycleMe, noPadding) { // answer a new Canvas of extentPoint dimensions containing // my thumbnail representation keeping the originial aspect ratio // a "recycleMe canvas can be passed for re-use // if "noPadding" is "true" the resulting thumbnail fits inside the // given extentPoint without padding it, i.e. one of the dimensions // is likely to be lesser than that of the extentPoint var src = this.contents, w = src ? src.width : 1, // could be an asynchronously loading SVG h = src ? src.height : 1, // could be an asynchronously loading SVG scale = Math.min( (extentPoint.x / w), (extentPoint.y / h) ), xOffset = noPadding ? 0 : Math.floor((extentPoint.x - (w * scale)) / 2), yOffset = noPadding ? 0 : Math.floor((extentPoint.y - (h * scale)) / 2), trg, ctx; trg = newCanvas( noPadding ? new Point(this.width() * scale, this.height() * scale) : extentPoint, true, // non-retina recycleMe ); if (!src || src.width + src.height === 0) {return trg; } ctx = trg.getContext('2d'); ctx.save(); ctx.scale(scale, scale); ctx.drawImage( src, Math.floor(xOffset / scale), Math.floor(yOffset / scale) ); ctx.restore(); return trg; }; // Costume pixel access Costume.prototype.rasterized = function () { return this; }; Costume.prototype.pixels = function () { var pixels = [], src, i; if (!this.contents.width || !this.contents.height) { return pixels; } src = this.contents.getContext('2d').getImageData( 0, 0, this.contents.width, this.contents.height ); for (i = 0; i < src.data.length; i += 4) { pixels.push(new List([ src.data[i], src.data[i + 1], src.data[i + 2], src.data[i + 3] ])); } return new List(pixels); }; // 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.shapes = []; 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(); cpy.shapes = this.shapes.map(shape => shape.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 render() method to scale the costume before flipping it. Stretching, OTOH, is achieved with real scaling and thus produces smooth, albeit rasterized results for vector graphics. */ // SVG_Costume thumbnail SVG_Costume.prototype.shrinkToFit = function (extentPoint) { // overridden for unrasterized SVGs nop(extentPoint); return; }; SVG_Costume.prototype.parseShapes = function () { // I try to parse my SVG as an editable collection of shapes var element = new XML_Element(), // remove 'data:image/svg+xml, ' from src contents = this.contents.src.replace(/^data:image\/.*?, */, ''); if (this.contents.src.indexOf('base64') > -1) { contents = atob(contents); } element.parseString(contents); if (this.shapes.length === 0 && element.attributes.snap) { this.shapes = element.children.map(child => window[child.attributes.prototype].fromSVG(child) ); } }; SVG_Costume.prototype.edit = function ( aWorld, anIDE, isnew, oncancel, onsubmit ) { var editor = new VectorPaintEditorMorph(), myself = this; editor.oncancel = oncancel || nop; editor.openIn( aWorld, isnew ? newCanvas(anIDE.stage.dimensions) : this.contents, isnew ? new Point(240, 180) : this.rotationCenter, (img, rc, shapes) => { myself.contents = img; myself.rotationCenter = rc; myself.shapes = shapes; myself.version = Date.now(); aWorld.changed(); if (anIDE) { if (isnew) {anIDE.currentSprite.addCostume(myself); } anIDE.currentSprite.wearCostume(myself); anIDE.hasChangedMedia = true; } (onsubmit || nop)(); }, anIDE, this.shapes || [] ); }; // SVG_Costume pixel access SVG_Costume.prototype.rasterized = function () { var canvas = newCanvas(this.extent(), true), ctx = canvas.getContext('2d'), rasterized; ctx.drawImage(this.contents, 0, 0); rasterized = new Costume( canvas, this.name, this.rotationCenter.copy() ); return rasterized; }; // 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 = ZERO; CostumeEditorMorph.uber.init.call(this); }; // CostumeEditorMorph edit ops CostumeEditorMorph.prototype.accept = function () { this.costume.rotationCenter = this.rotationCenter.copy(); this.costume.version = Date.now(); }; // CostumeEditorMorph displaying CostumeEditorMorph.prototype.fixLayout = function () { this.bounds.setExtent(this.size); }; CostumeEditorMorph.prototype.render = function (ctx) { var rp; this.margin = this.size.subtract(this.costume.extent()).divideBy(2); rp = this.rotationCenter.add(this.margin); // draw the background if (!this.cachedTexture) { this.cachedTexture = this.createTexture(); } this.renderCachedTexture(ctx); /* pattern = ctx.createPattern(this.background, 'repeat'); ctx.fillStyle = pattern; ctx.fillRect(0, 0, this.size.x, this.size.y); */ // 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.rerender(); }; CostumeEditorMorph.prototype.mouseMove = CostumeEditorMorph.prototype.mouseDownLeft; // Sound ///////////////////////////////////////////////////////////// // Sound instance creation function Sound(audio, name) { this.audio = audio; // mandatory this.name = name || "Sound"; // cached samples, don't persist this.cachedSamples = null; // internal for decoding, don't persist this.audioBuffer = null; // for decoding ops this.isDecoding = false; // internal for deserializing, don't persist this.loaded = null; // for de-serialization only } Sound.prototype.play = function () { // return an instance of an audio element which can be terminated // externally (i.e. by the stage) // Note: only to be used by the GUI, not by scripts, // because no effects like volume or panning are applied 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. // alternatively I can be used to play a frequency in hz // Note instance creation function Note(pitch) { this.pitch = pitch === 0 ? 0 : pitch || 69; this.frequency = null; // alternative for playing a non-note frequency this.setupContext(); this.oscillator = null; this.fader = null; // gain node for suppressing clicks this.ended = false; // for active sounds management } // Note shared properties Note.prototype.audioContext = null; Note.prototype.fadeIn = new Float32Array(2); Note.prototype.fadeIn[0] = [0.0]; Note.prototype.fadeIn[1] = [0.2]; Note.prototype.fadeOut = new Float32Array(2); Note.prototype.fadeOut[0] = [0.2]; Note.prototype.fadeOut[1] = [0.0]; Note.prototype.fadeTime = 0.01; // 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.getAudioContext = function () { // lazily initializes and shares the Note prototype's audio context // to be used by all other Snap! objects requiring audio, // e.g. the microphone, the sprites, etc. if (!this.audioContext) { this.setupContext(); } this.audioContext.resume(); return this.audioContext; }; // Note playing Note.prototype.play = function (type, gainNode, pannerNode) { if (!gainNode) { gainNode = this.audioContext.createGain(); } this.fader = this.audioContext.createGain(); 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.setInstrument(type); this.oscillator.frequency.value = isNil(this.frequency) ? Math.pow(2, (this.pitch - 69) / 12) * 440 : this.frequency; this.oscillator.connect(this.fader); this.fader.connect(gainNode); if (pannerNode) { gainNode.connect(pannerNode); pannerNode.connect(this.audioContext.destination); } else { gainNode.connect(this.audioContext.destination); } this.ended = false; this.fader.gain.setValueCurveAtTime( this.fadeIn, this.audioContext.currentTime, this.fadeTime ); this.oscillator.start(0); }; Note.prototype.setInstrument = function (type) { // private - make sure the oscillator node has been initialized before if (this.oscillator) { this.oscillator.type = [ 'sine', 'square', 'sawtooth', 'triangle' ][(type || 1) - 1]; } }; Note.prototype.stop = function (immediately) { // set "immediately" to true to terminate instantly // needed for widgets like the PianoKeyboard var fade = !immediately; if (immediately && this.oscillator) { this.oscillator.stop(0); return; } if (this.fader) { try { this.fader.gain.setValueCurveAtTime( this.fadeOut, this.audioContext.currentTime, this.fadeTime ); } catch (err) { fade = false; } } if (this.oscillator) { this.oscillator.stop( fade ? this.audioContext.currentTime + this.fadeTime : 0 ); this.oscillator = null; } this.ended = true; }; Note.prototype.pause = function () { // emulate a sound for active sounds mngmt this.stop(); }; // Microphone ///////////////////////////////////////////////////////// // I am a microphone and know about volume, note, pitch, as well as // signals and frequencies. // mostly meant to be a singleton of the stage // I stop when I'm not queried something for 5 seconds // to free up system resources // // modifying and metering output is currently experimental // and only fully works in Chrome. Modifiers work in Firefox, but only with // a significant lag, metering output is currently not supported by Firefox. // Safari... well, let's not talk about Safari :-) function Microphone() { // web audio components: this.audioContext = null; // shared with Note.prototype.audioContext this.sourceStream = null; this.processor = null; this.analyser = null; // parameters: this.resolution = 2; this.GOOD_ENOUGH_CORRELATION = 0.96; // modifier this.modifier = null; this.compiledModifier = null; this.compilerProcess = null; // memory alloc this.correlations = []; this.wrapper = new List([0]); this.outChannels = []; // metered values: this.volume = 0; this.signals = []; this.output = []; this.frequencies = []; this.pitch = -1; // asynch control: this.isStarted = false; this.isReady = false; // idling control: this.isAutoStop = (location.protocol !== 'file:'); this.lastTime = Date.now(); } Microphone.prototype.isOn = function () { if (this.isReady) { this.lastTime = Date.now(); return true; } this.start(); return false; }; // Microphone shared properties Microphone.prototype.binSizes = [256, 512, 1024, 2048, 4096]; // Microphone resolution Microphone.prototype.binSize = function () { return this.binSizes[this.resolution - 1]; }; Microphone.prototype.setResolution = function (num) { if (contains([1, 2, 3, 4], num)) { if (this.isReady) { this.stop(); } this.resolution = num; } }; // Microphone ops Microphone.prototype.start = function () { if (this.isStarted) {return; } this.isStarted = true; this.isReady = false; this.audioContext = Note.prototype.getAudioContext(); navigator.mediaDevices.getUserMedia( { "audio": { "mandatory": { "googEchoCancellation": "false", "googAutoGainControl": "false", "googNoiseSuppression": "false", "googHighpassFilter": "false" }, "optional": [] }, } ).then( stream => this.setupNodes(stream) ).catch(nop); }; Microphone.prototype.stop = function () { this.processor.onaudioprocess = null; this.sourceStream.getTracks().forEach(track => track.stop()); this.processor.disconnect(); this.analyser.disconnect(); this.processor = null; this.analyser = null; this.audioContext = null; this.isReady = false; this.isStarted = false; }; // Microphone initialization Microphone.prototype.setupNodes = function (stream) { this.sourceStream = stream; this.createProcessor(); this.createAnalyser(); this.analyser.connect(this.processor); this.processor.connect(this.audioContext.destination); this.audioContext.createMediaStreamSource(stream).connect(this.analyser); this.lastTime = Date.now(); }; Microphone.prototype.createAnalyser = function () { var bufLength; this.analyser = this.audioContext.createAnalyser(); this.analyser.fftSize = this.binSizes[this.resolution]; bufLength = this.analyser.frequencyBinCount; this.frequencies = new Uint8Array(bufLength); // setup pitch detection correlations: this.correlations = new Array(Math.floor(bufLength/2)); }; Microphone.prototype.createProcessor = function () { var myself = this; this.processor = this.audioContext.createScriptProcessor( this.binSizes[this.resolution - 1] ); this.processor.onaudioprocess = function (event) { myself.stepAudio(event); }; this.processor.clipping = false; this.processor.lastClip = 0; this.processor.clipLevel = 0.98; this.processor.averaging = 0.95; this.processor.clipLag = 750; }; // Microphone stepping Microphone.prototype.stepAudio = function (event) { var channels, i; if (this.isAutoStop && ((Date.now() - this.lastTime) > 5000) && !this.modifier ) { this.stop(); return; } // signals: this.signals = event.inputBuffer.getChannelData(0); // output: if (this.modifier) { channels = event.outputBuffer.numberOfChannels; if (this.outChannels.length !== channels) { this.outChannels = new Array(channels); } for (i = 0; i < channels; i += 1) { this.outChannels[i] = event.outputBuffer.getChannelData(i); } this.output = this.outChannels[0]; } else { this.output = event.outputBuffer.getChannelData(0); } // frequency bins: this.analyser.getByteFrequencyData(this.frequencies); // pitch & volume: this.pitch = this.detectPitchAndVolume( this.signals, this.audioContext.sampleRate ); // note: if (this.pitch > 0) { this.note = Math.round( 12 * (Math.log(this.pitch / 440) / Math.log(2)) ) + 69; } this.isReady = true; this.isStarted = false; }; Microphone.prototype.detectPitchAndVolume = function (buf, sampleRate) { // https://en.wikipedia.org/wiki/Autocorrelation // thanks to Chris Wilson: // https://plus.google.com/+ChrisWilson/posts/9zHsF9PCDAL // https://github.com/cwilso/PitchDetect/ var SIZE = buf.length, MAX_SAMPLES = Math.floor(SIZE/2), best_offset = -1, best_correlation = 0, rms = 0, foundGoodCorrelation = false, correlations = this.correlations, channels = this.outChannels.length, correlation, lastCorrelation, offset, shift, i, k, val, modified; for (i = 0; i < SIZE; i += 1) { val = buf[i]; if (Math.abs(val) >= this.processor.clipLevel) { this.processor.clipping = true; this.processor.lastClip = window.performance.now(); } rms += val * val; // apply modifier, if any if (this.modifier) { this.wrapper.contents[0] = val; modified = invoke( this.compiledModifier, this.wrapper, null, null, null, null, this.compilerProcess ); for (k = 0; k < channels; k += 1) { this.outChannels[k][i] = modified; } } } rms = Math.sqrt(rms/SIZE); this.volume = Math.max(rms, this.volume * this.processor.averaging); if (rms < 0.01) return this.pitch; lastCorrelation = 1; for (offset = 1; offset < MAX_SAMPLES; offset += 1) { correlation = 0; for (i = 0; i < MAX_SAMPLES; i += 1) { correlation += Math.abs((buf[i]) - (buf[i + offset])); } correlation = 1 - (correlation/MAX_SAMPLES); correlations[offset] = correlation; if ((correlation > this.GOOD_ENOUGH_CORRELATION) && (correlation > lastCorrelation) ) { foundGoodCorrelation = true; if (correlation > best_correlation) { best_correlation = correlation; best_offset = offset; } } else if (foundGoodCorrelation) { shift = (correlations[best_offset + 1] - correlations[best_offset - 1]) / correlations[best_offset]; return sampleRate / (best_offset + (8 * shift)); } lastCorrelation = correlation; } if (best_correlation > 0.01) { return sampleRate / best_offset; } return this.pitch; }; // 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, WHITE ); this.color = color || new Color(255, 140, 0); this.isBig = false; this.version = null; // only for observing sprites this.fixLayout(); }; // CellMorph accessing: CellMorph.prototype.big = function () { this.isBig = true; this.changed(); if (this.contentsMorph instanceof TextMorph) { this.contentsMorph.setFontSize( SyntaxElementMorph.prototype.fontSize * 1.5 ); } this.fixLayout(true); this.rerender(); }; CellMorph.prototype.normal = function () { this.isBig = false; this.changed(); if (this.contentsMorph instanceof TextMorph) { this.contentsMorph.setFontSize( SyntaxElementMorph.prototype.fontSize ); } this.fixLayout(true); this.rerender(); }; // 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 (justMe) { var isSameList = this.contentsMorph instanceof ListWatcherMorph && (this.contentsMorph.list === this.contents), isSameTable = this.contentsMorph instanceof TableFrameMorph && (this.contentsMorph.tableMorph.table === this.contents), listwatcher; if (justMe) {return; } this.createContents(); // adjust my dimensions this.bounds.setHeight(this.contentsMorph.height() + this.edge + this.border * 2); this.bounds.setWidth(Math.max( this.contentsMorph.width() + this.edge * 2, (this.contents instanceof Context || this.contents instanceof List ? 0 : SyntaxElementMorph.prototype.fontSize * 3.5) )); // position my contents if (!isSameList && !isSameTable) { this.contentsMorph.setCenter(this.center()); } if (this.parent) { this.parent.changed(); this.parent.fixLayout(); this.parent.rerender(); listwatcher = this.parentThatIsA(ListWatcherMorph); if (listwatcher) { listwatcher.changed(); listwatcher.fixLayout(); listwatcher.rerender(); } } }; CellMorph.prototype.createContents = function () { // re-build my contents var txt, img, myself = this, 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; } if (this.contentsMorph && !isSameList && !isSameTable) { this.contentsMorph.destroy(); this.version = null; } if (!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.isCachingImage = true; this.contentsMorph.bounds.setWidth(img.width); this.contentsMorph.bounds.setHeight(img.height); this.contentsMorph.cachedImage = 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(WHITE); } else if (typeof this.contents === 'boolean') { img = SpriteMorph.prototype.booleanMorph.call( null, this.contents ).fullImage(); this.contentsMorph = new Morph(); this.contentsMorph.isCachingImage = true; this.contentsMorph.bounds.setWidth(img.width); this.contentsMorph.bounds.setHeight(img.height); this.contentsMorph.cachedImage = img; } else if (this.contents instanceof HTMLCanvasElement) { img = this.contents; this.contentsMorph = new Morph(); this.contentsMorph.isCachingImage = true; this.contentsMorph.bounds.setWidth(img.width); this.contentsMorph.bounds.setHeight(img.height); this.contentsMorph.cachedImage = img; } else if (this.contents instanceof Context) { img = this.contents.image(); this.contentsMorph = new Morph(); this.contentsMorph.isCachingImage = true; this.contentsMorph.bounds.setWidth(img.width); this.contentsMorph.bounds.setHeight(img.height); this.contentsMorph.cachedImage = img; // support blocks to be dragged out of watchers: this.contentsMorph.isDraggable = true; this.contentsMorph.selectForEdit = function () { var script = myself.contents.toBlock(), prepare = script.prepareToBeGrabbed, ide = this.parentThatIsA(IDE_Morph) || this.world().childThatIsA(IDE_Morph); script.prepareToBeGrabbed = function (hand) { prepare.call(this, hand); hand.grabOrigin = { origin: ide.palette, position: ide.palette.center() }; this.prepareToBeGrabbed = prepare; }; if (ide.isAppMode) {return; } script.setPosition(this.position()); return script; }; } else if (this.contents instanceof Costume) { img = this.contents.thumbnail(new Point(40, 40)); this.contentsMorph = new Morph(); this.contentsMorph.isCachingImage = true; this.contentsMorph.bounds.setWidth(img.width); this.contentsMorph.bounds.setHeight(img.height); this.contentsMorph.cachedImage = img; // support costumes to be dragged out of watchers: this.contentsMorph.isDraggable = true; this.contentsMorph.selectForEdit = function () { var cst = myself.contents.copy(), icon, prepare, ide = this.parentThatIsA(IDE_Morph)|| this.world().childThatIsA(IDE_Morph); cst.name = ide.currentSprite.newCostumeName(cst.name); icon = new CostumeIconMorph(cst); prepare = icon.prepareToBeGrabbed; icon.prepareToBeGrabbed = function (hand) { hand.grabOrigin = { origin: ide.palette, position: ide.palette.center() }; this.prepareToBeGrabbed = prepare; }; if (ide.isAppMode) {return; } icon.setCenter(this.center()); return icon; }; } else if (this.contents instanceof Sound) { this.contentsMorph = new SymbolMorph('notes', 30); // support sounds to be dragged out of watchers: this.contentsMorph.isDraggable = true; this.contentsMorph.selectForEdit = function () { var snd = myself.contents.copy(), icon, prepare, ide = this.parentThatIsA(IDE_Morph)|| this.world().childThatIsA(IDE_Morph); snd.name = ide.currentSprite.newCostumeName(snd.name); icon = new SoundIconMorph(snd); prepare = icon.prepareToBeGrabbed; icon.prepareToBeGrabbed = function (hand) { hand.grabOrigin = { origin: ide.palette, position: ide.palette.center() }; this.prepareToBeGrabbed = prepare; }; if (ide.isAppMode) {return; } icon.setCenter(this.center()); return icon; }; } else if (this.contents instanceof List) { if (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(WHITE); } 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(WHITE); } this.add(this.contentsMorph); } }; // CellMorph drawing: CellMorph.prototype.update = function () { // special case for observing sprites if (!isSnapObject(this.contents) && !(this.contents instanceof Costume)) { return; } if (this.version !== this.contents.version) { this.fixLayout(); this.rerender(); this.version = this.contents.version; } }; CellMorph.prototype.render = function (ctx) { // draw my outline if ((this.edge === 0) && (this.border === 0)) { BoxMorph.uber.render.call(this, ctx); return null; } ctx.fillStyle = this.color.toString(); ctx.beginPath(); this.outlinePath( ctx, Math.max(this.edge - this.border, 0), this.border ); ctx.closePath(); ctx.fill(); if (this.border > 0 && !MorphicPreferences.isFlat) { ctx.lineWidth = this.border; ctx.strokeStyle = this.borderColor.toString(); ctx.beginPath(); this.outlinePath(ctx, this.edge, this.border / 2); ctx.closePath(); ctx.stroke(); if (useBlurredShadows) { ctx.shadowOffsetX = this.border; ctx.shadowOffsetY = this.border; ctx.shadowBlur = this.border; ctx.shadowColor = this.color.darker(80).toString(); this.drawShadow(ctx, this.edge, 0); } } }; 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); // top left: context.arc( offset, offset, radius, radians(-180), radians(-90), false ); // top right: context.lineTo(w - offset, 0); context.stroke(); }; // CellMorph editing (inside list watchers): CellMorph.prototype.layoutChanged = function () { var listWatcher = this.parentThatIsA(ListWatcherMorph); // adjust my layout this.bounds.setHeight(this.contentsMorph.height() + this.edge + this.border * 2); this.bounds.setWidth(Math.max( this.contentsMorph.width() + this.edge * 2, (this.contents instanceof Context || this.contents instanceof List ? 0 : this.height() * 2) )); // position my contents this.contentsMorph.setCenter(this.center()); this.rerender(); 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 + listWatcher.start - 1 ); } } }; 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 = ''; this.isGhosted = false; // transient, don't persist // 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, att, isInherited = false; 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](); // determine whether my getter is an inherited attribute att = { xPosition: 'x position', yPosition: 'y position', direction: 'direction', getCostumeIdx: 'costume #', getScale: 'size', getVolume: 'volume', getPan: 'balance', reportShown: 'shown?', getPenDown: 'pen down?' } [this.getter]; isInherited = att ? this.target.inheritsAttribute(att) : false; } if (newValue !== '' && !isNil(newValue)) { num = +newValue; if (typeof newValue !== 'boolean' && !isNaN(num)) { newValue = Math.round(newValue * 1000000000) / 1000000000; } } if (newValue === undefined) { // console.log('removing watcher for', this.labelText); this.destroy(); return; } if (newValue !== this.currentValue || isInherited !== this.isGhosted || (!isNil(newValue) && newValue.version && (newValue.version !== this.version) ) ) { this.changed(); this.cellMorph.contents = newValue; this.isGhosted = isInherited; if (isSnapObject(this.target)) { if (isInherited) { this.cellMorph.setColor(this.readoutColor.lighter(35)); } else { this.cellMorph.setColor(this.readoutColor); } } this.cellMorph.fixLayout(); if (!isNaN(newValue)) { this.sliderMorph.value = newValue; this.sliderMorph.fixLayout(); } this.fixLayout(); if (this.currentValue && this.currentValue.version) { this.version = this.currentValue.version; } else { this.version = Date.now(); } 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; // 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), WHITE ); 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.bounds.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.setPosition(new Point( this.labelMorph.left(), this.cellMorph.bottom() + SyntaxElementMorph.prototype.typeInPadding )); this.sliderMorph.setWidth(this.cellMorph.right() - this.labelMorph.left()); this.bounds.setHeight( 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; }; // 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); } }; // WatcherMorph dragging and dropping: WatcherMorph.prototype.rootForGrab = function () { // prevent watchers to be dragged in presentation mode var ide = this.parentThatIsA(IDE_Morph); if (ide && ide.isAppMode) { return ide; } return this; }; /* // 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, ide = this.parentThatIsA(IDE_Morph), shiftClicked = (this.world().currentKey === 16), 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, (morph) => 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(); } ); } if (ide && ide.isAppMode) { // prevent context menu in app mode return; } 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...', 'importData' ); menu.addItem( 'raw data...', () => this.importData(true), 'import without attempting to\nparse or format data'//, ); if (shiftClicked) { if (this.currentValue instanceof List && this.currentValue.canBeCSV()) { menu.addItem( 'export as CSV...', () => ide.saveFileAs( this.currentValue.asCSV(), 'text/csv;charset=utf-8', // RFC 4180 this.getter // variable name ), null, new Color(100, 0, 0) ); } if (this.currentValue instanceof List && this.currentValue.canBeJSON()) { menu.addItem( 'export as JSON...', () => ide.saveFileAs( this.currentValue.asJSON(true), // guess objects 'text/json;charset=utf-8', this.getter // variable name ), null, new Color(100, 0, 0) ); } } if (isString(this.currentValue) || !isNaN(+this.currentValue)) { if (shiftClicked) { menu.addItem( 'parse', 'parseTxt', 'try to convert\nraw data into a list', new Color(100, 0, 0) ); } menu.addItem( 'export...', () => ide.saveFileAs( this.currentValue.toString(), 'text/plain;charset=utf-8', this.getter // variable name ) ); } else if (this.currentValue instanceof Costume) { menu.addItem( 'export...', () => { if (this.currentValue instanceof SVG_Costume) { // don't show SVG costumes in a new tab (shows text) ide.saveFileAs( this.currentValue.contents.src, 'text/svg', this.currentValue.name ); } else { // rasterized Costume ide.saveCanvasAs( this.currentValue.contents, this.currentValue.name ); } } ); } else if (this.currentValue instanceof Sound) { menu.addItem( 'export...', () => ide.saveAudioAs( this.currentValue.audio, this.currentValue.name ) ); } else if (this.currentValue instanceof List && this.currentValue.canBeCSV()) { menu.addItem( 'export...', () => ide.saveFileAs( this.currentValue.asCSV(), 'text/csv;charset=utf-8', // RFC 4180 this.getter // variable name ) ); if (this.currentValue.canBeJSON()) { menu.addItem( 'blockify', () => { var world = ide.world(); this.currentValue.blockify().pickUp(world); world.hand.grabOrigin = { origin: ide.palette, position: ide.palette.center() }; } ); } } else if (this.currentValue instanceof List && this.currentValue.canBeJSON()) { menu.addItem( 'export...', () => ide.saveFileAs( this.currentValue.asJSON(true), // guessObjects 'text/json;charset=utf-8', this.getter // variable name ) ); } else if (this.currentValue instanceof Context) { vNames = this.currentValue.outerContext.variables.names(); if (vNames.length) { menu.addLine(); vNames.forEach(vName => monitor(vName)); } } } return menu; }; WatcherMorph.prototype.importData = function (raw) { // raw is a Boolean flag selecting to keep the data unparsed var inp = document.createElement('input'), ide = this.parentThatIsA(IDE_Morph), myself = this; function userImport() { function txtOnlyMsg(ftype, anyway) { ide.confirm( localize( 'Snap! can only import "text" files.\n' + 'You selected a file of type "' + ftype + '".' ) + '\n\n' + localize('Open anyway?'), 'Unable to import', anyway // callback ); } function readText(aFile) { var frd = new FileReader(), ext = aFile.name.split('.').pop().toLowerCase(); function isTextFile(aFile) { // special cases for Windows // check the file extension for text-like-ness return aFile.type.indexOf('text') !== -1 || contains(['txt', 'csv', 'xml', 'json', 'tsv'], ext); } function isType(aFile, string) { return aFile.type.indexOf(string) !== -1 || (ext === string); } frd.onloadend = function (e) { if (!raw && isType(aFile, 'csv')) { myself.target.setVar( myself.getter, Process.prototype.parseCSV(e.target.result) ); } else if (!raw && isType(aFile, 'json')) { myself.target.setVar( myself.getter, Process.prototype.parseJSON(e.target.result) ); } else { myself.target.setVar( myself.getter, e.target.result ); } }; if (raw || isTextFile(aFile)) { frd.readAsText(aFile); } else { // show a warning and an option // letting the user load the file anyway txtOnlyMsg( aFile.type, () => frd.readAsText(aFile) ); } } document.body.removeChild(inp); ide.filePicker = null; if (inp.files.length > 0) { readText(inp.files[inp.files.length - 1]); } } 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", userImport, false ); document.body.appendChild(inp); ide.filePicker = inp; inp.click(); }; WatcherMorph.prototype.parseTxt = function () { // experimental! var src = this.target.vars[this.getter].value; this.target.setVar( this.getter, src.indexOf('\[') === 0 ? Process.prototype.parseJSON(src) : Process.prototype.parseCSV(src) ); }; WatcherMorph.prototype.setStyle = function (style) { this.style = style; this.changed(); this.fixLayout(); this.rerender(); }; 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.render = function (ctx) { var gradient; if (MorphicPreferences.isFlat || (this.edge === 0 && this.border === 0)) { BoxMorph.uber.render.call(this, ctx); return; } gradient = ctx.createLinearGradient(0, 0, 0, this.height()); gradient.addColorStop(0, this.color.lighter().toString()); gradient.addColorStop(1, this.color.darker().toString()); ctx.fillStyle = gradient; ctx.beginPath(); this.outlinePath( ctx, Math.max(this.edge - this.border, 0), this.border ); ctx.closePath(); ctx.fill(); if (this.border > 0) { gradient = ctx.createLinearGradient(0, 0, 0, this.height()); gradient.addColorStop(0, this.borderColor.lighter().toString()); gradient.addColorStop(1, this.borderColor.darker().toString()); ctx.lineWidth = this.border; ctx.strokeStyle = gradient; ctx.beginPath(); this.outlinePath(ctx, this.edge, this.border / 2); ctx.closePath(); ctx.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 // 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, () => this.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 = WHITE; if (this.label) {this.add(this.label); } this.add(this.inputField); this.add(this.button); 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.bounds.setHeight( this.inputField.bottom() - this.top() + this.edge ); }; // StagePrompterMorph events: StagePrompterMorph.prototype.mouseClickLeft = function () { this.inputField.edit(); }; StagePrompterMorph.prototype.accept = function () { this.isDone = true; };