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