kopia lustrzana https://github.com/backface/turtlestitch
8632 wiersze
250 KiB
JavaScript
8632 wiersze
250 KiB
JavaScript
/*
|
|
|
|
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) 2016 by Jens Mönig
|
|
|
|
This file is part of Snap!.
|
|
|
|
Snap! is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of
|
|
the License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
prerequisites:
|
|
--------------
|
|
needs blocks.js, threads.js, morphic.js and widgets.js
|
|
|
|
|
|
toc
|
|
---
|
|
the following list shows the order in which all constructors are
|
|
defined. Use this list to locate code in this document:
|
|
|
|
SpriteMorph
|
|
SpriteHighlightMorph
|
|
StageMorph
|
|
Costume
|
|
SVG_Costume
|
|
CostumeEditorMorph
|
|
Sound
|
|
Note
|
|
CellMorph
|
|
WatcherMorph
|
|
StagePrompterMorph
|
|
|
|
SpeechBubbleMorph*
|
|
SpriteBubbleMorph
|
|
|
|
* defined in Morphic.js
|
|
|
|
|
|
credits
|
|
-------
|
|
Ian Reynolds contributed initial porting of primitives from Squeak and
|
|
sound handling
|
|
Achal Dave contributed research and prototyping for creating music
|
|
using the Web Audio API
|
|
Yuan Yuan and Dylan Servilla contributed graphic effects for costumes
|
|
|
|
*/
|
|
|
|
// Global stuff ////////////////////////////////////////////////////////
|
|
|
|
/*global PaintEditorMorph, ListWatcherMorph, PushButtonMorph, ToggleMorph,
|
|
DialogBoxMorph, InputFieldMorph, SpriteIconMorph, BlockMorph,
|
|
ThreadManager, VariableFrame, detect, BlockMorph, BoxMorph, Color,
|
|
CommandBlockMorph, FrameMorph, HatBlockMorph, MenuMorph, Morph, MultiArgMorph,
|
|
Point, ReporterBlockMorph, ScriptsMorph, StringMorph, SyntaxElementMorph,
|
|
TextMorph, contains, degrees, detect, newCanvas, nop, radians, Array,
|
|
CursorMorph, Date, FrameMorph, HandMorph, Math, MenuMorph, Morph,
|
|
MorphicPreferences, Object, PenMorph, Point, Rectangle, ScrollFrameMorph,
|
|
SliderMorph, String, StringMorph, TextMorph, contains, copy, degrees, detect,
|
|
document, isNaN, isString, newCanvas, nop, parseFloat, radians, window,
|
|
modules, IDE_Morph, VariableDialogMorph, HTMLCanvasElement, Context, List,
|
|
SpeechBubbleMorph, RingMorph, isNil, FileReader, TableDialogMorph,
|
|
BlockEditorMorph, BlockDialogMorph, PrototypeHatBlockMorph, localize,
|
|
TableMorph, TableFrameMorph, normalizeCanvas, BooleanSlotMorph*/
|
|
|
|
modules.objects = '2016-December-21';
|
|
|
|
var SpriteMorph;
|
|
var StageMorph;
|
|
var SpriteBubbleMorph;
|
|
var Costume;
|
|
var SVG_Costume;
|
|
var CostumeEditorMorph;
|
|
var Sound;
|
|
var Note;
|
|
var CellMorph;
|
|
var WatcherMorph;
|
|
var StagePrompterMorph;
|
|
var Note;
|
|
var SpriteHighlightMorph;
|
|
|
|
function isSnapObject(thing) {
|
|
return thing instanceof SpriteMorph || (thing instanceof StageMorph);
|
|
}
|
|
|
|
// SpriteMorph /////////////////////////////////////////////////////////
|
|
|
|
// I am a scriptable object
|
|
|
|
// SpriteMorph inherits from PenMorph:
|
|
|
|
SpriteMorph.prototype = new PenMorph();
|
|
SpriteMorph.prototype.constructor = SpriteMorph;
|
|
SpriteMorph.uber = PenMorph.prototype;
|
|
|
|
// SpriteMorph settings
|
|
|
|
SpriteMorph.prototype.categories =
|
|
[
|
|
'motion',
|
|
'control',
|
|
'looks',
|
|
'sensing',
|
|
'sound',
|
|
'operators',
|
|
'pen',
|
|
'variables',
|
|
'lists',
|
|
'other'
|
|
];
|
|
|
|
SpriteMorph.prototype.blockColor = {
|
|
motion : new Color(74, 108, 212),
|
|
looks : new Color(143, 86, 227),
|
|
sound : new Color(207, 74, 217),
|
|
pen : new Color(0, 161, 120),
|
|
control : new Color(230, 168, 34),
|
|
sensing : new Color(4, 148, 220),
|
|
operators : new Color(98, 194, 19),
|
|
variables : new Color(243, 118, 29),
|
|
lists : new Color(217, 77, 17),
|
|
other: new Color(150, 150, 150)
|
|
};
|
|
|
|
SpriteMorph.prototype.paletteColor = new Color(55, 55, 55);
|
|
SpriteMorph.prototype.paletteTextColor = new Color(230, 230, 230);
|
|
SpriteMorph.prototype.sliderColor
|
|
= SpriteMorph.prototype.paletteColor.lighter(30);
|
|
SpriteMorph.prototype.isCachingPrimitives = true;
|
|
|
|
SpriteMorph.prototype.enableNesting = true;
|
|
SpriteMorph.prototype.enableFirstClass = true;
|
|
SpriteMorph.prototype.useFlatLineEnds = false;
|
|
SpriteMorph.prototype.highlightColor = new Color(250, 200, 130);
|
|
SpriteMorph.prototype.highlightBorder = 8;
|
|
|
|
SpriteMorph.prototype.bubbleColor = new Color(255, 255, 255);
|
|
SpriteMorph.prototype.bubbleFontSize = 14;
|
|
SpriteMorph.prototype.bubbleFontIsBold = true;
|
|
SpriteMorph.prototype.bubbleCorner = 10;
|
|
SpriteMorph.prototype.bubbleBorder = 3;
|
|
SpriteMorph.prototype.bubbleBorderColor = new Color(190, 190, 190);
|
|
SpriteMorph.prototype.bubbleMaxTextWidth = 130;
|
|
|
|
SpriteMorph.prototype.initBlocks = function () {
|
|
SpriteMorph.prototype.blocks = {
|
|
|
|
// Motion
|
|
forward: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'move %n steps',
|
|
defaults: [10]
|
|
},
|
|
turn: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'turn %clockwise %n degrees',
|
|
defaults: [15]
|
|
},
|
|
turnLeft: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'turn %counterclockwise %n degrees',
|
|
defaults: [15]
|
|
},
|
|
setHeading: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'point in direction %dir'
|
|
},
|
|
doFaceTowards: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'point towards %dst'
|
|
},
|
|
gotoXY: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'go to x: %n y: %n',
|
|
defaults: [0, 0]
|
|
},
|
|
doGotoObject: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'go to %dst'
|
|
},
|
|
doGlide: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'glide %n secs to x: %n y: %n',
|
|
defaults: [1, 0, 0]
|
|
},
|
|
changeXPosition: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'change x by %n',
|
|
defaults: [10]
|
|
},
|
|
setXPosition: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'set x to %n',
|
|
defaults: [0]
|
|
},
|
|
changeYPosition: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'change y by %n',
|
|
defaults: [10]
|
|
},
|
|
setYPosition: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'set y to %n',
|
|
defaults: [0]
|
|
},
|
|
bounceOffEdge: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'if on edge, bounce'
|
|
},
|
|
xPosition: {
|
|
only: SpriteMorph,
|
|
type: 'reporter',
|
|
category: 'motion',
|
|
spec: 'x position'
|
|
},
|
|
yPosition: {
|
|
only: SpriteMorph,
|
|
type: 'reporter',
|
|
category: 'motion',
|
|
spec: 'y position'
|
|
},
|
|
direction: {
|
|
only: SpriteMorph,
|
|
type: 'reporter',
|
|
category: 'motion',
|
|
spec: 'direction'
|
|
},
|
|
|
|
// Looks
|
|
doSwitchToCostume: {
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'switch to costume %cst'
|
|
},
|
|
doWearNextCostume: {
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'next costume'
|
|
},
|
|
getCostumeIdx: {
|
|
type: 'reporter',
|
|
category: 'looks',
|
|
spec: 'costume #'
|
|
},
|
|
doSayFor: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'say %s for %n secs',
|
|
defaults: [localize('Hello!'), 2]
|
|
},
|
|
bubble: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'say %s',
|
|
defaults: [localize('Hello!')]
|
|
},
|
|
doThinkFor: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'think %s for %n secs',
|
|
defaults: [localize('Hmm...'), 2]
|
|
},
|
|
doThink: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'think %s',
|
|
defaults: [localize('Hmm...')]
|
|
},
|
|
changeEffect: {
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'change %eff effect by %n',
|
|
defaults: [null, 25]
|
|
},
|
|
setEffect: {
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'set %eff effect to %n',
|
|
defaults: [null, 0]
|
|
},
|
|
clearEffects: {
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'clear graphic effects'
|
|
},
|
|
changeScale: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'change size by %n',
|
|
defaults: [10]
|
|
},
|
|
setScale: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'set size to %n %',
|
|
defaults: [100]
|
|
},
|
|
getScale: {
|
|
only: SpriteMorph,
|
|
type: 'reporter',
|
|
category: 'looks',
|
|
spec: 'size'
|
|
},
|
|
show: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'show'
|
|
},
|
|
hide: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'hide'
|
|
},
|
|
comeToFront: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'go to front'
|
|
},
|
|
goBack: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'go back %n layers',
|
|
defaults: [1]
|
|
},
|
|
doScreenshot: {
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'save %imgsource as costume named %s',
|
|
defaults: [['pen trails'], localize('screenshot')]
|
|
},
|
|
|
|
// Looks - Debugging primitives for development mode
|
|
reportCostumes: {
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'looks',
|
|
spec: 'wardrobe'
|
|
},
|
|
|
|
alert: {
|
|
dev: true,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'alert %mult%s'
|
|
},
|
|
log: {
|
|
dev: true,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'console log %mult%s'
|
|
},
|
|
|
|
// Sound
|
|
playSound: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'play sound %snd'
|
|
},
|
|
doPlaySoundUntilDone: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'play sound %snd until done'
|
|
},
|
|
doStopAllSounds: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'stop all sounds'
|
|
},
|
|
doRest: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'rest for %n beats',
|
|
defaults: [0.2]
|
|
},
|
|
doPlayNote: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'play note %n for %n beats',
|
|
defaults: [60, 0.5]
|
|
},
|
|
doChangeTempo: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'change tempo by %n',
|
|
defaults: [20]
|
|
},
|
|
doSetTempo: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'set tempo to %n bpm',
|
|
defaults: [60]
|
|
},
|
|
getTempo: {
|
|
type: 'reporter',
|
|
category: 'sound',
|
|
spec: 'tempo'
|
|
},
|
|
|
|
// Sound - Debugging primitives for development mode
|
|
reportSounds: {
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'sound',
|
|
spec: 'jukebox'
|
|
},
|
|
|
|
// Pen
|
|
clear: {
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'clear'
|
|
},
|
|
down: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'pen down'
|
|
},
|
|
up: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'pen up'
|
|
},
|
|
setColor: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'set pen color to %clr'
|
|
},
|
|
changeHue: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'change pen color by %n',
|
|
defaults: [10]
|
|
},
|
|
setHue: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'set pen color to %n',
|
|
defaults: [0]
|
|
},
|
|
changeBrightness: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'change pen shade by %n',
|
|
defaults: [10]
|
|
},
|
|
setBrightness: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'set pen shade to %n',
|
|
defaults: [100]
|
|
},
|
|
changeSize: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'change pen size by %n',
|
|
defaults: [1]
|
|
},
|
|
setSize: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'set pen size to %n',
|
|
defaults: [1]
|
|
},
|
|
doStamp: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'stamp'
|
|
},
|
|
floodFill: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'fill'
|
|
},
|
|
|
|
// Control
|
|
receiveGo: {
|
|
type: 'hat',
|
|
category: 'control',
|
|
spec: 'when %greenflag clicked'
|
|
},
|
|
receiveKey: {
|
|
type: 'hat',
|
|
category: 'control',
|
|
spec: 'when %keyHat key pressed'
|
|
},
|
|
|
|
/* migrated to a newer block version:
|
|
|
|
receiveClick: {
|
|
type: 'hat',
|
|
category: 'control',
|
|
spec: 'when I am clicked'
|
|
},
|
|
*/
|
|
|
|
receiveInteraction: {
|
|
type: 'hat',
|
|
category: 'control',
|
|
spec: 'when I am %interaction',
|
|
defaults: ['clicked']
|
|
},
|
|
receiveMessage: {
|
|
type: 'hat',
|
|
category: 'control',
|
|
spec: 'when I receive %msgHat'
|
|
},
|
|
receiveCondition: {
|
|
type: 'hat',
|
|
category: 'control',
|
|
spec: 'when %b'
|
|
},
|
|
doBroadcast: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'broadcast %msg'
|
|
},
|
|
doBroadcastAndWait: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'broadcast %msg and wait'
|
|
},
|
|
getLastMessage: {
|
|
type: 'reporter',
|
|
category: 'control',
|
|
spec: 'message'
|
|
},
|
|
doWait: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'wait %n secs',
|
|
defaults: [1]
|
|
},
|
|
doWaitUntil: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'wait until %b'
|
|
},
|
|
doForever: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'forever %c'
|
|
},
|
|
doRepeat: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'repeat %n %c',
|
|
defaults: [10]
|
|
},
|
|
doUntil: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'repeat until %b %c'
|
|
},
|
|
doIf: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'if %b %c'
|
|
},
|
|
doIfElse: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'if %b %c else %c'
|
|
},
|
|
|
|
/* migrated to a newer block version:
|
|
|
|
doStop: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'stop script'
|
|
},
|
|
doStopAll: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'stop all %stop'
|
|
},
|
|
*/
|
|
|
|
doStopThis: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'stop %stopChoices'
|
|
},
|
|
doStopOthers: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'stop %stopOthersChoices'
|
|
},
|
|
doRun: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'run %cmdRing %inputs'
|
|
},
|
|
fork: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'launch %cmdRing %inputs'
|
|
},
|
|
evaluate: {
|
|
type: 'reporter',
|
|
category: 'control',
|
|
spec: 'call %repRing %inputs'
|
|
},
|
|
/*
|
|
doRunWithInputList: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'run %cmd with input list %l'
|
|
},
|
|
|
|
forkWithInputList: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'launch %cmd with input list %l'
|
|
},
|
|
|
|
evaluateWithInputList: {
|
|
type: 'reporter',
|
|
category: 'control',
|
|
spec: 'call %r with input list %l'
|
|
},
|
|
*/
|
|
doReport: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'report %s'
|
|
},
|
|
/*
|
|
doStopBlock: { // migrated to a newer block version
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'stop block'
|
|
},
|
|
*/
|
|
doCallCC: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'run %cmdRing w/continuation'
|
|
},
|
|
reportCallCC: {
|
|
type: 'reporter',
|
|
category: 'control',
|
|
spec: 'call %cmdRing w/continuation'
|
|
},
|
|
doWarp: {
|
|
type: 'command',
|
|
category: 'other',
|
|
spec: 'warp %c'
|
|
},
|
|
|
|
// Cloning - very experimental
|
|
receiveOnClone: {
|
|
type: 'hat',
|
|
category: 'control',
|
|
spec: 'when I start as a clone'
|
|
},
|
|
createClone: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'create a clone of %cln'
|
|
},
|
|
removeClone: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'delete this clone'
|
|
},
|
|
|
|
// Debugging - pausing
|
|
|
|
doPauseAll: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'pause all %pause'
|
|
},
|
|
|
|
// Sensing
|
|
|
|
reportTouchingObject: {
|
|
only: SpriteMorph,
|
|
type: 'predicate',
|
|
category: 'sensing',
|
|
spec: 'touching %col ?'
|
|
},
|
|
reportTouchingColor: {
|
|
only: SpriteMorph,
|
|
type: 'predicate',
|
|
category: 'sensing',
|
|
spec: 'touching %clr ?'
|
|
},
|
|
reportColorIsTouchingColor: {
|
|
only: SpriteMorph,
|
|
type: 'predicate',
|
|
category: 'sensing',
|
|
spec: 'color %clr is touching %clr ?'
|
|
},
|
|
colorFiltered: {
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'filtered for %clr'
|
|
},
|
|
reportStackSize: {
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'stack size'
|
|
},
|
|
reportFrameCount: {
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'frames'
|
|
},
|
|
reportThreadCount: {
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'processes'
|
|
},
|
|
doAsk: {
|
|
type: 'command',
|
|
category: 'sensing',
|
|
spec: 'ask %s and wait',
|
|
defaults: [localize('what\'s your name?')]
|
|
},
|
|
reportLastAnswer: { // retained for legacy compatibility
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'answer'
|
|
},
|
|
getLastAnswer: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'answer'
|
|
},
|
|
reportMouseX: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'mouse x'
|
|
},
|
|
reportMouseY: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'mouse y'
|
|
},
|
|
reportMouseDown: {
|
|
type: 'predicate',
|
|
category: 'sensing',
|
|
spec: 'mouse down?'
|
|
},
|
|
reportKeyPressed: {
|
|
type: 'predicate',
|
|
category: 'sensing',
|
|
spec: 'key %key pressed?'
|
|
},
|
|
reportDistanceTo: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'distance to %dst'
|
|
},
|
|
doResetTimer: {
|
|
type: 'command',
|
|
category: 'sensing',
|
|
spec: 'reset timer'
|
|
},
|
|
reportTimer: { // retained for legacy compatibility
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'timer'
|
|
},
|
|
getTimer: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'timer'
|
|
},
|
|
reportAttributeOf: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: '%att of %spr',
|
|
defaults: [['costume #']]
|
|
},
|
|
reportURL: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'http:// %s',
|
|
defaults: ['snap.berkeley.edu']
|
|
},
|
|
reportIsFastTracking: {
|
|
type: 'predicate',
|
|
category: 'sensing',
|
|
spec: 'turbo mode?'
|
|
},
|
|
doSetFastTracking: {
|
|
type: 'command',
|
|
category: 'sensing',
|
|
spec: 'set turbo mode to %b'
|
|
},
|
|
reportDate: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'current %dates'
|
|
},
|
|
reportGet: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'my %get',
|
|
defaults: [['neighbors']]
|
|
},
|
|
|
|
// Operators
|
|
reifyScript: {
|
|
type: 'ring',
|
|
category: 'other',
|
|
spec: '%rc %ringparms',
|
|
alias: 'command ring lambda'
|
|
},
|
|
reifyReporter: {
|
|
type: 'ring',
|
|
category: 'other',
|
|
spec: '%rr %ringparms',
|
|
alias: 'reporter ring lambda'
|
|
},
|
|
reifyPredicate: {
|
|
type: 'ring',
|
|
category: 'other',
|
|
spec: '%rp %ringparms',
|
|
alias: 'predicate ring lambda'
|
|
},
|
|
reportSum: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: '%n + %n'
|
|
},
|
|
reportDifference: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: '%n \u2212 %n',
|
|
alias: '-'
|
|
},
|
|
reportProduct: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: '%n \u00D7 %n',
|
|
alias: '*'
|
|
},
|
|
reportQuotient: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: '%n / %n' // '%n \u00F7 %n'
|
|
},
|
|
reportRound: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'round %n'
|
|
},
|
|
reportMonadic: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: '%fun of %n',
|
|
defaults: [null, 10]
|
|
},
|
|
reportModulus: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: '%n mod %n'
|
|
},
|
|
reportRandom: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'pick random %n to %n',
|
|
defaults: [1, 10]
|
|
},
|
|
reportLessThan: {
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: '%s < %s'
|
|
},
|
|
reportEquals: {
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: '%s = %s'
|
|
},
|
|
reportGreaterThan: {
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: '%s > %s'
|
|
},
|
|
reportAnd: {
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: '%b and %b'
|
|
},
|
|
reportOr: {
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: '%b or %b'
|
|
},
|
|
reportNot: {
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: 'not %b'
|
|
},
|
|
reportBoolean: {
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: '%bool',
|
|
alias: 'true boolean'
|
|
},
|
|
reportFalse: { // special case for keyboard entry and search
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: '%bool',
|
|
defaults: [false],
|
|
alias: 'false boolean'
|
|
},
|
|
reportJoinWords: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'join %words',
|
|
defaults: [localize('hello') + ' ', localize('world')]
|
|
},
|
|
reportLetter: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'letter %n of %s',
|
|
defaults: [1, localize('world')]
|
|
},
|
|
reportStringSize: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'length of %s',
|
|
defaults: [localize('world')]
|
|
},
|
|
reportUnicode: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'unicode of %s',
|
|
defaults: ['a']
|
|
},
|
|
reportUnicodeAsLetter: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'unicode %n as letter',
|
|
defaults: [65]
|
|
},
|
|
reportIsA: {
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: 'is %s a %typ ?',
|
|
defaults: [5]
|
|
},
|
|
reportIsIdentical: {
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: 'is %s identical to %s ?'
|
|
},
|
|
reportTextSplit: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'split %s by %delim',
|
|
defaults: [localize('hello') + ' ' + localize('world'), " "]
|
|
},
|
|
reportJSFunction: { // experimental
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'JavaScript function ( %mult%s ) { %code }'
|
|
},
|
|
reportTypeOf: { // only in dev mode for debugging
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'type of %s',
|
|
defaults: [5]
|
|
},
|
|
reportTextFunction: { // only in dev mode - experimental
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: '%txtfun of %s',
|
|
defaults: [null, "Abelson & Sussman"]
|
|
},
|
|
|
|
/*
|
|
reportScript: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'the script %parms %c'
|
|
},
|
|
reify: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'the %f block %parms'
|
|
},
|
|
*/
|
|
|
|
// Variables
|
|
doSetVar: {
|
|
type: 'command',
|
|
category: 'variables',
|
|
spec: 'set %var to %s',
|
|
defaults: [null, 0]
|
|
},
|
|
doChangeVar: {
|
|
type: 'command',
|
|
category: 'variables',
|
|
spec: 'change %var by %n',
|
|
defaults: [null, 1]
|
|
},
|
|
doShowVar: {
|
|
type: 'command',
|
|
category: 'variables',
|
|
spec: 'show variable %var'
|
|
},
|
|
doHideVar: {
|
|
type: 'command',
|
|
category: 'variables',
|
|
spec: 'hide variable %var'
|
|
},
|
|
doDeclareVariables: {
|
|
type: 'command',
|
|
category: 'other',
|
|
spec: 'script variables %scriptVars'
|
|
},
|
|
|
|
// inheritance - experimental
|
|
doDeleteAttr: {
|
|
type: 'command',
|
|
category: 'variables',
|
|
spec: 'delete %shd'
|
|
},
|
|
|
|
// Lists
|
|
reportNewList: {
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: 'list %exp'
|
|
},
|
|
reportCONS: {
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: '%s in front of %l'
|
|
},
|
|
reportListItem: {
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: 'item %idx of %l',
|
|
defaults: [1]
|
|
},
|
|
reportCDR: {
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: 'all but first of %l'
|
|
},
|
|
reportListLength: {
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: 'length of %l'
|
|
},
|
|
reportListContainsItem: {
|
|
type: 'predicate',
|
|
category: 'lists',
|
|
spec: '%l contains %s',
|
|
defaults: [null, localize('thing')]
|
|
},
|
|
doAddToList: {
|
|
type: 'command',
|
|
category: 'lists',
|
|
spec: 'add %s to %l',
|
|
defaults: [localize('thing')]
|
|
},
|
|
doDeleteFromList: {
|
|
type: 'command',
|
|
category: 'lists',
|
|
spec: 'delete %ida of %l',
|
|
defaults: [1]
|
|
},
|
|
doInsertInList: {
|
|
type: 'command',
|
|
category: 'lists',
|
|
spec: 'insert %s at %idx of %l',
|
|
defaults: [localize('thing'), 1]
|
|
},
|
|
doReplaceInList: {
|
|
type: 'command',
|
|
category: 'lists',
|
|
spec: 'replace item %idx of %l with %s',
|
|
defaults: [1, null, localize('thing')]
|
|
},
|
|
|
|
// MAP - experimental
|
|
reportMap: {
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: 'map %repRing over %l'
|
|
},
|
|
doForEach: {
|
|
dev: true,
|
|
type: 'command',
|
|
category: 'lists',
|
|
spec: 'for %upvar in %l %cl',
|
|
defaults: [localize('each item')]
|
|
},
|
|
|
|
// Tables - experimental
|
|
|
|
doShowTable: {
|
|
dev: true,
|
|
type: 'command',
|
|
category: 'lists',
|
|
spec: 'show table %l'
|
|
},
|
|
|
|
// Code mapping - experimental
|
|
doMapCodeOrHeader: { // experimental
|
|
type: 'command',
|
|
category: 'other',
|
|
spec: 'map %cmdRing to %codeKind %code'
|
|
},
|
|
doMapStringCode: { // experimental
|
|
type: 'command',
|
|
category: 'other',
|
|
spec: 'map String to code %code',
|
|
defaults: ['<#1>']
|
|
},
|
|
doMapListCode: { // experimental
|
|
type: 'command',
|
|
category: 'other',
|
|
spec: 'map %codeListPart of %codeListKind to code %code'
|
|
},
|
|
reportMappedCode: { // experimental
|
|
type: 'reporter',
|
|
category: 'other',
|
|
spec: 'code of %cmdRing'
|
|
}
|
|
};
|
|
};
|
|
|
|
SpriteMorph.prototype.initBlocks();
|
|
|
|
SpriteMorph.prototype.initBlockMigrations = function () {
|
|
SpriteMorph.prototype.blockMigrations = {
|
|
doStopAll: {
|
|
selector: 'doStopThis',
|
|
inputs: [['all']]
|
|
},
|
|
doStop: {
|
|
selector: 'doStopThis',
|
|
inputs: [['this script']]
|
|
},
|
|
doStopBlock: {
|
|
selector: 'doStopThis',
|
|
inputs: [['this block']]
|
|
},
|
|
receiveClick: {
|
|
selector: 'receiveInteraction',
|
|
inputs: [['clicked']]
|
|
},
|
|
reportTrue: {
|
|
selector: 'reportBoolean',
|
|
inputs: [true]
|
|
},
|
|
reportFalse: {
|
|
selector: 'reportBoolean',
|
|
inputs: [false]
|
|
}
|
|
};
|
|
};
|
|
|
|
SpriteMorph.prototype.initBlockMigrations();
|
|
|
|
SpriteMorph.prototype.blockAlternatives = {
|
|
// motion:
|
|
turn: ['turnLeft'],
|
|
turnLeft: ['turn'],
|
|
changeXPosition: ['changeYPosition', 'setXPosition', 'setYPosition'],
|
|
setXPosition: ['setYPosition', 'changeXPosition', 'changeYPosition'],
|
|
changeYPosition: ['changeXPosition', 'setYPosition', 'setXPosition'],
|
|
setYPosition: ['setXPosition', 'changeYPosition', 'changeXPosition'],
|
|
xPosition: ['yPosition'],
|
|
yPosition: ['xPosition'],
|
|
|
|
// looks:
|
|
doSayFor: ['doThinkFor', 'bubble', 'doThink', 'doAsk'],
|
|
doThinkFor: ['doSayFor', 'doThink', 'bubble', 'doAsk'],
|
|
bubble: ['doThink', 'doAsk', 'doSayFor', 'doThinkFor'],
|
|
doThink: ['bubble', 'doAsk', 'doSayFor', 'doThinkFor'],
|
|
show: ['hide'],
|
|
hide: ['show'],
|
|
changeEffect: ['setEffect'],
|
|
setEffect: ['changeEffect'],
|
|
changeScale: ['setScale'],
|
|
setScale: ['changeScale'],
|
|
|
|
// sound:
|
|
playSound: ['doPlaySoundUntilDone'],
|
|
doPlaySoundUntilDone: ['playSound'],
|
|
doChangeTempo: ['doSetTempo'],
|
|
doSetTempo: ['doChangeTempo'],
|
|
|
|
// pen:
|
|
clear: ['down', 'up', 'doStamp'],
|
|
down: ['up', 'clear', 'doStamp'],
|
|
up: ['down', 'clear', 'doStamp'],
|
|
doStamp: ['clear', 'down', 'up'],
|
|
changeHue: ['setHue', 'changeBrightness', 'setBrightness'],
|
|
setHue: ['changeHue', 'changeBrightness', 'setBrightness'],
|
|
changeBrightness: ['setBrightness', 'setHue', 'changeHue'],
|
|
setBrightness: ['changeBrightness', 'setHue', 'changeHue'],
|
|
changeSize: ['setSize'],
|
|
setSize: ['changeSize'],
|
|
|
|
// control:
|
|
doBroadcast: ['doBroadcastAndWait'],
|
|
doBroadcastAndWait: ['doBroadcast'],
|
|
doIf: ['doIfElse', 'doUntil'],
|
|
doIfElse: ['doIf', 'doUntil'],
|
|
doRepeat: ['doUntil'],
|
|
doUntil: ['doRepeat', 'doIf'],
|
|
|
|
// sensing:
|
|
doAsk: ['bubble', 'doThink', 'doSayFor', 'doThinkFor'],
|
|
getLastAnswer: ['getTimer'],
|
|
getTimer: ['getLastAnswer'],
|
|
reportMouseX: ['reportMouseY'],
|
|
reportMouseY: ['reportMouseX'],
|
|
|
|
// operators:
|
|
reportSum: ['reportDifference', 'reportProduct', 'reportQuotient'],
|
|
reportDifference: ['reportSum', 'reportProduct', 'reportQuotient'],
|
|
reportProduct: ['reportDifference', 'reportSum', 'reportQuotient'],
|
|
reportQuotient: ['reportDifference', 'reportProduct', 'reportSum'],
|
|
reportLessThan: ['reportEquals', 'reportGreaterThan'],
|
|
reportEquals: ['reportLessThan', 'reportGreaterThan'],
|
|
reportGreaterThan: ['reportEquals', 'reportLessThan'],
|
|
reportAnd: ['reportOr'],
|
|
reportOr: ['reportAnd'],
|
|
|
|
// variables
|
|
doSetVar: ['doChangeVar'],
|
|
doChangeVar: ['doSetVar'],
|
|
doShowVar: ['doHideVar'],
|
|
doHideVar: ['doShowVar']
|
|
};
|
|
|
|
// SpriteMorph instance creation
|
|
|
|
function SpriteMorph(globals) {
|
|
this.init(globals);
|
|
}
|
|
|
|
SpriteMorph.prototype.init = function (globals) {
|
|
this.name = localize('Sprite');
|
|
this.variables = new VariableFrame(globals || null, this);
|
|
this.scripts = new ScriptsMorph(this);
|
|
this.customBlocks = [];
|
|
this.costumes = new List();
|
|
this.costume = null;
|
|
this.sounds = new List();
|
|
this.normalExtent = new Point(60, 60); // only for costume-less situation
|
|
this.scale = 1;
|
|
this.rotationStyle = 1; // 1 = full, 2 = left/right, 0 = off
|
|
this.version = Date.now(); // for observer optimization
|
|
this.isClone = false; // indicate a "temporary" Scratch-style clone
|
|
this.isCorpse = false; // indicate whether a sprite/clone has been deleted
|
|
this.cloneOriginName = '';
|
|
|
|
// sprite nesting properties
|
|
this.parts = []; // not serialized, only anchor (name)
|
|
this.anchor = null;
|
|
this.nestingScale = 1;
|
|
this.rotatesWithAnchor = true;
|
|
this.layers = null; // cache for dragging nested sprites, don't serialize
|
|
|
|
this.blocksCache = {}; // not to be serialized (!)
|
|
this.paletteCache = {}; // not to be serialized (!)
|
|
this.rotationOffset = new Point(); // not to be serialized (!)
|
|
this.idx = 0; // not to be serialized (!) - used for de-serialization
|
|
this.wasWarped = false; // not to be serialized, used for fast-tracking
|
|
|
|
this.graphicsValues = {
|
|
'color': 0,
|
|
'fisheye': 0,
|
|
'whirl': 0,
|
|
'pixelate': 0,
|
|
'mosaic': 0,
|
|
'duplicate': 0,
|
|
'negative': 0,
|
|
'comic': 0,
|
|
'confetti': 0,
|
|
'saturation': 0,
|
|
'brightness': 0
|
|
};
|
|
|
|
// sprite inheritance
|
|
this.exemplar = null;
|
|
|
|
SpriteMorph.uber.init.call(this);
|
|
|
|
this.isDraggable = true;
|
|
this.isDown = false;
|
|
this.heading = 90;
|
|
this.changed();
|
|
this.drawNew();
|
|
this.changed();
|
|
};
|
|
|
|
// SpriteMorph duplicating (fullCopy)
|
|
|
|
SpriteMorph.prototype.fullCopy = function (forClone) {
|
|
var c = SpriteMorph.uber.fullCopy.call(this),
|
|
myself = this,
|
|
arr = [],
|
|
cb, effect;
|
|
|
|
c.stopTalking();
|
|
c.color = this.color.copy();
|
|
c.blocksCache = {};
|
|
c.paletteCache = {};
|
|
c.scripts = this.scripts.fullCopy(forClone);
|
|
c.scripts.owner = c;
|
|
c.variables = this.variables.copy();
|
|
c.variables.owner = c;
|
|
c.customBlocks = [];
|
|
if (!forClone) {
|
|
this.customBlocks.forEach(function (def) {
|
|
cb = def.copyAndBindTo(c);
|
|
c.customBlocks.push(cb);
|
|
c.allBlockInstances(def).forEach(function (block) {
|
|
block.definition = cb;
|
|
});
|
|
});
|
|
}
|
|
this.costumes.asArray().forEach(function (costume) {
|
|
var cst = forClone ? costume : costume.copy();
|
|
arr.push(cst);
|
|
if (costume === myself.costume) {
|
|
c.costume = cst;
|
|
}
|
|
});
|
|
c.costumes = new List(arr);
|
|
arr = [];
|
|
this.sounds.asArray().forEach(function (sound) {
|
|
var snd = forClone ? sound : sound.copy();
|
|
arr.push(snd);
|
|
});
|
|
c.sounds = new List(arr);
|
|
arr = [];
|
|
c.nestingScale = 1;
|
|
c.rotatesWithAnchor = true;
|
|
c.anchor = null;
|
|
c.parts = [];
|
|
this.parts.forEach(function (part) {
|
|
var dp = part.fullCopy(forClone);
|
|
dp.nestingScale = part.nestingScale;
|
|
dp.rotatesWithAnchor = part.rotatesWithAnchor;
|
|
c.attachPart(dp);
|
|
});
|
|
c.graphicsValues = {};
|
|
for (effect in this.graphicsValues) {
|
|
if (this.graphicsValues.hasOwnProperty(effect)) {
|
|
c.graphicsValues[effect] = this.graphicsValues[effect];
|
|
}
|
|
}
|
|
return c;
|
|
};
|
|
|
|
SpriteMorph.prototype.appearIn = function (ide) {
|
|
// private - used in IDE_Morph.duplicateSprite()
|
|
if (!this.isClone) {
|
|
this.name = ide.newSpriteName(this.name);
|
|
ide.corral.addSprite(this);
|
|
ide.sprites.add(this);
|
|
}
|
|
ide.stage.add(this);
|
|
this.parts.forEach(function (part) {
|
|
part.appearIn(ide);
|
|
});
|
|
};
|
|
|
|
// SpriteMorph versioning
|
|
|
|
SpriteMorph.prototype.setName = function (string) {
|
|
this.name = string || this.name;
|
|
this.version = Date.now();
|
|
};
|
|
|
|
// SpriteMorph rendering
|
|
|
|
SpriteMorph.prototype.drawNew = function () {
|
|
var myself = this,
|
|
currentCenter,
|
|
facing, // actual costume heading based on my rotation style
|
|
isFlipped,
|
|
isLoadingCostume,
|
|
cst,
|
|
pic, // (flipped copy of) actual costume based on my rotation style
|
|
stageScale,
|
|
newX,
|
|
corners = [],
|
|
origin,
|
|
shift,
|
|
corner,
|
|
costumeExtent,
|
|
ctx,
|
|
handle;
|
|
|
|
if (this.isWarped) {
|
|
this.wantsRedraw = true;
|
|
return;
|
|
}
|
|
currentCenter = this.center();
|
|
isLoadingCostume = this.costume &&
|
|
typeof this.costume.loaded === 'function';
|
|
stageScale = this.parent instanceof StageMorph ?
|
|
this.parent.scale : 1;
|
|
facing = this.rotationStyle ? this.heading : 90;
|
|
if (this.rotationStyle === 2) {
|
|
facing = 90;
|
|
if ((this.heading > 180 && (this.heading < 360))
|
|
|| (this.heading < 0 && (this.heading > -180))) {
|
|
isFlipped = true;
|
|
}
|
|
}
|
|
if (this.costume && !isLoadingCostume) {
|
|
pic = isFlipped ? this.costume.flipped() : this.costume;
|
|
|
|
// determine the rotated costume's bounding box
|
|
corners = pic.bounds().corners().map(function (point) {
|
|
return point.rotateBy(
|
|
radians(facing - 90),
|
|
myself.costume.center()
|
|
);
|
|
});
|
|
origin = corners[0];
|
|
corner = corners[0];
|
|
corners.forEach(function (point) {
|
|
origin = origin.min(point);
|
|
corner = corner.max(point);
|
|
});
|
|
costumeExtent = origin.corner(corner)
|
|
.extent().multiplyBy(this.scale * stageScale);
|
|
|
|
// determine the new relative origin of the rotated shape
|
|
shift = new Point(0, 0).rotateBy(
|
|
radians(-(facing - 90)),
|
|
pic.center()
|
|
).subtract(origin);
|
|
|
|
// create a new, adequately dimensioned canvas
|
|
// and draw the costume on it
|
|
this.image = newCanvas(costumeExtent, true);
|
|
this.silentSetExtent(costumeExtent);
|
|
ctx = this.image.getContext('2d');
|
|
ctx.scale(this.scale * stageScale, this.scale * stageScale);
|
|
ctx.translate(shift.x, shift.y);
|
|
ctx.rotate(radians(facing - 90));
|
|
ctx.drawImage(pic.contents, 0, 0);
|
|
|
|
// apply graphics effects to image
|
|
this.image = this.applyGraphicsEffects(this.image);
|
|
|
|
// adjust my position to the rotation
|
|
this.setCenter(currentCenter, true); // just me
|
|
|
|
// determine my rotation offset
|
|
this.rotationOffset = shift
|
|
.translateBy(pic.rotationCenter)
|
|
.rotateBy(radians(-(facing - 90)), shift)
|
|
.scaleBy(this.scale * stageScale);
|
|
} else {
|
|
facing = isFlipped ? -90 : facing;
|
|
newX = Math.min(
|
|
Math.max(
|
|
this.normalExtent.x * this.scale * stageScale,
|
|
5
|
|
),
|
|
1000
|
|
);
|
|
this.silentSetExtent(new Point(newX, newX));
|
|
this.image = newCanvas(this.extent(), true);
|
|
this.setCenter(currentCenter, true); // just me
|
|
SpriteMorph.uber.drawNew.call(this, facing);
|
|
this.rotationOffset = this.extent().divideBy(2);
|
|
this.image = this.applyGraphicsEffects(this.image);
|
|
if (isLoadingCostume) { // retry until costume is done loading
|
|
cst = this.costume;
|
|
handle = setInterval(
|
|
function () {
|
|
myself.wearCostume(cst);
|
|
clearInterval(handle);
|
|
},
|
|
100
|
|
);
|
|
return myself.wearCostume(null);
|
|
|
|
}
|
|
}
|
|
this.version = Date.now(); // for observer optimization
|
|
};
|
|
|
|
SpriteMorph.prototype.endWarp = function () {
|
|
this.isWarped = false;
|
|
if (this.wantsRedraw) {
|
|
var x = this.xPosition(),
|
|
y = this.yPosition();
|
|
this.drawNew();
|
|
this.silentGotoXY(x, y, true); // just me
|
|
this.wantsRedraw = false;
|
|
}
|
|
this.parent.changed();
|
|
};
|
|
|
|
SpriteMorph.prototype.rotationCenter = function () {
|
|
return this.position().add(this.rotationOffset);
|
|
};
|
|
|
|
SpriteMorph.prototype.colorFiltered = function (aColor) {
|
|
// answer a new Morph containing my image filtered by aColor
|
|
// ignore transparency (alpha)
|
|
var morph = new Morph(),
|
|
ext = this.extent(),
|
|
ctx,
|
|
src,
|
|
clr,
|
|
i,
|
|
dta;
|
|
|
|
src = normalizeCanvas(this.image, true).getContext('2d').getImageData(
|
|
0,
|
|
0,
|
|
ext.x,
|
|
ext.y
|
|
);
|
|
morph.image = newCanvas(ext, true);
|
|
morph.bounds = this.bounds.copy();
|
|
ctx = morph.image.getContext('2d');
|
|
dta = ctx.createImageData(ext.x, ext.y);
|
|
for (i = 0; i < ext.x * ext.y * 4; i += 4) {
|
|
clr = new Color(
|
|
src.data[i],
|
|
src.data[i + 1],
|
|
src.data[i + 2]
|
|
);
|
|
if (clr.eq(aColor)) {
|
|
dta.data[i] = src.data[i];
|
|
dta.data[i + 1] = src.data[i + 1];
|
|
dta.data[i + 2] = src.data[i + 2];
|
|
dta.data[i + 3] = 255;
|
|
}
|
|
}
|
|
ctx.putImageData(dta, 0, 0);
|
|
return morph;
|
|
};
|
|
|
|
// SpriteMorph block instantiation
|
|
|
|
SpriteMorph.prototype.blockForSelector = function (selector, setDefaults) {
|
|
var migration, info, block, defaults, inputs, i;
|
|
migration = this.blockMigrations[selector];
|
|
info = this.blocks[migration ? migration.selector : selector];
|
|
if (!info) {return null; }
|
|
block = info.type === 'command' ? new CommandBlockMorph()
|
|
: info.type === 'hat' ? new HatBlockMorph()
|
|
: info.type === 'ring' ? new RingMorph()
|
|
: new ReporterBlockMorph(info.type === 'predicate');
|
|
block.color = this.blockColor[info.category];
|
|
block.category = info.category;
|
|
block.selector = migration ? migration.selector : selector;
|
|
if (contains(['reifyReporter', 'reifyPredicate'], block.selector)) {
|
|
block.isStatic = true;
|
|
}
|
|
block.setSpec(localize(info.spec));
|
|
if ((setDefaults && info.defaults) || (migration && migration.inputs)) {
|
|
defaults = migration ? migration.inputs : info.defaults;
|
|
block.defaults = defaults;
|
|
inputs = block.inputs();
|
|
if (inputs[0] instanceof MultiArgMorph) {
|
|
inputs[0].setContents(defaults);
|
|
inputs[0].defaults = defaults;
|
|
} else {
|
|
for (i = 0; i < defaults.length; i += 1) {
|
|
if (defaults[i] !== null) {
|
|
inputs[i].setContents(defaults[i]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return block;
|
|
};
|
|
|
|
SpriteMorph.prototype.variableBlock = function (varName) {
|
|
var block = new ReporterBlockMorph(false);
|
|
block.selector = 'reportGetVar';
|
|
block.color = this.blockColor.variables;
|
|
block.category = 'variables';
|
|
block.setSpec(varName);
|
|
block.isDraggable = true;
|
|
return block;
|
|
};
|
|
|
|
// SpriteMorph block templates
|
|
|
|
SpriteMorph.prototype.blockTemplates = function (category) {
|
|
var blocks = [], myself = this, varNames, button,
|
|
cat = category || 'motion', txt,
|
|
inheritedVars = this.inheritedVariableNames();
|
|
|
|
function block(selector) {
|
|
if (StageMorph.prototype.hiddenPrimitives[selector]) {
|
|
return null;
|
|
}
|
|
var newBlock = SpriteMorph.prototype.blockForSelector(selector, true);
|
|
newBlock.isTemplate = true;
|
|
return newBlock;
|
|
}
|
|
|
|
function variableBlock(varName) {
|
|
var newBlock = SpriteMorph.prototype.variableBlock(varName);
|
|
newBlock.isDraggable = false;
|
|
newBlock.isTemplate = true;
|
|
if (contains(inheritedVars, varName)) {
|
|
newBlock.ghost();
|
|
}
|
|
return newBlock;
|
|
}
|
|
|
|
function watcherToggle(selector) {
|
|
if (StageMorph.prototype.hiddenPrimitives[selector]) {
|
|
return null;
|
|
}
|
|
var info = SpriteMorph.prototype.blocks[selector];
|
|
return new ToggleMorph(
|
|
'checkbox',
|
|
this,
|
|
function () {
|
|
myself.toggleWatcher(
|
|
selector,
|
|
localize(info.spec),
|
|
myself.blockColor[info.category]
|
|
);
|
|
},
|
|
null,
|
|
function () {
|
|
return myself.showingWatcher(selector);
|
|
},
|
|
null
|
|
);
|
|
}
|
|
|
|
function variableWatcherToggle(varName) {
|
|
return new ToggleMorph(
|
|
'checkbox',
|
|
this,
|
|
function () {
|
|
myself.toggleVariableWatcher(varName);
|
|
},
|
|
null,
|
|
function () {
|
|
return myself.showingVariableWatcher(varName);
|
|
},
|
|
null
|
|
);
|
|
}
|
|
|
|
function helpMenu() {
|
|
var menu = new MenuMorph(this);
|
|
menu.addItem('help...', 'showHelp');
|
|
return menu;
|
|
}
|
|
|
|
function addVar(pair) {
|
|
var ide;
|
|
if (pair) {
|
|
if (myself.isVariableNameInUse(pair[0], pair[1])) {
|
|
myself.inform('that name is already in use');
|
|
} else {
|
|
ide = myself.parentThatIsA(IDE_Morph);
|
|
myself.addVariable(pair[0], pair[1]);
|
|
if (!myself.showingVariableWatcher(pair[0])) {
|
|
myself.toggleVariableWatcher(pair[0], pair[1]);
|
|
}
|
|
ide.flushBlocksCache('variables'); // b/c of inheritance
|
|
ide.refreshPalette();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (cat === 'motion') {
|
|
|
|
blocks.push(block('forward'));
|
|
blocks.push(block('turn'));
|
|
blocks.push(block('turnLeft'));
|
|
blocks.push('-');
|
|
blocks.push(block('setHeading'));
|
|
blocks.push(block('doFaceTowards'));
|
|
blocks.push('-');
|
|
blocks.push(block('gotoXY'));
|
|
blocks.push(block('doGotoObject'));
|
|
blocks.push(block('doGlide'));
|
|
blocks.push('-');
|
|
blocks.push(block('changeXPosition'));
|
|
blocks.push(block('setXPosition'));
|
|
blocks.push(block('changeYPosition'));
|
|
blocks.push(block('setYPosition'));
|
|
blocks.push('-');
|
|
blocks.push(block('bounceOffEdge'));
|
|
blocks.push('-');
|
|
blocks.push(watcherToggle('xPosition'));
|
|
blocks.push(block('xPosition'));
|
|
blocks.push(watcherToggle('yPosition'));
|
|
blocks.push(block('yPosition'));
|
|
blocks.push(watcherToggle('direction'));
|
|
blocks.push(block('direction'));
|
|
|
|
} else if (cat === 'looks') {
|
|
|
|
blocks.push(block('doSwitchToCostume'));
|
|
blocks.push(block('doWearNextCostume'));
|
|
blocks.push(watcherToggle('getCostumeIdx'));
|
|
blocks.push(block('getCostumeIdx'));
|
|
blocks.push('-');
|
|
blocks.push(block('doSayFor'));
|
|
blocks.push(block('bubble'));
|
|
blocks.push(block('doThinkFor'));
|
|
blocks.push(block('doThink'));
|
|
blocks.push('-');
|
|
blocks.push(block('changeEffect'));
|
|
blocks.push(block('setEffect'));
|
|
blocks.push(block('clearEffects'));
|
|
blocks.push('-');
|
|
blocks.push(block('changeScale'));
|
|
blocks.push(block('setScale'));
|
|
blocks.push(watcherToggle('getScale'));
|
|
blocks.push(block('getScale'));
|
|
blocks.push('-');
|
|
blocks.push(block('show'));
|
|
blocks.push(block('hide'));
|
|
blocks.push('-');
|
|
blocks.push(block('comeToFront'));
|
|
blocks.push(block('goBack'));
|
|
|
|
// for debugging: ///////////////
|
|
|
|
if (this.world().isDevMode) {
|
|
blocks.push('-');
|
|
txt = new TextMorph(localize(
|
|
'development mode \ndebugging primitives:'
|
|
));
|
|
txt.fontSize = 9;
|
|
txt.setColor(this.paletteTextColor);
|
|
blocks.push(txt);
|
|
blocks.push('-');
|
|
blocks.push(block('reportCostumes'));
|
|
blocks.push('-');
|
|
blocks.push(block('log'));
|
|
blocks.push(block('alert'));
|
|
blocks.push('-');
|
|
blocks.push(block('doScreenshot'));
|
|
}
|
|
|
|
/////////////////////////////////
|
|
|
|
} else if (cat === 'sound') {
|
|
|
|
blocks.push(block('playSound'));
|
|
blocks.push(block('doPlaySoundUntilDone'));
|
|
blocks.push(block('doStopAllSounds'));
|
|
blocks.push('-');
|
|
blocks.push(block('doRest'));
|
|
blocks.push('-');
|
|
blocks.push(block('doPlayNote'));
|
|
blocks.push('-');
|
|
blocks.push(block('doChangeTempo'));
|
|
blocks.push(block('doSetTempo'));
|
|
blocks.push(watcherToggle('getTempo'));
|
|
blocks.push(block('getTempo'));
|
|
|
|
// for debugging: ///////////////
|
|
|
|
if (this.world().isDevMode) {
|
|
blocks.push('-');
|
|
txt = new TextMorph(localize(
|
|
'development mode \ndebugging primitives:'
|
|
));
|
|
txt.fontSize = 9;
|
|
txt.setColor(this.paletteTextColor);
|
|
blocks.push(txt);
|
|
blocks.push('-');
|
|
blocks.push(block('reportSounds'));
|
|
}
|
|
|
|
} else if (cat === 'pen') {
|
|
|
|
blocks.push(block('clear'));
|
|
blocks.push('-');
|
|
blocks.push(block('down'));
|
|
blocks.push(block('up'));
|
|
blocks.push('-');
|
|
blocks.push(block('setColor'));
|
|
blocks.push(block('changeHue'));
|
|
blocks.push(block('setHue'));
|
|
blocks.push('-');
|
|
blocks.push(block('changeBrightness'));
|
|
blocks.push(block('setBrightness'));
|
|
blocks.push('-');
|
|
blocks.push(block('changeSize'));
|
|
blocks.push(block('setSize'));
|
|
blocks.push('-');
|
|
blocks.push(block('doStamp'));
|
|
blocks.push(block('floodFill'));
|
|
|
|
} else if (cat === 'control') {
|
|
|
|
blocks.push(block('receiveGo'));
|
|
blocks.push(block('receiveKey'));
|
|
blocks.push(block('receiveInteraction'));
|
|
blocks.push(block('receiveCondition'));
|
|
blocks.push(block('receiveMessage'));
|
|
blocks.push('-');
|
|
blocks.push(block('doBroadcast'));
|
|
blocks.push(block('doBroadcastAndWait'));
|
|
blocks.push(watcherToggle('getLastMessage'));
|
|
blocks.push(block('getLastMessage'));
|
|
blocks.push('-');
|
|
blocks.push(block('doWarp'));
|
|
blocks.push('-');
|
|
blocks.push(block('doWait'));
|
|
blocks.push(block('doWaitUntil'));
|
|
blocks.push('-');
|
|
blocks.push(block('doForever'));
|
|
blocks.push(block('doRepeat'));
|
|
blocks.push(block('doUntil'));
|
|
blocks.push('-');
|
|
blocks.push(block('doIf'));
|
|
blocks.push(block('doIfElse'));
|
|
blocks.push('-');
|
|
blocks.push(block('doReport'));
|
|
blocks.push('-');
|
|
/*
|
|
// old STOP variants, migrated to a newer version, now redundant
|
|
blocks.push(block('doStopBlock'));
|
|
blocks.push(block('doStop'));
|
|
blocks.push(block('doStopAll'));
|
|
*/
|
|
blocks.push(block('doStopThis'));
|
|
blocks.push(block('doStopOthers'));
|
|
blocks.push('-');
|
|
blocks.push(block('doRun'));
|
|
blocks.push(block('fork'));
|
|
blocks.push(block('evaluate'));
|
|
blocks.push('-');
|
|
/*
|
|
// list variants commented out for now (redundant)
|
|
blocks.push(block('doRunWithInputList'));
|
|
blocks.push(block('forkWithInputList'));
|
|
blocks.push(block('evaluateWithInputList'));
|
|
blocks.push('-');
|
|
*/
|
|
blocks.push(block('doCallCC'));
|
|
blocks.push(block('reportCallCC'));
|
|
blocks.push('-');
|
|
blocks.push(block('receiveOnClone'));
|
|
blocks.push(block('createClone'));
|
|
blocks.push(block('removeClone'));
|
|
blocks.push('-');
|
|
blocks.push(block('doPauseAll'));
|
|
|
|
} else if (cat === 'sensing') {
|
|
|
|
blocks.push(block('reportTouchingObject'));
|
|
blocks.push(block('reportTouchingColor'));
|
|
blocks.push(block('reportColorIsTouchingColor'));
|
|
blocks.push('-');
|
|
blocks.push(block('doAsk'));
|
|
blocks.push(watcherToggle('getLastAnswer'));
|
|
blocks.push(block('getLastAnswer'));
|
|
blocks.push('-');
|
|
blocks.push(watcherToggle('reportMouseX'));
|
|
blocks.push(block('reportMouseX'));
|
|
blocks.push(watcherToggle('reportMouseY'));
|
|
blocks.push(block('reportMouseY'));
|
|
blocks.push(block('reportMouseDown'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportKeyPressed'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportDistanceTo'));
|
|
blocks.push('-');
|
|
blocks.push(block('doResetTimer'));
|
|
blocks.push(watcherToggle('getTimer'));
|
|
blocks.push(block('getTimer'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportAttributeOf'));
|
|
|
|
if (SpriteMorph.prototype.enableFirstClass) {
|
|
blocks.push(block('reportGet'));
|
|
}
|
|
blocks.push('-');
|
|
|
|
blocks.push(block('reportURL'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportIsFastTracking'));
|
|
blocks.push(block('doSetFastTracking'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportDate'));
|
|
|
|
// for debugging: ///////////////
|
|
|
|
if (this.world().isDevMode) {
|
|
|
|
blocks.push('-');
|
|
txt = new TextMorph(localize(
|
|
'development mode \ndebugging primitives:'
|
|
));
|
|
txt.fontSize = 9;
|
|
txt.setColor(this.paletteTextColor);
|
|
blocks.push(txt);
|
|
blocks.push('-');
|
|
blocks.push(watcherToggle('reportThreadCount'));
|
|
blocks.push(block('reportThreadCount'));
|
|
blocks.push(block('colorFiltered'));
|
|
blocks.push(block('reportStackSize'));
|
|
blocks.push(block('reportFrameCount'));
|
|
}
|
|
|
|
} else if (cat === 'operators') {
|
|
|
|
blocks.push(block('reifyScript'));
|
|
blocks.push(block('reifyReporter'));
|
|
blocks.push(block('reifyPredicate'));
|
|
blocks.push('#');
|
|
blocks.push('-');
|
|
blocks.push(block('reportSum'));
|
|
blocks.push(block('reportDifference'));
|
|
blocks.push(block('reportProduct'));
|
|
blocks.push(block('reportQuotient'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportModulus'));
|
|
blocks.push(block('reportRound'));
|
|
blocks.push(block('reportMonadic'));
|
|
blocks.push(block('reportRandom'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportLessThan'));
|
|
blocks.push(block('reportEquals'));
|
|
blocks.push(block('reportGreaterThan'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportAnd'));
|
|
blocks.push(block('reportOr'));
|
|
blocks.push(block('reportNot'));
|
|
blocks.push(block('reportBoolean'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportJoinWords'));
|
|
blocks.push(block('reportTextSplit'));
|
|
blocks.push(block('reportLetter'));
|
|
blocks.push(block('reportStringSize'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportUnicode'));
|
|
blocks.push(block('reportUnicodeAsLetter'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportIsA'));
|
|
blocks.push(block('reportIsIdentical'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportJSFunction'));
|
|
|
|
// for debugging: ///////////////
|
|
|
|
if (this.world().isDevMode) {
|
|
blocks.push('-');
|
|
txt = new TextMorph(localize(
|
|
'development mode \ndebugging primitives:'
|
|
));
|
|
txt.fontSize = 9;
|
|
txt.setColor(this.paletteTextColor);
|
|
blocks.push(txt);
|
|
blocks.push('-');
|
|
blocks.push(block('reportTypeOf'));
|
|
blocks.push(block('reportTextFunction'));
|
|
}
|
|
|
|
/////////////////////////////////
|
|
|
|
} else if (cat === 'variables') {
|
|
|
|
button = new PushButtonMorph(
|
|
null,
|
|
function () {
|
|
new VariableDialogMorph(
|
|
null,
|
|
addVar,
|
|
myself
|
|
).prompt(
|
|
'Variable name',
|
|
null,
|
|
myself.world()
|
|
);
|
|
},
|
|
'Make a variable'
|
|
);
|
|
button.userMenu = helpMenu;
|
|
button.selector = 'addVariable';
|
|
button.showHelp = BlockMorph.prototype.showHelp;
|
|
blocks.push(button);
|
|
|
|
if (this.deletableVariableNames().length > 0) {
|
|
button = new PushButtonMorph(
|
|
null,
|
|
function () {
|
|
var menu = new MenuMorph(
|
|
myself.deleteVariable,
|
|
null,
|
|
myself
|
|
);
|
|
myself.deletableVariableNames().forEach(function (name) {
|
|
menu.addItem(name, name);
|
|
});
|
|
menu.popUpAtHand(myself.world());
|
|
},
|
|
'Delete a variable'
|
|
);
|
|
button.userMenu = helpMenu;
|
|
button.selector = 'deleteVariable';
|
|
button.showHelp = BlockMorph.prototype.showHelp;
|
|
blocks.push(button);
|
|
}
|
|
|
|
blocks.push('-');
|
|
|
|
varNames = this.variables.allNames();
|
|
if (varNames.length > 0) {
|
|
varNames.forEach(function (name) {
|
|
blocks.push(variableWatcherToggle(name));
|
|
blocks.push(variableBlock(name));
|
|
});
|
|
blocks.push('-');
|
|
}
|
|
|
|
blocks.push(block('doSetVar'));
|
|
blocks.push(block('doChangeVar'));
|
|
blocks.push(block('doShowVar'));
|
|
blocks.push(block('doHideVar'));
|
|
blocks.push(block('doDeclareVariables'));
|
|
|
|
// inheritance:
|
|
|
|
if (StageMorph.prototype.enableInheritance) {
|
|
blocks.push('-');
|
|
blocks.push(block('doDeleteAttr'));
|
|
}
|
|
|
|
///////////////////////////////
|
|
|
|
blocks.push('=');
|
|
|
|
blocks.push(block('reportNewList'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportCONS'));
|
|
blocks.push(block('reportListItem'));
|
|
blocks.push(block('reportCDR'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportListLength'));
|
|
blocks.push(block('reportListContainsItem'));
|
|
blocks.push('-');
|
|
blocks.push(block('doAddToList'));
|
|
blocks.push(block('doDeleteFromList'));
|
|
blocks.push(block('doInsertInList'));
|
|
blocks.push(block('doReplaceInList'));
|
|
|
|
// for debugging: ///////////////
|
|
|
|
if (this.world().isDevMode) {
|
|
blocks.push('-');
|
|
txt = new TextMorph(localize(
|
|
'development mode \ndebugging primitives:'
|
|
));
|
|
txt.fontSize = 9;
|
|
txt.setColor(this.paletteTextColor);
|
|
blocks.push(txt);
|
|
blocks.push('-');
|
|
blocks.push(block('reportMap'));
|
|
blocks.push('-');
|
|
blocks.push(block('doForEach'));
|
|
blocks.push(block('doShowTable'));
|
|
}
|
|
|
|
/////////////////////////////////
|
|
|
|
blocks.push('=');
|
|
|
|
if (StageMorph.prototype.enableCodeMapping) {
|
|
blocks.push(block('doMapCodeOrHeader'));
|
|
blocks.push(block('doMapStringCode'));
|
|
blocks.push(block('doMapListCode'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportMappedCode'));
|
|
blocks.push('=');
|
|
}
|
|
|
|
button = new PushButtonMorph(
|
|
null,
|
|
function () {
|
|
var ide = myself.parentThatIsA(IDE_Morph),
|
|
stage = myself.parentThatIsA(StageMorph);
|
|
new BlockDialogMorph(
|
|
null,
|
|
function (definition) {
|
|
if (definition.spec !== '') {
|
|
if (definition.isGlobal) {
|
|
stage.globalBlocks.push(definition);
|
|
} else {
|
|
myself.customBlocks.push(definition);
|
|
}
|
|
ide.flushPaletteCache();
|
|
ide.refreshPalette();
|
|
new BlockEditorMorph(definition, myself).popUp();
|
|
}
|
|
},
|
|
myself
|
|
).prompt(
|
|
'Make a block',
|
|
null,
|
|
myself.world()
|
|
);
|
|
},
|
|
'Make a block'
|
|
);
|
|
button.userMenu = helpMenu;
|
|
button.selector = 'addCustomBlock';
|
|
button.showHelp = BlockMorph.prototype.showHelp;
|
|
blocks.push(button);
|
|
}
|
|
return blocks;
|
|
};
|
|
|
|
SpriteMorph.prototype.palette = function (category) {
|
|
if (!this.paletteCache[category]) {
|
|
this.paletteCache[category] = this.freshPalette(category);
|
|
}
|
|
return this.paletteCache[category];
|
|
};
|
|
|
|
SpriteMorph.prototype.freshPalette = function (category) {
|
|
var palette = new ScrollFrameMorph(null, null, this.sliderColor),
|
|
unit = SyntaxElementMorph.prototype.fontSize,
|
|
x = 0,
|
|
y = 5,
|
|
ry = 0,
|
|
blocks,
|
|
hideNextSpace = false,
|
|
myself = this,
|
|
stage = this.parentThatIsA(StageMorph),
|
|
oldFlag = Morph.prototype.trackChanges;
|
|
|
|
Morph.prototype.trackChanges = false;
|
|
|
|
palette.owner = this;
|
|
palette.padding = unit / 2;
|
|
palette.color = this.paletteColor;
|
|
palette.growth = new Point(0, MorphicPreferences.scrollBarSize);
|
|
|
|
// menu:
|
|
|
|
palette.userMenu = function () {
|
|
var menu = new MenuMorph(),
|
|
ide = this.parentThatIsA(IDE_Morph),
|
|
more = {
|
|
operators:
|
|
['reifyScript', 'reifyReporter', 'reifyPredicate'],
|
|
control:
|
|
['doWarp'],
|
|
variables:
|
|
[
|
|
'doDeclareVariables',
|
|
'reportNewList',
|
|
'reportCONS',
|
|
'reportListItem',
|
|
'reportCDR',
|
|
'reportListLength',
|
|
'reportListContainsItem',
|
|
'doAddToList',
|
|
'doDeleteFromList',
|
|
'doInsertInList',
|
|
'doReplaceInList'
|
|
]
|
|
};
|
|
|
|
function hasHiddenPrimitives() {
|
|
var defs = SpriteMorph.prototype.blocks,
|
|
hiddens = StageMorph.prototype.hiddenPrimitives;
|
|
return Object.keys(hiddens).some(function (any) {
|
|
return !isNil(defs[any]) && (defs[any].category === category
|
|
|| contains((more[category] || []), any));
|
|
});
|
|
}
|
|
|
|
function canHidePrimitives() {
|
|
return palette.contents.children.some(function (any) {
|
|
return contains(
|
|
Object.keys(SpriteMorph.prototype.blocks),
|
|
any.selector
|
|
);
|
|
});
|
|
}
|
|
|
|
menu.addItem('find blocks...', function () {myself.searchBlocks(); });
|
|
if (canHidePrimitives()) {
|
|
menu.addItem(
|
|
'hide primitives',
|
|
function () {
|
|
var defs = SpriteMorph.prototype.blocks;
|
|
Object.keys(defs).forEach(function (sel) {
|
|
if (defs[sel].category === category) {
|
|
StageMorph.prototype.hiddenPrimitives[sel] = true;
|
|
}
|
|
});
|
|
(more[category] || []).forEach(function (sel) {
|
|
StageMorph.prototype.hiddenPrimitives[sel] = true;
|
|
});
|
|
ide.flushBlocksCache(category);
|
|
ide.refreshPalette();
|
|
}
|
|
);
|
|
}
|
|
if (hasHiddenPrimitives()) {
|
|
menu.addItem(
|
|
'show primitives',
|
|
function () {
|
|
var hiddens = StageMorph.prototype.hiddenPrimitives,
|
|
defs = SpriteMorph.prototype.blocks;
|
|
Object.keys(hiddens).forEach(function (sel) {
|
|
if (defs[sel] && (defs[sel].category === category)) {
|
|
delete StageMorph.prototype.hiddenPrimitives[sel];
|
|
}
|
|
});
|
|
(more[category] || []).forEach(function (sel) {
|
|
delete StageMorph.prototype.hiddenPrimitives[sel];
|
|
});
|
|
ide.flushBlocksCache(category);
|
|
ide.refreshPalette();
|
|
}
|
|
);
|
|
}
|
|
return menu;
|
|
};
|
|
|
|
// primitives:
|
|
|
|
blocks = this.blocksCache[category];
|
|
if (!blocks) {
|
|
blocks = myself.blockTemplates(category);
|
|
if (this.isCachingPrimitives) {
|
|
myself.blocksCache[category] = blocks;
|
|
}
|
|
}
|
|
|
|
blocks.forEach(function (block) {
|
|
if (block === null) {
|
|
return;
|
|
}
|
|
if (block === '-') {
|
|
if (hideNextSpace) {return; }
|
|
y += unit * 0.8;
|
|
hideNextSpace = true;
|
|
} else if (block === '=') {
|
|
if (hideNextSpace) {return; }
|
|
y += unit * 1.6;
|
|
hideNextSpace = true;
|
|
} else if (block === '#') {
|
|
x = 0;
|
|
y = ry;
|
|
} else {
|
|
hideNextSpace = false;
|
|
if (x === 0) {
|
|
y += unit * 0.3;
|
|
}
|
|
block.setPosition(new Point(x, y));
|
|
palette.addContents(block);
|
|
if (block instanceof ToggleMorph
|
|
|| (block instanceof RingMorph)) {
|
|
x = block.right() + unit / 2;
|
|
ry = block.bottom();
|
|
} else {
|
|
// if (block.fixLayout) {block.fixLayout(); }
|
|
x = 0;
|
|
y += block.height();
|
|
}
|
|
}
|
|
});
|
|
|
|
// global custom blocks:
|
|
|
|
if (stage) {
|
|
y += unit * 1.6;
|
|
|
|
stage.globalBlocks.forEach(function (definition) {
|
|
var block;
|
|
if (definition.category === category ||
|
|
(category === 'variables'
|
|
&& contains(
|
|
['lists', 'other'],
|
|
definition.category
|
|
))) {
|
|
block = definition.templateInstance();
|
|
y += unit * 0.3;
|
|
block.setPosition(new Point(x, y));
|
|
palette.addContents(block);
|
|
x = 0;
|
|
y += block.height();
|
|
}
|
|
});
|
|
}
|
|
|
|
// local custom blocks:
|
|
|
|
y += unit * 1.6;
|
|
this.customBlocks.forEach(function (definition) {
|
|
var block;
|
|
if (definition.category === category ||
|
|
(category === 'variables'
|
|
&& contains(
|
|
['lists', 'other'],
|
|
definition.category
|
|
))) {
|
|
block = definition.templateInstance();
|
|
y += unit * 0.3;
|
|
block.setPosition(new Point(x, y));
|
|
palette.addContents(block);
|
|
x = 0;
|
|
y += block.height();
|
|
}
|
|
});
|
|
|
|
//layout
|
|
|
|
palette.scrollX(palette.padding);
|
|
palette.scrollY(palette.padding);
|
|
|
|
Morph.prototype.trackChanges = oldFlag;
|
|
return palette;
|
|
};
|
|
|
|
// SpriteMorph blocks searching
|
|
|
|
SpriteMorph.prototype.blocksMatching = function (
|
|
searchString,
|
|
strictly,
|
|
types, // optional, ['hat', 'command', 'reporter', 'predicate']
|
|
varNames // optional, list of reachable unique variable names
|
|
) {
|
|
// answer an array of block templates whose spec contains
|
|
// the given search string, ordered by descending relevance
|
|
// types is an optional array containing block types the search
|
|
// is limited to, e.g. "command", "hat", "reporter", "predicate".
|
|
// Note that "predicate" is not subsumed by "reporter" and has
|
|
// to be specified explicitly.
|
|
// if no types are specified all blocks are searched
|
|
var blocks = [],
|
|
blocksDict,
|
|
myself = this,
|
|
search = searchString.toLowerCase(),
|
|
stage = this.parentThatIsA(StageMorph),
|
|
reporterized;
|
|
|
|
if (!types || !types.length) {
|
|
types = ['hat', 'command', 'reporter', 'predicate', 'ring'];
|
|
}
|
|
if (!varNames) {varNames = []; }
|
|
|
|
function labelOf(aBlockSpec) {
|
|
var words = (BlockMorph.prototype.parseSpec(aBlockSpec)),
|
|
filtered = words.filter(
|
|
function (each) {return (each.indexOf('%') !== 0); }
|
|
);
|
|
return filtered.join(' ');
|
|
}
|
|
|
|
function fillDigits(anInt, totalDigits, fillChar) {
|
|
var ans = String(anInt);
|
|
while (ans.length < totalDigits) {ans = fillChar + ans; }
|
|
return ans;
|
|
}
|
|
|
|
function relevance(aBlockLabel, aSearchString) {
|
|
var lbl = ' ' + aBlockLabel,
|
|
idx = lbl.indexOf(aSearchString),
|
|
atWord;
|
|
if (idx === -1) {return -1; }
|
|
atWord = (lbl.charAt(idx - 1) === ' ');
|
|
if (strictly && !atWord) {return -1; }
|
|
return (atWord ? '1' : '2') + fillDigits(idx, 4, '0');
|
|
}
|
|
|
|
function primitive(selector) {
|
|
var newBlock = SpriteMorph.prototype.blockForSelector(selector, true);
|
|
newBlock.isTemplate = true;
|
|
return newBlock;
|
|
}
|
|
|
|
// variable getters
|
|
varNames.forEach(function (vName) {
|
|
var rel = relevance(labelOf(vName.toLowerCase()), search);
|
|
if (rel !== -1) {
|
|
blocks.push([myself.variableBlock(vName), rel + '1']);
|
|
}
|
|
});
|
|
// custom blocks
|
|
[this.customBlocks, stage.globalBlocks].forEach(function (blocksList) {
|
|
blocksList.forEach(function (definition) {
|
|
if (contains(types, definition.type)) {
|
|
var spec = localize(definition.blockSpec()).toLowerCase(),
|
|
rel = relevance(labelOf(spec), search);
|
|
if (rel !== -1) {
|
|
blocks.push([definition.templateInstance(), rel + '2']);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
// primitives
|
|
blocksDict = SpriteMorph.prototype.blocks;
|
|
Object.keys(blocksDict).forEach(function (selector) {
|
|
if (!StageMorph.prototype.hiddenPrimitives[selector] &&
|
|
contains(types, blocksDict[selector].type)) {
|
|
var block = blocksDict[selector],
|
|
spec = localize(block.alias || block.spec).toLowerCase(),
|
|
rel = relevance(labelOf(spec), search);
|
|
if (
|
|
(rel !== -1) &&
|
|
(!block.dev) &&
|
|
(!block.only || (block.only === myself.constructor))
|
|
) {
|
|
blocks.push([primitive(selector), rel + '3']);
|
|
}
|
|
}
|
|
});
|
|
// infix arithmetic expression
|
|
if (contains(types, 'reporter')) {
|
|
reporterized = this.reporterize(searchString);
|
|
if (reporterized) {
|
|
// reporterized.isTemplate = true;
|
|
// reporterized.isDraggable = false;
|
|
blocks.push([reporterized, '']);
|
|
}
|
|
}
|
|
blocks.sort(function (x, y) {return x[1] < y[1] ? -1 : 1; });
|
|
return blocks.map(function (each) {return each[0]; });
|
|
};
|
|
|
|
SpriteMorph.prototype.searchBlocks = function (
|
|
searchString,
|
|
types,
|
|
varNames,
|
|
scriptFocus
|
|
) {
|
|
var myself = this,
|
|
unit = SyntaxElementMorph.prototype.fontSize,
|
|
ide = this.parentThatIsA(IDE_Morph),
|
|
oldSearch = '',
|
|
searchBar = new InputFieldMorph(searchString || ''),
|
|
searchPane = ide.createPalette('forSearch'),
|
|
blocksList = [],
|
|
selection,
|
|
focus;
|
|
|
|
function showSelection() {
|
|
if (focus) {focus.destroy(); }
|
|
if (!selection || !scriptFocus) {return; }
|
|
focus = selection.outline(
|
|
MorphicPreferences.isFlat ? new Color(150, 200, 255)
|
|
: new Color(255, 255, 255),
|
|
2
|
|
);
|
|
searchPane.contents.add(focus);
|
|
focus.scrollIntoView();
|
|
}
|
|
|
|
function show(blocks) {
|
|
var oldFlag = Morph.prototype.trackChanges,
|
|
x = searchPane.contents.left() + 5,
|
|
y = (searchBar.bottom() + unit);
|
|
blocksList = blocks;
|
|
selection = null;
|
|
if (blocks.length && scriptFocus) {
|
|
selection = blocks[0];
|
|
}
|
|
Morph.prototype.trackChanges = false;
|
|
searchPane.contents.children = [searchPane.contents.children[0]];
|
|
blocks.forEach(function (block) {
|
|
block.setPosition(new Point(x, y));
|
|
searchPane.addContents(block);
|
|
y += block.height();
|
|
y += unit * 0.3;
|
|
});
|
|
Morph.prototype.trackChanges = oldFlag;
|
|
showSelection();
|
|
searchPane.changed();
|
|
}
|
|
|
|
searchPane.owner = this;
|
|
searchPane.color = myself.paletteColor;
|
|
searchPane.contents.color = myself.paletteColor;
|
|
searchPane.addContents(searchBar);
|
|
searchBar.drawNew();
|
|
searchBar.setWidth(ide.logo.width() - 30);
|
|
searchBar.contrast = 90;
|
|
searchBar.setPosition(
|
|
searchPane.contents.topLeft().add(new Point(10, 10))
|
|
);
|
|
searchBar.drawNew();
|
|
|
|
searchPane.accept = function () {
|
|
var search;
|
|
if (scriptFocus) {
|
|
searchBar.cancel();
|
|
if (selection) {
|
|
scriptFocus.insertBlock(selection);
|
|
}
|
|
} else {
|
|
search = searchBar.getValue();
|
|
if (search.length > 0) {
|
|
show(myself.blocksMatching(search));
|
|
}
|
|
}
|
|
};
|
|
|
|
searchPane.reactToKeystroke = function (evt) {
|
|
var search, idx, code = evt ? evt.keyCode : 0;
|
|
switch (code) {
|
|
case 38: // up arrow
|
|
if (!scriptFocus || !selection) {return; }
|
|
idx = blocksList.indexOf(selection) - 1;
|
|
if (idx < 0) {
|
|
idx = blocksList.length - 1;
|
|
}
|
|
selection = blocksList[idx];
|
|
showSelection();
|
|
return;
|
|
case 40: // down arrow
|
|
if (!scriptFocus || !selection) {return; }
|
|
idx = blocksList.indexOf(selection) + 1;
|
|
if (idx >= blocksList.length) {
|
|
idx = 0;
|
|
}
|
|
selection = blocksList[idx];
|
|
showSelection();
|
|
return;
|
|
default:
|
|
search = searchBar.getValue();
|
|
if (search !== oldSearch) {
|
|
oldSearch = search;
|
|
show(myself.blocksMatching(
|
|
search,
|
|
search.length < 2,
|
|
types,
|
|
varNames
|
|
));
|
|
}
|
|
}
|
|
};
|
|
|
|
searchBar.cancel = function () {
|
|
ide.refreshPalette();
|
|
ide.palette.adjustScrollBars();
|
|
};
|
|
|
|
ide.fixLayout('refreshPalette');
|
|
searchBar.edit();
|
|
if (searchString) {searchPane.reactToKeystroke(); }
|
|
};
|
|
|
|
// SpritMorph parsing simple arithmetic expressions to reporter blocks
|
|
|
|
SpriteMorph.prototype.reporterize = function (expressionString) {
|
|
// highly experimental Christmas Easter Egg 2016 :-)
|
|
var ast;
|
|
|
|
function parseInfix(expression, operator, already) {
|
|
// very basic binary infix parser for arithmetic expressions
|
|
// with strict left-to-right operator precedence (like in Smalltalk)
|
|
// which can be overriden by - nested - parentheses.
|
|
// assumes well-formed expressions, no graceful error handling yet.
|
|
|
|
var inputs = ['', ''],
|
|
idx = 0,
|
|
ch;
|
|
|
|
function format(value) {
|
|
return value instanceof Array || isNaN(+value) ? value : +value;
|
|
}
|
|
|
|
function nested() {
|
|
var level = 1,
|
|
expr = '';
|
|
while (idx < expression.length) {
|
|
ch = expression[idx];
|
|
idx += 1;
|
|
switch (ch) {
|
|
case '(':
|
|
level += 1;
|
|
break;
|
|
case ')':
|
|
level -= 1;
|
|
if (!level) {
|
|
return expr;
|
|
}
|
|
break;
|
|
}
|
|
expr += ch;
|
|
}
|
|
}
|
|
|
|
while (idx < expression.length) {
|
|
ch = expression[idx];
|
|
idx += 1;
|
|
switch (ch) {
|
|
case ' ':
|
|
break;
|
|
case '(':
|
|
if (inputs[operator ? 1 : 0].length) {
|
|
inputs[operator ? 1 : 0] = [
|
|
inputs[operator ? 1 : 0],
|
|
parseInfix(nested())
|
|
];
|
|
} else {
|
|
inputs[operator ? 1 : 0] = parseInfix(nested());
|
|
}
|
|
break;
|
|
case '-':
|
|
case '+':
|
|
case '*':
|
|
case '/':
|
|
case '%':
|
|
case '=':
|
|
case '<':
|
|
case '>':
|
|
case '&':
|
|
case '|':
|
|
if (!operator && !inputs[0].length) {
|
|
inputs[0] += ch;
|
|
} else if (operator) {
|
|
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;
|
|
selectors = {
|
|
'+': 'reportSum',
|
|
'-': 'reportDifference',
|
|
'*': 'reportProduct',
|
|
'/': 'reportQuotient',
|
|
'%': 'reportModulus',
|
|
'=': 'reportEquals',
|
|
'<': 'reportLessThan',
|
|
'>': 'reportGreaterThan',
|
|
'&': 'reportAnd',
|
|
'|': 'reportOr',
|
|
round: 'reportRound',
|
|
not: 'reportNot'
|
|
};
|
|
monads = ['abs', 'ceiling', 'floor', 'sqrt', 'sin', 'cos', 'tan',
|
|
'asin', 'acos', 'atan', 'ln', 'log', 'round', 'not'];
|
|
alias = {
|
|
ceil: 'ceiling',
|
|
'!' : 'not'
|
|
};
|
|
key = alias[ast[0]] || ast[0];
|
|
if (contains(monads, key)) {
|
|
sel = selectors[key];
|
|
if (sel) {
|
|
block = SpriteMorph.prototype.blockForSelector(sel);
|
|
inps = block.inputs();
|
|
i = 0;
|
|
} else {
|
|
block = SpriteMorph.prototype.blockForSelector('reportMonadic');
|
|
inps = block.inputs();
|
|
inps[0].setContents([key]);
|
|
i = 1;
|
|
}
|
|
if (ast[1] instanceof Array) {
|
|
block.silentReplaceInput(inps[i], blockFromAST(ast[1]));
|
|
} else if (isString(ast[1])) {
|
|
if (contains(['true', 'false'], ast[1])) {
|
|
block.silentReplaceInput(
|
|
inps[i],
|
|
SpriteMorph.prototype.blockForSelector(
|
|
ast[1] === 'true' ? 'reportTrue' : 'reportFalse'
|
|
)
|
|
);
|
|
} else if (ast[1] !== '_') {
|
|
block.silentReplaceInput(
|
|
inps[i],
|
|
SpriteMorph.prototype.variableBlock(ast[1])
|
|
);
|
|
}
|
|
} else { // number
|
|
inps[i].setContents(ast[1]);
|
|
}
|
|
} else { // diadic
|
|
block = SpriteMorph.prototype.blockForSelector(selectors[ast[0]]);
|
|
inps = block.inputs();
|
|
for (i = 1; i < 3; i += 1) {
|
|
if (ast[i] instanceof Array) {
|
|
block.silentReplaceInput(inps[i - 1], blockFromAST(ast[i]));
|
|
} else if (isString(ast[i])) {
|
|
if (contains(['true', 'false'], ast[i])) {
|
|
block.silentReplaceInput(
|
|
inps[i - 1],
|
|
SpriteMorph.prototype.blockForSelector(
|
|
ast[i] === 'true' ? 'reportTrue' : 'reportFalse'
|
|
)
|
|
);
|
|
} else if (ast[i] !== '_') {
|
|
block.silentReplaceInput(
|
|
inps[i - 1],
|
|
SpriteMorph.prototype.variableBlock(ast[i])
|
|
);
|
|
}
|
|
} else { // number
|
|
inps[i - 1].setContents(ast[i]);
|
|
}
|
|
}
|
|
}
|
|
block.isDraggable = true;
|
|
block.fixLayout();
|
|
block.fixBlockColor(null, true);
|
|
return block;
|
|
}
|
|
|
|
if (expressionString.length > 100) {return null; }
|
|
try {
|
|
ast = parseInfix(expressionString);
|
|
return ast instanceof Array ? blockFromAST(ast) : null;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// SpriteMorph variable management
|
|
|
|
SpriteMorph.prototype.addVariable = function (name, isGlobal) {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
if (isGlobal) {
|
|
this.globalVariables().addVar(name);
|
|
if (ide) {
|
|
ide.flushBlocksCache('variables');
|
|
}
|
|
} else {
|
|
this.variables.addVar(name);
|
|
this.blocksCache.variables = null;
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.deleteVariable = function (varName) {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
if (!contains(this.inheritedVariableNames(true), varName)) {
|
|
// check only shadowed variables
|
|
this.deleteVariableWatcher(varName);
|
|
}
|
|
this.variables.deleteVar(varName);
|
|
if (ide) {
|
|
ide.flushBlocksCache('variables'); // b/c the var could be global
|
|
ide.refreshPalette();
|
|
}
|
|
};
|
|
|
|
// SpriteMorph costume management
|
|
|
|
SpriteMorph.prototype.addCostume = function (costume) {
|
|
if (!costume.name) {
|
|
costume.name = 'costume' + (this.costumes.length() + 1);
|
|
}
|
|
this.costumes.add(costume);
|
|
};
|
|
|
|
SpriteMorph.prototype.wearCostume = function (costume) {
|
|
var x = this.xPosition ? this.xPosition() : null,
|
|
y = this.yPosition ? this.yPosition() : null,
|
|
isWarped = this.isWarped;
|
|
if (isWarped) {
|
|
this.endWarp();
|
|
}
|
|
this.changed();
|
|
this.costume = costume;
|
|
this.drawNew();
|
|
this.changed();
|
|
if (isWarped) {
|
|
this.startWarp();
|
|
}
|
|
if (x !== null) {
|
|
this.silentGotoXY(x, y, true); // just me
|
|
}
|
|
if (this.positionTalkBubble) { // the stage doesn't talk
|
|
this.positionTalkBubble();
|
|
}
|
|
this.version = Date.now();
|
|
};
|
|
|
|
SpriteMorph.prototype.getCostumeIdx = function () {
|
|
return this.costumes.asArray().indexOf(this.costume) + 1;
|
|
};
|
|
|
|
SpriteMorph.prototype.doWearNextCostume = function () {
|
|
var arr = this.costumes.asArray(),
|
|
idx;
|
|
if (arr.length > 1) {
|
|
idx = arr.indexOf(this.costume);
|
|
if (idx > -1) {
|
|
idx += 1;
|
|
if (idx > (arr.length - 1)) {
|
|
idx = 0;
|
|
}
|
|
this.wearCostume(arr[idx]);
|
|
}
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.doWearPreviousCostume = function () {
|
|
var arr = this.costumes.asArray(),
|
|
idx;
|
|
if (arr.length > 1) {
|
|
idx = arr.indexOf(this.costume);
|
|
if (idx > -1) {
|
|
idx -= 1;
|
|
if (idx < 0) {
|
|
idx = arr.length - 1;
|
|
}
|
|
this.wearCostume(arr[idx]);
|
|
}
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.doSwitchToCostume = function (id) {
|
|
if (id instanceof Costume) { // allow first-class costumes
|
|
this.wearCostume(id);
|
|
return;
|
|
}
|
|
|
|
var num,
|
|
arr = this.costumes.asArray(),
|
|
costume;
|
|
if (
|
|
contains(
|
|
[localize('Turtle'), localize('Empty')],
|
|
(id instanceof Array ? id[0] : null)
|
|
)
|
|
) {
|
|
costume = null;
|
|
} else {
|
|
if (id === -1) {
|
|
this.doWearPreviousCostume();
|
|
return;
|
|
}
|
|
costume = detect(arr, function (cst) {
|
|
return cst.name === id;
|
|
});
|
|
if (costume === null) {
|
|
num = parseFloat(id);
|
|
if (num === 0) {
|
|
costume = null;
|
|
} else {
|
|
costume = arr[num - 1] || null;
|
|
}
|
|
}
|
|
}
|
|
this.wearCostume(costume);
|
|
};
|
|
|
|
SpriteMorph.prototype.reportCostumes = function () {
|
|
return this.costumes;
|
|
};
|
|
|
|
// SpriteMorph sound management
|
|
|
|
SpriteMorph.prototype.addSound = function (audio, name) {
|
|
this.sounds.add(new Sound(audio, name));
|
|
};
|
|
|
|
SpriteMorph.prototype.playSound = function (name) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
sound = detect(
|
|
this.sounds.asArray(),
|
|
function (s) {return s.name === name; }
|
|
),
|
|
active;
|
|
if (sound) {
|
|
active = sound.play();
|
|
if (stage) {
|
|
stage.activeSounds.push(active);
|
|
stage.activeSounds = stage.activeSounds.filter(function (aud) {
|
|
return !aud.ended && !aud.terminated;
|
|
});
|
|
}
|
|
return active;
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.reportSounds = function () {
|
|
return this.sounds;
|
|
};
|
|
|
|
// SpriteMorph user menu
|
|
|
|
SpriteMorph.prototype.userMenu = function () {
|
|
var ide = this.parentThatIsA(IDE_Morph),
|
|
menu = new MenuMorph(this);
|
|
|
|
if (ide && ide.isAppMode) {
|
|
// menu.addItem('help', 'nop');
|
|
return menu;
|
|
}
|
|
if (!this.isClone) {
|
|
menu.addItem("duplicate", 'duplicate');
|
|
}
|
|
menu.addItem("delete", 'remove');
|
|
menu.addItem("move", 'moveCenter');
|
|
if (!this.isClone) {
|
|
menu.addItem("edit", 'edit');
|
|
}
|
|
menu.addLine();
|
|
if (this.anchor) {
|
|
menu.addItem(
|
|
localize('detach from') + ' ' + this.anchor.name,
|
|
'detachFromAnchor'
|
|
);
|
|
}
|
|
if (this.parts.length) {
|
|
menu.addItem('detach all parts', 'detachAllParts');
|
|
}
|
|
menu.addItem("export...", 'exportSprite');
|
|
return menu;
|
|
};
|
|
|
|
SpriteMorph.prototype.exportSprite = function () {
|
|
if (this.isClone) {return; }
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
if (ide) {
|
|
ide.exportSprite(this);
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.edit = function () {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
if (ide && !ide.isAppMode) {
|
|
ide.selectSprite(this);
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.showOnStage = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (stage) {
|
|
this.keepWithin(stage);
|
|
stage.add(this);
|
|
}
|
|
this.show();
|
|
};
|
|
|
|
SpriteMorph.prototype.duplicate = function () {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
if (ide) {
|
|
ide.duplicateSprite(this);
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.remove = function () {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
if (ide) {
|
|
ide.removeSprite(this);
|
|
}
|
|
};
|
|
|
|
// SpriteMorph cloning
|
|
|
|
/*
|
|
clones are temporary, partially shallow copies of sprites that don't
|
|
appear as icons in the corral. Clones get deleted when the red stop button
|
|
is pressed. Shallow-copying clones' scripts and costumes makes spawning
|
|
very fast, so they can be used for particle system simulations.
|
|
This speed-up, however, comes at the cost of some detrimental side
|
|
effects: Changes to a costume or a script of the original sprite are
|
|
in some cases shared with all of its clones, however such shared changes
|
|
are hard to predict for users and not actively propagated, so they don't
|
|
offer any reliable feature, and will not be supported as such.
|
|
Changes to the original sprite's scripts affect all of its clones, unless
|
|
the script contains any custom block whose definition contains one or more
|
|
block variables (in which case the script does get deep-copied).
|
|
The original sprite's scripting area, costumes wardrobe or sounds jukebox
|
|
are also not shared. therefore adding or deleting a script, sound or
|
|
costume in the original sprite has no effect on any of its clones.
|
|
*/
|
|
|
|
SpriteMorph.prototype.createClone = function (immediately) {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (stage && stage.cloneCount <= 2000) {
|
|
this.fullCopy(true).clonify(stage, immediately);
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.clonify = function (stage, immediately) {
|
|
var hats;
|
|
this.parts.forEach(function (part) {
|
|
part.clonify(stage);
|
|
});
|
|
stage.cloneCount += 1;
|
|
this.cloneOriginName = this.isClone ?
|
|
this.cloneOriginName : this.name;
|
|
this.isClone = true;
|
|
this.name = '';
|
|
stage.add(this);
|
|
hats = this.allHatBlocksFor('__clone__init__');
|
|
hats.forEach(function (block) {
|
|
stage.threads.startProcess(
|
|
block,
|
|
stage.isThreadSafe,
|
|
null, // export result
|
|
null, // callback
|
|
null, // is clicked
|
|
immediately // without yielding
|
|
);
|
|
});
|
|
this.endWarp();
|
|
};
|
|
|
|
SpriteMorph.prototype.removeClone = function () {
|
|
if (this.isClone) {
|
|
// this.stopTalking();
|
|
this.parent.threads.stopAllForReceiver(this);
|
|
this.corpsify();
|
|
this.destroy();
|
|
this.parent.cloneCount -= 1;
|
|
}
|
|
};
|
|
|
|
// SpriteMorph deleting
|
|
|
|
SpriteMorph.prototype.corpsify = function () {
|
|
this.isCorpse = true;
|
|
this.version = Date.now();
|
|
};
|
|
|
|
// SpriteMorph primitives
|
|
|
|
// SpriteMorph hiding and showing:
|
|
|
|
/*
|
|
override the inherited behavior to also hide/show all
|
|
nested parts.
|
|
*/
|
|
|
|
SpriteMorph.prototype.hide = function () {
|
|
SpriteMorph.uber.hide.call(this);
|
|
this.parts.forEach(function (part) {part.hide(); });
|
|
};
|
|
|
|
SpriteMorph.prototype.show = function () {
|
|
SpriteMorph.uber.show.call(this);
|
|
this.parts.forEach(function (part) {part.show(); });
|
|
};
|
|
|
|
// SpriteMorph pen color
|
|
|
|
SpriteMorph.prototype.setColor = function (aColor) {
|
|
var x = this.xPosition(),
|
|
y = this.yPosition();
|
|
if (!this.color.eq(aColor)) {
|
|
this.color = aColor.copy();
|
|
if (!this.costume) {
|
|
this.drawNew();
|
|
this.silentGotoXY(x, y);
|
|
}
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.getHue = function () {
|
|
return this.color.hsv()[0] * 100;
|
|
};
|
|
|
|
SpriteMorph.prototype.setHue = function (num) {
|
|
var hsv = this.color.hsv(),
|
|
x = this.xPosition(),
|
|
y = this.yPosition();
|
|
|
|
hsv[0] = Math.max(Math.min(+num || 0, 100), 0) / 100;
|
|
hsv[1] = 1; // we gotta fix this at some time
|
|
this.color.set_hsv.apply(this.color, hsv);
|
|
if (!this.costume) {
|
|
this.drawNew();
|
|
this.changed();
|
|
}
|
|
this.gotoXY(x, y);
|
|
};
|
|
|
|
SpriteMorph.prototype.changeHue = function (delta) {
|
|
this.setHue(this.getHue() + (+delta || 0));
|
|
};
|
|
|
|
SpriteMorph.prototype.getBrightness = function () {
|
|
return this.color.hsv()[2] * 100;
|
|
};
|
|
|
|
SpriteMorph.prototype.setBrightness = function (num) {
|
|
var hsv = this.color.hsv(),
|
|
x = this.xPosition(),
|
|
y = this.yPosition();
|
|
|
|
hsv[1] = 1; // we gotta fix this at some time
|
|
hsv[2] = Math.max(Math.min(+num || 0, 100), 0) / 100;
|
|
this.color.set_hsv.apply(this.color, hsv);
|
|
if (!this.costume) {
|
|
this.drawNew();
|
|
this.changed();
|
|
}
|
|
this.gotoXY(x, y);
|
|
};
|
|
|
|
SpriteMorph.prototype.changeBrightness = function (delta) {
|
|
this.setBrightness(this.getBrightness() + (+delta || 0));
|
|
};
|
|
|
|
// SpriteMorph layers
|
|
|
|
SpriteMorph.prototype.comeToFront = function () {
|
|
if (this.parent) {
|
|
this.parent.add(this);
|
|
this.changed();
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.goBack = function (layers) {
|
|
var layer, newLayer = +layers || 0;
|
|
if (!this.parent) {return null; }
|
|
layer = this.parent.children.indexOf(this);
|
|
if (layer < newLayer) {return null; }
|
|
this.parent.removeChild(this);
|
|
this.parent.children.splice(layer - newLayer, null, this);
|
|
this.parent.changed();
|
|
};
|
|
|
|
// SpriteMorph collision detection optimization
|
|
|
|
SpriteMorph.prototype.overlappingImage = function (otherSprite) {
|
|
// overrides method from Morph because Sprites aren't nested Morphs
|
|
var oRect = this.bounds.intersect(otherSprite.bounds),
|
|
oImg = newCanvas(oRect.extent(), true),
|
|
ctx = oImg.getContext('2d');
|
|
|
|
if (oRect.width() < 1 || oRect.height() < 1) {
|
|
return newCanvas(new Point(1, 1), true);
|
|
}
|
|
ctx.drawImage(
|
|
this.image,
|
|
this.left() - oRect.left(),
|
|
this.top() - oRect.top()
|
|
);
|
|
ctx.globalCompositeOperation = 'source-in';
|
|
ctx.drawImage(
|
|
otherSprite.image,
|
|
otherSprite.left() - oRect.left(),
|
|
otherSprite.top() - oRect.top()
|
|
);
|
|
return oImg;
|
|
};
|
|
|
|
// SpriteMorph stamping
|
|
|
|
SpriteMorph.prototype.doStamp = function () {
|
|
var stage = this.parent,
|
|
context = stage.penTrails().getContext('2d'),
|
|
isWarped = this.isWarped,
|
|
originalAlpha = context.globalAlpha;
|
|
|
|
if (isWarped) {
|
|
this.endWarp();
|
|
}
|
|
context.save();
|
|
context.scale(1 / stage.scale, 1 / stage.scale);
|
|
context.globalAlpha = this.alpha;
|
|
context.drawImage(
|
|
this.image,
|
|
(this.left() - stage.left()),
|
|
(this.top() - stage.top())
|
|
);
|
|
context.globalAlpha = originalAlpha;
|
|
context.restore();
|
|
this.changed();
|
|
if (isWarped) {
|
|
this.startWarp();
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.clear = function () {
|
|
this.parent.clearPenTrails();
|
|
};
|
|
|
|
// SpriteMorph pen size
|
|
|
|
SpriteMorph.prototype.setSize = function (size) {
|
|
// pen size
|
|
if (!isNaN(size)) {
|
|
this.size = Math.min(Math.max(+size, 0.0001), 1000);
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.changeSize = function (delta) {
|
|
this.setSize(this.size + (+delta || 0));
|
|
};
|
|
|
|
// SpriteMorph scale
|
|
|
|
SpriteMorph.prototype.getScale = function () {
|
|
// answer my scale in percent
|
|
return this.scale * 100;
|
|
};
|
|
|
|
SpriteMorph.prototype.setScale = function (percentage) {
|
|
// set my (absolute) scale in percent
|
|
var x = this.xPosition(),
|
|
y = this.yPosition(),
|
|
isWarped = this.isWarped,
|
|
realScale,
|
|
growth;
|
|
|
|
if (isWarped) {
|
|
this.endWarp();
|
|
}
|
|
realScale = (+percentage || 0) / 100;
|
|
growth = realScale / this.nestingScale;
|
|
this.nestingScale = realScale;
|
|
this.scale = Math.max(realScale, 0.01);
|
|
|
|
// apply to myself
|
|
this.changed();
|
|
this.drawNew();
|
|
this.changed();
|
|
if (isWarped) {
|
|
this.startWarp();
|
|
}
|
|
this.silentGotoXY(x, y, true); // just me
|
|
this.positionTalkBubble();
|
|
|
|
// propagate to nested parts
|
|
this.parts.forEach(function (part) {
|
|
var xDist = part.xPosition() - x,
|
|
yDist = part.yPosition() - y;
|
|
part.setScale(part.scale * 100 * growth);
|
|
part.silentGotoXY(
|
|
x + (xDist * growth),
|
|
y + (yDist * growth)
|
|
);
|
|
});
|
|
};
|
|
|
|
SpriteMorph.prototype.changeScale = function (delta) {
|
|
this.setScale(this.getScale() + (+delta || 0));
|
|
};
|
|
|
|
// Spritemorph graphic effects
|
|
|
|
SpriteMorph.prototype.graphicsChanged = function () {
|
|
var myself = this;
|
|
return Object.keys(this.graphicsValues).some(
|
|
function (any) {
|
|
return myself.graphicsValues[any] < 0 ||
|
|
myself.graphicsValues[any] > 0;
|
|
}
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.applyGraphicsEffects = function (canvas) {
|
|
// For every effect: apply transform of that effect(canvas, stored value)
|
|
// Graphic effects from Scratch are heavily based on ScratchPlugin.c
|
|
|
|
var ctx, imagedata;
|
|
|
|
function transform_fisheye (imagedata, value) {
|
|
var pixels, newImageData, newPixels, centerX, centerY,
|
|
w, h, x, y, dx, dy, r, angle, srcX, srcY, i, srcI;
|
|
|
|
w = imagedata.width;
|
|
h = imagedata.height;
|
|
pixels = imagedata.data;
|
|
newImageData = ctx.createImageData(w, h);
|
|
newPixels = newImageData.data;
|
|
|
|
centerX = w / 2;
|
|
centerY = h / 2;
|
|
value = Math.max(0, (value + 100) / 100);
|
|
for (y = 0; y < h; y++) {
|
|
for (x = 0; x < w; x++) {
|
|
dx = (x - centerX) / centerX;
|
|
dy = (y - centerY) / centerY;
|
|
r = Math.pow(Math.sqrt(dx * dx + dy * dy), value);
|
|
if (r <= 1) {
|
|
angle = Math.atan2(dy, dx);
|
|
srcX = Math.floor(centerX + (r * Math.cos(angle) * centerX));
|
|
srcY = Math.floor(centerY + (r * Math.sin(angle) * centerY));
|
|
} else {
|
|
srcX = x;
|
|
srcY = y;
|
|
}
|
|
i = (y * w + x) * 4;
|
|
srcI = (srcY * w + srcX) * 4;
|
|
newPixels[i] = pixels[srcI];
|
|
newPixels[i + 1] = pixels[srcI + 1];
|
|
newPixels[i + 2] = pixels[srcI + 2];
|
|
newPixels[i + 3] = pixels[srcI + 3];
|
|
}
|
|
}
|
|
return newImageData;
|
|
}
|
|
|
|
function transform_whirl (imagedata, value) {
|
|
var pixels, newImageData, newPixels, w, h, centerX, centerY,
|
|
x, y, radius, scaleX, scaleY, whirlRadians, radiusSquared,
|
|
dx, dy, d, factor, angle, srcX, srcY, i, srcI, sina, cosa;
|
|
|
|
w = imagedata.width;
|
|
h = imagedata.height;
|
|
pixels = imagedata.data;
|
|
newImageData = ctx.createImageData(w, h);
|
|
newPixels = newImageData.data;
|
|
|
|
centerX = w / 2;
|
|
centerY = h / 2;
|
|
radius = Math.min(centerX, centerY);
|
|
if (w < h) {
|
|
scaleX = h / w;
|
|
scaleY = 1;
|
|
} else {
|
|
scaleX = 1;
|
|
scaleY = w / h;
|
|
}
|
|
whirlRadians = -radians(value);
|
|
radiusSquared = radius * radius;
|
|
for (y = 0; y < h; y++) {
|
|
for (x = 0; x < w; x++) {
|
|
dx = scaleX * (x - centerX);
|
|
dy = scaleY * (y - centerY);
|
|
d = dx * dx + dy * dy;
|
|
if (d < radiusSquared) {
|
|
factor = 1 - (Math.sqrt(d) / radius);
|
|
angle = whirlRadians * (factor * factor);
|
|
sina = Math.sin(angle);
|
|
cosa = Math.cos(angle);
|
|
srcX = Math.floor((cosa * dx - sina * dy) / scaleX + centerX);
|
|
srcY = Math.floor((sina * dx + cosa * dy) / scaleY + centerY);
|
|
} else {
|
|
srcX = x;
|
|
srcY = y;
|
|
}
|
|
i = (y * w + x) * 4;
|
|
srcI = (srcY * w + srcX) * 4;
|
|
newPixels[i] = pixels[srcI];
|
|
newPixels[i + 1] = pixels[srcI + 1];
|
|
newPixels[i + 2] = pixels[srcI + 2];
|
|
newPixels[i + 3] = pixels[srcI + 3];
|
|
}
|
|
}
|
|
return newImageData;
|
|
}
|
|
|
|
function transform_pixelate (imagedata, value) {
|
|
var pixels, newImageData, newPixels, w, h,
|
|
x, y, srcX, srcY, i, srcI;
|
|
|
|
w = imagedata.width;
|
|
h = imagedata.height;
|
|
pixels = imagedata.data;
|
|
newImageData = ctx.createImageData(w, h);
|
|
newPixels = newImageData.data;
|
|
|
|
value = Math.floor(Math.abs(value / 10) + 1);
|
|
for (y = 0; y < h; y++) {
|
|
for (x = 0; x < w; x++) {
|
|
srcX = Math.floor(x / value) * value;
|
|
srcY = Math.floor(y / value) * value;
|
|
i = (y * w + x) * 4;
|
|
srcI = (srcY * w + srcX) * 4;
|
|
newPixels[i] = pixels[srcI];
|
|
newPixels[i + 1] = pixels[srcI + 1];
|
|
newPixels[i + 2] = pixels[srcI + 2];
|
|
newPixels[i + 3] = pixels[srcI + 3];
|
|
}
|
|
}
|
|
return newImageData;
|
|
}
|
|
|
|
function transform_mosaic (imagedata, value) {
|
|
var pixels, i, l, newImageData, newPixels, srcI;
|
|
pixels = imagedata.data;
|
|
newImageData = ctx.createImageData(imagedata.width, imagedata.height);
|
|
newPixels = newImageData.data;
|
|
|
|
value = Math.round((Math.abs(value) + 10) / 10);
|
|
value = Math.max(0, Math.min(value, Math.min(imagedata.width, imagedata.height)));
|
|
for (i = 0, l = pixels.length; i < l; i += 4) {
|
|
srcI = i * value % l;
|
|
newPixels[i] = pixels[srcI];
|
|
newPixels[i + 1] = pixels[srcI + 1];
|
|
newPixels[i + 2] = pixels[srcI + 2];
|
|
newPixels[i + 3] = pixels[srcI + 3];
|
|
}
|
|
return newImageData;
|
|
}
|
|
|
|
function transform_duplicate (imagedata, value) {
|
|
var pixels, i;
|
|
pixels = imagedata.data;
|
|
for (i = 0; i < pixels.length; i += 4) {
|
|
pixels[i] = pixels[i * value];
|
|
pixels[i + 1] = pixels[i * value + 1];
|
|
pixels[i + 2] = pixels[i * value + 2];
|
|
pixels[i + 3] = pixels[i * value + 3];
|
|
}
|
|
return imagedata;
|
|
}
|
|
|
|
function transform_HSV (imagedata, hueShift, saturationShift, brightnessShift) {
|
|
var pixels, index, l, r, g, b, max, min, span,
|
|
h, s, v, i, f, p, q, t, newR, newG, newB;
|
|
pixels = imagedata.data;
|
|
for (index = 0, l = pixels.length; index < l; index += 4) {
|
|
r = pixels[index];
|
|
g = pixels[index + 1];
|
|
b = pixels[index + 2];
|
|
|
|
max = Math.max(r, g, b);
|
|
min = Math.min(r, g, b);
|
|
span = max - min;
|
|
if (span === 0) {
|
|
h = s = 0;
|
|
} else {
|
|
if (max === r) {
|
|
h = (60 * (g - b)) / span;
|
|
} else if (max === g) {
|
|
h = 120 + ((60 * (b - r)) / span);
|
|
} else if (max === b) {
|
|
h = 240 + ((60 * (r - g)) / span);
|
|
}
|
|
s = (max - min) / max;
|
|
}
|
|
if (h < 0) {
|
|
h += 360;
|
|
}
|
|
v = max / 255;
|
|
|
|
h = (h + hueShift * 360 / 200) % 360;
|
|
s = Math.max(0, Math.min(s + saturationShift / 100, 1));
|
|
v = Math.max(0, Math.min(v + brightnessShift / 100, 1));
|
|
|
|
i = Math.floor(h / 60);
|
|
f = (h / 60) - i;
|
|
p = v * (1 - s);
|
|
q = v * (1 - (s * f));
|
|
t = v * (1 - (s * (1 - f)));
|
|
|
|
if (i === 0 || i === 6) {
|
|
newR = v;
|
|
newG = t;
|
|
newB = p;
|
|
} else if (i === 1) {
|
|
newR = q;
|
|
newG = v;
|
|
newB = p;
|
|
} else if (i === 2) {
|
|
newR = p;
|
|
newG = v;
|
|
newB = t;
|
|
} else if (i === 3) {
|
|
newR = p;
|
|
newG = q;
|
|
newB = v;
|
|
} else if (i === 4) {
|
|
newR = t;
|
|
newG = p;
|
|
newB = v;
|
|
} else if (i === 5) {
|
|
newR = v;
|
|
newG = p;
|
|
newB = q;
|
|
}
|
|
|
|
pixels[index] = newR * 255;
|
|
pixels[index + 1] = newG * 255;
|
|
pixels[index + 2] = newB * 255;
|
|
}
|
|
return imagedata;
|
|
}
|
|
|
|
function transform_negative (imagedata, value) {
|
|
var pixels, i, l, rcom, gcom, bcom;
|
|
pixels = imagedata.data;
|
|
for (i = 0, l = pixels.length; i < l; i += 4) {
|
|
rcom = 255 - pixels[i];
|
|
gcom = 255 - pixels[i + 1];
|
|
bcom = 255 - pixels[i + 2];
|
|
|
|
if (pixels[i] < rcom) { //compare to the complement
|
|
pixels[i] += value;
|
|
} else if (pixels[i] > rcom) {
|
|
pixels[i] -= value;
|
|
}
|
|
if (pixels[i + 1] < gcom) {
|
|
pixels[i + 1] += value;
|
|
} else if (pixels[i + 1] > gcom) {
|
|
pixels[i + 1] -= value;
|
|
}
|
|
if (pixels[i + 2] < bcom) {
|
|
pixels[i + 2] += value;
|
|
} else if (pixels[i + 2] > bcom) {
|
|
pixels[i + 2] -= value;
|
|
}
|
|
}
|
|
return imagedata;
|
|
}
|
|
|
|
function transform_comic (imagedata, value) {
|
|
var pixels, i, l;
|
|
pixels = imagedata.data;
|
|
for (i = 0, l = pixels.length; i < l; i += 4) {
|
|
pixels[i] += Math.sin(i * value) * 127 + 128;
|
|
pixels[i + 1] += Math.sin(i * value) * 127 + 128;
|
|
pixels[i + 2] += Math.sin(i * value) * 127 + 128;
|
|
}
|
|
return imagedata;
|
|
}
|
|
|
|
function transform_confetti (imagedata, value) {
|
|
var pixels, i, l;
|
|
pixels = imagedata.data;
|
|
for (i = 0, l = pixels.length; i < l; i += 1) {
|
|
pixels[i] = Math.sin(value * pixels[i]) * 127 + pixels[i];
|
|
}
|
|
return imagedata;
|
|
}
|
|
|
|
if (this.graphicsChanged()) {
|
|
ctx = canvas.getContext("2d");
|
|
imagedata = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
|
|
if (this.graphicsValues.fisheye) {
|
|
imagedata = transform_fisheye(imagedata, this.graphicsValues.fisheye);
|
|
}
|
|
if (this.graphicsValues.whirl) {
|
|
imagedata = transform_whirl(imagedata, this.graphicsValues.whirl);
|
|
}
|
|
if (this.graphicsValues.pixelate) {
|
|
imagedata = transform_pixelate(imagedata, this.graphicsValues.pixelate);
|
|
}
|
|
if (this.graphicsValues.mosaic) {
|
|
imagedata = transform_mosaic(imagedata, this.graphicsValues.mosaic);
|
|
}
|
|
if (this.graphicsValues.duplicate) {
|
|
imagedata = transform_duplicate(imagedata, this.graphicsValues.duplicate);
|
|
}
|
|
if (this.graphicsValues.color || this.graphicsValues.saturation || this.graphicsValues.brightness) {
|
|
imagedata = transform_HSV(
|
|
imagedata,
|
|
this.graphicsValues.color,
|
|
this.graphicsValues.saturation,
|
|
this.graphicsValues.brightness
|
|
);
|
|
}
|
|
if (this.graphicsValues.negative) {
|
|
imagedata = transform_negative(imagedata, this.graphicsValues.negative);
|
|
}
|
|
if (this.graphicsValues.comic) {
|
|
imagedata = transform_comic(imagedata, this.graphicsValues.comic);
|
|
}
|
|
if (this.graphicsValues.confetti) {
|
|
imagedata = transform_confetti(imagedata, this.graphicsValues.confetti);
|
|
}
|
|
|
|
ctx.putImageData(imagedata, 0, 0);
|
|
}
|
|
|
|
return canvas;
|
|
};
|
|
|
|
SpriteMorph.prototype.setEffect = function (effect, value) {
|
|
var eff = effect instanceof Array ? effect[0] : null;
|
|
if (eff === 'ghost') {
|
|
this.alpha = 1 - Math.min(Math.max(+value || 0, 0), 100) / 100;
|
|
} else {
|
|
this.graphicsValues[eff] = +value;
|
|
}
|
|
this.drawNew();
|
|
this.changed();
|
|
};
|
|
|
|
SpriteMorph.prototype.getGhostEffect = function () {
|
|
return (1 - this.alpha) * 100;
|
|
};
|
|
|
|
SpriteMorph.prototype.changeEffect = function (effect, value) {
|
|
var eff = effect instanceof Array ? effect[0] : null;
|
|
if (eff === 'ghost') {
|
|
this.setEffect(effect, this.getGhostEffect() + (+value || 0));
|
|
} else {
|
|
this.setEffect(effect, +this.graphicsValues[eff] + (+value));
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.clearEffects = function () {
|
|
var effect;
|
|
for (effect in this.graphicsValues) {
|
|
if (this.graphicsValues.hasOwnProperty(effect)) {
|
|
this.setEffect([effect], 0);
|
|
}
|
|
}
|
|
this.setEffect(['ghost'], 0);
|
|
};
|
|
|
|
// SpriteMorph talk bubble
|
|
|
|
SpriteMorph.prototype.stopTalking = function () {
|
|
var bubble = this.talkBubble();
|
|
if (bubble) {bubble.destroy(); }
|
|
};
|
|
|
|
SpriteMorph.prototype.doThink = function (data) {
|
|
this.bubble(data, true);
|
|
};
|
|
|
|
SpriteMorph.prototype.bubble = function (data, isThought, isQuestion) {
|
|
var bubble,
|
|
stage = this.parentThatIsA(StageMorph);
|
|
|
|
this.stopTalking();
|
|
if (data === '' || isNil(data)) {return; }
|
|
bubble = new SpriteBubbleMorph(
|
|
data,
|
|
stage,
|
|
isThought,
|
|
isQuestion
|
|
);
|
|
this.add(bubble);
|
|
this.positionTalkBubble();
|
|
};
|
|
|
|
SpriteMorph.prototype.talkBubble = function () {
|
|
return detect(
|
|
this.children,
|
|
function (morph) {return morph instanceof SpeechBubbleMorph; }
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.positionTalkBubble = function () {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
stageScale = stage ? stage.scale : 1,
|
|
bubble = this.talkBubble(),
|
|
middle = this.center().y;
|
|
if (!bubble) {return null; }
|
|
bubble.show();
|
|
if (!bubble.isPointingRight) {
|
|
bubble.isPointingRight = true;
|
|
bubble.drawNew();
|
|
bubble.changed();
|
|
}
|
|
bubble.setLeft(this.right());
|
|
bubble.setBottom(this.top());
|
|
while (!this.isTouching(bubble) && bubble.bottom() < middle) {
|
|
bubble.silentMoveBy(new Point(-1, 1).scaleBy(stageScale));
|
|
}
|
|
if (!stage) {return null; }
|
|
if (bubble.right() > stage.right()) {
|
|
bubble.isPointingRight = false;
|
|
bubble.drawNew();
|
|
bubble.setRight(this.center().x);
|
|
}
|
|
bubble.keepWithin(stage);
|
|
bubble.changed();
|
|
};
|
|
|
|
// dragging and dropping adjustments b/c of talk bubbles and parts
|
|
|
|
SpriteMorph.prototype.prepareToBeGrabbed = function (hand) {
|
|
this.removeShadow();
|
|
this.recordLayers();
|
|
if (!this.bounds.containsPoint(hand.position()) &&
|
|
this.isCorrectingOutsideDrag()) {
|
|
this.setCenter(hand.position());
|
|
}
|
|
this.addShadow();
|
|
};
|
|
|
|
SpriteMorph.prototype.isCorrectingOutsideDrag = function () {
|
|
// make sure I don't "trail behind" the hand when dragged
|
|
// override for morphs that you want to be dragged outside
|
|
// their full bounds
|
|
return !this.parts.length;
|
|
};
|
|
|
|
SpriteMorph.prototype.justDropped = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (stage) {
|
|
stage.enableCustomHatBlocks = true;
|
|
}
|
|
this.restoreLayers();
|
|
this.positionTalkBubble();
|
|
this.receiveUserInteraction('dropped');
|
|
};
|
|
|
|
// SpriteMorph drawing:
|
|
|
|
SpriteMorph.prototype.drawLine = function (start, dest) {
|
|
var stagePos = this.parent.bounds.origin,
|
|
stageScale = this.parent.scale,
|
|
context = this.parent.penTrails().getContext('2d'),
|
|
from = start.subtract(stagePos).divideBy(stageScale),
|
|
to = dest.subtract(stagePos).divideBy(stageScale),
|
|
damagedFrom = from.multiplyBy(stageScale).add(stagePos),
|
|
damagedTo = to.multiplyBy(stageScale).add(stagePos),
|
|
damaged = damagedFrom.rectangle(damagedTo).expandBy(
|
|
Math.max(this.size * stageScale / 2, 1)
|
|
).intersect(this.parent.visibleBounds()).spread();
|
|
|
|
if (this.isDown) {
|
|
context.lineWidth = this.size;
|
|
context.strokeStyle = this.color.toString();
|
|
if (this.useFlatLineEnds) {
|
|
context.lineCap = 'butt';
|
|
context.lineJoin = 'miter';
|
|
} else {
|
|
context.lineCap = 'round';
|
|
context.lineJoin = 'round';
|
|
}
|
|
context.beginPath();
|
|
context.moveTo(from.x, from.y);
|
|
context.lineTo(to.x, to.y);
|
|
context.stroke();
|
|
if (this.isWarped === false) {
|
|
this.world().broken.push(damaged);
|
|
}
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.floodFill = function () {
|
|
if (!this.parent.bounds.containsPoint(this.rotationCenter())) {
|
|
return;
|
|
}
|
|
var layer = normalizeCanvas(this.parent.penTrails()),
|
|
width = layer.width,
|
|
height = layer.height,
|
|
ctx = layer.getContext('2d'),
|
|
img = ctx.getImageData(0, 0, width, height),
|
|
dta = img.data,
|
|
stack = [
|
|
((height / 2) - Math.round(this.yPosition())) * width +
|
|
Math.round(this.xPosition() + (width / 2))
|
|
],
|
|
current,
|
|
src;
|
|
|
|
function read(p) {
|
|
var d = p * 4;
|
|
return [dta[d], dta[d + 1], dta[d + 2], dta[d + 3]];
|
|
}
|
|
|
|
function check(p) {
|
|
return p[0] === src[0] &&
|
|
p[1] === src[1] &&
|
|
p[2] === src[2] &&
|
|
p[3] === src[3];
|
|
}
|
|
|
|
src = read(stack[0]);
|
|
if (src[0] === Math.round(this.color.r) &&
|
|
src[1] === Math.round(this.color.g) &&
|
|
src[2] === Math.round(this.color.b) &&
|
|
src[3] === Math.round(this.color.a * 255)) {
|
|
return;
|
|
}
|
|
while (stack.length > 0) {
|
|
current = stack.pop();
|
|
if (check(read(current))) {
|
|
if (current % width > 1) {
|
|
stack.push(current + 1);
|
|
stack.push(current - 1);
|
|
}
|
|
if (current > 0 && current < height * width) {
|
|
stack.push(current + width);
|
|
stack.push(current - width);
|
|
}
|
|
}
|
|
dta[current * 4] = Math.round(this.color.r);
|
|
dta[current * 4 + 1] = Math.round(this.color.g);
|
|
dta[current * 4 + 2] = Math.round(this.color.b);
|
|
dta[current * 4 + 3] = Math.round(this.color.a * 255);
|
|
}
|
|
ctx.putImageData(img, 0, 0);
|
|
this.parent.changed();
|
|
};
|
|
|
|
// SpriteMorph motion - adjustments due to nesting
|
|
|
|
SpriteMorph.prototype.moveBy = function (delta, justMe) {
|
|
// override the inherited default to make sure my parts follow
|
|
// unless it's justMe (a correction)
|
|
var start = this.isDown && !justMe && this.parent ?
|
|
this.rotationCenter() : null;
|
|
SpriteMorph.uber.moveBy.call(this, delta);
|
|
if (start) {
|
|
this.drawLine(start, this.rotationCenter());
|
|
}
|
|
if (!justMe) {
|
|
this.parts.forEach(function (part) {
|
|
part.moveBy(delta);
|
|
});
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.silentMoveBy = function (delta, justMe) {
|
|
SpriteMorph.uber.silentMoveBy.call(this, delta);
|
|
if (!justMe && this.parent instanceof HandMorph) {
|
|
this.parts.forEach(function (part) {
|
|
part.moveBy(delta);
|
|
});
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.rootForGrab = function () {
|
|
if (this.anchor) {
|
|
return this.anchor.rootForGrab();
|
|
}
|
|
return SpriteMorph.uber.rootForGrab.call(this);
|
|
};
|
|
|
|
SpriteMorph.prototype.setCenter = function (aPoint, justMe) {
|
|
// override the inherited default to make sure my parts follow
|
|
// unless it's justMe
|
|
var delta = aPoint.subtract(this.center());
|
|
this.moveBy(delta, justMe);
|
|
};
|
|
|
|
SpriteMorph.prototype.nestingBounds = function () {
|
|
// same as fullBounds(), except that it uses "parts" instead of children
|
|
// and special cases the costume-less "arrow" shape's bounding box
|
|
var result = this.bounds;
|
|
if (!this.costume && this.penBounds) {
|
|
result = this.penBounds.translateBy(this.position());
|
|
}
|
|
this.parts.forEach(function (part) {
|
|
if (part.isVisible) {
|
|
result = result.merge(part.nestingBounds());
|
|
}
|
|
});
|
|
return result;
|
|
};
|
|
|
|
// SpriteMorph motion primitives
|
|
|
|
SpriteMorph.prototype.setPosition = function (aPoint, justMe) {
|
|
// override the inherited default to make sure my parts follow
|
|
// unless it's justMe
|
|
var delta = aPoint.subtract(this.topLeft());
|
|
if ((delta.x !== 0) || (delta.y !== 0)) {
|
|
this.moveBy(delta, justMe);
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.forward = function (steps) {
|
|
var dest,
|
|
dist = steps * this.parent.scale || 0;
|
|
|
|
if (dist >= 0) {
|
|
dest = this.position().distanceAngle(dist, this.heading);
|
|
} else {
|
|
dest = this.position().distanceAngle(
|
|
Math.abs(dist),
|
|
(this.heading - 180)
|
|
);
|
|
}
|
|
this.setPosition(dest);
|
|
this.positionTalkBubble();
|
|
};
|
|
|
|
SpriteMorph.prototype.setHeading = function (degrees) {
|
|
var x = this.xPosition(),
|
|
y = this.yPosition(),
|
|
dir = (+degrees || 0),
|
|
turn = dir - this.heading;
|
|
|
|
// apply to myself
|
|
if (this.rotationStyle) { // optimization, only redraw if rotatable
|
|
this.changed();
|
|
SpriteMorph.uber.setHeading.call(this, dir);
|
|
this.silentGotoXY(x, y, true); // just me
|
|
this.positionTalkBubble();
|
|
} else {
|
|
this.heading = parseFloat(degrees) % 360;
|
|
}
|
|
|
|
// propagate to my parts
|
|
this.parts.forEach(function (part) {
|
|
var pos = new Point(part.xPosition(), part.yPosition()),
|
|
trg = pos.rotateBy(radians(turn), new Point(x, y));
|
|
if (part.rotatesWithAnchor) {
|
|
part.turn(turn);
|
|
}
|
|
part.gotoXY(trg.x, trg.y);
|
|
});
|
|
};
|
|
|
|
SpriteMorph.prototype.faceToXY = function (x, y) {
|
|
var deltaX = (x - this.xPosition()) * this.parent.scale,
|
|
deltaY = (y - this.yPosition()) * this.parent.scale,
|
|
angle = Math.abs(deltaX) < 0.001 ? (deltaY < 0 ? 90 : 270)
|
|
: Math.round(
|
|
(deltaX >= 0 ? 0 : 180)
|
|
- (Math.atan(deltaY / deltaX) * 57.2957795131)
|
|
);
|
|
this.setHeading(angle + 90);
|
|
};
|
|
|
|
SpriteMorph.prototype.turn = function (degrees) {
|
|
this.setHeading(this.heading + (+degrees || 0));
|
|
};
|
|
|
|
SpriteMorph.prototype.turnLeft = function (degrees) {
|
|
this.setHeading(this.heading - (+degrees || 0));
|
|
};
|
|
|
|
SpriteMorph.prototype.xPosition = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
|
|
if (!stage && this.parent.grabOrigin) { // I'm currently being dragged
|
|
stage = this.parent.grabOrigin.origin;
|
|
}
|
|
if (stage) {
|
|
return (this.rotationCenter().x - stage.center().x) / stage.scale;
|
|
}
|
|
return this.rotationCenter().x;
|
|
};
|
|
|
|
SpriteMorph.prototype.yPosition = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
|
|
if (!stage && this.parent.grabOrigin) { // I'm currently being dragged
|
|
stage = this.parent.grabOrigin.origin;
|
|
}
|
|
if (stage) {
|
|
return (stage.center().y - this.rotationCenter().y) / stage.scale;
|
|
}
|
|
return this.rotationCenter().y;
|
|
};
|
|
|
|
SpriteMorph.prototype.direction = function () {
|
|
return this.heading;
|
|
};
|
|
|
|
SpriteMorph.prototype.penSize = function () {
|
|
return this.size;
|
|
};
|
|
|
|
SpriteMorph.prototype.gotoXY = function (x, y, justMe) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
newX,
|
|
newY,
|
|
dest;
|
|
|
|
if (!stage) {return; }
|
|
newX = stage.center().x + (+x || 0) * stage.scale;
|
|
newY = stage.center().y - (+y || 0) * stage.scale;
|
|
if (this.costume) {
|
|
dest = new Point(newX, newY).subtract(this.rotationOffset);
|
|
} else {
|
|
dest = new Point(newX, newY).subtract(this.extent().divideBy(2));
|
|
}
|
|
this.setPosition(dest, justMe);
|
|
this.positionTalkBubble();
|
|
};
|
|
|
|
SpriteMorph.prototype.silentGotoXY = function (x, y, justMe) {
|
|
// move without drawing
|
|
var penState = this.isDown;
|
|
this.isDown = false;
|
|
this.gotoXY(x, y, justMe);
|
|
this.isDown = penState;
|
|
};
|
|
|
|
SpriteMorph.prototype.setXPosition = function (num) {
|
|
this.gotoXY(+num || 0, this.yPosition());
|
|
};
|
|
|
|
SpriteMorph.prototype.changeXPosition = function (delta) {
|
|
this.setXPosition(this.xPosition() + (+delta || 0));
|
|
};
|
|
|
|
SpriteMorph.prototype.setYPosition = function (num) {
|
|
this.gotoXY(this.xPosition(), +num || 0);
|
|
};
|
|
|
|
SpriteMorph.prototype.changeYPosition = function (delta) {
|
|
this.setYPosition(this.yPosition() + (+delta || 0));
|
|
};
|
|
|
|
SpriteMorph.prototype.glide = function (
|
|
duration,
|
|
endX,
|
|
endY,
|
|
elapsed,
|
|
startPoint
|
|
) {
|
|
var fraction, endPoint, rPos;
|
|
endPoint = new Point(endX, endY);
|
|
fraction = Math.max(Math.min(elapsed / duration, 1), 0);
|
|
rPos = startPoint.add(
|
|
endPoint.subtract(startPoint).multiplyBy(fraction)
|
|
);
|
|
this.gotoXY(rPos.x, rPos.y);
|
|
};
|
|
|
|
SpriteMorph.prototype.bounceOffEdge = function () {
|
|
// taking nested parts into account
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
fb = this.nestingBounds(),
|
|
dirX,
|
|
dirY;
|
|
|
|
if (!stage) {return null; }
|
|
if (stage.bounds.containsRectangle(fb)) {return null; }
|
|
|
|
dirX = Math.cos(radians(this.heading - 90));
|
|
dirY = -(Math.sin(radians(this.heading - 90)));
|
|
|
|
if (fb.left() < stage.left()) {
|
|
dirX = Math.abs(dirX);
|
|
}
|
|
if (fb.right() > stage.right()) {
|
|
dirX = -(Math.abs(dirX));
|
|
}
|
|
if (fb.top() < stage.top()) {
|
|
dirY = -(Math.abs(dirY));
|
|
}
|
|
if (fb.bottom() > stage.bottom()) {
|
|
dirY = Math.abs(dirY);
|
|
}
|
|
|
|
this.setHeading(degrees(Math.atan2(-dirY, dirX)) + 90);
|
|
this.setPosition(this.position().add(
|
|
fb.amountToTranslateWithin(stage.bounds)
|
|
));
|
|
this.positionTalkBubble();
|
|
};
|
|
|
|
// SpriteMorph rotation center / fixation point manipulation
|
|
|
|
SpriteMorph.prototype.setRotationX = function (absoluteX) {
|
|
this.setRotationCenter(new Point(absoluteX, this.yPosition()));
|
|
};
|
|
|
|
SpriteMorph.prototype.setRotationY = function (absoluteY) {
|
|
this.setRotationCenter(new Point(this.xPosition(), absoluteY));
|
|
};
|
|
|
|
SpriteMorph.prototype.setRotationCenter = function (absoluteCoordinate) {
|
|
var delta, normal;
|
|
if (!this.costume) {
|
|
throw new Error('setting the rotation center requires a costume');
|
|
}
|
|
delta = absoluteCoordinate.subtract(
|
|
new Point(this.xPosition(), this.yPosition())
|
|
).divideBy(this.scale).rotateBy(radians(90 - this.heading));
|
|
normal = this.costume.rotationCenter.add(new Point(delta.x, -delta.y));
|
|
this.costume.rotationCenter = normal;
|
|
this.drawNew();
|
|
};
|
|
|
|
SpriteMorph.prototype.xCenter = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
|
|
if (!stage && this.parent.grabOrigin) { // I'm currently being dragged
|
|
stage = this.parent.grabOrigin.origin;
|
|
}
|
|
if (stage) {
|
|
return (this.center().x - stage.center().x) / stage.scale;
|
|
}
|
|
return this.center().x;
|
|
};
|
|
|
|
SpriteMorph.prototype.yCenter = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
|
|
if (!stage && this.parent.grabOrigin) { // I'm currently being dragged
|
|
stage = this.parent.grabOrigin.origin;
|
|
}
|
|
if (stage) {
|
|
return (stage.center().y - this.center().y) / stage.scale;
|
|
}
|
|
return this.center().y;
|
|
};
|
|
|
|
// SpriteMorph message broadcasting
|
|
|
|
SpriteMorph.prototype.allMessageNames = function () {
|
|
var msgs = [],
|
|
all = this.scripts.children.slice();
|
|
this.customBlocks.forEach(function (def) {
|
|
if (def.body) {
|
|
all.push(def.body.expression);
|
|
}
|
|
def.scripts.forEach(function (scr) {
|
|
all.push(scr);
|
|
});
|
|
});
|
|
if (this.globalBlocks) {
|
|
this.globalBlocks.forEach(function (def) {
|
|
if (def.body) {
|
|
all.push(def.body.expression);
|
|
}
|
|
def.scripts.forEach(function (scr) {
|
|
all.push(scr);
|
|
});
|
|
});
|
|
}
|
|
all.forEach(function (script) {
|
|
script.allChildren().forEach(function (morph) {
|
|
var txt;
|
|
if (morph.selector && contains(
|
|
['receiveMessage', 'doBroadcast', 'doBroadcastAndWait'],
|
|
morph.selector
|
|
)) {
|
|
txt = morph.inputs()[0].evaluate();
|
|
if (isString(txt) && txt !== '') {
|
|
if (!contains(msgs, txt)) {
|
|
msgs.push(txt);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
return msgs;
|
|
};
|
|
|
|
SpriteMorph.prototype.allHatBlocksFor = function (message) {
|
|
if (typeof message === 'number') {message = message.toString(); }
|
|
return this.scripts.children.filter(function (morph) {
|
|
var event;
|
|
if (morph.selector) {
|
|
if (morph.selector === 'receiveMessage') {
|
|
event = morph.inputs()[0].evaluate();
|
|
return event === message
|
|
|| (event instanceof Array
|
|
&& message !== '__shout__go__'
|
|
&& message !== '__clone__init__');
|
|
}
|
|
if (morph.selector === 'receiveGo') {
|
|
return message === '__shout__go__';
|
|
}
|
|
if (morph.selector === 'receiveOnClone') {
|
|
return message === '__clone__init__';
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
};
|
|
|
|
SpriteMorph.prototype.allHatBlocksForKey = function (key) {
|
|
return this.scripts.children.filter(function (morph) {
|
|
if (morph.selector) {
|
|
if (morph.selector === 'receiveKey') {
|
|
var evt = morph.inputs()[0].evaluate()[0];
|
|
return evt === key || evt === 'any key';
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
};
|
|
|
|
SpriteMorph.prototype.allHatBlocksForInteraction = function (interaction) {
|
|
return this.scripts.children.filter(function (morph) {
|
|
if (morph.selector) {
|
|
if (morph.selector === 'receiveInteraction') {
|
|
return morph.inputs()[0].evaluate()[0] === interaction;
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
};
|
|
|
|
SpriteMorph.prototype.allGenericHatBlocks = function () {
|
|
return this.scripts.children.filter(function (morph) {
|
|
if (morph.selector) {
|
|
return morph.selector === 'receiveCondition';
|
|
}
|
|
return false;
|
|
});
|
|
};
|
|
|
|
// SpriteMorph events
|
|
|
|
SpriteMorph.prototype.mouseClickLeft = function () {
|
|
return this.receiveUserInteraction('clicked');
|
|
};
|
|
|
|
SpriteMorph.prototype.mouseEnter = function () {
|
|
return this.receiveUserInteraction('mouse-entered');
|
|
};
|
|
|
|
SpriteMorph.prototype.mouseDownLeft = function () {
|
|
return this.receiveUserInteraction('pressed');
|
|
};
|
|
|
|
SpriteMorph.prototype.receiveUserInteraction = function (interaction) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
procs = [],
|
|
hats;
|
|
if (!stage) {return; } // currently dragged
|
|
hats = this.allHatBlocksForInteraction(interaction);
|
|
hats.forEach(function (block) {
|
|
procs.push(stage.threads.startProcess(block, stage.isThreadSafe));
|
|
});
|
|
return procs;
|
|
};
|
|
|
|
SpriteMorph.prototype.mouseDoubleClick = function () {
|
|
if (this.isClone) {return; }
|
|
this.edit();
|
|
};
|
|
|
|
// SpriteMorph timer
|
|
|
|
SpriteMorph.prototype.getTimer = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (stage) {
|
|
return stage.getTimer();
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
// SpriteMorph tempo
|
|
|
|
SpriteMorph.prototype.getTempo = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (stage) {
|
|
return stage.getTempo();
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
// SpriteMorph last message
|
|
|
|
SpriteMorph.prototype.getLastMessage = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (stage) {
|
|
return stage.getLastMessage();
|
|
}
|
|
return '';
|
|
};
|
|
|
|
// SpriteMorph user prompting
|
|
|
|
SpriteMorph.prototype.getLastAnswer = function () {
|
|
return this.parentThatIsA(StageMorph).lastAnswer;
|
|
};
|
|
|
|
// SpriteMorph mouse coordinates
|
|
|
|
SpriteMorph.prototype.reportMouseX = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (stage) {
|
|
return stage.reportMouseX();
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
SpriteMorph.prototype.reportMouseY = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (stage) {
|
|
return stage.reportMouseY();
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
// SpriteMorph thread count (for debugging)
|
|
|
|
SpriteMorph.prototype.reportThreadCount = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (stage) {
|
|
return stage.threads.processes.length;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
// SpriteMorph variable watchers (for palette checkbox toggling)
|
|
|
|
SpriteMorph.prototype.findVariableWatcher = function (varName) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
globals = this.globalVariables(),
|
|
myself = this;
|
|
if (stage === null) {
|
|
return null;
|
|
}
|
|
return detect(
|
|
stage.children,
|
|
function (morph) {
|
|
return morph instanceof WatcherMorph
|
|
&& (morph.target === myself.variables
|
|
|| morph.target === globals)
|
|
&& morph.getter === varName;
|
|
}
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.toggleVariableWatcher = function (varName, isGlobal) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
globals = this.globalVariables(),
|
|
watcher,
|
|
others;
|
|
if (stage === null) {
|
|
return null;
|
|
}
|
|
watcher = this.findVariableWatcher(varName);
|
|
if (watcher !== null) {
|
|
if (watcher.isVisible) {
|
|
watcher.hide();
|
|
} else {
|
|
watcher.show();
|
|
watcher.fixLayout(); // re-hide hidden parts
|
|
watcher.keepWithin(stage);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// if no watcher exists, create a new one
|
|
if (isNil(isGlobal)) {
|
|
isGlobal = contains(globals.names(), varName);
|
|
}
|
|
watcher = new WatcherMorph(
|
|
varName,
|
|
this.blockColor.variables,
|
|
isGlobal ? globals : this.variables,
|
|
varName
|
|
);
|
|
watcher.setPosition(stage.position().add(10));
|
|
others = stage.watchers(watcher.left());
|
|
if (others.length > 0) {
|
|
watcher.setTop(others[others.length - 1].bottom());
|
|
}
|
|
stage.add(watcher);
|
|
watcher.fixLayout();
|
|
watcher.keepWithin(stage);
|
|
return watcher;
|
|
};
|
|
|
|
SpriteMorph.prototype.showingVariableWatcher = function (varName) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
watcher;
|
|
if (stage === null) {
|
|
return false;
|
|
}
|
|
watcher = this.findVariableWatcher(varName);
|
|
if (watcher) {
|
|
return watcher.isVisible;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
SpriteMorph.prototype.deleteVariableWatcher = function (varName) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
watcher;
|
|
if (stage === null) {
|
|
return null;
|
|
}
|
|
watcher = this.findVariableWatcher(varName);
|
|
if (watcher !== null) {
|
|
watcher.destroy();
|
|
}
|
|
};
|
|
|
|
// SpriteMorph non-variable watchers
|
|
|
|
SpriteMorph.prototype.toggleWatcher = function (selector, label, color) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
watcher,
|
|
others;
|
|
if (!stage) { return; }
|
|
watcher = this.watcherFor(stage, selector);
|
|
if (watcher) {
|
|
if (watcher.isVisible) {
|
|
watcher.hide();
|
|
} else {
|
|
watcher.show();
|
|
watcher.fixLayout(); // re-hide hidden parts
|
|
watcher.keepWithin(stage);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// if no watcher exists, create a new one
|
|
watcher = new WatcherMorph(
|
|
label,
|
|
color,
|
|
WatcherMorph.prototype.isGlobal(selector) ? stage : this,
|
|
selector
|
|
);
|
|
watcher.setPosition(stage.position().add(10));
|
|
others = stage.watchers(watcher.left());
|
|
if (others.length > 0) {
|
|
watcher.setTop(others[others.length - 1].bottom());
|
|
}
|
|
stage.add(watcher);
|
|
watcher.fixLayout();
|
|
watcher.keepWithin(stage);
|
|
};
|
|
|
|
SpriteMorph.prototype.showingWatcher = function (selector) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
watcher;
|
|
if (stage === null) {
|
|
return false;
|
|
}
|
|
watcher = this.watcherFor(stage, selector);
|
|
if (watcher) {
|
|
return watcher.isVisible;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
SpriteMorph.prototype.watcherFor = function (stage, selector) {
|
|
var myself = this;
|
|
return detect(stage.children, function (morph) {
|
|
return morph instanceof WatcherMorph &&
|
|
morph.getter === selector &&
|
|
morph.target === (morph.isGlobal(selector) ? stage : myself);
|
|
});
|
|
};
|
|
|
|
// SpriteMorph custom blocks
|
|
|
|
SpriteMorph.prototype.deleteAllBlockInstances = function (definition) {
|
|
this.allBlockInstances(definition).forEach(function (each) {
|
|
each.deleteBlock();
|
|
});
|
|
this.customBlocks.forEach(function (def) {
|
|
if (def.body && def.body.expression.isCorpse) {
|
|
def.body = null;
|
|
}
|
|
});
|
|
};
|
|
|
|
SpriteMorph.prototype.allBlockInstances = function (definition) {
|
|
var stage, objects, blocks = [], inDefinitions;
|
|
if (definition.isGlobal) {
|
|
stage = this.parentThatIsA(StageMorph);
|
|
objects = stage.children.filter(function (morph) {
|
|
return morph instanceof SpriteMorph;
|
|
});
|
|
objects.push(stage);
|
|
objects.forEach(function (sprite) {
|
|
blocks = blocks.concat(sprite.allLocalBlockInstances(definition));
|
|
});
|
|
inDefinitions = [];
|
|
stage.globalBlocks.forEach(function (def) {
|
|
if (def.body) {
|
|
def.body.expression.allChildren().forEach(function (c) {
|
|
if (c.definition && (c.definition === definition)) {
|
|
inDefinitions.push(c);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
return blocks.concat(inDefinitions);
|
|
}
|
|
return this.allLocalBlockInstances(definition);
|
|
};
|
|
|
|
SpriteMorph.prototype.allLocalBlockInstances = function (definition) {
|
|
var inScripts, inDefinitions, inBlockEditors, inPalette, result;
|
|
|
|
inScripts = this.scripts.allChildren().filter(function (c) {
|
|
return c.definition && (c.definition === definition);
|
|
});
|
|
|
|
inDefinitions = [];
|
|
this.customBlocks.forEach(function (def) {
|
|
if (def.body) {
|
|
def.body.expression.allChildren().forEach(function (c) {
|
|
if (c.definition && (c.definition === definition)) {
|
|
inDefinitions.push(c);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
inBlockEditors = this.allEditorBlockInstances(definition);
|
|
inPalette = this.paletteBlockInstance(definition);
|
|
|
|
result = inScripts.concat(inDefinitions).concat(inBlockEditors);
|
|
if (inPalette) {
|
|
result.push(inPalette);
|
|
}
|
|
return result;
|
|
};
|
|
|
|
SpriteMorph.prototype.allEditorBlockInstances = function (definition) {
|
|
var inBlockEditors = [],
|
|
world = this.world();
|
|
|
|
if (!world) {return []; } // when copying a sprite
|
|
|
|
this.world().children.forEach(function (morph) {
|
|
if (morph instanceof BlockEditorMorph) {
|
|
morph.body.contents.allChildren().forEach(function (block) {
|
|
if (!block.isPrototype
|
|
&& !(block instanceof PrototypeHatBlockMorph)
|
|
&& (block.definition === definition)) {
|
|
inBlockEditors.push(block);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
return inBlockEditors;
|
|
};
|
|
|
|
|
|
SpriteMorph.prototype.paletteBlockInstance = function (definition) {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
if (!ide) {return null; }
|
|
return detect(
|
|
ide.palette.contents.children,
|
|
function (block) {
|
|
return block.definition === definition;
|
|
}
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.usesBlockInstance = function (
|
|
definition,
|
|
forRemoval, // optional bool
|
|
skipGlobals, // optional bool
|
|
skipBlocks // optional array with ignorable definitions
|
|
) {
|
|
var inDefinitions,
|
|
inScripts = detect(
|
|
this.scripts.allChildren(),
|
|
function (c) {
|
|
return c.definition && (c.definition === definition);
|
|
}
|
|
);
|
|
|
|
if (inScripts) {return true; }
|
|
|
|
if (definition.isGlobal && !skipGlobals) {
|
|
inDefinitions = [];
|
|
this.parentThatIsA(StageMorph).globalBlocks.forEach(
|
|
function (def) {
|
|
if (forRemoval && (definition === def)) {return; }
|
|
if (skipBlocks && contains(skipBlocks, def)) {return; }
|
|
if (def.body) {
|
|
def.body.expression.allChildren().forEach(function (c) {
|
|
if (c.definition && (c.definition === definition)) {
|
|
inDefinitions.push(c);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
);
|
|
if (inDefinitions.length > 0) {return true; }
|
|
}
|
|
|
|
inDefinitions = [];
|
|
this.customBlocks.forEach(function (def) {
|
|
if (def.body) {
|
|
def.body.expression.allChildren().forEach(function (c) {
|
|
if (c.definition && (c.definition === definition)) {
|
|
inDefinitions.push(c);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
return (inDefinitions.length > 0);
|
|
};
|
|
|
|
SpriteMorph.prototype.doubleDefinitionsFor = function (definition) {
|
|
var spec = definition.blockSpec(),
|
|
blockList,
|
|
idx,
|
|
stage;
|
|
|
|
if (definition.isGlobal) {
|
|
stage = this.parentThatIsA(StageMorph);
|
|
if (!stage) {return []; }
|
|
blockList = stage.globalBlocks;
|
|
} else {
|
|
blockList = this.customBlocks;
|
|
}
|
|
idx = blockList.indexOf(definition);
|
|
if (idx === -1) {return []; }
|
|
return blockList.filter(function (def, i) {
|
|
return def.blockSpec() === spec && (i !== idx);
|
|
});
|
|
};
|
|
|
|
SpriteMorph.prototype.replaceDoubleDefinitionsFor = function (definition) {
|
|
var doubles = this.doubleDefinitionsFor(definition),
|
|
myself = this,
|
|
stage,
|
|
ide;
|
|
doubles.forEach(function (double) {
|
|
myself.allBlockInstances(double).forEach(function (block) {
|
|
block.definition = definition;
|
|
block.refresh();
|
|
});
|
|
});
|
|
if (definition.isGlobal) {
|
|
stage = this.parentThatIsA(StageMorph);
|
|
stage.globalBlocks = stage.globalBlocks.filter(function (def) {
|
|
return !contains(doubles, def);
|
|
});
|
|
} else {
|
|
this.customBlocks = this.customBlocks.filter(function (def) {
|
|
return !contains(doubles, def);
|
|
});
|
|
}
|
|
ide = this.parentThatIsA(IDE_Morph);
|
|
if (ide) {
|
|
ide.flushPaletteCache();
|
|
ide.refreshPalette();
|
|
}
|
|
};
|
|
|
|
// SpriteMorph inheritance - general
|
|
|
|
SpriteMorph.prototype.chooseExemplar = function () {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
myself = this,
|
|
other = stage.children.filter(function (m) {
|
|
return m instanceof SpriteMorph &&
|
|
(!contains(m.allExemplars(), myself));
|
|
}),
|
|
menu;
|
|
menu = new MenuMorph(
|
|
function (aSprite) {myself.setExemplar(aSprite); },
|
|
localize('current parent') +
|
|
':\n' +
|
|
(this.exemplar ? this.exemplar.name : localize('none'))
|
|
);
|
|
other.forEach(function (eachSprite) {
|
|
menu.addItem(eachSprite.name, eachSprite);
|
|
});
|
|
menu.addLine();
|
|
menu.addItem(localize('none'), null);
|
|
menu.popUpAtHand(this.world());
|
|
};
|
|
|
|
SpriteMorph.prototype.setExemplar = function (another) {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
this.exemplar = another;
|
|
if (isNil(another)) {
|
|
this.variables.parentFrame = (this.globalVariables());
|
|
} else {
|
|
this.variables.parentFrame = (another.variables);
|
|
}
|
|
if (ide) {
|
|
ide.flushBlocksCache('variables');
|
|
ide.refreshPalette();
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.allExemplars = function () {
|
|
// including myself
|
|
var all = [],
|
|
current = this;
|
|
while (!isNil(current)) {
|
|
all.push(current);
|
|
current = current.exemplar;
|
|
}
|
|
return all;
|
|
};
|
|
|
|
SpriteMorph.prototype.specimens = function () {
|
|
// without myself
|
|
var myself = this;
|
|
return this.siblings().filter(function (m) {
|
|
return m instanceof SpriteMorph && (m.exemplar === myself);
|
|
});
|
|
};
|
|
|
|
SpriteMorph.prototype.allSpecimens = function () {
|
|
// without myself
|
|
var myself = this;
|
|
return this.siblings().filter(function (m) {
|
|
return m instanceof SpriteMorph && contains(m.allExemplars(), myself);
|
|
});
|
|
};
|
|
|
|
// SpriteMorph inheritance - variables
|
|
|
|
SpriteMorph.prototype.isVariableNameInUse = function (vName, isGlobal) {
|
|
if (isGlobal) {
|
|
return contains(this.variables.allNames(), vName);
|
|
}
|
|
if (contains(this.variables.names(), vName)) {return true; }
|
|
return contains(this.globalVariables().names(), vName);
|
|
};
|
|
|
|
SpriteMorph.prototype.globalVariables = function () {
|
|
var current = this.variables.parentFrame;
|
|
while (current.owner) {
|
|
current = current.parentFrame;
|
|
}
|
|
return current;
|
|
};
|
|
|
|
SpriteMorph.prototype.shadowVar = function (name, value) {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
this.variables.addVar(name, value);
|
|
if (ide) {
|
|
ide.flushBlocksCache('variables');
|
|
ide.refreshPalette();
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.inheritedVariableNames = function (shadowedOnly) {
|
|
var names = [],
|
|
own = this.variables.names(),
|
|
current = this.variables.parentFrame;
|
|
|
|
function test(each) {
|
|
return shadowedOnly ? contains(own, each) : !contains(own, each);
|
|
}
|
|
|
|
while (current.owner instanceof SpriteMorph) {
|
|
names.push.apply(
|
|
names,
|
|
current.names().filter(test)
|
|
);
|
|
current = current.parentFrame;
|
|
}
|
|
return names;
|
|
};
|
|
|
|
SpriteMorph.prototype.deletableVariableNames = function () {
|
|
var locals = this.variables.names(),
|
|
inherited = this.inheritedVariableNames();
|
|
return locals.concat(
|
|
this.globalVariables().names().filter(
|
|
function (each) {
|
|
return !contains(locals, each) && !contains(inherited, each);
|
|
}
|
|
)
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.hasSpriteVariable = function (varName) {
|
|
return contains(this.variables.names(), varName);
|
|
};
|
|
|
|
// Variable refactoring
|
|
|
|
SpriteMorph.prototype.refactorVariableInstances = function (oldName, newName, isGlobal) {
|
|
var oldValue;
|
|
|
|
if (isGlobal && this.hasSpriteVariable(oldName)) {
|
|
return;
|
|
}
|
|
|
|
if (!isGlobal) {
|
|
oldValue = this.variables.vars[oldName];
|
|
this.deleteVariable(oldName);
|
|
this.addVariable(newName, false);
|
|
this.variables.vars[newName] = oldValue;
|
|
this.blocksCache['variables'] = null;
|
|
this.paletteCache['variables'] = null;
|
|
this.parentThatIsA(IDE_Morph).refreshPalette();
|
|
}
|
|
|
|
this.scripts.children.forEach(function (child) {
|
|
if (child instanceof BlockMorph) {
|
|
child.refactorVarInStack(oldName, newName);
|
|
}
|
|
});
|
|
|
|
};
|
|
|
|
// SpriteMorph inheritance - custom blocks
|
|
|
|
/*
|
|
// under construction, commented out for now
|
|
|
|
SpriteMorph.prototype.ownBlocks = function () {
|
|
var dict = {};
|
|
this.customBlocks.forEach(function (def) {
|
|
dict[def.blockSpec()] = def;
|
|
});
|
|
return dict;
|
|
};
|
|
|
|
SpriteMorph.prototype.allBlocks = function (valuesOnly) {
|
|
var dict = {};
|
|
this.allExemplars().reverse().forEach(function (sprite) {
|
|
sprite.customBlocks.forEach(function (def) {
|
|
dict[def.blockSpec()] = def;
|
|
});
|
|
});
|
|
if (valuesOnly) {
|
|
return Object.keys(dict).map(function (key) {return dict[key]; });
|
|
}
|
|
return dict;
|
|
};
|
|
|
|
SpriteMorph.prototype.inheritedBlocks = function (valuesOnly) {
|
|
var dict = {},
|
|
own = Object.keys(this.ownBlocks()),
|
|
others = this.allExemplars().reverse();
|
|
others.pop();
|
|
others.forEach(function (sprite) {
|
|
sprite.customBlocks.forEach(function (def) {
|
|
var spec = def.blockSpec();
|
|
if (!contains(own, spec)) {
|
|
dict[spec] = def;
|
|
}
|
|
});
|
|
});
|
|
if (valuesOnly) {
|
|
return Object.keys(dict).map(function (key) {return dict[key]; });
|
|
}
|
|
return dict;
|
|
};
|
|
|
|
*/
|
|
|
|
// SpriteMorph thumbnail
|
|
|
|
SpriteMorph.prototype.thumbnail = function (extentPoint) {
|
|
/*
|
|
answer a new Canvas of extentPoint dimensions containing
|
|
my thumbnail representation keeping the originial aspect ratio
|
|
*/
|
|
var src = this.image, // at this time sprites aren't composite morphs
|
|
scale = Math.min(
|
|
(extentPoint.x / src.width),
|
|
(extentPoint.y / src.height)
|
|
),
|
|
xOffset = (extentPoint.x - (src.width * scale)) / 2,
|
|
yOffset = (extentPoint.y - (src.height * scale)) / 2,
|
|
trg = newCanvas(extentPoint),
|
|
ctx = trg.getContext('2d');
|
|
|
|
function xOut(style, alpha, width) {
|
|
var inset = Math.min(extentPoint.x, extentPoint.y) / 10;
|
|
ctx.strokeStyle = style;
|
|
ctx.globalAlpha = alpha;
|
|
ctx.compositeOperation = 'lighter';
|
|
ctx.lineWidth = width || 1;
|
|
ctx.moveTo(inset, inset);
|
|
ctx.lineTo(trg.width - inset, trg.height - inset);
|
|
ctx.moveTo(inset, trg.height - inset);
|
|
ctx.lineTo(trg.width - inset, inset);
|
|
ctx.stroke();
|
|
}
|
|
|
|
ctx.save();
|
|
if (this.isCorpse) {
|
|
ctx.globalAlpha = 0.3;
|
|
}
|
|
if (src.width && src.height) {
|
|
ctx.scale(scale, scale);
|
|
ctx.drawImage(
|
|
src,
|
|
Math.floor(xOffset / scale),
|
|
Math.floor(yOffset / scale)
|
|
);
|
|
}
|
|
if (this.isCorpse) {
|
|
ctx.restore();
|
|
xOut('white', 0.8, 6);
|
|
xOut('black', 0.8, 1);
|
|
}
|
|
return trg;
|
|
};
|
|
|
|
SpriteMorph.prototype.fullThumbnail = function (extentPoint) {
|
|
// containing parts and anchor symbols, if any
|
|
var thumb = this.thumbnail(extentPoint),
|
|
ctx = thumb.getContext('2d'),
|
|
ext = extentPoint.divideBy(3),
|
|
i = 0;
|
|
|
|
ctx.restore();
|
|
if (this.anchor) {
|
|
ctx.drawImage(
|
|
this.anchor.thumbnail(ext),
|
|
0,
|
|
0
|
|
);
|
|
}
|
|
for (i = 0; i < 3; i += 1) {
|
|
if (this.parts[i]) {
|
|
ctx.drawImage(
|
|
this.parts[i].thumbnail(ext),
|
|
i * ext.x,
|
|
extentPoint.y - ext.y
|
|
);
|
|
}
|
|
}
|
|
return thumb;
|
|
};
|
|
|
|
// SpriteMorph Boolean visual representation
|
|
|
|
SpriteMorph.prototype.booleanMorph = function (bool) {
|
|
var sym = new BooleanSlotMorph(bool);
|
|
sym.isStatic = true;
|
|
sym.drawNew();
|
|
return sym;
|
|
};
|
|
|
|
// SpriteMorph nesting
|
|
/*
|
|
simulate Morphic trees
|
|
*/
|
|
|
|
SpriteMorph.prototype.attachPart = function (aSprite) {
|
|
var v = Date.now();
|
|
if (aSprite.anchor) {
|
|
aSprite.anchor.detachPart(aSprite);
|
|
}
|
|
this.parts.push(aSprite);
|
|
this.version = v;
|
|
aSprite.anchor = this;
|
|
this.allParts().forEach(function (part) {
|
|
part.nestingScale = part.scale;
|
|
});
|
|
aSprite.version = v;
|
|
};
|
|
|
|
SpriteMorph.prototype.detachPart = function (aSprite) {
|
|
var idx = this.parts.indexOf(aSprite),
|
|
v;
|
|
if (idx !== -1) {
|
|
v = Date.now();
|
|
this.parts.splice(idx, 1);
|
|
this.version = v;
|
|
aSprite.anchor = null;
|
|
aSprite.version = v;
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.detachAllParts = function () {
|
|
var v = Date.now();
|
|
|
|
this.parts.forEach(function (part) {
|
|
part.anchor = null;
|
|
part.version = v;
|
|
});
|
|
this.parts = [];
|
|
this.version = v;
|
|
};
|
|
|
|
SpriteMorph.prototype.detachFromAnchor = function () {
|
|
if (this.anchor) {
|
|
this.anchor.detachPart(this);
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.allParts = function () {
|
|
// includes myself
|
|
var result = [this];
|
|
this.parts.forEach(function (part) {
|
|
result = result.concat(part.allParts());
|
|
});
|
|
return result;
|
|
};
|
|
|
|
SpriteMorph.prototype.allAnchors = function () {
|
|
// includes myself
|
|
var result = [this];
|
|
if (this.anchor !== null) {
|
|
result = result.concat(this.anchor.allAnchors());
|
|
}
|
|
return result;
|
|
};
|
|
|
|
SpriteMorph.prototype.recordLayers = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (!stage) {
|
|
this.layerCache = null;
|
|
return;
|
|
}
|
|
this.layers = this.allParts();
|
|
this.layers.forEach(function (part) {
|
|
var bubble = part.talkBubble();
|
|
if (bubble) {bubble.hide(); }
|
|
});
|
|
this.layers.sort(function (x, y) {
|
|
return stage.children.indexOf(x) < stage.children.indexOf(y) ?
|
|
-1 : 1;
|
|
});
|
|
};
|
|
|
|
SpriteMorph.prototype.restoreLayers = function () {
|
|
if (this.layers && this.layers.length > 1) {
|
|
this.layers.forEach(function (sprite) {
|
|
sprite.comeToFront();
|
|
sprite.positionTalkBubble();
|
|
});
|
|
}
|
|
this.layers = null;
|
|
};
|
|
|
|
// SpriteMorph highlighting
|
|
|
|
SpriteMorph.prototype.addHighlight = function (oldHighlight) {
|
|
var isHidden = !this.isVisible,
|
|
highlight;
|
|
|
|
if (isHidden) {this.show(); }
|
|
highlight = this.highlight(
|
|
oldHighlight ? oldHighlight.color : this.highlightColor,
|
|
this.highlightBorder
|
|
);
|
|
this.addBack(highlight);
|
|
this.fullChanged();
|
|
if (isHidden) {this.hide(); }
|
|
return highlight;
|
|
};
|
|
|
|
SpriteMorph.prototype.removeHighlight = function () {
|
|
var highlight = this.getHighlight();
|
|
if (highlight !== null) {
|
|
this.fullChanged();
|
|
this.removeChild(highlight);
|
|
}
|
|
return highlight;
|
|
};
|
|
|
|
SpriteMorph.prototype.toggleHighlight = function () {
|
|
if (this.getHighlight()) {
|
|
this.removeHighlight();
|
|
} else {
|
|
this.addHighlight();
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.highlight = function (color, border) {
|
|
var highlight = new SpriteHighlightMorph(),
|
|
fb = this.bounds, // sprites are not nested in a Morphic way
|
|
edge = border,
|
|
ctx;
|
|
|
|
highlight.setExtent(fb.extent().add(edge * 2));
|
|
highlight.color = color;
|
|
highlight.image = this.highlightImage(color, border);
|
|
ctx = highlight.image.getContext('2d');
|
|
ctx.drawImage(
|
|
this.highlightImage(new Color(255, 255, 255), 4),
|
|
border - 4,
|
|
border - 4
|
|
);
|
|
ctx.drawImage(
|
|
this.highlightImage(new Color(50, 50, 50), 2),
|
|
border - 2,
|
|
border - 2
|
|
);
|
|
ctx.drawImage(
|
|
this.highlightImage(new Color(255, 255, 255), 1),
|
|
border - 1,
|
|
border - 1
|
|
);
|
|
highlight.setPosition(fb.origin.subtract(new Point(edge, edge)));
|
|
return highlight;
|
|
};
|
|
|
|
SpriteMorph.prototype.highlightImage = function (color, border) {
|
|
var fb, img, hi, ctx, out;
|
|
fb = this.extent();
|
|
img = this.image;
|
|
|
|
hi = newCanvas(fb.add(border * 2));
|
|
ctx = hi.getContext('2d');
|
|
|
|
ctx.drawImage(img, 0, 0);
|
|
ctx.drawImage(img, border, 0);
|
|
ctx.drawImage(img, border * 2, 0);
|
|
ctx.drawImage(img, border * 2, border);
|
|
ctx.drawImage(img, border * 2, border * 2);
|
|
ctx.drawImage(img, border, border * 2);
|
|
ctx.drawImage(img, 0, border * 2);
|
|
ctx.drawImage(img, 0, border);
|
|
|
|
ctx.globalCompositeOperation = 'destination-out';
|
|
ctx.drawImage(img, border, border);
|
|
|
|
out = newCanvas(fb.add(border * 2));
|
|
ctx = out.getContext('2d');
|
|
ctx.drawImage(hi, 0, 0);
|
|
ctx.globalCompositeOperation = 'source-atop';
|
|
ctx.fillStyle = color.toString();
|
|
ctx.fillRect(0, 0, out.width, out.height);
|
|
|
|
return out;
|
|
};
|
|
|
|
SpriteMorph.prototype.getHighlight = function () {
|
|
var highlights;
|
|
highlights = this.children.slice(0).reverse().filter(
|
|
function (child) {
|
|
return child instanceof SpriteHighlightMorph;
|
|
}
|
|
);
|
|
if (highlights.length !== 0) {
|
|
return highlights[0];
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// SpriteMorph nesting events
|
|
|
|
SpriteMorph.prototype.mouseEnterDragging = function () {
|
|
var obj;
|
|
if (!this.enableNesting) {return; }
|
|
obj = this.world().hand.children[0];
|
|
if (this.wantsDropOf(obj)) {
|
|
this.addHighlight();
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.mouseLeave = function () {
|
|
this.receiveUserInteraction('mouse-departed');
|
|
if (!this.enableNesting) {return; }
|
|
this.removeHighlight();
|
|
};
|
|
|
|
SpriteMorph.prototype.wantsDropOf = function (morph) {
|
|
// allow myself to be the anchor of another sprite
|
|
// by drag & drop
|
|
return this.enableNesting
|
|
&& morph instanceof SpriteIconMorph
|
|
&& !contains(morph.object.allParts(), this);
|
|
};
|
|
|
|
SpriteMorph.prototype.reactToDropOf = function (morph, hand) {
|
|
this.removeHighlight();
|
|
this.attachPart(morph.object);
|
|
this.world().add(morph);
|
|
morph.slideBackTo(hand.grabOrigin);
|
|
};
|
|
|
|
// SpriteMorph screenshots
|
|
|
|
SpriteMorph.prototype.newCostumeName = function (name, ignoredCostume) {
|
|
var ix = name.indexOf('('),
|
|
stem = (ix < 0) ? name : name.substring(0, ix),
|
|
count = 1,
|
|
newName = stem,
|
|
all = this.costumes.asArray().filter(
|
|
function (each) {return each !== ignoredCostume; }
|
|
).map(
|
|
function (each) {return each.name; }
|
|
);
|
|
while (contains(all, newName)) {
|
|
count += 1;
|
|
newName = stem + '(' + count + ')';
|
|
}
|
|
return newName;
|
|
};
|
|
|
|
SpriteMorph.prototype.doScreenshot = function (imgSource, data) {
|
|
var canvas,
|
|
stage = this.parentThatIsA(StageMorph),
|
|
costume;
|
|
data = this.newCostumeName(data);
|
|
if (imgSource[0] === undefined) {
|
|
return;
|
|
}
|
|
if (imgSource[0] === "pen trails") {
|
|
canvas = stage.trailsCanvas;
|
|
costume = new Costume(canvas, data).copy(); // prevent mutation
|
|
} else if (imgSource[0] === "stage image") {
|
|
canvas = stage.fullImageClassic();
|
|
costume = new Costume(canvas, data);
|
|
}
|
|
this.addCostume(costume);
|
|
};
|
|
|
|
// SpriteHighlightMorph /////////////////////////////////////////////////
|
|
|
|
// SpriteHighlightMorph inherits from Morph:
|
|
|
|
SpriteHighlightMorph.prototype = new Morph();
|
|
SpriteHighlightMorph.prototype.constructor = SpriteHighlightMorph;
|
|
SpriteHighlightMorph.uber = Morph.prototype;
|
|
|
|
// SpriteHighlightMorph instance creation:
|
|
|
|
function SpriteHighlightMorph() {
|
|
this.init();
|
|
}
|
|
|
|
// StageMorph /////////////////////////////////////////////////////////
|
|
|
|
/*
|
|
I inherit from FrameMorph and copy from SpriteMorph.
|
|
*/
|
|
|
|
// StageMorph inherits from FrameMorph:
|
|
|
|
StageMorph.prototype = new FrameMorph();
|
|
StageMorph.prototype.constructor = StageMorph;
|
|
StageMorph.uber = FrameMorph.prototype;
|
|
|
|
// StageMorph preferences settings
|
|
|
|
StageMorph.prototype.dimensions = new Point(480, 360); // unscaled extent
|
|
StageMorph.prototype.frameRate = 0; // unscheduled per default
|
|
|
|
StageMorph.prototype.isCachingPrimitives
|
|
= SpriteMorph.prototype.isCachingPrimitives;
|
|
|
|
StageMorph.prototype.sliderColor
|
|
= SpriteMorph.prototype.sliderColor;
|
|
|
|
StageMorph.prototype.paletteTextColor
|
|
= SpriteMorph.prototype.paletteTextColor;
|
|
|
|
StageMorph.prototype.hiddenPrimitives = {};
|
|
StageMorph.prototype.codeMappings = {};
|
|
StageMorph.prototype.codeHeaders = {};
|
|
StageMorph.prototype.enableCodeMapping = false;
|
|
StageMorph.prototype.enableInheritance = false;
|
|
StageMorph.prototype.enableSublistIDs = false;
|
|
|
|
// StageMorph instance creation
|
|
|
|
function StageMorph(globals) {
|
|
this.init(globals);
|
|
}
|
|
|
|
StageMorph.prototype.init = function (globals) {
|
|
this.name = localize('Stage');
|
|
this.threads = new ThreadManager();
|
|
this.variables = new VariableFrame(globals || null, this);
|
|
this.scripts = new ScriptsMorph(this);
|
|
this.customBlocks = [];
|
|
this.globalBlocks = [];
|
|
this.costumes = new List();
|
|
this.costume = null;
|
|
this.sounds = new List();
|
|
this.version = Date.now(); // for observers
|
|
this.isFastTracked = false;
|
|
this.enableCustomHatBlocks = true;
|
|
this.cloneCount = 0;
|
|
|
|
this.timerStart = Date.now();
|
|
this.tempo = 60; // bpm
|
|
this.lastMessage = '';
|
|
|
|
this.watcherUpdateFrequency = 2;
|
|
this.lastWatcherUpdate = Date.now();
|
|
|
|
this.scale = 1; // for display modes, do not persist
|
|
|
|
this.keysPressed = {}; // for handling keyboard events, do not persist
|
|
this.blocksCache = {}; // not to be serialized (!)
|
|
this.paletteCache = {}; // not to be serialized (!)
|
|
this.lastAnswer = ''; // last user input, do not persist
|
|
this.activeSounds = []; // do not persist
|
|
|
|
this.trailsCanvas = null;
|
|
this.isThreadSafe = false;
|
|
|
|
this.graphicsValues = {
|
|
'color': 0,
|
|
'fisheye': 0,
|
|
'whirl': 0,
|
|
'pixelate': 0,
|
|
'mosaic': 0,
|
|
'duplicate': 0,
|
|
'negative': 0,
|
|
'comic': 0,
|
|
'confetti': 0,
|
|
'saturation': 0,
|
|
'brightness': 0
|
|
};
|
|
|
|
StageMorph.uber.init.call(this);
|
|
|
|
this.acceptsDrops = false;
|
|
this.setColor(new Color(255, 255, 255));
|
|
this.fps = this.frameRate;
|
|
};
|
|
|
|
// StageMorph scaling
|
|
|
|
StageMorph.prototype.setScale = function (number) {
|
|
var delta = number / this.scale,
|
|
pos = this.position(),
|
|
relativePos,
|
|
bubble,
|
|
oldFlag = Morph.prototype.trackChanges,
|
|
myself = this;
|
|
|
|
if (delta === 1) {return; }
|
|
Morph.prototype.trackChanges = false;
|
|
this.scale = number;
|
|
this.setExtent(this.dimensions.multiplyBy(number));
|
|
|
|
// now move and resize all children - sprites, bubbles, watchers etc..
|
|
this.children.forEach(function (morph) {
|
|
relativePos = morph.position().subtract(pos);
|
|
morph.drawNew();
|
|
morph.setPosition(
|
|
relativePos.multiplyBy(delta).add(pos),
|
|
true // just me (for nested sprites)
|
|
);
|
|
if (morph instanceof SpriteMorph) {
|
|
bubble = morph.talkBubble();
|
|
if (bubble) {
|
|
bubble.setScale(number);
|
|
morph.positionTalkBubble();
|
|
}
|
|
} else if (morph instanceof StagePrompterMorph) {
|
|
if (myself.scale < 1) {
|
|
morph.setWidth(myself.width() - 10);
|
|
} else {
|
|
morph.setWidth(myself.dimensions.x - 20);
|
|
}
|
|
morph.fixLayout();
|
|
morph.setCenter(myself.center());
|
|
morph.setBottom(myself.bottom());
|
|
}
|
|
});
|
|
Morph.prototype.trackChanges = oldFlag;
|
|
this.changed();
|
|
};
|
|
|
|
// StageMorph rendering
|
|
|
|
StageMorph.prototype.drawNew = function () {
|
|
var ctx;
|
|
StageMorph.uber.drawNew.call(this);
|
|
if (this.costume) {
|
|
ctx = this.image.getContext('2d');
|
|
ctx.scale(this.scale, this.scale);
|
|
ctx.drawImage(
|
|
this.costume.contents,
|
|
(this.width() / this.scale - this.costume.width()) / 2,
|
|
(this.height() / this.scale - this.costume.height()) / 2
|
|
);
|
|
this.image = this.applyGraphicsEffects(this.image);
|
|
}
|
|
this.version = Date.now(); // for observer optimization
|
|
};
|
|
|
|
StageMorph.prototype.drawOn = function (aCanvas, aRect) {
|
|
// make sure to draw the pen trails canvas as well
|
|
var rectangle, area, delta, src, context, w, h, sl, st, ws, hs;
|
|
if (!this.isVisible) {
|
|
return null;
|
|
}
|
|
rectangle = aRect || this.bounds;
|
|
area = rectangle.intersect(this.bounds);
|
|
if (area.extent().gt(new Point(0, 0))) {
|
|
delta = this.position().neg();
|
|
src = area.copy().translateBy(delta);
|
|
context = aCanvas.getContext('2d');
|
|
context.globalAlpha = this.alpha;
|
|
|
|
sl = src.left();
|
|
st = src.top();
|
|
w = Math.min(src.width(), this.image.width - sl);
|
|
h = Math.min(src.height(), this.image.height - st);
|
|
|
|
if (w < 1 || h < 1) {
|
|
return null;
|
|
}
|
|
context.drawImage(
|
|
this.image,
|
|
sl,
|
|
st,
|
|
w,
|
|
h,
|
|
area.left(),
|
|
area.top(),
|
|
w,
|
|
h
|
|
);
|
|
|
|
// pen trails
|
|
ws = w / this.scale;
|
|
hs = h / this.scale;
|
|
context.save();
|
|
context.scale(this.scale, this.scale);
|
|
try {
|
|
context.drawImage(
|
|
this.penTrails(),
|
|
sl / this.scale,
|
|
st / this.scale,
|
|
ws,
|
|
hs,
|
|
area.left() / this.scale,
|
|
area.top() / this.scale,
|
|
ws,
|
|
hs
|
|
);
|
|
} catch (err) { // sometimes triggered only by Firefox
|
|
// console.log(err);
|
|
context.restore();
|
|
context.drawImage(
|
|
this.penTrails(),
|
|
0,
|
|
0,
|
|
this.dimensions.x,
|
|
this.dimensions.y,
|
|
this.left(),
|
|
this.top(),
|
|
this.dimensions.x * this.scale,
|
|
this.dimensions.y * this.scale
|
|
);
|
|
}
|
|
context.restore();
|
|
}
|
|
};
|
|
|
|
StageMorph.prototype.clearPenTrails = function () {
|
|
this.trailsCanvas = newCanvas(this.dimensions);
|
|
this.changed();
|
|
};
|
|
|
|
StageMorph.prototype.penTrails = function () {
|
|
if (!this.trailsCanvas) {
|
|
this.trailsCanvas = newCanvas(this.dimensions);
|
|
}
|
|
return this.trailsCanvas;
|
|
};
|
|
|
|
StageMorph.prototype.penTrailsMorph = function () {
|
|
// for collision detection purposes
|
|
var morph = new Morph(),
|
|
trails = this.penTrails(),
|
|
ctx;
|
|
morph.bounds = this.bounds.copy();
|
|
morph.image = newCanvas(this.extent());
|
|
ctx = morph.image.getContext('2d');
|
|
ctx.drawImage(
|
|
trails,
|
|
0,
|
|
0,
|
|
trails.width,
|
|
trails.height,
|
|
0,
|
|
0,
|
|
this.image.width,
|
|
this.image.height
|
|
);
|
|
return morph;
|
|
};
|
|
|
|
StageMorph.prototype.colorFiltered = function (aColor, excludedSprite) {
|
|
// answer a new Morph containing my image filtered by aColor
|
|
// ignore the excludedSprite, because its collision is checked
|
|
// ignore transparency (alpha)
|
|
var morph = new Morph(),
|
|
ext = this.extent(),
|
|
img = this.thumbnail(ext, excludedSprite),
|
|
ctx,
|
|
src,
|
|
clr,
|
|
i,
|
|
dta;
|
|
|
|
src = normalizeCanvas(img, true).getContext('2d').getImageData(
|
|
0,
|
|
0,
|
|
ext.x,
|
|
ext.y
|
|
);
|
|
morph.bounds = this.bounds.copy();
|
|
morph.image = newCanvas(ext, true);
|
|
ctx = morph.image.getContext('2d');
|
|
dta = ctx.createImageData(ext.x, ext.y);
|
|
for (i = 0; i < ext.x * ext.y * 4; i += 4) {
|
|
clr = new Color(
|
|
src.data[i],
|
|
src.data[i + 1],
|
|
src.data[i + 2]
|
|
);
|
|
if (clr.eq(aColor)) {
|
|
dta.data[i] = src.data[i];
|
|
dta.data[i + 1] = src.data[i + 1];
|
|
dta.data[i + 2] = src.data[i + 2];
|
|
dta.data[i + 3] = 255;
|
|
}
|
|
}
|
|
ctx.putImageData(dta, 0, 0);
|
|
return morph;
|
|
};
|
|
|
|
// StageMorph accessing
|
|
|
|
StageMorph.prototype.watchers = function (leftPos) {
|
|
/*
|
|
answer an array of all currently visible watchers.
|
|
If leftPos is specified, filter the list for all
|
|
shown or hidden watchers whose left side equals
|
|
the given border (for automatic positioning)
|
|
*/
|
|
return this.children.filter(function (morph) {
|
|
if (morph instanceof WatcherMorph) {
|
|
if (leftPos) {
|
|
return morph.left() === leftPos;
|
|
}
|
|
return morph.isVisible;
|
|
}
|
|
return false;
|
|
});
|
|
};
|
|
|
|
// StageMorph timer
|
|
|
|
StageMorph.prototype.resetTimer = function () {
|
|
this.timerStart = Date.now();
|
|
};
|
|
|
|
StageMorph.prototype.getTimer = function () {
|
|
var elapsed = Math.floor((Date.now() - this.timerStart) / 100);
|
|
return elapsed / 10;
|
|
};
|
|
|
|
// StageMorph tempo
|
|
|
|
StageMorph.prototype.setTempo = function (bpm) {
|
|
this.tempo = Math.max(20, (+bpm || 0));
|
|
};
|
|
|
|
StageMorph.prototype.changeTempo = function (delta) {
|
|
this.setTempo(this.getTempo() + (+delta || 0));
|
|
};
|
|
|
|
StageMorph.prototype.getTempo = function () {
|
|
return +this.tempo;
|
|
};
|
|
|
|
// StageMorph messages
|
|
|
|
StageMorph.prototype.getLastMessage = function () {
|
|
return this.lastMessage || '';
|
|
};
|
|
|
|
// StageMorph Mouse Coordinates
|
|
|
|
StageMorph.prototype.reportMouseX = function () {
|
|
var world = this.world();
|
|
if (world) {
|
|
return (world.hand.position().x - this.center().x) / this.scale;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
StageMorph.prototype.reportMouseY = function () {
|
|
var world = this.world();
|
|
if (world) {
|
|
return (this.center().y - world.hand.position().y) / this.scale;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
// StageMorph drag & drop
|
|
|
|
StageMorph.prototype.wantsDropOf = function (aMorph) {
|
|
return aMorph instanceof SpriteMorph ||
|
|
aMorph instanceof WatcherMorph ||
|
|
aMorph instanceof ListWatcherMorph ||
|
|
aMorph instanceof SpriteIconMorph;
|
|
};
|
|
|
|
StageMorph.prototype.reactToDropOf = function (morph, hand) {
|
|
if (morph instanceof SpriteIconMorph) { // detach sprite from anchor
|
|
if (morph.object.anchor) {
|
|
morph.object.anchor.detachPart(morph.object);
|
|
}
|
|
this.world().add(morph);
|
|
morph.slideBackTo(hand.grabOrigin);
|
|
}
|
|
};
|
|
|
|
// StageMorph stepping
|
|
|
|
StageMorph.prototype.step = function () {
|
|
var current, elapsed, leftover, ide, world = this.world();
|
|
|
|
// handle keyboard events
|
|
if (world.keyboardReceiver === null) {
|
|
world.keyboardReceiver = this;
|
|
}
|
|
if (world.currentKey === null) {
|
|
this.keyPressed = null;
|
|
}
|
|
|
|
// manage threads
|
|
if (this.enableCustomHatBlocks) {
|
|
this.stepGenericConditions();
|
|
}
|
|
if (this.isFastTracked && this.threads.processes.length) {
|
|
this.children.forEach(function (morph) {
|
|
if (morph instanceof SpriteMorph) {
|
|
morph.wasWarped = morph.isWarped;
|
|
if (!morph.isWarped) {
|
|
morph.startWarp();
|
|
}
|
|
}
|
|
});
|
|
while ((Date.now() - this.lastTime) < 100) {
|
|
this.threads.step();
|
|
}
|
|
this.children.forEach(function (morph) {
|
|
if (morph instanceof SpriteMorph) {
|
|
if (!morph.wasWarped) {
|
|
morph.endWarp();
|
|
}
|
|
}
|
|
});
|
|
this.changed();
|
|
} else {
|
|
this.threads.step();
|
|
|
|
// single-stepping hook:
|
|
if (this.threads.wantsToPause) {
|
|
ide = this.parentThatIsA(IDE_Morph);
|
|
if (ide) {
|
|
ide.controlBar.pauseButton.refresh();
|
|
}
|
|
}
|
|
}
|
|
|
|
// update watchers
|
|
current = Date.now();
|
|
elapsed = current - this.lastWatcherUpdate;
|
|
leftover = (1000 / this.watcherUpdateFrequency) - elapsed;
|
|
if (leftover < 1) {
|
|
this.watchers().forEach(function (w) {
|
|
w.update();
|
|
});
|
|
this.lastWatcherUpdate = Date.now();
|
|
}
|
|
};
|
|
|
|
StageMorph.prototype.stepGenericConditions = function (stopAll) {
|
|
var hats = [],
|
|
myself = this,
|
|
ide;
|
|
this.children.concat(this).forEach(function (morph) {
|
|
if (morph instanceof SpriteMorph || morph instanceof StageMorph) {
|
|
hats = hats.concat(morph.allGenericHatBlocks());
|
|
}
|
|
});
|
|
if (!hats.length) {
|
|
this.enableCustomHatBlocks = false;
|
|
ide = this.parentThatIsA(IDE_Morph);
|
|
if (ide) {
|
|
ide.controlBar.stopButton.refresh();
|
|
}
|
|
return;
|
|
}
|
|
hats.forEach(function (block) {
|
|
myself.threads.doWhen(block, stopAll);
|
|
});
|
|
};
|
|
|
|
StageMorph.prototype.developersMenu = function () {
|
|
var myself = this,
|
|
menu = StageMorph.uber.developersMenu.call(this);
|
|
menu.addItem(
|
|
"stop",
|
|
function () {
|
|
myself.threads.stopAll();
|
|
},
|
|
'terminate all running threads'
|
|
);
|
|
return menu;
|
|
};
|
|
|
|
// StageMorph keyboard events
|
|
|
|
StageMorph.prototype.processKeyDown = function (event) {
|
|
this.processKeyEvent(
|
|
event,
|
|
this.fireKeyEvent
|
|
);
|
|
};
|
|
|
|
StageMorph.prototype.processKeyUp = function (event) {
|
|
this.processKeyEvent(
|
|
event,
|
|
this.removePressedKey
|
|
);
|
|
};
|
|
|
|
StageMorph.prototype.processKeyEvent = function (event, action) {
|
|
var keyName;
|
|
|
|
// this.inspectKeyEvent(event);
|
|
switch (event.keyCode) {
|
|
case 13:
|
|
keyName = 'enter';
|
|
if (event.ctrlKey || event.metaKey) {
|
|
keyName = 'ctrl enter';
|
|
} else if (event.shiftKey) {
|
|
keyName = 'shift enter';
|
|
}
|
|
break;
|
|
case 27:
|
|
keyName = 'esc';
|
|
break;
|
|
case 32:
|
|
keyName = 'space';
|
|
break;
|
|
case 37:
|
|
keyName = 'left arrow';
|
|
break;
|
|
case 39:
|
|
keyName = 'right arrow';
|
|
break;
|
|
case 38:
|
|
keyName = 'up arrow';
|
|
break;
|
|
case 40:
|
|
keyName = 'down arrow';
|
|
break;
|
|
default:
|
|
keyName = String.fromCharCode(event.keyCode || event.charCode);
|
|
if (event.ctrlKey || event.metaKey) {
|
|
keyName = 'ctrl ' + (event.shiftKey ? 'shift ' : '') + keyName;
|
|
}
|
|
}
|
|
action.call(this, keyName);
|
|
};
|
|
|
|
StageMorph.prototype.fireKeyEvent = function (key) {
|
|
var evt = key.toLowerCase(),
|
|
hats = [],
|
|
procs = [],
|
|
ide = this.parentThatIsA(IDE_Morph),
|
|
myself = this;
|
|
|
|
this.keysPressed[evt] = true;
|
|
if (evt === 'ctrl enter') {
|
|
return this.fireGreenFlagEvent();
|
|
}
|
|
if (evt === 'shift enter') {
|
|
return this.editScripts();
|
|
}
|
|
if (evt === 'ctrl f') {
|
|
if (!ide.isAppMode) {ide.currentSprite.searchBlocks(); }
|
|
return;
|
|
}
|
|
if (evt === 'ctrl z') {
|
|
if (!ide.isAppMode) {ide.currentSprite.scripts.undrop(); }
|
|
return;
|
|
}
|
|
if (evt === 'ctrl shift z' || (evt === 'ctrl y')) {
|
|
if (!ide.isAppMode) {ide.currentSprite.scripts.redrop(); }
|
|
return;
|
|
}
|
|
if (evt === 'ctrl n') {
|
|
if (!ide.isAppMode) {ide.createNewProject(); }
|
|
return;
|
|
}
|
|
if (evt === 'ctrl o') {
|
|
if (!ide.isAppMode) {ide.openProjectsBrowser(); }
|
|
return;
|
|
}
|
|
if (evt === 'ctrl s') {
|
|
if (!ide.isAppMode) {ide.save(); }
|
|
return;
|
|
}
|
|
if (evt === 'ctrl shift s') {
|
|
if (!ide.isAppMode) {return ide.saveProjectsBrowser(); }
|
|
return;
|
|
}
|
|
if (evt === 'esc') {
|
|
return this.fireStopAllEvent();
|
|
}
|
|
this.children.concat(this).forEach(function (morph) {
|
|
if (isSnapObject(morph)) {
|
|
hats = hats.concat(morph.allHatBlocksForKey(evt));
|
|
}
|
|
});
|
|
hats.forEach(function (block) {
|
|
procs.push(myself.threads.startProcess(block, myself.isThreadSafe));
|
|
});
|
|
return procs;
|
|
};
|
|
|
|
StageMorph.prototype.removePressedKey = function (key) {
|
|
delete this.keysPressed[key.toLowerCase()];
|
|
};
|
|
|
|
StageMorph.prototype.processKeyPress = function (event) {
|
|
nop(event);
|
|
};
|
|
|
|
StageMorph.prototype.inspectKeyEvent
|
|
= CursorMorph.prototype.inspectKeyEvent;
|
|
|
|
StageMorph.prototype.fireGreenFlagEvent = function () {
|
|
var procs = [],
|
|
hats = [],
|
|
ide = this.parentThatIsA(IDE_Morph),
|
|
myself = this;
|
|
|
|
this.children.concat(this).forEach(function (morph) {
|
|
if (isSnapObject(morph)) {
|
|
hats = hats.concat(morph.allHatBlocksFor('__shout__go__'));
|
|
}
|
|
});
|
|
hats.forEach(function (block) {
|
|
procs.push(myself.threads.startProcess(
|
|
block,
|
|
myself.isThreadSafe
|
|
));
|
|
});
|
|
if (ide) {
|
|
ide.controlBar.pauseButton.refresh();
|
|
}
|
|
return procs;
|
|
};
|
|
|
|
StageMorph.prototype.fireStopAllEvent = function () {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
this.threads.resumeAll(this.stage);
|
|
this.keysPressed = {};
|
|
this.threads.stopAll();
|
|
this.stopAllActiveSounds();
|
|
this.children.forEach(function (morph) {
|
|
if (morph.stopTalking) {
|
|
morph.stopTalking();
|
|
}
|
|
});
|
|
this.removeAllClones();
|
|
if (ide) {
|
|
ide.nextSteps([
|
|
nop,
|
|
function () {ide.controlBar.pauseButton.refresh(); }
|
|
]);
|
|
}
|
|
};
|
|
|
|
StageMorph.prototype.removeAllClones = function () {
|
|
var myself = this,
|
|
clones = this.children.filter(
|
|
function (morph) {return morph.isClone; }
|
|
);
|
|
clones.forEach(function (clone) {
|
|
myself.threads.stopAllForReceiver(clone);
|
|
clone.detachFromAnchor();
|
|
clone.corpsify();
|
|
clone.destroy();
|
|
});
|
|
this.cloneCount = 0;
|
|
};
|
|
|
|
StageMorph.prototype.editScripts = function () {
|
|
var ide = this.parentThatIsA(IDE_Morph),
|
|
scripts,
|
|
sorted;
|
|
if (ide.isAppMode || !ScriptsMorph.prototype.enableKeyboard) {return; }
|
|
scripts = this.parentThatIsA(IDE_Morph).currentSprite.scripts;
|
|
scripts.edit(scripts.position());
|
|
sorted = scripts.focus.sortedScripts();
|
|
if (sorted.length) {
|
|
scripts.focus.element = sorted[0];
|
|
if (scripts.focus.element instanceof HatBlockMorph) {
|
|
scripts.focus.nextCommand();
|
|
}
|
|
} else {
|
|
scripts.focus.moveBy(new Point(50, 50));
|
|
}
|
|
scripts.focus.fixLayout();
|
|
};
|
|
|
|
// StageMorph block templates
|
|
|
|
StageMorph.prototype.blockTemplates = function (category) {
|
|
var blocks = [], myself = this, varNames, button,
|
|
cat = category || 'motion', txt;
|
|
|
|
function block(selector) {
|
|
if (myself.hiddenPrimitives[selector]) {
|
|
return null;
|
|
}
|
|
var newBlock = SpriteMorph.prototype.blockForSelector(selector, true);
|
|
newBlock.isTemplate = true;
|
|
return newBlock;
|
|
}
|
|
|
|
function variableBlock(varName) {
|
|
var newBlock = SpriteMorph.prototype.variableBlock(varName);
|
|
newBlock.isDraggable = false;
|
|
newBlock.isTemplate = true;
|
|
return newBlock;
|
|
}
|
|
|
|
function watcherToggle(selector) {
|
|
if (myself.hiddenPrimitives[selector]) {
|
|
return null;
|
|
}
|
|
var info = SpriteMorph.prototype.blocks[selector];
|
|
return new ToggleMorph(
|
|
'checkbox',
|
|
this,
|
|
function () {
|
|
myself.toggleWatcher(
|
|
selector,
|
|
localize(info.spec),
|
|
myself.blockColor[info.category]
|
|
);
|
|
},
|
|
null,
|
|
function () {
|
|
return myself.showingWatcher(selector);
|
|
},
|
|
null
|
|
);
|
|
}
|
|
|
|
function variableWatcherToggle(varName) {
|
|
return new ToggleMorph(
|
|
'checkbox',
|
|
this,
|
|
function () {
|
|
myself.toggleVariableWatcher(varName);
|
|
},
|
|
null,
|
|
function () {
|
|
return myself.showingVariableWatcher(varName);
|
|
},
|
|
null
|
|
);
|
|
}
|
|
|
|
function addVar(pair) {
|
|
if (pair) {
|
|
if (myself.isVariableNameInUse(pair[0])) {
|
|
myself.inform('that name is already in use');
|
|
} else {
|
|
myself.addVariable(pair[0], pair[1]);
|
|
myself.toggleVariableWatcher(pair[0], pair[1]);
|
|
myself.blocksCache[cat] = null;
|
|
myself.paletteCache[cat] = null;
|
|
myself.parentThatIsA(IDE_Morph).refreshPalette();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (cat === 'motion') {
|
|
|
|
txt = new TextMorph(localize(
|
|
'Stage selected:\nno motion primitives'
|
|
));
|
|
txt.fontSize = 9;
|
|
txt.setColor(this.paletteTextColor);
|
|
blocks.push(txt);
|
|
|
|
} else if (cat === 'looks') {
|
|
|
|
blocks.push(block('doSwitchToCostume'));
|
|
blocks.push(block('doWearNextCostume'));
|
|
blocks.push(watcherToggle('getCostumeIdx'));
|
|
blocks.push(block('getCostumeIdx'));
|
|
blocks.push('-');
|
|
blocks.push(block('changeEffect'));
|
|
blocks.push(block('setEffect'));
|
|
blocks.push(block('clearEffects'));
|
|
blocks.push('-');
|
|
blocks.push(block('show'));
|
|
blocks.push(block('hide'));
|
|
|
|
// for debugging: ///////////////
|
|
|
|
if (this.world().isDevMode) {
|
|
blocks.push('-');
|
|
txt = new TextMorph(localize(
|
|
'development mode \ndebugging primitives:'
|
|
));
|
|
txt.fontSize = 9;
|
|
txt.setColor(this.paletteTextColor);
|
|
blocks.push(txt);
|
|
blocks.push('-');
|
|
blocks.push(block('reportCostumes'));
|
|
blocks.push('-');
|
|
blocks.push(block('log'));
|
|
blocks.push(block('alert'));
|
|
blocks.push('-');
|
|
blocks.push(block('doScreenshot'));
|
|
}
|
|
|
|
/////////////////////////////////
|
|
|
|
} else if (cat === 'sound') {
|
|
|
|
blocks.push(block('playSound'));
|
|
blocks.push(block('doPlaySoundUntilDone'));
|
|
blocks.push(block('doStopAllSounds'));
|
|
blocks.push('-');
|
|
blocks.push(block('doRest'));
|
|
blocks.push('-');
|
|
blocks.push(block('doPlayNote'));
|
|
blocks.push('-');
|
|
blocks.push(block('doChangeTempo'));
|
|
blocks.push(block('doSetTempo'));
|
|
blocks.push(watcherToggle('getTempo'));
|
|
blocks.push(block('getTempo'));
|
|
|
|
// for debugging: ///////////////
|
|
|
|
if (this.world().isDevMode) {
|
|
blocks.push('-');
|
|
txt = new TextMorph(localize(
|
|
'development mode \ndebugging primitives:'
|
|
));
|
|
txt.fontSize = 9;
|
|
txt.setColor(this.paletteTextColor);
|
|
blocks.push(txt);
|
|
blocks.push('-');
|
|
blocks.push(block('reportSounds'));
|
|
}
|
|
|
|
} else if (cat === 'pen') {
|
|
|
|
blocks.push(block('clear'));
|
|
|
|
} else if (cat === 'control') {
|
|
|
|
blocks.push(block('receiveGo'));
|
|
blocks.push(block('receiveKey'));
|
|
blocks.push(block('receiveInteraction'));
|
|
blocks.push(block('receiveCondition'));
|
|
blocks.push(block('receiveMessage'));
|
|
blocks.push('-');
|
|
blocks.push(block('doBroadcast'));
|
|
blocks.push(block('doBroadcastAndWait'));
|
|
blocks.push(watcherToggle('getLastMessage'));
|
|
blocks.push(block('getLastMessage'));
|
|
blocks.push('-');
|
|
blocks.push(block('doWarp'));
|
|
blocks.push('-');
|
|
blocks.push(block('doWait'));
|
|
blocks.push(block('doWaitUntil'));
|
|
blocks.push('-');
|
|
blocks.push(block('doForever'));
|
|
blocks.push(block('doRepeat'));
|
|
blocks.push(block('doUntil'));
|
|
blocks.push('-');
|
|
blocks.push(block('doIf'));
|
|
blocks.push(block('doIfElse'));
|
|
blocks.push('-');
|
|
blocks.push(block('doReport'));
|
|
blocks.push('-');
|
|
/*
|
|
// old STOP variants, migrated to a newer version, now redundant
|
|
blocks.push(block('doStopBlock'));
|
|
blocks.push(block('doStop'));
|
|
blocks.push(block('doStopAll'));
|
|
*/
|
|
blocks.push(block('doStopThis'));
|
|
blocks.push(block('doStopOthers'));
|
|
blocks.push('-');
|
|
blocks.push(block('doRun'));
|
|
blocks.push(block('fork'));
|
|
blocks.push(block('evaluate'));
|
|
blocks.push('-');
|
|
/*
|
|
// list variants commented out for now (redundant)
|
|
blocks.push(block('doRunWithInputList'));
|
|
blocks.push(block('forkWithInputList'));
|
|
blocks.push(block('evaluateWithInputList'));
|
|
blocks.push('-');
|
|
*/
|
|
blocks.push(block('doCallCC'));
|
|
blocks.push(block('reportCallCC'));
|
|
blocks.push('-');
|
|
blocks.push(block('createClone'));
|
|
blocks.push('-');
|
|
blocks.push(block('doPauseAll'));
|
|
|
|
} else if (cat === 'sensing') {
|
|
|
|
blocks.push(block('doAsk'));
|
|
blocks.push(watcherToggle('getLastAnswer'));
|
|
blocks.push(block('getLastAnswer'));
|
|
blocks.push('-');
|
|
blocks.push(watcherToggle('reportMouseX'));
|
|
blocks.push(block('reportMouseX'));
|
|
blocks.push(watcherToggle('reportMouseY'));
|
|
blocks.push(block('reportMouseY'));
|
|
blocks.push(block('reportMouseDown'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportKeyPressed'));
|
|
blocks.push('-');
|
|
blocks.push(block('doResetTimer'));
|
|
blocks.push(watcherToggle('getTimer'));
|
|
blocks.push(block('getTimer'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportAttributeOf'));
|
|
|
|
if (SpriteMorph.prototype.enableFirstClass) {
|
|
blocks.push(block('reportGet'));
|
|
}
|
|
blocks.push('-');
|
|
|
|
blocks.push(block('reportURL'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportIsFastTracking'));
|
|
blocks.push(block('doSetFastTracking'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportDate'));
|
|
|
|
// for debugging: ///////////////
|
|
|
|
if (this.world().isDevMode) {
|
|
|
|
blocks.push('-');
|
|
txt = new TextMorph(localize(
|
|
'development mode \ndebugging primitives:'
|
|
));
|
|
txt.fontSize = 9;
|
|
txt.setColor(this.paletteTextColor);
|
|
blocks.push(txt);
|
|
blocks.push('-');
|
|
blocks.push(watcherToggle('reportThreadCount'));
|
|
blocks.push(block('reportThreadCount'));
|
|
blocks.push(block('colorFiltered'));
|
|
blocks.push(block('reportStackSize'));
|
|
blocks.push(block('reportFrameCount'));
|
|
}
|
|
|
|
/////////////////////////////////
|
|
|
|
} else if (cat === 'operators') {
|
|
|
|
blocks.push(block('reifyScript'));
|
|
blocks.push(block('reifyReporter'));
|
|
blocks.push(block('reifyPredicate'));
|
|
blocks.push('#');
|
|
blocks.push('-');
|
|
blocks.push(block('reportSum'));
|
|
blocks.push(block('reportDifference'));
|
|
blocks.push(block('reportProduct'));
|
|
blocks.push(block('reportQuotient'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportModulus'));
|
|
blocks.push(block('reportRound'));
|
|
blocks.push(block('reportMonadic'));
|
|
blocks.push(block('reportRandom'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportLessThan'));
|
|
blocks.push(block('reportEquals'));
|
|
blocks.push(block('reportGreaterThan'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportAnd'));
|
|
blocks.push(block('reportOr'));
|
|
blocks.push(block('reportNot'));
|
|
blocks.push(block('reportBoolean'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportJoinWords'));
|
|
blocks.push(block('reportTextSplit'));
|
|
blocks.push(block('reportLetter'));
|
|
blocks.push(block('reportStringSize'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportUnicode'));
|
|
blocks.push(block('reportUnicodeAsLetter'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportIsA'));
|
|
blocks.push(block('reportIsIdentical'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportJSFunction'));
|
|
|
|
// for debugging: ///////////////
|
|
|
|
if (this.world().isDevMode) {
|
|
blocks.push('-');
|
|
txt = new TextMorph(
|
|
'development mode \ndebugging primitives:'
|
|
);
|
|
txt.fontSize = 9;
|
|
txt.setColor(this.paletteTextColor);
|
|
blocks.push(txt);
|
|
blocks.push('-');
|
|
blocks.push(block('reportTypeOf'));
|
|
blocks.push(block('reportTextFunction'));
|
|
}
|
|
|
|
//////////////////////////////////
|
|
|
|
} else if (cat === 'variables') {
|
|
|
|
button = new PushButtonMorph(
|
|
null,
|
|
function () {
|
|
new VariableDialogMorph(
|
|
null,
|
|
addVar,
|
|
myself
|
|
).prompt(
|
|
'Variable name',
|
|
null,
|
|
myself.world()
|
|
);
|
|
},
|
|
'Make a variable'
|
|
);
|
|
blocks.push(button);
|
|
|
|
if (this.variables.allNames().length > 0) {
|
|
button = new PushButtonMorph(
|
|
null,
|
|
function () {
|
|
var menu = new MenuMorph(
|
|
myself.deleteVariable,
|
|
null,
|
|
myself
|
|
);
|
|
myself.variables.allNames().forEach(function (name) {
|
|
menu.addItem(name, name);
|
|
});
|
|
menu.popUpAtHand(myself.world());
|
|
},
|
|
'Delete a variable'
|
|
);
|
|
blocks.push(button);
|
|
}
|
|
|
|
blocks.push('-');
|
|
|
|
varNames = this.variables.allNames();
|
|
if (varNames.length > 0) {
|
|
varNames.forEach(function (name) {
|
|
blocks.push(variableWatcherToggle(name));
|
|
blocks.push(variableBlock(name));
|
|
});
|
|
blocks.push('-');
|
|
}
|
|
|
|
blocks.push(block('doSetVar'));
|
|
blocks.push(block('doChangeVar'));
|
|
blocks.push(block('doShowVar'));
|
|
blocks.push(block('doHideVar'));
|
|
blocks.push(block('doDeclareVariables'));
|
|
blocks.push('=');
|
|
blocks.push(block('reportNewList'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportCONS'));
|
|
blocks.push(block('reportListItem'));
|
|
blocks.push(block('reportCDR'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportListLength'));
|
|
blocks.push(block('reportListContainsItem'));
|
|
blocks.push('-');
|
|
blocks.push(block('doAddToList'));
|
|
blocks.push(block('doDeleteFromList'));
|
|
blocks.push(block('doInsertInList'));
|
|
blocks.push(block('doReplaceInList'));
|
|
|
|
// for debugging: ///////////////
|
|
|
|
if (this.world().isDevMode) {
|
|
blocks.push('-');
|
|
txt = new TextMorph(localize(
|
|
'development mode \ndebugging primitives:'
|
|
));
|
|
txt.fontSize = 9;
|
|
txt.setColor(this.paletteTextColor);
|
|
blocks.push(txt);
|
|
blocks.push('-');
|
|
blocks.push(block('reportMap'));
|
|
blocks.push('-');
|
|
blocks.push(block('doForEach'));
|
|
blocks.push(block('doShowTable'));
|
|
}
|
|
|
|
/////////////////////////////////
|
|
|
|
blocks.push('=');
|
|
|
|
if (StageMorph.prototype.enableCodeMapping) {
|
|
blocks.push(block('doMapCodeOrHeader'));
|
|
blocks.push(block('doMapStringCode'));
|
|
blocks.push(block('doMapListCode'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportMappedCode'));
|
|
blocks.push('=');
|
|
}
|
|
|
|
button = new PushButtonMorph(
|
|
null,
|
|
function () {
|
|
var ide = myself.parentThatIsA(IDE_Morph);
|
|
new BlockDialogMorph(
|
|
null,
|
|
function (definition) {
|
|
if (definition.spec !== '') {
|
|
if (definition.isGlobal) {
|
|
myself.globalBlocks.push(definition);
|
|
} else {
|
|
myself.customBlocks.push(definition);
|
|
}
|
|
ide.flushPaletteCache();
|
|
ide.refreshPalette();
|
|
new BlockEditorMorph(definition, myself).popUp();
|
|
}
|
|
},
|
|
myself
|
|
).prompt(
|
|
'Make a block',
|
|
null,
|
|
myself.world()
|
|
);
|
|
},
|
|
'Make a block'
|
|
);
|
|
blocks.push(button);
|
|
}
|
|
return blocks;
|
|
};
|
|
|
|
// StageMorph primitives
|
|
|
|
StageMorph.prototype.clear = function () {
|
|
this.clearPenTrails();
|
|
};
|
|
|
|
// StageMorph user menu
|
|
|
|
StageMorph.prototype.userMenu = function () {
|
|
var ide = this.parentThatIsA(IDE_Morph),
|
|
menu = new MenuMorph(this),
|
|
shiftClicked = this.world().currentKey === 16,
|
|
myself = this;
|
|
|
|
if (ide && ide.isAppMode) {
|
|
// menu.addItem('help', 'nop');
|
|
return menu;
|
|
}
|
|
menu.addItem("edit", 'edit');
|
|
menu.addItem("show all", 'showAll');
|
|
menu.addItem(
|
|
"pic...",
|
|
function () {
|
|
ide.saveCanvasAs(
|
|
myself.fullImageClassic(),
|
|
myself.name,
|
|
true // open as new window
|
|
);
|
|
},
|
|
'open a new window\nwith a picture of the stage'
|
|
);
|
|
if (shiftClicked) {
|
|
menu.addLine();
|
|
menu.addItem(
|
|
ide.currentSprite instanceof SpriteMorph ?
|
|
"turn pen trails into new costume..."
|
|
: "turn pen trails into new background...",
|
|
function () {
|
|
var costume = new Costume(
|
|
myself.trailsCanvas,
|
|
Date.now().toString()
|
|
).copy();
|
|
ide.currentSprite.addCostume(costume);
|
|
ide.currentSprite.wearCostume(costume);
|
|
ide.hasChangedMedia = true;
|
|
},
|
|
ide.currentSprite instanceof SpriteMorph ?
|
|
'turn all pen trails and stamps\n' +
|
|
'into a new costume for the\ncurrently selected sprite'
|
|
: 'turn all pen trails and stamps\n' +
|
|
'into a new background for the stage',
|
|
new Color(100, 0, 0)
|
|
);
|
|
}
|
|
return menu;
|
|
};
|
|
|
|
StageMorph.prototype.showAll = function () {
|
|
var myself = this;
|
|
this.children.forEach(function (m) {
|
|
if (m instanceof SpriteMorph) {
|
|
if (!m.anchor) {
|
|
m.show();
|
|
m.keepWithin(myself);
|
|
}
|
|
} else {
|
|
m.show();
|
|
m.keepWithin(myself);
|
|
if (m.fixLayout) {m.fixLayout(); }
|
|
}
|
|
});
|
|
};
|
|
|
|
StageMorph.prototype.edit = SpriteMorph.prototype.edit;
|
|
|
|
// StageMorph thumbnail
|
|
|
|
StageMorph.prototype.thumbnail = function (extentPoint, excludedSprite) {
|
|
/*
|
|
answer a new Canvas of extentPoint dimensions containing
|
|
my thumbnail representation keeping the originial aspect ratio
|
|
*/
|
|
var myself = this,
|
|
src = this.image,
|
|
scale = Math.min(
|
|
(extentPoint.x / src.width),
|
|
(extentPoint.y / src.height)
|
|
),
|
|
trg = newCanvas(extentPoint),
|
|
ctx = trg.getContext('2d'),
|
|
fb,
|
|
fimg;
|
|
|
|
ctx.scale(scale, scale);
|
|
ctx.drawImage(
|
|
src,
|
|
0,
|
|
0
|
|
);
|
|
ctx.drawImage(
|
|
this.penTrails(),
|
|
0,
|
|
0,
|
|
this.dimensions.x * this.scale,
|
|
this.dimensions.y * this.scale
|
|
);
|
|
this.children.forEach(function (morph) {
|
|
if (morph.isVisible && (morph !== excludedSprite)) {
|
|
fb = morph.fullBounds();
|
|
fimg = morph.fullImage();
|
|
if (fimg.width && fimg.height) {
|
|
ctx.drawImage(
|
|
morph.fullImage(),
|
|
fb.origin.x - myself.bounds.origin.x,
|
|
fb.origin.y - myself.bounds.origin.y
|
|
);
|
|
}
|
|
}
|
|
});
|
|
return trg;
|
|
};
|
|
|
|
// StageMorph hiding and showing:
|
|
|
|
/*
|
|
override the inherited behavior to recursively hide/show all
|
|
children.
|
|
*/
|
|
|
|
StageMorph.prototype.hide = function () {
|
|
this.isVisible = false;
|
|
this.changed();
|
|
};
|
|
|
|
StageMorph.prototype.show = function () {
|
|
this.isVisible = true;
|
|
this.changed();
|
|
};
|
|
|
|
// StageMorph cloning override
|
|
|
|
StageMorph.prototype.createClone = nop;
|
|
|
|
// StageMorph pseudo-inherited behavior
|
|
|
|
StageMorph.prototype.categories = SpriteMorph.prototype.categories;
|
|
StageMorph.prototype.blockColor = SpriteMorph.prototype.blockColor;
|
|
StageMorph.prototype.paletteColor = SpriteMorph.prototype.paletteColor;
|
|
StageMorph.prototype.setName = SpriteMorph.prototype.setName;
|
|
StageMorph.prototype.palette = SpriteMorph.prototype.palette;
|
|
StageMorph.prototype.freshPalette = SpriteMorph.prototype.freshPalette;
|
|
StageMorph.prototype.blocksMatching = SpriteMorph.prototype.blocksMatching;
|
|
StageMorph.prototype.searchBlocks = SpriteMorph.prototype.searchBlocks;
|
|
StageMorph.prototype.reporterize = SpriteMorph.prototype.reporterize;
|
|
StageMorph.prototype.showingWatcher = SpriteMorph.prototype.showingWatcher;
|
|
StageMorph.prototype.addVariable = SpriteMorph.prototype.addVariable;
|
|
StageMorph.prototype.deleteVariable = SpriteMorph.prototype.deleteVariable;
|
|
|
|
// StageMorph block rendering
|
|
|
|
StageMorph.prototype.doScreenshot
|
|
= SpriteMorph.prototype.doScreenshot;
|
|
|
|
StageMorph.prototype.newCostumeName
|
|
= SpriteMorph.prototype.newCostumeName;
|
|
|
|
StageMorph.prototype.blockForSelector
|
|
= SpriteMorph.prototype.blockForSelector;
|
|
|
|
// StageMorph variable watchers (for palette checkbox toggling)
|
|
|
|
StageMorph.prototype.findVariableWatcher
|
|
= SpriteMorph.prototype.findVariableWatcher;
|
|
|
|
StageMorph.prototype.toggleVariableWatcher
|
|
= SpriteMorph.prototype.toggleVariableWatcher;
|
|
|
|
StageMorph.prototype.showingVariableWatcher
|
|
= SpriteMorph.prototype.showingVariableWatcher;
|
|
|
|
StageMorph.prototype.deleteVariableWatcher
|
|
= SpriteMorph.prototype.deleteVariableWatcher;
|
|
|
|
// StageMorph background management
|
|
|
|
StageMorph.prototype.addCostume
|
|
= SpriteMorph.prototype.addCostume;
|
|
|
|
StageMorph.prototype.wearCostume
|
|
= SpriteMorph.prototype.wearCostume;
|
|
|
|
StageMorph.prototype.getCostumeIdx
|
|
= SpriteMorph.prototype.getCostumeIdx;
|
|
|
|
StageMorph.prototype.doWearNextCostume
|
|
= SpriteMorph.prototype.doWearNextCostume;
|
|
|
|
StageMorph.prototype.doWearPreviousCostume
|
|
= SpriteMorph.prototype.doWearPreviousCostume;
|
|
|
|
StageMorph.prototype.doSwitchToCostume
|
|
= SpriteMorph.prototype.doSwitchToCostume;
|
|
|
|
StageMorph.prototype.reportCostumes
|
|
= SpriteMorph.prototype.reportCostumes;
|
|
|
|
// StageMorph graphic effects
|
|
|
|
StageMorph.prototype.graphicsChanged
|
|
= SpriteMorph.prototype.graphicsChanged;
|
|
|
|
StageMorph.prototype.applyGraphicsEffects
|
|
= SpriteMorph.prototype.applyGraphicsEffects;
|
|
|
|
StageMorph.prototype.setEffect
|
|
= SpriteMorph.prototype.setEffect;
|
|
|
|
StageMorph.prototype.getGhostEffect
|
|
= SpriteMorph.prototype.getGhostEffect;
|
|
|
|
StageMorph.prototype.changeEffect
|
|
= SpriteMorph.prototype.changeEffect;
|
|
|
|
StageMorph.prototype.clearEffects
|
|
= SpriteMorph.prototype.clearEffects;
|
|
|
|
// StageMorph sound management
|
|
|
|
StageMorph.prototype.addSound
|
|
= SpriteMorph.prototype.addSound;
|
|
|
|
StageMorph.prototype.playSound
|
|
= SpriteMorph.prototype.playSound;
|
|
|
|
StageMorph.prototype.stopAllActiveSounds = function () {
|
|
this.activeSounds.forEach(function (audio) {
|
|
audio.pause();
|
|
});
|
|
this.activeSounds = [];
|
|
};
|
|
|
|
StageMorph.prototype.pauseAllActiveSounds = function () {
|
|
this.activeSounds.forEach(function (audio) {
|
|
audio.pause();
|
|
});
|
|
};
|
|
|
|
StageMorph.prototype.resumeAllActiveSounds = function () {
|
|
this.activeSounds.forEach(function (audio) {
|
|
audio.play();
|
|
});
|
|
};
|
|
|
|
StageMorph.prototype.reportSounds
|
|
= SpriteMorph.prototype.reportSounds;
|
|
|
|
// StageMorph non-variable watchers
|
|
|
|
StageMorph.prototype.toggleWatcher
|
|
= SpriteMorph.prototype.toggleWatcher;
|
|
|
|
StageMorph.prototype.showingWatcher
|
|
= SpriteMorph.prototype.showingWatcher;
|
|
|
|
StageMorph.prototype.watcherFor =
|
|
SpriteMorph.prototype.watcherFor;
|
|
|
|
StageMorph.prototype.getLastAnswer
|
|
= SpriteMorph.prototype.getLastAnswer;
|
|
|
|
StageMorph.prototype.reportThreadCount
|
|
= SpriteMorph.prototype.reportThreadCount;
|
|
|
|
// StageMorph message broadcasting
|
|
|
|
StageMorph.prototype.allMessageNames
|
|
= SpriteMorph.prototype.allMessageNames;
|
|
|
|
StageMorph.prototype.allHatBlocksFor
|
|
= SpriteMorph.prototype.allHatBlocksFor;
|
|
|
|
StageMorph.prototype.allHatBlocksForKey
|
|
= SpriteMorph.prototype.allHatBlocksForKey;
|
|
|
|
StageMorph.prototype.allHatBlocksForInteraction
|
|
= SpriteMorph.prototype.allHatBlocksForInteraction;
|
|
|
|
StageMorph.prototype.allGenericHatBlocks
|
|
= SpriteMorph.prototype.allGenericHatBlocks;
|
|
|
|
// StageMorph events
|
|
|
|
StageMorph.prototype.mouseClickLeft
|
|
= SpriteMorph.prototype.mouseClickLeft;
|
|
|
|
StageMorph.prototype.mouseEnter
|
|
= SpriteMorph.prototype.mouseEnter;
|
|
|
|
StageMorph.prototype.mouseLeave = function () {
|
|
this.receiveUserInteraction('mouse-departed');
|
|
};
|
|
|
|
StageMorph.prototype.mouseDownLeft
|
|
= SpriteMorph.prototype.mouseDownLeft;
|
|
|
|
StageMorph.prototype.receiveUserInteraction
|
|
= SpriteMorph.prototype.receiveUserInteraction;
|
|
|
|
// StageMorph custom blocks
|
|
|
|
StageMorph.prototype.deleteAllBlockInstances
|
|
= SpriteMorph.prototype.deleteAllBlockInstances;
|
|
|
|
StageMorph.prototype.allBlockInstances
|
|
= SpriteMorph.prototype.allBlockInstances;
|
|
|
|
StageMorph.prototype.allLocalBlockInstances
|
|
= SpriteMorph.prototype.allLocalBlockInstances;
|
|
|
|
StageMorph.prototype.allEditorBlockInstances
|
|
= SpriteMorph.prototype.allEditorBlockInstances;
|
|
|
|
StageMorph.prototype.paletteBlockInstance
|
|
= SpriteMorph.prototype.paletteBlockInstance;
|
|
|
|
StageMorph.prototype.usesBlockInstance
|
|
= SpriteMorph.prototype.usesBlockInstance;
|
|
|
|
StageMorph.prototype.doubleDefinitionsFor
|
|
= SpriteMorph.prototype.doubleDefinitionsFor;
|
|
|
|
StageMorph.prototype.replaceDoubleDefinitionsFor
|
|
= SpriteMorph.prototype.replaceDoubleDefinitionsFor;
|
|
|
|
// StageMorph inheritance support - variables
|
|
|
|
StageMorph.prototype.isVariableNameInUse
|
|
= SpriteMorph.prototype.isVariableNameInUse;
|
|
|
|
StageMorph.prototype.globalVariables
|
|
= SpriteMorph.prototype.globalVariables;
|
|
|
|
StageMorph.prototype.inheritedVariableNames = function () {
|
|
return [];
|
|
};
|
|
|
|
// StageMorph variable refactoring
|
|
|
|
StageMorph.prototype.hasSpriteVariable
|
|
= SpriteMorph.prototype.hasSpriteVariable;
|
|
|
|
StageMorph.prototype.refactorVariableInstances
|
|
= SpriteMorph.prototype.refactorVariableInstances;
|
|
|
|
// SpriteBubbleMorph ////////////////////////////////////////////////////////
|
|
|
|
/*
|
|
I am a sprite's scaleable speech bubble. I rely on SpriteMorph
|
|
for my preferences settings
|
|
*/
|
|
|
|
// SpriteBubbleMorph inherits from SpeechBubbleMorph:
|
|
|
|
SpriteBubbleMorph.prototype = new SpeechBubbleMorph();
|
|
SpriteBubbleMorph.prototype.constructor = SpriteBubbleMorph;
|
|
SpriteBubbleMorph.uber = SpeechBubbleMorph.prototype;
|
|
|
|
// SpriteBubbleMorph instance creation:
|
|
|
|
function SpriteBubbleMorph(data, stage, isThought, isQuestion) {
|
|
this.init(data, stage, isThought, isQuestion);
|
|
}
|
|
|
|
SpriteBubbleMorph.prototype.init = function (
|
|
data,
|
|
stage,
|
|
isThought,
|
|
isQuestion
|
|
) {
|
|
var sprite = SpriteMorph.prototype;
|
|
this.stage = stage;
|
|
this.scale = stage ? stage.scale : 1;
|
|
this.data = data;
|
|
this.isQuestion = isQuestion;
|
|
|
|
SpriteBubbleMorph.uber.init.call(
|
|
this,
|
|
this.dataAsMorph(data),
|
|
sprite.bubbleColor,
|
|
null,
|
|
null,
|
|
isQuestion ? sprite.blockColor.sensing : sprite.bubbleBorderColor,
|
|
null,
|
|
isThought
|
|
);
|
|
};
|
|
|
|
// SpriteBubbleMorph contents formatting
|
|
|
|
SpriteBubbleMorph.prototype.dataAsMorph = function (data, toggle) {
|
|
var contents,
|
|
isTable,
|
|
sprite = SpriteMorph.prototype,
|
|
isText,
|
|
img,
|
|
scaledImg,
|
|
width;
|
|
if (data instanceof Morph) {
|
|
if (isSnapObject(data)) {
|
|
img = data.thumbnail(new Point(40, 40));
|
|
contents = new Morph();
|
|
contents.silentSetWidth(img.width);
|
|
contents.silentSetHeight(img.height);
|
|
contents.image = img;
|
|
contents.version = data.version;
|
|
contents.step = function () {
|
|
if (this.version !== data.version) {
|
|
img = data.thumbnail(new Point(40, 40));
|
|
this.image = img;
|
|
this.version = data.version;
|
|
this.changed();
|
|
}
|
|
};
|
|
} else {
|
|
contents = data;
|
|
}
|
|
} else if (isString(data)) {
|
|
isText = true;
|
|
contents = new TextMorph(
|
|
data,
|
|
sprite.bubbleFontSize * this.scale,
|
|
null, // fontStyle
|
|
sprite.bubbleFontIsBold,
|
|
false, // italic
|
|
'center'
|
|
);
|
|
} else if (typeof data === 'boolean') {
|
|
img = sprite.booleanMorph(data).fullImage();
|
|
contents = new Morph();
|
|
contents.silentSetWidth(img.width);
|
|
contents.silentSetHeight(img.height);
|
|
contents.image = img;
|
|
} else if (data instanceof Costume) {
|
|
img = data.thumbnail(new Point(40, 40));
|
|
contents = new Morph();
|
|
contents.silentSetWidth(img.width);
|
|
contents.silentSetHeight(img.height);
|
|
contents.image = img;
|
|
} else if (data instanceof HTMLCanvasElement) {
|
|
contents = new Morph();
|
|
contents.silentSetWidth(data.width);
|
|
contents.silentSetHeight(data.height);
|
|
contents.image = data;
|
|
} else if (data instanceof List) {
|
|
if (toggle && this.contentsMorph) {
|
|
isTable = (this.contentsMorph instanceof ListWatcherMorph);
|
|
} else {
|
|
isTable = data.isTable();
|
|
}
|
|
|
|
if (isTable) { // (!toggle && data.isTable()) {
|
|
contents = new TableFrameMorph(new TableMorph(data, 10));
|
|
if (this.stage) {
|
|
contents.expand(this.stage.extent().translateBy(
|
|
-2 * (this.edge + this.border + this.padding)
|
|
));
|
|
}
|
|
} else {
|
|
contents = new ListWatcherMorph(data);
|
|
contents.update(true);
|
|
contents.step = contents.update;
|
|
if (this.stage) {
|
|
contents.expand(this.stage.extent().translateBy(
|
|
-2 * (this.edge + this.border + this.padding)
|
|
));
|
|
}
|
|
}
|
|
contents.isDraggable = false;
|
|
} else if (data instanceof Context) {
|
|
img = data.image();
|
|
contents = new Morph();
|
|
contents.silentSetWidth(img.width);
|
|
contents.silentSetHeight(img.height);
|
|
contents.image = img;
|
|
} else {
|
|
contents = new TextMorph(
|
|
data.toString(),
|
|
sprite.bubbleFontSize * this.scale,
|
|
null, // fontStyle
|
|
sprite.bubbleFontIsBold,
|
|
false, // italic
|
|
'center'
|
|
);
|
|
}
|
|
if (contents instanceof TextMorph) {
|
|
// reflow text boundaries
|
|
width = Math.max(
|
|
contents.width(),
|
|
sprite.bubbleCorner * 2 * this.scale
|
|
);
|
|
if (isText) {
|
|
width = Math.min(width, sprite.bubbleMaxTextWidth * this.scale);
|
|
}
|
|
contents.setWidth(width);
|
|
} else if (!(data instanceof List)) {
|
|
// scale contents image
|
|
scaledImg = newCanvas(contents.extent().multiplyBy(this.scale));
|
|
scaledImg.getContext('2d').drawImage(
|
|
contents.image,
|
|
0,
|
|
0,
|
|
scaledImg.width,
|
|
scaledImg.height
|
|
);
|
|
contents.image = scaledImg;
|
|
contents.bounds = contents.bounds.scaleBy(this.scale);
|
|
}
|
|
return contents;
|
|
};
|
|
|
|
// SpriteBubbleMorph scaling
|
|
|
|
SpriteBubbleMorph.prototype.setScale = function (scale) {
|
|
this.scale = scale;
|
|
this.changed();
|
|
this.drawNew();
|
|
this.changed();
|
|
};
|
|
|
|
// SpriteBubbleMorph drawing:
|
|
|
|
SpriteBubbleMorph.prototype.drawNew = function (toggle) {
|
|
var sprite = SpriteMorph.prototype;
|
|
|
|
// scale my settings
|
|
this.edge = sprite.bubbleCorner * this.scale;
|
|
this.border = sprite.bubbleBorder * this.scale;
|
|
this.padding = sprite.bubbleCorner / 2 * this.scale;
|
|
|
|
// re-build my contents
|
|
if (this.contentsMorph) {
|
|
this.contentsMorph.destroy();
|
|
}
|
|
this.contentsMorph = this.dataAsMorph(this.data, toggle);
|
|
this.add(this.contentsMorph);
|
|
|
|
// adjust my layout
|
|
this.silentSetWidth(this.contentsMorph.width()
|
|
+ (this.padding ? this.padding * 2 : this.edge * 2));
|
|
this.silentSetHeight(this.contentsMorph.height()
|
|
+ this.edge
|
|
+ this.border * 2
|
|
+ this.padding * 2
|
|
+ 2);
|
|
|
|
// draw my outline
|
|
SpeechBubbleMorph.uber.drawNew.call(this);
|
|
|
|
// position my contents
|
|
this.contentsMorph.setPosition(this.position().add(
|
|
new Point(
|
|
this.padding || this.edge,
|
|
this.border + this.padding + 1
|
|
)
|
|
));
|
|
};
|
|
|
|
// SpriteBubbleMorph resizing:
|
|
|
|
SpriteBubbleMorph.prototype.fixLayout = function () {
|
|
// to be used when resizing list watchers
|
|
// otherwise use drawNew() to force re-layout
|
|
|
|
var sprite = SpriteMorph.prototype;
|
|
|
|
this.changed();
|
|
// scale my settings
|
|
this.edge = sprite.bubbleCorner * this.scale;
|
|
this.border = sprite.bubbleBorder * this.scale;
|
|
this.padding = sprite.bubbleCorner / 2 * this.scale;
|
|
|
|
// adjust my layout
|
|
this.silentSetWidth(this.contentsMorph.width()
|
|
+ (this.padding ? this.padding * 2 : this.edge * 2));
|
|
this.silentSetHeight(this.contentsMorph.height()
|
|
+ this.edge
|
|
+ this.border * 2
|
|
+ this.padding * 2
|
|
+ 2);
|
|
|
|
// draw my outline
|
|
SpeechBubbleMorph.uber.drawNew.call(this);
|
|
|
|
// position my contents
|
|
this.contentsMorph.setPosition(this.position().add(
|
|
new Point(
|
|
this.padding || this.edge,
|
|
this.border + this.padding + 1
|
|
)
|
|
));
|
|
this.changed();
|
|
};
|
|
|
|
// Costume /////////////////////////////////////////////////////////////
|
|
|
|
/*
|
|
I am a picture that's "wearable" by a sprite. My rotationCenter is
|
|
relative to my contents position.
|
|
*/
|
|
|
|
// Costume instance creation
|
|
|
|
function Costume(canvas, name, rotationCenter) {
|
|
this.contents = canvas ? normalizeCanvas(canvas, true)
|
|
: newCanvas(null, true);
|
|
this.shrinkToFit(this.maxExtent());
|
|
this.name = name || null;
|
|
this.rotationCenter = rotationCenter || this.center();
|
|
this.version = Date.now(); // for observer optimization
|
|
this.loaded = null; // for de-serialization only
|
|
}
|
|
|
|
Costume.prototype.maxExtent = function () {
|
|
return StageMorph.prototype.dimensions;
|
|
};
|
|
|
|
Costume.prototype.toString = function () {
|
|
return 'a Costume(' + this.name + ')';
|
|
};
|
|
|
|
// Costume dimensions - all relative
|
|
|
|
Costume.prototype.extent = function () {
|
|
return new Point(this.contents.width, this.contents.height);
|
|
};
|
|
|
|
Costume.prototype.center = function () {
|
|
return this.extent().divideBy(2);
|
|
};
|
|
|
|
Costume.prototype.width = function () {
|
|
return this.contents.width;
|
|
};
|
|
|
|
Costume.prototype.height = function () {
|
|
return this.contents.height;
|
|
};
|
|
|
|
Costume.prototype.bounds = function () {
|
|
return new Rectangle(0, 0, this.width(), this.height());
|
|
};
|
|
|
|
// Costume shrink-wrapping
|
|
|
|
Costume.prototype.shrinkWrap = function () {
|
|
// adjust my contents' bounds to my visible bounding box
|
|
var bb = this.boundingBox(),
|
|
ext = bb.extent(),
|
|
pic = newCanvas(ext, true),
|
|
ctx = pic.getContext('2d');
|
|
|
|
ctx.drawImage(
|
|
this.contents,
|
|
bb.origin.x,
|
|
bb.origin.y,
|
|
ext.x,
|
|
ext.y,
|
|
0,
|
|
0,
|
|
ext.x,
|
|
ext.y
|
|
);
|
|
this.rotationCenter = this.rotationCenter.subtract(bb.origin);
|
|
this.contents = pic;
|
|
this.version = Date.now();
|
|
};
|
|
|
|
Costume.prototype.canvasBoundingBox = function (pic) {
|
|
// answer the rectangle surrounding my contents' non-transparent pixels
|
|
var row,
|
|
col,
|
|
w = pic.width,
|
|
h = pic.height,
|
|
ctx = pic.getContext('2d'),
|
|
dta = ctx.getImageData(0, 0, w, h);
|
|
|
|
function getAlpha(x, y) {
|
|
return dta.data[((y * w * 4) + (x * 4)) + 3];
|
|
}
|
|
|
|
function getLeft() {
|
|
for (col = 0; col <= w; col += 1) {
|
|
for (row = 0; row <= h; row += 1) {
|
|
if (getAlpha(col, row)) {
|
|
return col;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function getTop() {
|
|
for (row = 0; row <= h; row += 1) {
|
|
for (col = 0; col <= h; col += 1) {
|
|
if (getAlpha(col, row)) {
|
|
return row;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function getRight() {
|
|
for (col = w; col >= 0; col -= 1) {
|
|
for (row = h; row >= 0; row -= 1) {
|
|
if (getAlpha(col, row)) {
|
|
return Math.min(col + 1, w);
|
|
}
|
|
}
|
|
}
|
|
return w;
|
|
}
|
|
|
|
function getBottom() {
|
|
for (row = h; row >= 0; row -= 1) {
|
|
for (col = w; col >= 0; col -= 1) {
|
|
if (getAlpha(col, row)) {
|
|
return Math.min(row + 1, h);
|
|
}
|
|
}
|
|
}
|
|
return h;
|
|
}
|
|
|
|
return new Rectangle(getLeft(), getTop(), getRight(), getBottom());
|
|
};
|
|
|
|
Costume.prototype.boundingBox = function () {
|
|
return this.canvasBoundingBox(this.contents);
|
|
};
|
|
|
|
// Costume duplication
|
|
|
|
Costume.prototype.copy = function () {
|
|
var canvas = newCanvas(this.extent(), true),
|
|
cpy,
|
|
ctx;
|
|
ctx = canvas.getContext('2d');
|
|
ctx.drawImage(this.contents, 0, 0);
|
|
cpy = new Costume(canvas, this.name ? copy(this.name) : null);
|
|
cpy.rotationCenter = this.rotationCenter.copy();
|
|
return cpy;
|
|
};
|
|
|
|
// Costume flipping
|
|
|
|
Costume.prototype.flipped = function () {
|
|
/*
|
|
answer a copy of myself flipped horizontally
|
|
(mirrored along a vertical axis), used for
|
|
SpriteMorph's rotation style type 2
|
|
*/
|
|
var canvas = newCanvas(this.extent(), true),
|
|
ctx = canvas.getContext('2d'),
|
|
flipped;
|
|
|
|
ctx.translate(this.width(), 0);
|
|
ctx.scale(-1, 1);
|
|
ctx.drawImage(this.contents, 0, 0);
|
|
flipped = new Costume(
|
|
canvas,
|
|
new Point(
|
|
this.width() - this.rotationCenter.x,
|
|
this.rotationCenter.y
|
|
)
|
|
);
|
|
return flipped;
|
|
};
|
|
|
|
// Costume actions
|
|
|
|
Costume.prototype.edit = function (aWorld, anIDE, isnew, oncancel, onsubmit) {
|
|
var myself = this,
|
|
editor = new PaintEditorMorph();
|
|
editor.oncancel = oncancel || nop;
|
|
editor.openIn(
|
|
aWorld,
|
|
isnew ?
|
|
newCanvas(StageMorph.prototype.dimensions, true) :
|
|
this.contents,
|
|
isnew ?
|
|
null :
|
|
this.rotationCenter,
|
|
function (img, rc) {
|
|
myself.contents = img;
|
|
myself.rotationCenter = rc;
|
|
myself.version = Date.now();
|
|
aWorld.changed();
|
|
if (anIDE) {
|
|
if (anIDE.currentSprite instanceof SpriteMorph) {
|
|
// don't shrinkwrap stage costumes
|
|
myself.shrinkWrap();
|
|
}
|
|
anIDE.currentSprite.wearCostume(myself);
|
|
anIDE.hasChangedMedia = true;
|
|
}
|
|
(onsubmit || nop)();
|
|
}
|
|
);
|
|
};
|
|
|
|
Costume.prototype.editRotationPointOnly = function (aWorld) {
|
|
var editor = new CostumeEditorMorph(this),
|
|
action,
|
|
dialog,
|
|
txt;
|
|
|
|
action = function () {editor.accept(); };
|
|
dialog = new DialogBoxMorph(this, action);
|
|
txt = new TextMorph(
|
|
localize('click or drag crosshairs to move the rotation center'),
|
|
dialog.fontSize,
|
|
dialog.fontStyle,
|
|
true,
|
|
false,
|
|
'center',
|
|
null,
|
|
null,
|
|
new Point(1, 1),
|
|
new Color(255, 255, 255)
|
|
);
|
|
|
|
dialog.labelString = 'Costume Editor';
|
|
dialog.createLabel();
|
|
dialog.setPicture(editor);
|
|
dialog.addBody(txt);
|
|
dialog.addButton('ok', 'Ok');
|
|
dialog.addButton('cancel', 'Cancel');
|
|
dialog.fixLayout();
|
|
dialog.drawNew();
|
|
dialog.fixLayout();
|
|
dialog.popUp(aWorld);
|
|
};
|
|
|
|
// Costume thumbnail
|
|
|
|
Costume.prototype.shrinkToFit = function (extentPoint) {
|
|
if (extentPoint.x < this.width() || (extentPoint.y < this.height())) {
|
|
this.contents = this.thumbnail(extentPoint);
|
|
}
|
|
};
|
|
|
|
Costume.prototype.thumbnail = function (extentPoint) {
|
|
/*
|
|
answer a new Canvas of extentPoint dimensions containing
|
|
my thumbnail representation keeping the originial aspect ratio
|
|
*/
|
|
var src = this.contents, // at this time sprites aren't composite morphs
|
|
scale = Math.min(
|
|
(extentPoint.x / src.width),
|
|
(extentPoint.y / src.height)
|
|
),
|
|
xOffset = (extentPoint.x - (src.width * scale)) / 2,
|
|
yOffset = (extentPoint.y - (src.height * scale)) / 2,
|
|
trg = newCanvas(extentPoint),
|
|
ctx = trg.getContext('2d');
|
|
|
|
if (!src || src.width + src.height === 0) {return trg; }
|
|
ctx.scale(scale, scale);
|
|
ctx.drawImage(
|
|
src,
|
|
Math.floor(xOffset / scale),
|
|
Math.floor(yOffset / scale)
|
|
);
|
|
return trg;
|
|
};
|
|
|
|
// Costume catching "tainted" canvases
|
|
|
|
Costume.prototype.isTainted = function () {
|
|
// find out whether the canvas has been tainted by cross-origin data
|
|
// assumes that if reading image data throws an error it is tainted
|
|
try {
|
|
this.contents.getContext('2d').getImageData(
|
|
0,
|
|
0,
|
|
this.contents.width,
|
|
this.contents.height
|
|
);
|
|
} catch (err) {
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// SVG_Costume /////////////////////////////////////////////////////////////
|
|
|
|
/*
|
|
I am a costume containing an SVG image.
|
|
*/
|
|
|
|
// SVG_Costume inherits from Costume:
|
|
|
|
SVG_Costume.prototype = new Costume();
|
|
SVG_Costume.prototype.constructor = SVG_Costume;
|
|
SVG_Costume.uber = Costume.prototype;
|
|
|
|
// SVG_Costume instance creation
|
|
|
|
function SVG_Costume(svgImage, name, rotationCenter) {
|
|
this.contents = svgImage;
|
|
this.shrinkToFit(this.maxExtent());
|
|
this.name = name || null;
|
|
this.rotationCenter = rotationCenter || this.center();
|
|
this.version = Date.now(); // for observer optimization
|
|
this.loaded = null; // for de-serialization only
|
|
}
|
|
|
|
SVG_Costume.prototype.toString = function () {
|
|
return 'an SVG_Costume(' + this.name + ')';
|
|
};
|
|
|
|
// SVG_Costume duplication
|
|
|
|
SVG_Costume.prototype.copy = function () {
|
|
var img = new Image(),
|
|
cpy;
|
|
img.src = this.contents.src;
|
|
cpy = new SVG_Costume(img, this.name ? copy(this.name) : null);
|
|
cpy.rotationCenter = this.rotationCenter.copy();
|
|
return cpy;
|
|
};
|
|
|
|
// SVG_Costume flipping
|
|
|
|
/*
|
|
flipping is currently inherited from Costume, which rasterizes it.
|
|
Therefore flipped SVG costumes may appear pixelated until we add
|
|
a method to either truly flip SVGs or change the Sprite's drawNew()
|
|
method to scale the costume before flipping it
|
|
*/
|
|
|
|
// SVG_Costume thumbnail
|
|
|
|
SVG_Costume.prototype.shrinkToFit = function (extentPoint) {
|
|
// overridden for unrasterized SVGs
|
|
nop(extentPoint);
|
|
return;
|
|
};
|
|
|
|
// CostumeEditorMorph ////////////////////////////////////////////////////////
|
|
|
|
// CostumeEditorMorph inherits from Morph:
|
|
|
|
CostumeEditorMorph.prototype = new Morph();
|
|
CostumeEditorMorph.prototype.constructor = CostumeEditorMorph;
|
|
CostumeEditorMorph.uber = Morph.prototype;
|
|
|
|
// CostumeEditorMorph preferences settings:
|
|
CostumeEditorMorph.prototype.size = Costume.prototype.maxExtent();
|
|
|
|
// CostumeEditorMorph instance creation
|
|
|
|
function CostumeEditorMorph(costume) {
|
|
this.init(costume);
|
|
}
|
|
|
|
CostumeEditorMorph.prototype.init = function (costume) {
|
|
this.costume = costume || new Costume();
|
|
this.rotationCenter = this.costume.rotationCenter.copy();
|
|
this.margin = new Point(0, 0);
|
|
CostumeEditorMorph.uber.init.call(this);
|
|
this.noticesTransparentClick = true;
|
|
};
|
|
|
|
// CostumeEditorMorph edit ops
|
|
|
|
CostumeEditorMorph.prototype.accept = function () {
|
|
this.costume.rotationCenter = this.rotationCenter.copy();
|
|
this.costume.version = Date.now();
|
|
};
|
|
|
|
// CostumeEditorMorph displaying
|
|
|
|
CostumeEditorMorph.prototype.drawNew = function () {
|
|
var rp, ctx;
|
|
|
|
this.margin = this.size.subtract(this.costume.extent()).divideBy(2);
|
|
rp = this.rotationCenter.add(this.margin);
|
|
|
|
this.silentSetExtent(this.size);
|
|
|
|
this.image = newCanvas(this.extent());
|
|
|
|
// draw the background
|
|
if (!this.cachedTexture) {
|
|
this.cachedTexture = this.createTexture();
|
|
|
|
}
|
|
this.drawCachedTexture();
|
|
|
|
/*
|
|
pattern = ctx.createPattern(this.background, 'repeat');
|
|
ctx.fillStyle = pattern;
|
|
ctx.fillRect(0, 0, this.size.x, this.size.y);
|
|
*/
|
|
|
|
ctx = this.image.getContext('2d');
|
|
|
|
// draw the costume
|
|
ctx.drawImage(this.costume.contents, this.margin.x, this.margin.y);
|
|
|
|
// draw crosshairs:
|
|
ctx.globalAlpha = 0.5;
|
|
|
|
// circle around center:
|
|
ctx.fillStyle = 'white';
|
|
ctx.beginPath();
|
|
ctx.arc(
|
|
rp.x,
|
|
rp.y,
|
|
20,
|
|
radians(0),
|
|
radians(360),
|
|
false
|
|
);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(
|
|
rp.x,
|
|
rp.y,
|
|
10,
|
|
radians(0),
|
|
radians(360),
|
|
false
|
|
);
|
|
ctx.stroke();
|
|
|
|
// horizontal line:
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, rp.y);
|
|
ctx.lineTo(this.costume.width() + this.margin.x * 2, rp.y);
|
|
ctx.stroke();
|
|
|
|
// vertical line:
|
|
ctx.beginPath();
|
|
ctx.moveTo(rp.x, 0);
|
|
ctx.lineTo(rp.x, this.costume.height() + this.margin.y * 2);
|
|
ctx.stroke();
|
|
};
|
|
|
|
CostumeEditorMorph.prototype.createTexture = function () {
|
|
var size = 5,
|
|
texture = newCanvas(new Point(size * 2, size * 2)),
|
|
ctx = texture.getContext('2d'),
|
|
grey = new Color(230, 230, 230);
|
|
|
|
ctx.fillStyle = 'white';
|
|
ctx.fillRect(0, 0, size * 2, size * 2);
|
|
ctx.fillStyle = grey.toString();
|
|
ctx.fillRect(0, 0, size, size);
|
|
ctx.fillRect(size, size, size, size);
|
|
return texture;
|
|
};
|
|
|
|
|
|
// CostumeEditorMorph events
|
|
|
|
CostumeEditorMorph.prototype.mouseDownLeft = function (pos) {
|
|
this.rotationCenter = pos.subtract(
|
|
this.position().add(this.margin)
|
|
);
|
|
this.drawNew();
|
|
this.changed();
|
|
};
|
|
|
|
CostumeEditorMorph.prototype.mouseMove
|
|
= CostumeEditorMorph.prototype.mouseDownLeft;
|
|
|
|
// Sound /////////////////////////////////////////////////////////////
|
|
|
|
// Sound instance creation
|
|
|
|
function Sound(audio, name) {
|
|
this.audio = audio; // mandatory
|
|
this.name = name || "Sound";
|
|
}
|
|
|
|
Sound.prototype.play = function () {
|
|
// return an instance of an audio element which can be terminated
|
|
// externally (i.e. by the stage)
|
|
var aud = document.createElement('audio');
|
|
aud.src = this.audio.src;
|
|
aud.play();
|
|
return aud;
|
|
};
|
|
|
|
Sound.prototype.copy = function () {
|
|
var snd = document.createElement('audio'),
|
|
cpy;
|
|
|
|
snd.src = this.audio.src;
|
|
cpy = new Sound(snd, this.name ? copy(this.name) : null);
|
|
return cpy;
|
|
};
|
|
|
|
Sound.prototype.toDataURL = function () {
|
|
return this.audio.src;
|
|
};
|
|
|
|
// Note /////////////////////////////////////////////////////////
|
|
|
|
// I am a single musical note
|
|
|
|
// Note instance creation
|
|
|
|
function Note(pitch) {
|
|
this.pitch = pitch === 0 ? 0 : pitch || 69;
|
|
this.setupContext();
|
|
this.oscillator = null;
|
|
}
|
|
|
|
// Note shared properties
|
|
|
|
Note.prototype.audioContext = null;
|
|
Note.prototype.gainNode = null;
|
|
|
|
// Note audio context
|
|
|
|
Note.prototype.setupContext = function () {
|
|
if (this.audioContext) { return; }
|
|
var AudioContext = (function () {
|
|
// cross browser some day?
|
|
var ctx = window.AudioContext ||
|
|
window.mozAudioContext ||
|
|
window.msAudioContext ||
|
|
window.oAudioContext ||
|
|
window.webkitAudioContext;
|
|
if (!ctx.prototype.createGain) {
|
|
ctx.prototype.createGain = ctx.prototype.createGainNode;
|
|
}
|
|
return ctx;
|
|
}());
|
|
if (!AudioContext) {
|
|
throw new Error('Web Audio API is not supported\nin this browser');
|
|
}
|
|
Note.prototype.audioContext = new AudioContext();
|
|
Note.prototype.gainNode = Note.prototype.audioContext.createGain();
|
|
Note.prototype.gainNode.gain.value = 0.25; // reduce volume by 1/4
|
|
};
|
|
|
|
// Note playing
|
|
|
|
Note.prototype.play = function () {
|
|
this.oscillator = this.audioContext.createOscillator();
|
|
if (!this.oscillator.start) {
|
|
this.oscillator.start = this.oscillator.noteOn;
|
|
}
|
|
if (!this.oscillator.stop) {
|
|
this.oscillator.stop = this.oscillator.noteOff;
|
|
}
|
|
this.oscillator.type = 'sine';
|
|
this.oscillator.frequency.value =
|
|
Math.pow(2, (this.pitch - 69) / 12) * 440;
|
|
this.oscillator.connect(this.gainNode);
|
|
this.gainNode.connect(this.audioContext.destination);
|
|
this.oscillator.start(0);
|
|
};
|
|
|
|
Note.prototype.stop = function () {
|
|
if (this.oscillator) {
|
|
this.oscillator.stop(0);
|
|
this.oscillator = null;
|
|
}
|
|
};
|
|
|
|
// CellMorph //////////////////////////////////////////////////////////
|
|
|
|
/*
|
|
I am a spreadsheet style cell that can display either a string,
|
|
a Morph, a Canvas or a toString() representation of anything else.
|
|
I can be used in variable watchers or list view element cells.
|
|
*/
|
|
|
|
// CellMorph inherits from BoxMorph:
|
|
|
|
CellMorph.prototype = new BoxMorph();
|
|
CellMorph.prototype.constructor = CellMorph;
|
|
CellMorph.uber = BoxMorph.prototype;
|
|
|
|
// CellMorph instance creation:
|
|
|
|
function CellMorph(contents, color, idx, parentCell) {
|
|
this.init(contents, color, idx, parentCell);
|
|
}
|
|
|
|
CellMorph.prototype.init = function (contents, color, idx, parentCell) {
|
|
this.contents = (contents === 0 ? 0
|
|
: contents === false ? false
|
|
: contents || '');
|
|
this.isEditable = isNil(idx) ? false : true;
|
|
this.idx = idx || null; // for list watchers
|
|
this.parentCell = parentCell || null; // for list circularity detection
|
|
CellMorph.uber.init.call(
|
|
this,
|
|
SyntaxElementMorph.prototype.corner,
|
|
1.000001, // shadow bug in Chrome,
|
|
new Color(255, 255, 255)
|
|
);
|
|
this.color = color || new Color(255, 140, 0);
|
|
this.isBig = false;
|
|
this.version = null; // only for observing sprites
|
|
this.drawNew();
|
|
};
|
|
|
|
// CellMorph accessing:
|
|
|
|
CellMorph.prototype.big = function () {
|
|
this.isBig = true;
|
|
this.changed();
|
|
this.drawNew();
|
|
this.changed();
|
|
};
|
|
|
|
CellMorph.prototype.normal = function () {
|
|
this.isBig = false;
|
|
this.changed();
|
|
this.drawNew();
|
|
this.changed();
|
|
};
|
|
|
|
// CellMorph circularity testing:
|
|
|
|
|
|
CellMorph.prototype.isCircular = function (list) {
|
|
if (!this.parentCell) {return false; }
|
|
if (list instanceof List) {
|
|
return this.contents === list || this.parentCell.isCircular(list);
|
|
}
|
|
return this.parentCell.isCircular(this.contents);
|
|
};
|
|
|
|
// CellMorph layout:
|
|
|
|
CellMorph.prototype.fixLayout = function () {
|
|
var listwatcher;
|
|
this.changed();
|
|
this.drawNew();
|
|
this.changed();
|
|
if (this.parent && this.parent.fixLayout) { // variable watcher
|
|
this.parent.fixLayout();
|
|
} else {
|
|
listwatcher = this.parentThatIsA(ListWatcherMorph);
|
|
if (listwatcher) {
|
|
listwatcher.fixLayout();
|
|
}
|
|
}
|
|
};
|
|
|
|
// CellMorph drawing:
|
|
|
|
CellMorph.prototype.update = function () {
|
|
// special case for observing sprites
|
|
if (!isSnapObject(this.contents)) {
|
|
return;
|
|
}
|
|
if (this.version !== this.contents.version) {
|
|
this.drawNew();
|
|
}
|
|
};
|
|
|
|
CellMorph.prototype.drawNew = function (toggle, type) {
|
|
var context,
|
|
txt,
|
|
img,
|
|
fontSize = SyntaxElementMorph.prototype.fontSize,
|
|
isSameList = this.contentsMorph instanceof ListWatcherMorph
|
|
&& (this.contentsMorph.list === this.contents),
|
|
isSameTable = this.contentsMorph instanceof TableFrameMorph
|
|
&& (this.contentsMorph.tableMorph.table === this.contents);
|
|
|
|
if (this.isBig) {
|
|
fontSize = fontSize * 1.5;
|
|
}
|
|
|
|
// re-build my contents
|
|
if (toggle || (this.contentsMorph && !isSameList && !isSameTable)) {
|
|
this.contentsMorph.destroy();
|
|
this.version = null;
|
|
}
|
|
|
|
if (toggle || (!isSameList && !isSameTable)) {
|
|
if (this.contents instanceof Morph) {
|
|
if (isSnapObject(this.contents)) {
|
|
img = this.contents.thumbnail(new Point(40, 40));
|
|
this.contentsMorph = new Morph();
|
|
this.contentsMorph.silentSetWidth(img.width);
|
|
this.contentsMorph.silentSetHeight(img.height);
|
|
this.contentsMorph.image = img;
|
|
this.version = this.contents.version;
|
|
} else {
|
|
this.contentsMorph = this.contents;
|
|
}
|
|
} else if (isString(this.contents)) {
|
|
txt = this.contents.length > 500 ?
|
|
this.contents.slice(0, 500) + '...' : this.contents;
|
|
this.contentsMorph = new TextMorph(
|
|
txt,
|
|
fontSize,
|
|
null,
|
|
true,
|
|
false,
|
|
'left' // was formerly 'center', reverted b/c of code-mapping
|
|
);
|
|
if (this.isEditable) {
|
|
this.contentsMorph.isEditable = true;
|
|
this.contentsMorph.enableSelecting();
|
|
}
|
|
this.contentsMorph.setColor(new Color(255, 255, 255));
|
|
} else if (typeof this.contents === 'boolean') {
|
|
img = SpriteMorph.prototype.booleanMorph.call(
|
|
null,
|
|
this.contents
|
|
).fullImage();
|
|
this.contentsMorph = new Morph();
|
|
this.contentsMorph.silentSetWidth(img.width);
|
|
this.contentsMorph.silentSetHeight(img.height);
|
|
this.contentsMorph.image = img;
|
|
} else if (this.contents instanceof HTMLCanvasElement) {
|
|
this.contentsMorph = new Morph();
|
|
this.contentsMorph.silentSetWidth(this.contents.width);
|
|
this.contentsMorph.silentSetHeight(this.contents.height);
|
|
this.contentsMorph.image = this.contents;
|
|
} else if (this.contents instanceof Context) {
|
|
img = this.contents.image();
|
|
this.contentsMorph = new Morph();
|
|
this.contentsMorph.silentSetWidth(img.width);
|
|
this.contentsMorph.silentSetHeight(img.height);
|
|
this.contentsMorph.image = img;
|
|
} else if (this.contents instanceof Costume) {
|
|
img = this.contents.thumbnail(new Point(40, 40));
|
|
this.contentsMorph = new Morph();
|
|
this.contentsMorph.silentSetWidth(img.width);
|
|
this.contentsMorph.silentSetHeight(img.height);
|
|
this.contentsMorph.image = img;
|
|
} else if (this.contents instanceof List) {
|
|
if ('table' === type || (!toggle && this.contents.isTable())) {
|
|
this.contentsMorph = new TableFrameMorph(new TableMorph(
|
|
this.contents,
|
|
10
|
|
));
|
|
this.contentsMorph.expand(new Point(200, 150));
|
|
} else {
|
|
if (this.isCircular()) {
|
|
this.contentsMorph = new TextMorph(
|
|
'(...)',
|
|
fontSize,
|
|
null,
|
|
false, // bold
|
|
true, // italic
|
|
'center'
|
|
);
|
|
this.contentsMorph.setColor(new Color(255, 255, 255));
|
|
} else {
|
|
this.contentsMorph = new ListWatcherMorph(
|
|
this.contents,
|
|
this
|
|
);
|
|
}
|
|
}
|
|
this.contentsMorph.isDraggable = false;
|
|
} else {
|
|
this.contentsMorph = new TextMorph(
|
|
!isNil(this.contents) ? this.contents.toString() : '',
|
|
fontSize,
|
|
null,
|
|
true,
|
|
false,
|
|
'center'
|
|
);
|
|
if (this.isEditable) {
|
|
this.contentsMorph.isEditable = true;
|
|
this.contentsMorph.enableSelecting();
|
|
}
|
|
this.contentsMorph.setColor(new Color(255, 255, 255));
|
|
}
|
|
this.add(this.contentsMorph);
|
|
}
|
|
|
|
// adjust my layout
|
|
this.silentSetHeight(this.contentsMorph.height()
|
|
+ this.edge
|
|
+ this.border * 2);
|
|
this.silentSetWidth(Math.max(
|
|
this.contentsMorph.width() + this.edge * 2,
|
|
(this.contents instanceof Context ||
|
|
this.contents instanceof List ? 0 :
|
|
SyntaxElementMorph.prototype.fontSize * 3.5)
|
|
));
|
|
|
|
// draw my outline
|
|
this.image = newCanvas(this.extent());
|
|
context = this.image.getContext('2d');
|
|
if ((this.edge === 0) && (this.border === 0)) {
|
|
BoxMorph.uber.drawNew.call(this);
|
|
return null;
|
|
}
|
|
context.fillStyle = this.color.toString();
|
|
context.beginPath();
|
|
this.outlinePath(
|
|
context,
|
|
Math.max(this.edge - this.border, 0),
|
|
this.border
|
|
);
|
|
context.closePath();
|
|
context.fill();
|
|
if (this.border > 0 && !MorphicPreferences.isFlat) {
|
|
context.lineWidth = this.border;
|
|
context.strokeStyle = this.borderColor.toString();
|
|
context.beginPath();
|
|
this.outlinePath(context, this.edge, this.border / 2);
|
|
context.closePath();
|
|
context.stroke();
|
|
|
|
context.shadowOffsetX = this.border;
|
|
context.shadowOffsetY = this.border;
|
|
context.shadowBlur = this.border;
|
|
context.shadowColor = this.color.darker(80).toString();
|
|
this.drawShadow(context, this.edge, this.border / 2);
|
|
}
|
|
|
|
// position my contents
|
|
if (toggle || (!isSameList && !isSameTable)) {
|
|
this.contentsMorph.setCenter(this.center());
|
|
}
|
|
};
|
|
|
|
CellMorph.prototype.drawShadow = function (context, radius, inset) {
|
|
var offset = radius + inset,
|
|
w = this.width(),
|
|
h = this.height();
|
|
|
|
// bottom left:
|
|
context.beginPath();
|
|
context.moveTo(0, h - offset);
|
|
context.lineTo(0, offset);
|
|
context.stroke();
|
|
|
|
// top left:
|
|
context.beginPath();
|
|
context.arc(
|
|
offset,
|
|
offset,
|
|
radius,
|
|
radians(-180),
|
|
radians(-90),
|
|
false
|
|
);
|
|
context.stroke();
|
|
|
|
// top right:
|
|
context.beginPath();
|
|
context.moveTo(offset, 0);
|
|
context.lineTo(w - offset, 0);
|
|
context.stroke();
|
|
};
|
|
|
|
// CellMorph editing (inside list watchers):
|
|
|
|
CellMorph.prototype.layoutChanged = function () {
|
|
var context,
|
|
fontSize = SyntaxElementMorph.prototype.fontSize,
|
|
listWatcher = this.parentThatIsA(ListWatcherMorph);
|
|
|
|
if (this.isBig) {
|
|
fontSize = fontSize * 1.5;
|
|
}
|
|
|
|
// adjust my layout
|
|
this.silentSetHeight(this.contentsMorph.height()
|
|
+ this.edge
|
|
+ this.border * 2);
|
|
this.silentSetWidth(Math.max(
|
|
this.contentsMorph.width() + this.edge * 2,
|
|
(this.contents instanceof Context ||
|
|
this.contents instanceof List ? 0 : this.height() * 2)
|
|
));
|
|
|
|
|
|
// draw my outline
|
|
this.image = newCanvas(this.extent());
|
|
context = this.image.getContext('2d');
|
|
if ((this.edge === 0) && (this.border === 0)) {
|
|
BoxMorph.uber.drawNew.call(this);
|
|
return null;
|
|
}
|
|
context.fillStyle = this.color.toString();
|
|
context.beginPath();
|
|
this.outlinePath(
|
|
context,
|
|
Math.max(this.edge - this.border, 0),
|
|
this.border
|
|
);
|
|
context.closePath();
|
|
context.fill();
|
|
if (this.border > 0 && !MorphicPreferences.isFlat) {
|
|
context.lineWidth = this.border;
|
|
context.strokeStyle = this.borderColor.toString();
|
|
context.beginPath();
|
|
this.outlinePath(context, this.edge, this.border / 2);
|
|
context.closePath();
|
|
context.stroke();
|
|
|
|
context.shadowOffsetX = this.border;
|
|
context.shadowOffsetY = this.border;
|
|
context.shadowBlur = this.border;
|
|
context.shadowColor = this.color.darker(80).toString();
|
|
this.drawShadow(context, this.edge, this.border / 2);
|
|
}
|
|
|
|
// position my contents
|
|
this.contentsMorph.setCenter(this.center());
|
|
|
|
if (listWatcher) {
|
|
listWatcher.fixLayout();
|
|
}
|
|
};
|
|
|
|
CellMorph.prototype.reactToEdit = function (textMorph) {
|
|
var listWatcher;
|
|
if (!isNil(this.idx)) {
|
|
listWatcher = this.parentThatIsA(ListWatcherMorph);
|
|
if (listWatcher) {
|
|
listWatcher.list.put(textMorph.text, this.idx);
|
|
}
|
|
}
|
|
};
|
|
|
|
CellMorph.prototype.mouseClickLeft = function (pos) {
|
|
if (this.isEditable && this.contentsMorph instanceof TextMorph) {
|
|
this.contentsMorph.selectAllAndEdit();
|
|
} else {
|
|
this.escalateEvent('mouseClickLeft', pos);
|
|
}
|
|
};
|
|
|
|
CellMorph.prototype.mouseDoubleClick = function (pos) {
|
|
if (List.prototype.enableTables &&
|
|
this.currentValue instanceof List) {
|
|
new TableDialogMorph(this.contents).popUp(this.world());
|
|
} else {
|
|
this.escalateEvent('mouseDoubleClick', pos);
|
|
}
|
|
};
|
|
|
|
// WatcherMorph //////////////////////////////////////////////////////////
|
|
|
|
/*
|
|
I am a little window which observes some value and continuously
|
|
updates itself accordingly.
|
|
|
|
My target can be either a SpriteMorph or a VariableFrame.
|
|
*/
|
|
|
|
// WatcherMorph inherits from BoxMorph:
|
|
|
|
WatcherMorph.prototype = new BoxMorph();
|
|
WatcherMorph.prototype.constructor = WatcherMorph;
|
|
WatcherMorph.uber = BoxMorph.prototype;
|
|
|
|
// WatcherMorph instance creation:
|
|
|
|
function WatcherMorph(label, color, target, getter, isHidden) {
|
|
this.init(label, color, target, getter, isHidden);
|
|
}
|
|
|
|
WatcherMorph.prototype.init = function (
|
|
label,
|
|
color,
|
|
target,
|
|
getter,
|
|
isHidden
|
|
) {
|
|
// additional properties
|
|
this.labelText = label || '';
|
|
this.version = null;
|
|
this.objName = '';
|
|
|
|
// initialize inherited properties
|
|
WatcherMorph.uber.init.call(
|
|
this,
|
|
SyntaxElementMorph.prototype.rounding,
|
|
1.000001, // shadow bug in Chrome,
|
|
new Color(120, 120, 120)
|
|
);
|
|
|
|
// override inherited behavior
|
|
this.color = new Color(220, 220, 220);
|
|
this.readoutColor = color;
|
|
this.style = 'normal';
|
|
this.target = target || null; // target obj (Sprite) or VariableFrame
|
|
this.getter = getter || null; // callback or variable name (string)
|
|
this.currentValue = null;
|
|
this.labelMorph = null;
|
|
this.sliderMorph = null;
|
|
this.cellMorph = null;
|
|
this.isDraggable = true;
|
|
this.fixLayout();
|
|
this.update();
|
|
if (isHidden) { // for de-serializing
|
|
this.hide();
|
|
}
|
|
};
|
|
|
|
// WatcherMorph accessing:
|
|
|
|
WatcherMorph.prototype.isTemporary = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (this.target instanceof VariableFrame) {
|
|
if (stage) {
|
|
if (this.target === stage.variables.parentFrame) {
|
|
return false; // global
|
|
}
|
|
}
|
|
return this.target.owner === null;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
WatcherMorph.prototype.object = function () {
|
|
// answer the actual sprite I refer to
|
|
return this.target instanceof VariableFrame ?
|
|
this.target.owner : this.target;
|
|
};
|
|
|
|
WatcherMorph.prototype.isGlobal = function (selector) {
|
|
return contains(
|
|
['getLastAnswer', 'getLastMessage', 'getTempo', 'getTimer',
|
|
'reportMouseX', 'reportMouseY', 'reportThreadCount'],
|
|
selector
|
|
);
|
|
};
|
|
|
|
// WatcherMorph slider accessing:
|
|
|
|
WatcherMorph.prototype.setSliderMin = function (num, noUpdate) {
|
|
if (this.target instanceof VariableFrame) {
|
|
this.sliderMorph.setSize(1, noUpdate);
|
|
this.sliderMorph.setStart(num, noUpdate);
|
|
this.sliderMorph.setSize(this.sliderMorph.rangeSize() / 5, noUpdate);
|
|
}
|
|
};
|
|
|
|
WatcherMorph.prototype.setSliderMax = function (num, noUpdate) {
|
|
if (this.target instanceof VariableFrame) {
|
|
this.sliderMorph.setSize(1, noUpdate);
|
|
this.sliderMorph.setStop(num, noUpdate);
|
|
this.sliderMorph.setSize(this.sliderMorph.rangeSize() / 5, noUpdate);
|
|
}
|
|
};
|
|
|
|
// WatcherMorph updating:
|
|
|
|
WatcherMorph.prototype.update = function () {
|
|
var newValue, sprite, num;
|
|
|
|
if (this.target && this.getter) {
|
|
this.updateLabel();
|
|
if (this.target instanceof VariableFrame) {
|
|
newValue = this.target.vars[this.getter] ?
|
|
this.target.vars[this.getter].value : undefined;
|
|
if (newValue === undefined && this.target.owner) {
|
|
sprite = this.target.owner;
|
|
if (contains(sprite.inheritedVariableNames(), this.getter)) {
|
|
newValue = this.target.getVar(this.getter);
|
|
// ghost cell color
|
|
this.cellMorph.setColor(
|
|
SpriteMorph.prototype.blockColor.variables
|
|
.lighter(35)
|
|
);
|
|
} else {
|
|
this.destroy();
|
|
return;
|
|
}
|
|
} else {
|
|
// un-ghost the cell color
|
|
this.cellMorph.setColor(
|
|
SpriteMorph.prototype.blockColor.variables
|
|
);
|
|
}
|
|
} else {
|
|
newValue = this.target[this.getter]();
|
|
}
|
|
if (newValue !== '' && !isNil(newValue)) {
|
|
num = +newValue;
|
|
if (typeof newValue !== 'boolean' && !isNaN(num)) {
|
|
newValue = Math.round(newValue * 1000000000) / 1000000000;
|
|
}
|
|
}
|
|
if (newValue !== this.currentValue) {
|
|
this.changed();
|
|
this.cellMorph.contents = newValue;
|
|
this.cellMorph.drawNew();
|
|
if (!isNaN(newValue)) {
|
|
this.sliderMorph.value = newValue;
|
|
this.sliderMorph.drawNew();
|
|
}
|
|
this.fixLayout();
|
|
this.currentValue = newValue;
|
|
}
|
|
}
|
|
if (this.cellMorph.contentsMorph instanceof ListWatcherMorph) {
|
|
this.cellMorph.contentsMorph.update();
|
|
} else if (isSnapObject(this.cellMorph.contents)) {
|
|
this.cellMorph.update();
|
|
}
|
|
};
|
|
|
|
WatcherMorph.prototype.updateLabel = function () {
|
|
// check whether the target object's name has been changed
|
|
var obj = this.object();
|
|
|
|
if (!obj || this.isGlobal(this.getter)) { return; }
|
|
if (obj.version !== this.version) {
|
|
this.objName = obj.name ? obj.name + ' ' : ' ';
|
|
if (this.labelMorph) {
|
|
this.labelMorph.destroy();
|
|
this.labelMorph = null;
|
|
this.fixLayout();
|
|
}
|
|
}
|
|
};
|
|
|
|
// WatcherMorph layout:
|
|
|
|
WatcherMorph.prototype.fixLayout = function () {
|
|
var fontSize = SyntaxElementMorph.prototype.fontSize, isList,
|
|
myself = this;
|
|
|
|
this.changed();
|
|
|
|
// create my parts
|
|
if (this.labelMorph === null) {
|
|
this.labelMorph = new StringMorph(
|
|
this.objName + this.labelText,
|
|
fontSize,
|
|
null,
|
|
true,
|
|
false,
|
|
false,
|
|
MorphicPreferences.isFlat ? new Point() : new Point(1, 1),
|
|
new Color(255, 255, 255)
|
|
);
|
|
this.add(this.labelMorph);
|
|
}
|
|
if (this.cellMorph === null) {
|
|
this.cellMorph = new CellMorph('', this.readoutColor);
|
|
this.add(this.cellMorph);
|
|
}
|
|
if (this.sliderMorph === null) {
|
|
this.sliderMorph = new SliderMorph(
|
|
0,
|
|
100,
|
|
0,
|
|
20,
|
|
'horizontal'
|
|
);
|
|
this.sliderMorph.alpha = 1;
|
|
this.sliderMorph.button.color = this.color.darker();
|
|
this.sliderMorph.color = this.color.lighter(60);
|
|
this.sliderMorph.button.highlightColor = this.color.darker();
|
|
this.sliderMorph.button.highlightColor.b += 50;
|
|
this.sliderMorph.button.pressColor = this.color.darker();
|
|
this.sliderMorph.button.pressColor.b += 100;
|
|
this.sliderMorph.setHeight(fontSize);
|
|
this.sliderMorph.action = function (num) {
|
|
myself.target.setVar(
|
|
myself.getter,
|
|
Math.round(num),
|
|
myself.target.owner
|
|
);
|
|
};
|
|
this.add(this.sliderMorph);
|
|
}
|
|
|
|
// adjust my layout
|
|
isList = this.cellMorph.contents instanceof List;
|
|
if (isList) { this.style = 'normal'; }
|
|
|
|
if (this.style === 'large') {
|
|
this.labelMorph.hide();
|
|
this.sliderMorph.hide();
|
|
this.cellMorph.big();
|
|
this.cellMorph.setPosition(this.position());
|
|
this.setExtent(this.cellMorph.extent().subtract(1));
|
|
return;
|
|
}
|
|
|
|
this.labelMorph.show();
|
|
this.sliderMorph.show();
|
|
this.cellMorph.normal();
|
|
this.labelMorph.setPosition(this.position().add(new Point(
|
|
this.edge,
|
|
this.border + SyntaxElementMorph.prototype.typeInPadding
|
|
)));
|
|
|
|
if (isList) {
|
|
this.cellMorph.setPosition(this.labelMorph.bottomLeft().add(
|
|
new Point(0, SyntaxElementMorph.prototype.typeInPadding)
|
|
));
|
|
} else {
|
|
this.cellMorph.setPosition(this.labelMorph.topRight().add(new Point(
|
|
fontSize / 3,
|
|
0
|
|
)));
|
|
this.labelMorph.setTop(
|
|
this.cellMorph.top()
|
|
+ (this.cellMorph.height() - this.labelMorph.height()) / 2
|
|
);
|
|
}
|
|
|
|
if (this.style === 'slider') {
|
|
this.sliderMorph.silentSetPosition(new Point(
|
|
this.labelMorph.left(),
|
|
this.cellMorph.bottom()
|
|
+ SyntaxElementMorph.prototype.typeInPadding
|
|
));
|
|
this.sliderMorph.setWidth(this.cellMorph.right()
|
|
- this.labelMorph.left());
|
|
this.silentSetHeight(
|
|
this.cellMorph.height()
|
|
+ this.sliderMorph.height()
|
|
+ this.border * 2
|
|
+ SyntaxElementMorph.prototype.typeInPadding * 3
|
|
);
|
|
} else {
|
|
this.sliderMorph.hide();
|
|
this.bounds.corner.y = this.cellMorph.bottom()
|
|
+ this.border
|
|
+ SyntaxElementMorph.prototype.typeInPadding;
|
|
}
|
|
this.bounds.corner.x = Math.max(
|
|
this.cellMorph.right(),
|
|
this.labelMorph.right()
|
|
) + this.edge
|
|
+ SyntaxElementMorph.prototype.typeInPadding;
|
|
this.drawNew();
|
|
this.changed();
|
|
};
|
|
|
|
// WatcherMorph events:
|
|
|
|
WatcherMorph.prototype.mouseDoubleClick = function (pos) {
|
|
if (List.prototype.enableTables &&
|
|
this.currentValue instanceof List) {
|
|
new TableDialogMorph(this.currentValue).popUp(this.world());
|
|
} else {
|
|
this.escalateEvent('mouseDoubleClick', pos);
|
|
}
|
|
};
|
|
|
|
/*
|
|
// Scratch-like watcher-toggling, commented out b/c we have a drop-down menu
|
|
|
|
WatcherMorph.prototype.mouseClickLeft = function () {
|
|
if (this.style === 'normal') {
|
|
if (this.target instanceof VariableFrame) {
|
|
this.style = 'slider';
|
|
} else {
|
|
this.style = 'large';
|
|
}
|
|
} else if (this.style === 'slider') {
|
|
this.style = 'large';
|
|
} else {
|
|
this.style = 'normal';
|
|
}
|
|
this.fixLayout();
|
|
};
|
|
*/
|
|
|
|
// WatcherMorph user menu:
|
|
|
|
WatcherMorph.prototype.userMenu = function () {
|
|
var myself = this,
|
|
menu = new MenuMorph(this),
|
|
on = '\u25CF',
|
|
off = '\u25CB',
|
|
vNames;
|
|
|
|
function monitor(vName) {
|
|
var stage = myself.parentThatIsA(StageMorph),
|
|
varFrame = myself.currentValue.outerContext.variables;
|
|
menu.addItem(
|
|
vName + '...',
|
|
function () {
|
|
var watcher = detect(
|
|
stage.children,
|
|
function (morph) {
|
|
return morph instanceof WatcherMorph
|
|
&& morph.target === varFrame
|
|
&& morph.getter === vName;
|
|
}
|
|
),
|
|
others;
|
|
if (watcher !== null) {
|
|
watcher.show();
|
|
watcher.fixLayout(); // re-hide hidden parts
|
|
return;
|
|
}
|
|
watcher = new WatcherMorph(
|
|
vName + ' ' + localize('(temporary)'),
|
|
SpriteMorph.prototype.blockColor.variables,
|
|
varFrame,
|
|
vName
|
|
);
|
|
watcher.setPosition(stage.position().add(10));
|
|
others = stage.watchers(watcher.left());
|
|
if (others.length > 0) {
|
|
watcher.setTop(others[others.length - 1].bottom());
|
|
}
|
|
stage.add(watcher);
|
|
watcher.fixLayout();
|
|
}
|
|
);
|
|
}
|
|
|
|
menu.addItem(
|
|
(this.style === 'normal' ? on : off) + ' ' + localize('normal'),
|
|
'styleNormal'
|
|
);
|
|
menu.addItem(
|
|
(this.style === 'large' ? on : off) + ' ' + localize('large'),
|
|
'styleLarge'
|
|
);
|
|
if (this.target instanceof VariableFrame) {
|
|
menu.addItem(
|
|
(this.style === 'slider' ? on : off) + ' ' + localize('slider'),
|
|
'styleSlider'
|
|
);
|
|
menu.addLine();
|
|
menu.addItem(
|
|
'slider min...',
|
|
'userSetSliderMin'
|
|
);
|
|
menu.addItem(
|
|
'slider max...',
|
|
'userSetSliderMax'
|
|
);
|
|
menu.addLine();
|
|
menu.addItem(
|
|
'import...',
|
|
function () {
|
|
var inp = document.createElement('input'),
|
|
ide = myself.parentThatIsA(IDE_Morph);
|
|
if (ide.filePicker) {
|
|
document.body.removeChild(ide.filePicker);
|
|
ide.filePicker = null;
|
|
}
|
|
inp.type = 'file';
|
|
inp.style.color = "transparent";
|
|
inp.style.backgroundColor = "transparent";
|
|
inp.style.border = "none";
|
|
inp.style.outline = "none";
|
|
inp.style.position = "absolute";
|
|
inp.style.top = "0px";
|
|
inp.style.left = "0px";
|
|
inp.style.width = "0px";
|
|
inp.style.height = "0px";
|
|
inp.addEventListener(
|
|
"change",
|
|
function () {
|
|
var file;
|
|
|
|
function txtOnlyMsg(ftype) {
|
|
ide.inform(
|
|
'Unable to import',
|
|
'Snap! can only import "text" files.\n' +
|
|
'You selected a file of type "' +
|
|
ftype +
|
|
'".'
|
|
);
|
|
}
|
|
|
|
function readText(aFile) {
|
|
var frd = new FileReader();
|
|
frd.onloadend = function (e) {
|
|
myself.target.setVar(
|
|
myself.getter,
|
|
e.target.result
|
|
);
|
|
};
|
|
|
|
if (aFile.type.indexOf("text") === 0) {
|
|
frd.readAsText(aFile);
|
|
} else {
|
|
txtOnlyMsg(aFile.type);
|
|
}
|
|
}
|
|
|
|
document.body.removeChild(inp);
|
|
ide.filePicker = null;
|
|
if (inp.files.length > 0) {
|
|
file = inp.files[inp.files.length - 1];
|
|
readText(file);
|
|
}
|
|
},
|
|
false
|
|
);
|
|
document.body.appendChild(inp);
|
|
ide.filePicker = inp;
|
|
inp.click();
|
|
}
|
|
);
|
|
if (this.currentValue &&
|
|
(isString(this.currentValue) || !isNaN(+this.currentValue))) {
|
|
menu.addItem(
|
|
'export...',
|
|
function () {
|
|
var ide = myself.parentThatIsA(IDE_Morph);
|
|
ide.saveFileAs(
|
|
myself.currentValue.toString(),
|
|
'text/plain;charset=utf-8',
|
|
myself.getter // variable name
|
|
);
|
|
}
|
|
);
|
|
} else if (this.currentValue instanceof Context) {
|
|
vNames = this.currentValue.outerContext.variables.names();
|
|
if (vNames.length) {
|
|
menu.addLine();
|
|
vNames.forEach(function (vName) {
|
|
monitor(vName);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return menu;
|
|
};
|
|
|
|
WatcherMorph.prototype.setStyle = function (style) {
|
|
this.style = style;
|
|
this.fixLayout();
|
|
};
|
|
|
|
WatcherMorph.prototype.styleNormal = function () {
|
|
this.setStyle('normal');
|
|
};
|
|
|
|
WatcherMorph.prototype.styleLarge = function () {
|
|
this.setStyle('large');
|
|
};
|
|
|
|
WatcherMorph.prototype.styleSlider = function () {
|
|
this.setStyle('slider');
|
|
};
|
|
|
|
WatcherMorph.prototype.userSetSliderMin = function () {
|
|
new DialogBoxMorph(
|
|
this,
|
|
this.setSliderMin,
|
|
this
|
|
).prompt(
|
|
"Slider minimum value",
|
|
this.sliderMorph.start.toString(),
|
|
this.world(),
|
|
null, // pic
|
|
null, // choices
|
|
null, // read only
|
|
true // numeric
|
|
);
|
|
};
|
|
|
|
WatcherMorph.prototype.userSetSliderMax = function () {
|
|
new DialogBoxMorph(
|
|
this,
|
|
this.setSliderMax,
|
|
this
|
|
).prompt(
|
|
"Slider maximum value",
|
|
this.sliderMorph.stop.toString(),
|
|
this.world(),
|
|
null, // pic
|
|
null, // choices
|
|
null, // read only
|
|
true // numeric
|
|
);
|
|
};
|
|
|
|
// WatcherMorph drawing:
|
|
|
|
WatcherMorph.prototype.drawNew = function () {
|
|
var context,
|
|
gradient;
|
|
this.image = newCanvas(this.extent());
|
|
context = this.image.getContext('2d');
|
|
if (MorphicPreferences.isFlat || (this.edge === 0 && this.border === 0)) {
|
|
BoxMorph.uber.drawNew.call(this);
|
|
return;
|
|
}
|
|
gradient = context.createLinearGradient(0, 0, 0, this.height());
|
|
gradient.addColorStop(0, this.color.lighter().toString());
|
|
gradient.addColorStop(1, this.color.darker().toString());
|
|
context.fillStyle = gradient;
|
|
context.beginPath();
|
|
this.outlinePath(
|
|
context,
|
|
Math.max(this.edge - this.border, 0),
|
|
this.border
|
|
);
|
|
context.closePath();
|
|
context.fill();
|
|
if (this.border > 0) {
|
|
gradient = context.createLinearGradient(0, 0, 0, this.height());
|
|
gradient.addColorStop(0, this.borderColor.lighter().toString());
|
|
gradient.addColorStop(1, this.borderColor.darker().toString());
|
|
context.lineWidth = this.border;
|
|
context.strokeStyle = gradient;
|
|
context.beginPath();
|
|
this.outlinePath(context, this.edge, this.border / 2);
|
|
context.closePath();
|
|
context.stroke();
|
|
}
|
|
};
|
|
|
|
// StagePrompterMorph ////////////////////////////////////////////////////////
|
|
|
|
/*
|
|
I am a sensor-category-colored input box at the bottom of the stage
|
|
which lets the user answer to a question. If I am opened from within
|
|
the context of a sprite, my question can be anything that is displayable
|
|
in a SpeechBubble and will be, if I am opened from within the stage
|
|
my question will be shown as a single line of text within my label morph.
|
|
*/
|
|
|
|
// StagePrompterMorph inherits from BoxMorph:
|
|
|
|
StagePrompterMorph.prototype = new BoxMorph();
|
|
StagePrompterMorph.prototype.constructor = StagePrompterMorph;
|
|
StagePrompterMorph.uber = BoxMorph.prototype;
|
|
|
|
// StagePrompterMorph instance creation:
|
|
|
|
function StagePrompterMorph(question) {
|
|
this.init(question);
|
|
}
|
|
|
|
StagePrompterMorph.prototype.init = function (question) {
|
|
// question is optional in case the Stage is asking
|
|
var myself = this;
|
|
|
|
// additional properties
|
|
this.isDone = false;
|
|
if (question) {
|
|
this.label = new StringMorph(
|
|
question,
|
|
SpriteMorph.prototype.bubbleFontSize,
|
|
null, // fontStyle
|
|
SpriteMorph.prototype.bubbleFontIsBold,
|
|
false, // italic
|
|
'left'
|
|
);
|
|
} else {
|
|
this.label = null;
|
|
}
|
|
this.inputField = new InputFieldMorph();
|
|
this.button = new PushButtonMorph(
|
|
null,
|
|
function () {myself.accept(); },
|
|
'\u2713'
|
|
);
|
|
|
|
// initialize inherited properties
|
|
StagePrompterMorph.uber.init.call(
|
|
this,
|
|
SyntaxElementMorph.prototype.rounding,
|
|
SpriteMorph.prototype.bubbleBorder,
|
|
SpriteMorph.prototype.blockColor.sensing
|
|
);
|
|
|
|
// override inherited behavior
|
|
this.color = new Color(255, 255, 255);
|
|
if (this.label) {this.add(this.label); }
|
|
this.add(this.inputField);
|
|
this.add(this.button);
|
|
this.setWidth(StageMorph.prototype.dimensions.x - 20);
|
|
this.fixLayout();
|
|
};
|
|
|
|
// StagePrompterMorph layout:
|
|
|
|
StagePrompterMorph.prototype.fixLayout = function () {
|
|
var y = 0;
|
|
if (this.label) {
|
|
this.label.setPosition(new Point(
|
|
this.left() + this.edge,
|
|
this.top() + this.edge
|
|
));
|
|
y = this.label.bottom() - this.top();
|
|
}
|
|
this.inputField.setPosition(new Point(
|
|
this.left() + this.edge,
|
|
this.top() + y + this.edge
|
|
));
|
|
this.inputField.setWidth(
|
|
this.width()
|
|
- this.edge * 2
|
|
- this.button.width()
|
|
- this.border
|
|
);
|
|
this.button.setCenter(this.inputField.center());
|
|
this.button.setLeft(this.inputField.right() + this.border);
|
|
this.setHeight(
|
|
this.inputField.bottom()
|
|
- this.top()
|
|
+ this.edge
|
|
);
|
|
};
|
|
|
|
// StagePrompterMorph events:
|
|
|
|
StagePrompterMorph.prototype.mouseClickLeft = function () {
|
|
this.inputField.edit();
|
|
};
|
|
|
|
StagePrompterMorph.prototype.accept = function () {
|
|
this.isDone = true;
|
|
};
|