kopia lustrzana https://github.com/backface/turtlestitch
12718 wiersze
377 KiB
JavaScript
12718 wiersze
377 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) 2022 by Jens Mönig
|
|
|
|
This file is part of Snap!.
|
|
|
|
Snap! is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of
|
|
the License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
prerequisites:
|
|
--------------
|
|
needs blocks.js, threads.js, morphic.js and widgets.js
|
|
|
|
|
|
toc
|
|
---
|
|
the following list shows the order in which all constructors are
|
|
defined. Use this list to locate code in this document:
|
|
|
|
SpriteMorph
|
|
SpriteHighlightMorph
|
|
StageMorph
|
|
Costume
|
|
SVG_Costume
|
|
CostumeEditorMorph
|
|
Sound
|
|
Note
|
|
Microphone
|
|
CellMorph
|
|
WatcherMorph
|
|
StagePrompterMorph
|
|
|
|
SpeechBubbleMorph*
|
|
SpriteBubbleMorph
|
|
|
|
* defined in Morphic.js
|
|
|
|
|
|
credits
|
|
-------
|
|
Ian Reynolds contributed initial porting of primitives from Squeak and
|
|
sound handling
|
|
Achal Dave contributed research and prototyping for creating music
|
|
using the Web Audio API
|
|
Yuan Yuan and Dylan Servilla contributed graphic effects for costumes
|
|
|
|
*/
|
|
|
|
// Global stuff ////////////////////////////////////////////////////////
|
|
|
|
/*global PaintEditorMorph, ListWatcherMorph, PushButtonMorph, ToggleMorph, ZERO,
|
|
DialogBoxMorph, InputFieldMorph, SpriteIconMorph, BlockMorph, SymbolMorph, nop,
|
|
ThreadManager, VariableFrame, detect, BlockMorph, BoxMorph, Color, Animation,
|
|
CommandBlockMorph, FrameMorph, HatBlockMorph, MenuMorph, Morph, MultiArgMorph,
|
|
ReporterBlockMorph, ScriptsMorph, StringMorph, SyntaxElementMorph, XML_Element,
|
|
TextMorph, contains, degrees, detect, newCanvas, radians, Array, CursorMorph,
|
|
Date, FrameMorph, Math, MenuMorph, Morph, invoke, MorphicPreferences, WHITE,
|
|
Object, PenMorph, Point, Rectangle, ScrollFrameMorph, SliderMorph, VideoMotion,
|
|
StringMorph, TextMorph, contains, copy, degrees, detect, document, isNaN, Point,
|
|
isString, newCanvas, nop, parseFloat, radians, window, modules, IDE_Morph,
|
|
VariableDialogMorph, HTMLCanvasElement, Context, List, RingMorph, HandleMorph,
|
|
SpeechBubbleMorph, InputSlotMorph, isNil, FileReader, TableDialogMorph, String,
|
|
BlockEditorMorph, BlockDialogMorph, PrototypeHatBlockMorph, BooleanSlotMorph,
|
|
localize, TableMorph, TableFrameMorph, normalizeCanvas, VectorPaintEditorMorph,
|
|
AlignmentMorph, Process, WorldMap, copyCanvas, useBlurredShadows,
|
|
BlockVisibilityDialogMorph, CostumeIconMorph, SoundIconMorph*/
|
|
|
|
/*jshint esversion: 6*/
|
|
|
|
modules.objects = '2022-February-07';
|
|
|
|
var SpriteMorph;
|
|
var StageMorph;
|
|
var SpriteBubbleMorph;
|
|
var Costume;
|
|
var SVG_Costume;
|
|
var CostumeEditorMorph;
|
|
var Sound;
|
|
var Note;
|
|
var Microphone;
|
|
var CellMorph;
|
|
var WatcherMorph;
|
|
var StagePrompterMorph;
|
|
var Note;
|
|
var SpriteHighlightMorph;
|
|
|
|
function isSnapObject(thing) {
|
|
return thing instanceof SpriteMorph || (thing instanceof StageMorph);
|
|
}
|
|
|
|
// SpriteMorph /////////////////////////////////////////////////////////
|
|
|
|
// I am a scriptable object
|
|
|
|
// SpriteMorph inherits from PenMorph:
|
|
|
|
SpriteMorph.prototype = new PenMorph();
|
|
SpriteMorph.prototype.constructor = SpriteMorph;
|
|
SpriteMorph.uber = PenMorph.prototype;
|
|
|
|
// SpriteMorph settings
|
|
|
|
SpriteMorph.prototype.attributes =
|
|
[
|
|
'x position',
|
|
'y position',
|
|
'direction',
|
|
'size',
|
|
'costumes',
|
|
'costume #',
|
|
'volume',
|
|
'balance',
|
|
'sounds',
|
|
'shown?',
|
|
'pen down?',
|
|
'scripts'
|
|
];
|
|
|
|
SpriteMorph.prototype.categories =
|
|
[
|
|
'motion',
|
|
'looks',
|
|
'sound',
|
|
'pen',
|
|
'control',
|
|
'sensing',
|
|
'operators',
|
|
'variables',
|
|
'lists',
|
|
'other'
|
|
];
|
|
|
|
SpriteMorph.prototype.blockColor = {
|
|
motion : new Color(74, 108, 212),
|
|
looks : new Color(143, 86, 227),
|
|
sound : new Color(207, 74, 217),
|
|
pen : new Color(0, 161, 120),
|
|
control : new Color(230, 168, 34),
|
|
sensing : new Color(4, 148, 220),
|
|
operators : new Color(98, 194, 19),
|
|
variables : new Color(243, 118, 29),
|
|
lists : new Color(217, 77, 17),
|
|
other: new Color(150, 150, 150)
|
|
};
|
|
|
|
SpriteMorph.prototype.customCategories = new Map(); // key: name, value: color
|
|
|
|
SpriteMorph.prototype.allCategories = function () {
|
|
return this.categories.concat(
|
|
Array.from(this.customCategories.keys()).sort()
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.blockColorFor = function (category) {
|
|
return this.blockColor[category] ||
|
|
this.customCategories.get(category) ||
|
|
this.blockColor.other;
|
|
};
|
|
|
|
SpriteMorph.prototype.paletteColor = new Color(55, 55, 55);
|
|
SpriteMorph.prototype.paletteTextColor = new Color(230, 230, 230);
|
|
SpriteMorph.prototype.sliderColor
|
|
= SpriteMorph.prototype.paletteColor.lighter(30);
|
|
SpriteMorph.prototype.isCachingPrimitives = true;
|
|
|
|
SpriteMorph.prototype.enableNesting = true;
|
|
SpriteMorph.prototype.enableFirstClass = true;
|
|
SpriteMorph.prototype.showingExtensions = false;
|
|
SpriteMorph.prototype.useFlatLineEnds = false;
|
|
SpriteMorph.prototype.penColorModel = 'hsv'; // or 'hsl'
|
|
SpriteMorph.prototype.highlightColor = new Color(250, 200, 130);
|
|
SpriteMorph.prototype.highlightBorder = 8;
|
|
|
|
SpriteMorph.prototype.bubbleColor = WHITE;
|
|
SpriteMorph.prototype.bubbleFontSize = 14;
|
|
SpriteMorph.prototype.bubbleFontIsBold = true;
|
|
SpriteMorph.prototype.bubbleCorner = 10;
|
|
SpriteMorph.prototype.bubbleBorder = 3;
|
|
SpriteMorph.prototype.bubbleBorderColor = new Color(190, 190, 190);
|
|
SpriteMorph.prototype.bubbleMaxTextWidth = 130;
|
|
|
|
SpriteMorph.prototype.initBlocks = function () {
|
|
SpriteMorph.prototype.blocks = {
|
|
|
|
// Motion
|
|
forward: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'move %n steps',
|
|
defaults: [10]
|
|
},
|
|
turn: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'turn %clockwise %n degrees',
|
|
defaults: [15]
|
|
},
|
|
turnLeft: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'turn %counterclockwise %n degrees',
|
|
defaults: [15]
|
|
},
|
|
setHeading: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'point in direction %dir',
|
|
defaults: [90]
|
|
},
|
|
doFaceTowards: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'point towards %dst',
|
|
defaults: [['mouse-pointer']]
|
|
},
|
|
gotoXY: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'go to x: %n y: %n',
|
|
defaults: [0, 0]
|
|
},
|
|
doGotoObject: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'go to %dst',
|
|
defaults: [['random position']]
|
|
},
|
|
doGlide: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'glide %n secs to x: %n y: %n',
|
|
defaults: [1, 0, 0]
|
|
},
|
|
changeXPosition: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'change x by %n',
|
|
defaults: [10]
|
|
},
|
|
setXPosition: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'set x to %n',
|
|
defaults: [0]
|
|
},
|
|
changeYPosition: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'change y by %n',
|
|
defaults: [10]
|
|
},
|
|
setYPosition: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'set y to %n',
|
|
defaults: [0]
|
|
},
|
|
bounceOffEdge: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'motion',
|
|
spec: 'if on edge, bounce'
|
|
},
|
|
xPosition: {
|
|
only: SpriteMorph,
|
|
type: 'reporter',
|
|
category: 'motion',
|
|
spec: 'x position'
|
|
},
|
|
yPosition: {
|
|
only: SpriteMorph,
|
|
type: 'reporter',
|
|
category: 'motion',
|
|
spec: 'y position'
|
|
},
|
|
direction: {
|
|
only: SpriteMorph,
|
|
type: 'reporter',
|
|
category: 'motion',
|
|
spec: 'direction'
|
|
},
|
|
|
|
// Looks
|
|
doSwitchToCostume: {
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'switch to costume %cst'
|
|
},
|
|
doWearNextCostume: {
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'next costume'
|
|
},
|
|
getCostumeIdx: {
|
|
type: 'reporter',
|
|
category: 'looks',
|
|
spec: 'costume #'
|
|
},
|
|
reportGetImageAttribute: {
|
|
type: 'reporter',
|
|
category: 'looks',
|
|
spec: '%img of costume %cst',
|
|
defaults: [['width'], ['current']]
|
|
},
|
|
reportNewCostume: {
|
|
type: 'reporter',
|
|
category: 'looks',
|
|
spec: 'new costume %l width %dim height %dim'
|
|
},
|
|
reportNewCostumeStretched: {
|
|
type: 'reporter',
|
|
category: 'looks',
|
|
spec: 'stretch %cst x: %n y: %n %',
|
|
defaults: [['current'], 100, 50]
|
|
},
|
|
doSayFor: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'say %s for %n secs',
|
|
defaults: [localize('Hello!'), 2]
|
|
},
|
|
bubble: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'say %s',
|
|
defaults: [localize('Hello!')]
|
|
},
|
|
doThinkFor: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'think %s for %n secs',
|
|
defaults: [localize('Hmm...'), 2]
|
|
},
|
|
doThink: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'think %s',
|
|
defaults: [localize('Hmm...')]
|
|
},
|
|
changeEffect: {
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'change %eff effect by %n',
|
|
defaults: [['ghost'], 25]
|
|
},
|
|
setEffect: {
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'set %eff effect to %n',
|
|
defaults: [['ghost'], 0]
|
|
},
|
|
getEffect: {
|
|
type: 'reporter',
|
|
category: 'looks',
|
|
spec: '%eff effect',
|
|
defaults: [['ghost']]
|
|
},
|
|
clearEffects: {
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'clear graphic effects'
|
|
},
|
|
changeScale: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'change size by %n',
|
|
defaults: [10]
|
|
},
|
|
setScale: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'set size to %n %',
|
|
defaults: [100]
|
|
},
|
|
getScale: {
|
|
only: SpriteMorph,
|
|
type: 'reporter',
|
|
category: 'looks',
|
|
spec: 'size'
|
|
},
|
|
show: {
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'show'
|
|
},
|
|
hide: {
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'hide'
|
|
},
|
|
reportShown: {
|
|
type: 'predicate',
|
|
category: 'looks',
|
|
spec: 'shown?'
|
|
},
|
|
goToLayer: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'go to %layer layer',
|
|
defaults: [['front']]
|
|
},
|
|
goBack: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'go back %n layers',
|
|
defaults: [1]
|
|
},
|
|
|
|
// Looks - Debugging primitives for development mode
|
|
doScreenshot: {
|
|
dev: true,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'save %imgsource as costume named %s',
|
|
defaults: [['pen trails'], localize('screenshot')]
|
|
},
|
|
reportCostumes: {
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'looks',
|
|
spec: 'wardrobe'
|
|
},
|
|
alert: {
|
|
dev: true,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'alert %mult%s'
|
|
},
|
|
log: {
|
|
dev: true,
|
|
type: 'command',
|
|
category: 'looks',
|
|
spec: 'console log %mult%s'
|
|
},
|
|
|
|
// Sound
|
|
playSound: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'play sound %snd'
|
|
},
|
|
doPlaySoundUntilDone: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'play sound %snd until done'
|
|
},
|
|
doPlaySoundAtRate: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'play sound %snd at %rate Hz',
|
|
defaults: ['', 44100]
|
|
},
|
|
doStopAllSounds: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'stop all sounds'
|
|
},
|
|
reportGetSoundAttribute: {
|
|
type: 'reporter',
|
|
category: 'sound',
|
|
spec: '%aa of sound %snd',
|
|
defaults: [['duration']]
|
|
},
|
|
reportNewSoundFromSamples: {
|
|
type: 'reporter',
|
|
category: 'sound',
|
|
spec: 'new sound %l rate %rate Hz',
|
|
defaults: [null, 44100]
|
|
},
|
|
doRest: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'rest for %n beats',
|
|
defaults: [0.2]
|
|
},
|
|
doPlayNote: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'play note %note for %n beats',
|
|
defaults: [60, 0.5]
|
|
},
|
|
doPlayFrequency: { // only in dev mode - experimental
|
|
dev: true,
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'play %n Hz for %n secs',
|
|
defaults: [440, 2]
|
|
},
|
|
doSetInstrument: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'set instrument to %inst',
|
|
defaults: [1]
|
|
},
|
|
doChangeTempo: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'change tempo by %n',
|
|
defaults: [20]
|
|
},
|
|
doSetTempo: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'set tempo to %n bpm',
|
|
defaults: [60]
|
|
},
|
|
getTempo: {
|
|
type: 'reporter',
|
|
category: 'sound',
|
|
spec: 'tempo'
|
|
},
|
|
changeVolume: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'change volume by %n',
|
|
defaults: [10]
|
|
},
|
|
setVolume: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'set volume to %n %',
|
|
defaults: [100]
|
|
},
|
|
getVolume: {
|
|
type: 'reporter',
|
|
category: 'sound',
|
|
spec: 'volume'
|
|
},
|
|
changePan: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'change balance by %n',
|
|
defaults: [10]
|
|
},
|
|
setPan: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'set balance to %n',
|
|
defaults: [0]
|
|
},
|
|
getPan: {
|
|
type: 'reporter',
|
|
category: 'sound',
|
|
spec: 'balance'
|
|
},
|
|
playFreq: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'play frequency %n Hz',
|
|
defaults: [440]
|
|
},
|
|
stopFreq: {
|
|
type: 'command',
|
|
category: 'sound',
|
|
spec: 'stop frequency'
|
|
},
|
|
|
|
// Sound - Debugging primitives for development mode
|
|
reportSounds: {
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'sound',
|
|
spec: 'jukebox'
|
|
},
|
|
|
|
// Pen
|
|
clear: {
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'clear'
|
|
},
|
|
down: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'pen down'
|
|
},
|
|
up: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'pen up'
|
|
},
|
|
getPenDown: {
|
|
only: SpriteMorph,
|
|
type: 'predicate',
|
|
category: 'pen',
|
|
spec: 'pen down?'
|
|
},
|
|
setColor: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'set pen color to %clr'
|
|
},
|
|
setPenColorDimension: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'set pen %clrdim to %n',
|
|
defaults: [['hue'], 50]
|
|
},
|
|
changePenColorDimension: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'change pen %clrdim by %n',
|
|
defaults: [['hue'], 10]
|
|
},
|
|
getPenAttribute: {
|
|
type: 'reporter',
|
|
category: 'pen',
|
|
spec: 'pen %pen',
|
|
defaults: [['hue']]
|
|
},
|
|
setBackgroundColor: {
|
|
only: StageMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'set background color to %clr'
|
|
},
|
|
setBackgroundColorDimension: {
|
|
only: StageMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'set background %clrdim to %n',
|
|
defaults: [['hue'], 50]
|
|
},
|
|
changeBackgroundColorDimension: {
|
|
only: StageMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'change background %clrdim by %n',
|
|
defaults: [['hue'], 10]
|
|
},
|
|
changeSize: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'change pen size by %n',
|
|
defaults: [1]
|
|
},
|
|
setSize: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'set pen size to %n',
|
|
defaults: [1]
|
|
},
|
|
doStamp: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'stamp'
|
|
},
|
|
floodFill: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'fill'
|
|
},
|
|
write: {
|
|
only: SpriteMorph,
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'write %s size %n',
|
|
defaults: [localize('Hello!'), 12]
|
|
},
|
|
reportPenTrailsAsCostume: {
|
|
type: 'reporter',
|
|
category: 'pen',
|
|
spec: 'pen trails'
|
|
},
|
|
reportPentrailsAsSVG: {
|
|
type: 'reporter',
|
|
category: 'pen',
|
|
spec: 'pen vectors'
|
|
},
|
|
doPasteOn: {
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'paste on %spr'
|
|
},
|
|
doCutFrom: {
|
|
type: 'command',
|
|
category: 'pen',
|
|
spec: 'cut from %spr'
|
|
},
|
|
|
|
// Control
|
|
receiveGo: {
|
|
type: 'hat',
|
|
category: 'control',
|
|
spec: 'when %greenflag clicked'
|
|
},
|
|
receiveKey: {
|
|
type: 'hat',
|
|
category: 'control',
|
|
spec: 'when %keyHat key pressed %keyName',
|
|
defaults: [['space']]
|
|
},
|
|
receiveInteraction: {
|
|
type: 'hat',
|
|
category: 'control',
|
|
spec: 'when I am %interaction',
|
|
defaults: ['clicked']
|
|
},
|
|
receiveMessage: {
|
|
type: 'hat',
|
|
category: 'control',
|
|
spec: 'when I receive %msgHat %message',
|
|
defaults: [''] // trigger the "message" expansion to refresh
|
|
},
|
|
receiveCondition: {
|
|
type: 'hat',
|
|
category: 'control',
|
|
spec: 'when %b'
|
|
},
|
|
getLastMessage: { // retained for legacy compatibility
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'control',
|
|
spec: 'message'
|
|
},
|
|
doBroadcast: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'broadcast %msg %receive'
|
|
},
|
|
doBroadcastAndWait: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'broadcast %msg %receive and wait'
|
|
},
|
|
doWait: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'wait %n secs',
|
|
defaults: [1]
|
|
},
|
|
doWaitUntil: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'wait until %b'
|
|
},
|
|
doForever: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'forever %loop'
|
|
},
|
|
doRepeat: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'repeat %n %loop',
|
|
defaults: [10]
|
|
},
|
|
doUntil: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'repeat until %b %loop'
|
|
},
|
|
doFor: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'for %upvar = %n to %n %cla',
|
|
defaults: ['i', 1, 10]
|
|
},
|
|
doIf: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'if %b %c'
|
|
},
|
|
doIfElse: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'if %b %c else %c'
|
|
},
|
|
reportIfElse: {
|
|
type: 'reporter',
|
|
category: 'control',
|
|
spec: 'if %b then %s else %s'
|
|
},
|
|
doStopThis: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'stop %stopChoices',
|
|
defaults: [['all']]
|
|
},
|
|
doRun: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'run %cmdRing %inputs'
|
|
},
|
|
fork: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'launch %cmdRing %inputs'
|
|
},
|
|
evaluate: {
|
|
type: 'reporter',
|
|
category: 'control',
|
|
spec: 'call %repRing %inputs'
|
|
},
|
|
doReport: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'report %s'
|
|
},
|
|
doCallCC: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'run %cmdRing w/continuation'
|
|
},
|
|
reportCallCC: {
|
|
type: 'reporter',
|
|
category: 'control',
|
|
spec: 'call %cmdRing w/continuation'
|
|
},
|
|
doWarp: {
|
|
type: 'command',
|
|
category: 'other',
|
|
spec: 'warp %c'
|
|
},
|
|
|
|
// Message passing
|
|
doTellTo: {
|
|
type: 'command',
|
|
category: 'control',
|
|
// spec: 'tell %spr to %cl' // I liked this version better, -Jens
|
|
spec: 'tell %spr to %cmdRing %inputs'
|
|
},
|
|
reportAskFor: {
|
|
type: 'reporter',
|
|
category: 'control',
|
|
spec: 'ask %spr for %repRing %inputs'
|
|
},
|
|
|
|
// Cloning
|
|
receiveOnClone: {
|
|
type: 'hat',
|
|
category: 'control',
|
|
spec: 'when I start as a clone'
|
|
},
|
|
createClone: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'create a clone of %cln',
|
|
defaults: [['myself']]
|
|
},
|
|
newClone: {
|
|
type: 'reporter',
|
|
category: 'control',
|
|
spec: 'a new clone of %cln',
|
|
defaults: [['myself']]
|
|
},
|
|
removeClone: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'delete this clone'
|
|
},
|
|
|
|
// Debugging - pausing
|
|
doPauseAll: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'pause all %pause'
|
|
},
|
|
|
|
// Scenes
|
|
doSwitchToScene: {
|
|
type: 'command',
|
|
category: 'control',
|
|
spec: 'switch to scene %scn %send',
|
|
defaults: [['next']]
|
|
},
|
|
|
|
// Sensing
|
|
reportTouchingObject: {
|
|
only: SpriteMorph,
|
|
type: 'predicate',
|
|
category: 'sensing',
|
|
spec: 'touching %col ?',
|
|
defaults: [['mouse-pointer']]
|
|
},
|
|
reportTouchingColor: {
|
|
only: SpriteMorph,
|
|
type: 'predicate',
|
|
category: 'sensing',
|
|
spec: 'touching %clr ?'
|
|
},
|
|
reportColorIsTouchingColor: {
|
|
only: SpriteMorph,
|
|
type: 'predicate',
|
|
category: 'sensing',
|
|
spec: 'color %clr is touching %clr ?'
|
|
},
|
|
reportAspect: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: '%asp at %loc',
|
|
defaults: [['hue'], ['mouse-pointer']]
|
|
},
|
|
reportStackSize: {
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'stack size'
|
|
},
|
|
reportFrameCount: {
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'frames'
|
|
},
|
|
reportYieldCount: {
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'yields'
|
|
},
|
|
reportThreadCount: {
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'processes'
|
|
},
|
|
doAsk: {
|
|
type: 'command',
|
|
category: 'sensing',
|
|
spec: 'ask %s and wait',
|
|
defaults: [localize('what\'s your name?')]
|
|
},
|
|
reportLastAnswer: { // retained for legacy compatibility
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'answer'
|
|
},
|
|
getLastAnswer: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'answer'
|
|
},
|
|
reportMouseX: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'mouse x'
|
|
},
|
|
reportMouseY: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'mouse y'
|
|
},
|
|
reportMouseDown: {
|
|
type: 'predicate',
|
|
category: 'sensing',
|
|
spec: 'mouse down?'
|
|
},
|
|
reportKeyPressed: {
|
|
type: 'predicate',
|
|
category: 'sensing',
|
|
spec: 'key %key pressed?',
|
|
defaults: [['space']]
|
|
},
|
|
reportRelationTo: {
|
|
only: SpriteMorph,
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: '%rel to %dst',
|
|
defaults: [['distance'], ['mouse-pointer']]
|
|
},
|
|
doResetTimer: {
|
|
type: 'command',
|
|
category: 'sensing',
|
|
spec: 'reset timer'
|
|
},
|
|
reportTimer: { // retained for legacy compatibility
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'timer'
|
|
},
|
|
getTimer: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'timer'
|
|
},
|
|
reportAttributeOf: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: '%att of %spr',
|
|
defaults: [['costume #']]
|
|
},
|
|
reportObject: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'object %self',
|
|
defaults: [['myself']]
|
|
},
|
|
reportURL: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'url %s',
|
|
defaults: ['snap.berkeley.edu']
|
|
},
|
|
doSetGlobalFlag: {
|
|
type: 'command',
|
|
category: 'sensing',
|
|
spec: 'set %setting to %b',
|
|
defaults: [['video capture']]
|
|
},
|
|
reportGlobalFlag: {
|
|
type: 'predicate',
|
|
category: 'sensing',
|
|
spec: 'is %setting on?',
|
|
defaults: [['turbo mode']]
|
|
},
|
|
reportDate: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'current %dates',
|
|
defaults: [['date']]
|
|
},
|
|
reportGet: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'my %get',
|
|
defaults: [['neighbors']]
|
|
},
|
|
reportAudio: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'microphone %audio',
|
|
defaults: [['volume']]
|
|
},
|
|
reportBlockAttribute: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: '%block of block %repRing',
|
|
defaults: [['definition']]
|
|
},
|
|
|
|
// Operators
|
|
reifyScript: {
|
|
type: 'ring',
|
|
category: 'other',
|
|
spec: '%rc %ringparms',
|
|
alias: 'command ring lambda'
|
|
},
|
|
reifyReporter: {
|
|
type: 'ring',
|
|
category: 'other',
|
|
spec: '%rr %ringparms',
|
|
alias: 'reporter ring lambda'
|
|
},
|
|
reifyPredicate: {
|
|
type: 'ring',
|
|
category: 'other',
|
|
spec: '%rp %ringparms',
|
|
alias: 'predicate ring lambda'
|
|
},
|
|
reportSum: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: '%n + %n'
|
|
},
|
|
reportDifference: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: '%n \u2212 %n',
|
|
alias: '-'
|
|
},
|
|
reportProduct: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: '%n \u00D7 %n',
|
|
alias: '*'
|
|
},
|
|
reportQuotient: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: '%n / %n' // '%n \u00F7 %n'
|
|
},
|
|
reportRound: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'round %n'
|
|
},
|
|
reportMonadic: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: '%fun of %n',
|
|
defaults: [['sqrt'], 10]
|
|
},
|
|
reportPower: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: '%n ^ %n'
|
|
},
|
|
reportModulus: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: '%n mod %n'
|
|
},
|
|
reportAtan2: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'atan2 %n ÷ %n'
|
|
},
|
|
reportMin: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: '%n min %n'
|
|
},
|
|
reportMax: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: '%n max %n'
|
|
},
|
|
reportRandom: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'pick random %n to %n',
|
|
defaults: [1, 10]
|
|
},
|
|
reportEquals: {
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: '%s = %s'
|
|
},
|
|
reportNotEquals: {
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: '%s \u2260 %s'
|
|
},
|
|
reportLessThan: {
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: '%s < %s'
|
|
},
|
|
reportLessThanOrEquals: {
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: '%s \u2264 %s'
|
|
},
|
|
reportGreaterThan: {
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: '%s > %s'
|
|
},
|
|
reportGreaterThanOrEquals: {
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: '%s \u2265 %s'
|
|
},
|
|
reportAnd: {
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: '%b and %b'
|
|
},
|
|
reportOr: {
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: '%b or %b'
|
|
},
|
|
reportNot: {
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: 'not %b'
|
|
},
|
|
reportBoolean: {
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: '%bool',
|
|
defaults: [true],
|
|
alias: 'true boolean'
|
|
},
|
|
reportFalse: { // special case for keyboard entry and search
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: '%bool',
|
|
defaults: [false],
|
|
alias: 'false boolean'
|
|
},
|
|
reportJoinWords: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'join %words',
|
|
defaults: [localize('hello') + ' ', localize('world')]
|
|
},
|
|
reportLetter: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'letter %idx of %s',
|
|
defaults: [1, localize('world')]
|
|
},
|
|
reportStringSize: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'length of %s',
|
|
defaults: [localize('world')]
|
|
},
|
|
reportUnicode: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'unicode of %s',
|
|
defaults: ['a']
|
|
},
|
|
reportUnicodeAsLetter: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'unicode %n as letter',
|
|
defaults: [65]
|
|
},
|
|
reportIsA: {
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: 'is %s a %typ ?',
|
|
defaults: [5, ['number']]
|
|
},
|
|
reportIsIdentical: {
|
|
type: 'predicate',
|
|
category: 'operators',
|
|
spec: 'is %s identical to %s ?'
|
|
},
|
|
reportTextSplit: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'split %s by %delim',
|
|
defaults: [localize('hello') + ' ' + localize('world'), " "]
|
|
},
|
|
reportJSFunction: {
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'JavaScript function ( %mult%s ) { %code }'
|
|
},
|
|
reportTypeOf: { // only in dev mode for debugging
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'type of %s',
|
|
defaults: [5]
|
|
},
|
|
reportTextFunction: { // only in dev mode - experimental
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: '%txtfun of %s',
|
|
defaults: [['encode URI'], "Abelson & Sussman"]
|
|
},
|
|
reportCompiled: { // experimental
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'operators',
|
|
spec: 'compile %repRing for %n args',
|
|
defaults: [null, 0]
|
|
},
|
|
|
|
// Variables
|
|
doSetVar: {
|
|
type: 'command',
|
|
category: 'variables',
|
|
spec: 'set %var to %s',
|
|
defaults: [null, 0]
|
|
},
|
|
doChangeVar: {
|
|
type: 'command',
|
|
category: 'variables',
|
|
spec: 'change %var by %n',
|
|
defaults: [null, 1]
|
|
},
|
|
doShowVar: {
|
|
type: 'command',
|
|
category: 'variables',
|
|
spec: 'show variable %var'
|
|
},
|
|
doHideVar: {
|
|
type: 'command',
|
|
category: 'variables',
|
|
spec: 'hide variable %var'
|
|
},
|
|
doDeclareVariables: {
|
|
type: 'command',
|
|
category: 'other',
|
|
spec: 'script variables %scriptVars'
|
|
},
|
|
|
|
// inheritance
|
|
doDeleteAttr: {
|
|
type: 'command',
|
|
category: 'variables',
|
|
spec: 'inherit %shd'
|
|
},
|
|
|
|
// Lists
|
|
reportNewList: {
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: 'list %exp'
|
|
},
|
|
reportCONS: {
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: '%s in front of %l'
|
|
},
|
|
reportListItem: {
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: 'item %idx of %l',
|
|
defaults: [1]
|
|
},
|
|
reportCDR: {
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: 'all but first of %l'
|
|
},
|
|
reportListLength: { // deprecated as of v6.6
|
|
dev: true,
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: 'length of %l'
|
|
},
|
|
reportListAttribute: {
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: '%la of %l',
|
|
defaults: [['length']]
|
|
},
|
|
reportListContainsItem: {
|
|
type: 'predicate',
|
|
category: 'lists',
|
|
spec: '%l contains %s',
|
|
defaults: [null, localize('thing')]
|
|
},
|
|
reportListIsEmpty: {
|
|
type: 'predicate',
|
|
category: 'lists',
|
|
spec: 'is %l empty?'
|
|
},
|
|
reportListIndex: {
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: 'index of %s in %l',
|
|
defaults: [localize('thing')]
|
|
},
|
|
doAddToList: {
|
|
type: 'command',
|
|
category: 'lists',
|
|
spec: 'add %s to %l',
|
|
defaults: [localize('thing')]
|
|
},
|
|
doDeleteFromList: {
|
|
type: 'command',
|
|
category: 'lists',
|
|
spec: 'delete %ida of %l',
|
|
defaults: [1]
|
|
},
|
|
doInsertInList: {
|
|
type: 'command',
|
|
category: 'lists',
|
|
spec: 'insert %s at %idx of %l',
|
|
defaults: [localize('thing'), 1]
|
|
},
|
|
doReplaceInList: {
|
|
type: 'command',
|
|
category: 'lists',
|
|
spec: 'replace item %idx of %l with %s',
|
|
defaults: [1, null, localize('thing')]
|
|
},
|
|
|
|
// numbers - (arrayed when hyper-blocks is on, otherwise linked)
|
|
reportNumbers: {
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: 'numbers from %n to %n',
|
|
defaults: [1, 10]
|
|
},
|
|
/*
|
|
reportListCombination: { // currently not in use
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: '%mlfunc %lists',
|
|
defaults: [['append']]
|
|
},
|
|
*/
|
|
reportConcatenatedLists: {
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: 'append %lists'
|
|
},
|
|
reportTranspose: { // deprecated
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: 'transpose %l'
|
|
},
|
|
reportReshape: {
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: 'reshape %l to %nums',
|
|
defaults: [null, [4, 3]]
|
|
},
|
|
/*
|
|
reportSlice: { // currently not in use
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: 'slice %l by %nums',
|
|
defaults: [null, [2, -1]]
|
|
},
|
|
*/
|
|
|
|
// HOFs
|
|
reportMap: {
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: 'map %repRing over %l'
|
|
},
|
|
reportAtomicMap: {
|
|
dev: true, // not shown in palette, only accessible via relabelling
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: '%blitz map %repRing over %l'
|
|
},
|
|
reportKeep: {
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: 'keep items %predRing from %l'
|
|
},
|
|
reportAtomicKeep: {
|
|
dev: true, // not shown in palette, only accessible via relabelling
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: '%blitz keep items %predRing from %l'
|
|
},
|
|
reportFindFirst: {
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: 'find first item %predRing in %l'
|
|
},
|
|
reportAtomicFindFirst: {
|
|
dev: true, // not shown in palette, only accessible via relabelling
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: '%blitz find first item %predRing in %l'
|
|
},
|
|
reportCombine: {
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: 'combine %l using %repRing'
|
|
},
|
|
reportAtomicCombine: {
|
|
dev: true, // not shown in palette, only accessible via relabelling
|
|
type: 'reporter',
|
|
category: 'lists',
|
|
spec: '%blitz combine %l using %repRing'
|
|
},
|
|
doForEach: {
|
|
type: 'command',
|
|
category: 'lists',
|
|
spec: 'for each %upvar in %l %cla',
|
|
defaults: [localize('item')]
|
|
},
|
|
|
|
// Tables - experimental
|
|
doShowTable: {
|
|
dev: true,
|
|
type: 'command',
|
|
category: 'lists',
|
|
spec: 'show table %l'
|
|
},
|
|
|
|
// Code mapping
|
|
doMapCodeOrHeader: {
|
|
type: 'command',
|
|
category: 'other',
|
|
spec: 'map %cmdRing to %codeKind %code',
|
|
defaults: [null, ['code']]
|
|
},
|
|
doMapValueCode: {
|
|
type: 'command',
|
|
category: 'other',
|
|
spec: 'map %mapValue to code %code',
|
|
defaults: [['String'], '<#1>']
|
|
},
|
|
doMapListCode: {
|
|
type: 'command',
|
|
category: 'other',
|
|
spec: 'map %codeListPart of %codeListKind to code %code'
|
|
},
|
|
reportMappedCode: {
|
|
type: 'reporter',
|
|
category: 'other',
|
|
spec: 'code of %cmdRing'
|
|
},
|
|
|
|
// Extensions
|
|
doApplyExtension: {
|
|
type: 'command',
|
|
category: 'other',
|
|
spec: 'primitive %prim %mult%s'
|
|
},
|
|
reportApplyExtension: {
|
|
type: 'reporter',
|
|
category: 'other',
|
|
spec: 'primitive %prim %mult%s'
|
|
},
|
|
|
|
// Video motion
|
|
doSetVideoTransparency: {
|
|
type: 'command',
|
|
category: 'sensing',
|
|
spec: 'set video transparency to %n',
|
|
defaults: [50]
|
|
},
|
|
reportVideo: {
|
|
type: 'reporter',
|
|
category: 'sensing',
|
|
spec: 'video %vid on %self',
|
|
defaults: [['motion'], ['myself']]
|
|
}
|
|
};
|
|
};
|
|
|
|
SpriteMorph.prototype.initBlocks();
|
|
|
|
SpriteMorph.prototype.initBlockMigrations = function () {
|
|
// change blocks in existing projects to their updated version
|
|
SpriteMorph.prototype.blockMigrations = {
|
|
doStopAll: {
|
|
selector: 'doStopThis',
|
|
inputs: [['all']]
|
|
},
|
|
doStop: {
|
|
selector: 'doStopThis',
|
|
inputs: [['this script']]
|
|
},
|
|
doStopBlock: {
|
|
selector: 'doStopThis',
|
|
inputs: [['this block']]
|
|
},
|
|
doStopOthers: {
|
|
selector: 'doStopThis',
|
|
inputs: [['all']],
|
|
offset: 0
|
|
},
|
|
receiveClick: {
|
|
selector: 'receiveInteraction',
|
|
inputs: [['clicked']]
|
|
},
|
|
reportTrue: {
|
|
selector: 'reportBoolean',
|
|
inputs: [true]
|
|
},
|
|
reportFalse: {
|
|
selector: 'reportBoolean',
|
|
inputs: [false]
|
|
},
|
|
reportCostumes: {
|
|
selector: 'reportGet',
|
|
inputs: [['costumes']]
|
|
},
|
|
reportSounds: {
|
|
selector: 'reportGet',
|
|
inputs: [['sounds']]
|
|
},
|
|
doMapStringCode: {
|
|
selector: 'doMapValueCode',
|
|
inputs: [['String'], '<#1>'],
|
|
offset: 1
|
|
},
|
|
reportDistanceTo: {
|
|
selector: 'reportRelationTo',
|
|
inputs: [['distance']],
|
|
offset: 1
|
|
},
|
|
comeToFront: {
|
|
selector: 'goToLayer',
|
|
inputs: [['front']]
|
|
},
|
|
setHue: {
|
|
selector: 'setPenColorDimension',
|
|
inputs: [['hue']],
|
|
offset: 1
|
|
},
|
|
setBrightness: {
|
|
selector: 'setPenColorDimension',
|
|
inputs: [['brightness']],
|
|
offset: 1
|
|
},
|
|
setPenHSVA: {
|
|
selector: 'setPenColorDimension'
|
|
},
|
|
changeHue: {
|
|
selector: 'changePenColorDimension',
|
|
inputs: [['hue']],
|
|
offset: 1
|
|
},
|
|
changeBrightness: {
|
|
selector: 'changePenColorDimension',
|
|
inputs: [['brightness']],
|
|
offset: 1
|
|
},
|
|
changePenHSVA: {
|
|
selector: 'changePenColorDimension'
|
|
},
|
|
setBackgroundHSVA: {
|
|
selector: 'setBackgroundColorDimension'
|
|
},
|
|
changeBackgroundHSVA: {
|
|
selector: 'changeBackgroundColorDimension'
|
|
},
|
|
reportIsFastTracking: {
|
|
selector: 'reportGlobalFlag',
|
|
inputs: [['turbo mode']],
|
|
offset: 1
|
|
},
|
|
doSetFastTracking: {
|
|
selector: 'doSetGlobalFlag',
|
|
inputs: [['turbo mode']],
|
|
offset: 1
|
|
},
|
|
reportTableRotated: {
|
|
selector: 'reportListAttribute',
|
|
inputs: [['transpose']],
|
|
offset: 1
|
|
},
|
|
reportTranspose: {
|
|
selector: 'reportListAttribute',
|
|
inputs: [['transpose']],
|
|
offset: 1
|
|
},
|
|
reportListLength: {
|
|
selector: 'reportListAttribute',
|
|
inputs: [['length']],
|
|
offset: 1
|
|
},
|
|
doSend: {
|
|
selector: 'doBroadcast',
|
|
expand: 1
|
|
}
|
|
};
|
|
};
|
|
|
|
SpriteMorph.prototype.initBlockMigrations();
|
|
|
|
SpriteMorph.prototype.blockAlternatives = {
|
|
// structure:
|
|
// selector: [ersatz, ...]
|
|
// ersatz can also be a 2-item array: [selector, input-offset]
|
|
|
|
// motion:
|
|
forward: ['changeXPosition', 'changeYPosition'],
|
|
turn: ['turnLeft'],
|
|
turnLeft: ['turn'],
|
|
doFaceTowards: ['doGotoObject'],
|
|
gotoXY: [['doGlide', 1]],
|
|
doGotoObject: ['doFaceTowards'],
|
|
doGlide: [['gotoXY', -1]],
|
|
changeXPosition: ['changeYPosition', 'setXPosition', 'setYPosition',
|
|
'forward'],
|
|
setXPosition: ['setYPosition', 'changeXPosition', 'changeYPosition'],
|
|
changeYPosition: ['changeXPosition', 'setYPosition', 'setXPosition',
|
|
'forward'],
|
|
setYPosition: ['setXPosition', 'changeYPosition', 'changeXPosition'],
|
|
xPosition: ['yPosition'],
|
|
yPosition: ['xPosition'],
|
|
|
|
// looks:
|
|
doSayFor: ['doThinkFor', 'bubble', 'doThink', 'doAsk'],
|
|
doThinkFor: ['doSayFor', 'doThink', 'bubble', 'doAsk'],
|
|
bubble: ['doThink', 'doAsk', 'doSayFor', 'doThinkFor'],
|
|
doThink: ['bubble', 'doAsk', 'doSayFor', 'doThinkFor'],
|
|
show: ['hide'],
|
|
hide: ['show'],
|
|
changeEffect: ['setEffect'],
|
|
setEffect: ['changeEffect'],
|
|
changeScale: ['setScale'],
|
|
setScale: ['changeScale'],
|
|
|
|
// sound:
|
|
playSound: ['doPlaySoundUntilDone', 'doPlaySoundAtRate'],
|
|
doPlaySoundUntilDone: ['playSound', 'doPlaySoundAtRate'],
|
|
doPlaySoundAtRate: ['playSound', 'doPlaySoundUntilDone'],
|
|
doPlayNote: [['doRest', -1]],
|
|
doRest: [['doPlayNote', 1]],
|
|
doChangeTempo: ['doSetTempo'],
|
|
doSetTempo: ['doChangeTempo'],
|
|
setVolume: ['changeVolume'],
|
|
changeVolume: ['setVolume'],
|
|
setPan: ['changePan'],
|
|
changePan: ['setPan'],
|
|
getVolume: ['getTempo', 'getPan'],
|
|
getTempo: ['getVolume', 'getPan'],
|
|
getPan: ['getVolume', 'getTempo'],
|
|
|
|
// pen:
|
|
clear: ['down', 'up', 'doStamp'],
|
|
down: ['up', 'clear', 'doStamp'],
|
|
up: ['down', 'clear', 'doStamp'],
|
|
doPasteOn: ['doCutFrom'],
|
|
doCutFrom: ['doPasteOn'],
|
|
doStamp: ['clear', 'down', 'up'],
|
|
setPenColorDimension: ['changePenColorDimension'],
|
|
changePenColorDimension: ['setPenColorDimension'],
|
|
setBackgroundColorDimension: ['changeBackgroundColorDimension'],
|
|
changeBackgroundColorDimension: ['setBackgroundColorDimension'],
|
|
changeSize: ['setSize'],
|
|
setSize: ['changeSize'],
|
|
|
|
// control:
|
|
doBroadcast: ['doBroadcastAndWait'],
|
|
doBroadcastAndWait: ['doBroadcast'],
|
|
doIf: ['doIfElse', 'doUntil'],
|
|
doIfElse: ['doIf', 'doUntil'],
|
|
doRepeat: ['doUntil', ['doForever', -1], ['doFor', 2], ['doForEach', 1]],
|
|
doUntil: ['doRepeat', 'doIf', ['doForever', -1], ['doFor', 2],
|
|
['doForEach', 1]],
|
|
doForever: [['doUntil', 1], ['doRepeat', 1], ['doFor', 3],
|
|
['doForEach', 2]],
|
|
doFor: [['doForever', -3], ['doRepeat', -2], ['doUntil', -2],
|
|
['doForEach', -1]],
|
|
// doRun: ['fork'],
|
|
// fork: ['doRun'],
|
|
|
|
// sensing:
|
|
doAsk: ['bubble', 'doThink', 'doSayFor', 'doThinkFor'],
|
|
getLastAnswer: ['getTimer'],
|
|
getTimer: ['getLastAnswer'],
|
|
reportMouseX: ['reportMouseY'],
|
|
reportMouseY: ['reportMouseX'],
|
|
|
|
// operators:
|
|
reportSum: ['reportDifference', 'reportProduct', 'reportQuotient',
|
|
'reportPower', 'reportModulus', 'reportAtan2', 'reportMin',
|
|
'reportMax'],
|
|
reportDifference: ['reportSum', 'reportProduct', 'reportQuotient',
|
|
'reportPower', 'reportModulus', 'reportAtan2', 'reportMin',
|
|
'reportMax'],
|
|
reportProduct: ['reportDifference', 'reportSum', 'reportQuotient',
|
|
'reportPower', 'reportModulus', 'reportAtan2', 'reportMin',
|
|
'reportMax'],
|
|
reportQuotient: ['reportDifference', 'reportProduct', 'reportSum',
|
|
'reportPower', 'reportModulus', 'reportAtan2', 'reportMin',
|
|
'reportMax'],
|
|
reportPower: ['reportDifference', 'reportProduct', 'reportSum',
|
|
'reportQuotient', 'reportModulus', 'reportAtan2', 'reportMin',
|
|
'reportMax'],
|
|
reportModulus: ['reportAtan2', 'reportDifference', 'reportProduct',
|
|
'reportSum','reportQuotient', 'reportPower', 'reportMin', 'reportMax'],
|
|
reportAtan2: ['reportModulus', 'reportDifference', 'reportProduct',
|
|
'reportSum','reportQuotient', 'reportPower', 'reportMin', 'reportMax'],
|
|
reportMin: ['reportMax', 'reportSum', 'reportDifference', 'reportProduct',
|
|
'reportQuotient', 'reportPower', 'reportModulus', 'reportAtan2'],
|
|
reportMax: ['reportMin', 'reportSum', 'reportDifference', 'reportProduct',
|
|
'reportQuotient', 'reportPower', 'reportModulus', 'reportAtan2'],
|
|
reportLessThan: ['reportLessThanOrEquals', 'reportEquals',
|
|
'reportIsIdentical', 'reportNotEquals', 'reportGreaterThan',
|
|
'reportGreaterThanOrEquals'],
|
|
reportEquals: ['reportIsIdentical', 'reportNotEquals', 'reportLessThan',
|
|
'reportLessThanOrEquals', 'reportGreaterThan',
|
|
'reportGreaterThanOrEquals'],
|
|
reportNotEquals: ['reportEquals', 'reportIsIdentical', 'reportLessThan',
|
|
'reportLessThanOrEquals', 'reportGreaterThan',
|
|
'reportGreaterThanOrEquals'],
|
|
reportGreaterThan: ['reportGreaterThanOrEquals', 'reportEquals',
|
|
'reportIsIdentical', 'reportNotEquals', 'reportLessThan',
|
|
'reportLessThanOrEquals'],
|
|
reportLessThanOrEquals: ['reportLessThan', 'reportEquals',
|
|
'reportIsIdentical', 'reportNotEquals', 'reportGreaterThan',
|
|
'reportGreaterThanOrEquals'],
|
|
reportGreaterThanOrEquals: ['reportGreaterThan', 'reportEquals',
|
|
'reportIsIdentical', 'reportNotEquals', 'reportLessThan',
|
|
'reportLessThanOrEquals'],
|
|
reportIsIdentical: ['reportEquals', 'reportNotEquals', 'reportLessThan',
|
|
'reportLessThanOrEquals', 'reportGreaterThan',
|
|
'reportGreaterThanOrEquals'],
|
|
reportAnd: ['reportOr'],
|
|
reportOr: ['reportAnd'],
|
|
|
|
// variables
|
|
doSetVar: ['doChangeVar'],
|
|
doChangeVar: ['doSetVar'],
|
|
doShowVar: ['doHideVar'],
|
|
doHideVar: ['doShowVar'],
|
|
|
|
// HOFs
|
|
reportMap: ['reportKeep', 'reportFindFirst'],
|
|
reportKeep: ['reportFindFirst', 'reportMap'],
|
|
reportFindFirst: ['reportKeep', 'reportMap'],
|
|
doForEach: [['doFor', 1], ['doForever', -2], ['doRepeat', -1],
|
|
['doUntil', -1]]
|
|
};
|
|
|
|
// SpriteMorph instance creation
|
|
|
|
function SpriteMorph(globals) {
|
|
this.init(globals);
|
|
}
|
|
|
|
SpriteMorph.prototype.init = function (globals) {
|
|
this.name = localize('Sprite');
|
|
this.variables = new VariableFrame(globals || null, this);
|
|
this.scripts = new ScriptsMorph();
|
|
this.customBlocks = [];
|
|
this.costumes = new List();
|
|
this.costumes.type = 'costume';
|
|
this.costume = null;
|
|
this.sounds = new List();
|
|
this.sounds.type = 'sound';
|
|
this.normalExtent = new Point(60, 60); // only for costume-less situation
|
|
this.scale = 1;
|
|
this.rotationStyle = 1; // 1 = full, 2 = left/right, 0 = off
|
|
this.instrument = null;
|
|
this.version = Date.now(); // for observer optimization
|
|
this.isTemporary = false; // indicate a temporary Scratch-style clone
|
|
this.isCorpse = false; // indicate whether a sprite/clone has been deleted
|
|
this.cloneOriginName = '';
|
|
|
|
// volume and stereo-pan support, experimental:
|
|
this.volume = 100;
|
|
this.gainNode = null; // must be lazily initialized in Chrome, sigh...
|
|
this.pan = 0;
|
|
this.pannerNode = null; // must be lazily initialized in Chrome, sigh...
|
|
|
|
// frequency player, experimental
|
|
this.freqPlayer = null; // Note, to be lazily initialized
|
|
|
|
// pen color dimensions support
|
|
this.cachedColorDimensions = [0, 0, 0]; // not serialized
|
|
|
|
// only temporarily for serialization
|
|
this.inheritedMethodsCache = [];
|
|
|
|
// sprite nesting properties
|
|
this.parts = []; // not serialized, only anchor (name)
|
|
this.anchor = null;
|
|
this.nestingScale = 1;
|
|
this.rotatesWithAnchor = true;
|
|
this.layers = null; // cache for dragging nested sprites, don't serialize
|
|
|
|
this.primitivesCache = {}; // not to be serialized (!)
|
|
this.paletteCache = {}; // not to be serialized (!)
|
|
this.categoriesCache = null; // not to be serialized (!)
|
|
this.rotationOffset = ZERO; // not to be serialized (!)
|
|
this.idx = 0; // not to be serialized (!) - used for de-serialization
|
|
|
|
this.graphicsValues = {
|
|
'color': 0,
|
|
'fisheye': 0,
|
|
'whirl': 0,
|
|
'pixelate': 0,
|
|
'mosaic': 0,
|
|
'duplicate': 0,
|
|
'negative': 0,
|
|
'comic': 0,
|
|
'confetti': 0,
|
|
'saturation': 0,
|
|
'brightness': 0
|
|
};
|
|
|
|
// sprite inheritance
|
|
this.exemplar = null;
|
|
this.instances = [];
|
|
this.cachedPropagation = false; // not to be persisted
|
|
this.inheritedAttributes = []; // 'x position', 'direction', 'size' etc...
|
|
|
|
// video- and rendering state
|
|
this.imageExtent = ZERO;
|
|
this.imageOffset = ZERO;
|
|
this.imageData = {}; // version: date, pixels: Uint32Array
|
|
this.motionAmount = 0;
|
|
this.motionDirection = 0;
|
|
this.frameNumber = 0;
|
|
|
|
SpriteMorph.uber.init.call(this);
|
|
|
|
this.isCachingImage = true;
|
|
this.isFreeForm = true;
|
|
this.cachedColorDimensions = this.color[this.penColorModel]();
|
|
this.isDraggable = true;
|
|
this.isDown = false;
|
|
this.heading = 90;
|
|
this.fixLayout();
|
|
this.rerender();
|
|
};
|
|
|
|
// SpriteMorph duplicating (fullCopy)
|
|
|
|
SpriteMorph.prototype.fullCopy = function (forClone) {
|
|
var c = SpriteMorph.uber.fullCopy.call(this),
|
|
arr = [],
|
|
cb, effect;
|
|
|
|
// make sure the clone has its own canvas to recycle
|
|
// needs to be copied instead of redrawn, because at
|
|
// this time the clone is not yet onstage and therefore
|
|
// has no access to the stage's scale
|
|
c.cachedImage = copyCanvas(this.cachedImage);
|
|
|
|
// un-share individual properties
|
|
c.instances = [];
|
|
c.stopTalking();
|
|
c.color = this.color.copy();
|
|
c.gainNode = null;
|
|
c.pannerNode = null;
|
|
c.freqPlayer = null;
|
|
c.primitivesCache = {};
|
|
c.paletteCache = {};
|
|
c.categoriesCache = null;
|
|
c.imageData = {};
|
|
c.cachedColorDimensions = c.color[this.penColorModel]();
|
|
arr = [];
|
|
this.inheritedAttributes.forEach(att => arr.push(att));
|
|
c.inheritedAttributes = arr;
|
|
if (forClone) {
|
|
c.exemplar = this;
|
|
c.customBlocks = [];
|
|
c.variables = new VariableFrame(null, c);
|
|
c.variables.parentFrame = this.variables;
|
|
c.inheritedVariableNames().forEach(name =>
|
|
c.shadowVar(name, c.variables.getVar(name))
|
|
);
|
|
this.addSpecimen(c);
|
|
this.cachedPropagation = false;
|
|
['scripts', 'costumes', 'sounds'].forEach(att => {
|
|
if (!contains(c.inheritedAttributes, att)) {
|
|
c.inheritedAttributes.push(att);
|
|
}
|
|
});
|
|
} else {
|
|
c.variables = this.variables.copy();
|
|
c.variables.owner = c;
|
|
c.scripts = this.scripts.fullCopy();
|
|
c.customBlocks = [];
|
|
this.customBlocks.forEach(def => {
|
|
cb = def.copyAndBindTo(c);
|
|
c.customBlocks.push(cb);
|
|
c.allBlockInstances(def).forEach(block =>
|
|
block.definition = cb
|
|
);
|
|
});
|
|
arr = [];
|
|
this.costumes.asArray().forEach(costume => {
|
|
var cst = forClone ? costume : costume.copy();
|
|
arr.push(cst);
|
|
if (costume === this.costume) {
|
|
c.costume = cst;
|
|
}
|
|
});
|
|
c.costumes = new List(arr);
|
|
c.costumes.type = 'costume';
|
|
arr = [];
|
|
this.sounds.asArray().forEach(sound => {
|
|
var snd = forClone ? sound : sound.copy();
|
|
arr.push(snd);
|
|
});
|
|
c.sounds = new List(arr);
|
|
c.sounds.type = 'sound';
|
|
arr = [];
|
|
}
|
|
c.nestingScale = 1;
|
|
c.rotatesWithAnchor = true;
|
|
c.anchor = null;
|
|
c.parts = [];
|
|
this.parts.forEach(part => {
|
|
var dp = part.fullCopy(forClone);
|
|
dp.nestingScale = part.nestingScale;
|
|
dp.rotatesWithAnchor = part.rotatesWithAnchor;
|
|
c.attachPart(dp);
|
|
});
|
|
c.graphicsValues = {};
|
|
for (effect in this.graphicsValues) {
|
|
if (this.graphicsValues.hasOwnProperty(effect)) {
|
|
c.graphicsValues[effect] = this.graphicsValues[effect];
|
|
}
|
|
}
|
|
return c;
|
|
};
|
|
|
|
SpriteMorph.prototype.appearIn = function (ide) {
|
|
// private - used in IDE_Morph.duplicateSprite()
|
|
if (!this.isTemporary) {
|
|
this.name = ide.newSpriteName(this.name);
|
|
ide.corral.addSprite(this);
|
|
ide.sprites.add(this);
|
|
}
|
|
ide.stage.add(this);
|
|
this.parts.forEach(part => part.appearIn(ide));
|
|
};
|
|
|
|
// SpriteMorph versioning
|
|
|
|
SpriteMorph.prototype.setName = function (string) {
|
|
this.name = string || this.name;
|
|
this.version = Date.now();
|
|
};
|
|
|
|
// SpriteMorph rendering
|
|
|
|
SpriteMorph.prototype.getImage = function () {
|
|
// overrides inherited method to allow for an image exceeding my bounds
|
|
// to accommodate rotation and to disable retina resolution to
|
|
// optimize graphics performance
|
|
if (this.shouldRerender || !this.cachedImage) {
|
|
this.cachedImage = newCanvas(
|
|
this.costume ? this.imageExtent : this.extent(),
|
|
!isNil(this.costume), // retina
|
|
this.cachedImage
|
|
);
|
|
this.render(this.cachedImage.getContext('2d'));
|
|
this.shouldRerender = false;
|
|
}
|
|
return this.cachedImage;
|
|
};
|
|
|
|
SpriteMorph.prototype.fixLayout = function () {
|
|
// determine my extent and the extent designated for my cached image
|
|
var currentCenter,
|
|
facing, // actual costume heading based on my rotation style
|
|
isFlipped,
|
|
isLoadingCostume,
|
|
pic, // (flipped copy of) actual costume based on my rotation style
|
|
imageSide,
|
|
stageScale,
|
|
newX,
|
|
corners = [],
|
|
origin,
|
|
corner,
|
|
costumeExtent;
|
|
|
|
currentCenter = this.center();
|
|
isLoadingCostume = this.costume &&
|
|
typeof this.costume.loaded === 'function';
|
|
stageScale = this.parent instanceof StageMorph ?
|
|
this.parent.scale : 1;
|
|
facing = this.rotationStyle ? this.heading : 90;
|
|
if (this.rotationStyle === 2) {
|
|
facing = 90;
|
|
if ((this.heading > 180 && (this.heading < 360))
|
|
|| (this.heading < 0 && (this.heading > -180))) {
|
|
isFlipped = true;
|
|
}
|
|
}
|
|
if (this.costume && !isLoadingCostume) {
|
|
pic = isFlipped ? this.costume.flipped() : this.costume;
|
|
|
|
// determine the rotated costume's bounding box
|
|
corners = pic.bounds().corners().map(point =>
|
|
point.rotateBy(
|
|
radians(facing - 90),
|
|
this.costume.center()
|
|
)
|
|
);
|
|
origin = corners[0];
|
|
corner = corners[0];
|
|
corners.forEach(point => {
|
|
origin = origin.min(point);
|
|
corner = corner.max(point);
|
|
});
|
|
costumeExtent = origin.corner(corner)
|
|
.extent().multiplyBy(this.scale * stageScale);
|
|
|
|
// determine the new relative origin of the rotated shape
|
|
this.imageOffset = ZERO.rotateBy(
|
|
radians(-(facing - 90)),
|
|
pic.center()
|
|
).subtract(origin);
|
|
|
|
// determine an adequately dimensioned image extent, so the
|
|
// shape on the canvas ran be rotated without having to create
|
|
// a new canvas each time
|
|
|
|
if (this.rotationStyle === 1) { // rotate freely in all directions
|
|
// create a canvas that is big enough, so the sprite's current
|
|
// costume can be fully rotated inside, so we can recycle
|
|
// the canvas for re-rendering until the sprite's or the stage's
|
|
// scale changes.
|
|
// note that the canvas will be too big for most situations, but
|
|
// the sprite's bounds indicate the actually visible area.
|
|
// recycling canvas elements instead of creating new ones whenever
|
|
// we render a sprite boosts performance for recent browser
|
|
// architectures that move canvas elements to the GPU for
|
|
// rendering (which is a pain in the ass and an altogether
|
|
// bad idea for any serious GUI but looks oh-so-nice for
|
|
// some flashy graphic effects in the Apple store).
|
|
imageSide = Math.sqrt(
|
|
Math.pow(pic.width(), 2) + Math.pow(pic.height(), 2)
|
|
) * this.scale * stageScale;
|
|
this.imageExtent = new Point(imageSide, imageSide);
|
|
} else { // don't actually rotate
|
|
this.imageExtent = costumeExtent;
|
|
}
|
|
this.bounds.setExtent(costumeExtent);
|
|
|
|
// adjust my position to the rotation
|
|
this.setCenter(currentCenter, true);
|
|
|
|
// determine my rotation offset
|
|
this.rotationOffset = this.imageOffset
|
|
.translateBy(pic.rotationCenter)
|
|
.rotateBy(radians(-(facing - 90)), this.imageOffset)
|
|
.scaleBy(this.scale * stageScale);
|
|
} else {
|
|
facing = isFlipped ? -90 : facing;
|
|
newX = Math.min(
|
|
Math.max(
|
|
this.normalExtent.x * this.scale * stageScale,
|
|
5
|
|
),
|
|
1000
|
|
);
|
|
this.bounds.setWidth(newX);
|
|
this.bounds.setHeight(newX);
|
|
this.setCenter(currentCenter, true); // just me
|
|
this.rotationOffset = this.extent().divideBy(2);
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.render = function (ctx) {
|
|
var myself = this,
|
|
facing, // actual costume heading based on my rotation style
|
|
isFlipped,
|
|
isLoadingCostume,
|
|
cst,
|
|
pic, // (flipped copy of) actual costume based on my rotation style
|
|
stageScale,
|
|
handle;
|
|
|
|
isLoadingCostume = this.costume &&
|
|
typeof this.costume.loaded === 'function';
|
|
stageScale = this.parent instanceof StageMorph ?
|
|
this.parent.scale : 1;
|
|
facing = this.rotationStyle ? this.heading : 90;
|
|
if (this.rotationStyle === 2) {
|
|
facing = 90;
|
|
if ((this.heading > 180 && (this.heading < 360))
|
|
|| (this.heading < 0 && (this.heading > -180))) {
|
|
isFlipped = true;
|
|
}
|
|
}
|
|
if (this.costume && !isLoadingCostume) {
|
|
pic = isFlipped ? this.costume.flipped() : this.costume;
|
|
ctx.save();
|
|
ctx.scale(this.scale * stageScale, this.scale * stageScale);
|
|
ctx.translate(this.imageOffset.x, this.imageOffset.y);
|
|
ctx.rotate(radians(facing - 90));
|
|
ctx.drawImage(pic.contents, 0, 0);
|
|
ctx.restore();
|
|
|
|
} else {
|
|
facing = isFlipped ? -90 : facing;
|
|
SpriteMorph.uber.render.call(this, ctx, facing);
|
|
|
|
if (isLoadingCostume) { // retry until costume is done loading
|
|
cst = this.costume;
|
|
handle = setInterval(
|
|
function () {
|
|
myself.wearCostume(cst, true);
|
|
clearInterval(handle);
|
|
},
|
|
100
|
|
);
|
|
return this.wearCostume(null, true);
|
|
}
|
|
}
|
|
// apply graphics effects to image
|
|
this.cachedImage = this.applyGraphicsEffects(this.cachedImage);
|
|
this.version = Date.now();
|
|
};
|
|
|
|
SpriteMorph.prototype.rotationCenter = function () {
|
|
return this.position().add(this.rotationOffset);
|
|
};
|
|
|
|
SpriteMorph.prototype.getImageData = function () {
|
|
// used for video motion detection.
|
|
// Get sprite image data scaled to 1 an converted to ABGR array,
|
|
// cache to reduce GC load
|
|
if (this.version !== this.imageData.version) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
ext = this.extent(),
|
|
newExtent = new Point(
|
|
Math.floor(ext.x / stage.scale),
|
|
Math.floor(ext.y / stage.scale)
|
|
),
|
|
canvas = newCanvas(newExtent, true),
|
|
canvasContext,
|
|
imageData;
|
|
canvasContext = canvas.getContext("2d");
|
|
canvasContext.drawImage(
|
|
this.getImage(),
|
|
0, 0, Math.floor(ext.x),
|
|
Math.floor(ext.y),
|
|
0, 0, newExtent.x, newExtent.y
|
|
);
|
|
imageData = canvasContext.getImageData(
|
|
0,
|
|
0,
|
|
newExtent.x,
|
|
newExtent.y
|
|
).data;
|
|
this.imageData = {
|
|
version : this.version,
|
|
pixels : new Uint32Array(imageData.buffer.slice(0))
|
|
};
|
|
}
|
|
return this.imageData.pixels;
|
|
};
|
|
|
|
SpriteMorph.prototype.projectionSnap = function() {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
center = this.center().subtract(stage.position())
|
|
.divideBy(stage.scale),
|
|
cst = this.costume || this.getImage(),
|
|
w, h, rot,
|
|
offset,
|
|
snap,
|
|
ctx;
|
|
|
|
if (cst instanceof Costume) {
|
|
rot = cst.rotationCenter.copy();
|
|
cst = cst.contents;
|
|
w = cst.width;
|
|
h = cst.height;
|
|
} else {
|
|
w = this.width();
|
|
h = this.height();
|
|
}
|
|
offset = new Point(
|
|
Math.floor(center.x - (w / 2)),
|
|
Math.floor(center.y - (h / 2))
|
|
);
|
|
snap = newCanvas(new Point(w, h), true);
|
|
ctx = snap.getContext('2d');
|
|
ctx.drawImage(cst, 0, 0);
|
|
ctx.globalCompositeOperation = 'source-atop';
|
|
ctx.drawImage(stage.projectionLayer(), -offset.x, -offset.y);
|
|
return new Costume(snap, this.newCostumeName(localize('snap')), rot);
|
|
};
|
|
|
|
// SpriteMorph block instantiation
|
|
|
|
SpriteMorph.prototype.blockForSelector = function (selector, setDefaults) {
|
|
var migration, info, block, defaults, inputs, i;
|
|
migration = this.blockMigrations[selector];
|
|
info = this.blocks[migration ? migration.selector : selector];
|
|
if (!info) {return null; }
|
|
block = info.type === 'command' ? new CommandBlockMorph()
|
|
: info.type === 'hat' ? new HatBlockMorph()
|
|
: info.type === 'ring' ? new RingMorph()
|
|
: new ReporterBlockMorph(info.type === 'predicate');
|
|
block.color = this.blockColorFor(info.category);
|
|
block.category = info.category;
|
|
block.selector = migration ? migration.selector : selector;
|
|
if (contains(['reifyReporter', 'reifyPredicate'], block.selector)) {
|
|
block.isStatic = true;
|
|
}
|
|
block.setSpec(localize(info.spec));
|
|
if (migration && migration.expand) {
|
|
block.inputs()[migration.expand].addInput();
|
|
}
|
|
if ((setDefaults && info.defaults) || (migration && migration.inputs)) {
|
|
defaults = migration ? migration.inputs : info.defaults;
|
|
block.defaults = defaults;
|
|
inputs = block.inputs();
|
|
if (inputs[0] instanceof MultiArgMorph) {
|
|
inputs[0].setContents(defaults);
|
|
inputs[0].defaults = defaults;
|
|
} else {
|
|
for (i = 0; i < defaults.length; i += 1) {
|
|
if (defaults[i] !== null) {
|
|
inputs[i].setContents(defaults[i]);
|
|
if (inputs[i] instanceof MultiArgMorph) {
|
|
inputs[i].defaults = defaults[i];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return block;
|
|
};
|
|
|
|
SpriteMorph.prototype.variableBlock = function (varName, isLocalTemplate) {
|
|
var block = new ReporterBlockMorph(false);
|
|
block.selector = 'reportGetVar';
|
|
block.color = this.blockColor.variables;
|
|
block.category = 'variables';
|
|
block.isLocalVarTemplate = isLocalTemplate;
|
|
block.setSpec(varName);
|
|
block.isDraggable = true;
|
|
return block;
|
|
};
|
|
|
|
// SpriteMorph block templates
|
|
|
|
SpriteMorph.prototype.blockTemplates = function (
|
|
category = 'motion',
|
|
all = false // include hidden blocks
|
|
) {
|
|
var blocks = [], myself = this, varNames,
|
|
inheritedVars = this.inheritedVariableNames(),
|
|
wrld = this.world(),
|
|
devMode = wrld && wrld.isDevMode;
|
|
|
|
function block(selector, isGhosted) {
|
|
if (StageMorph.prototype.hiddenPrimitives[selector] && !all) {
|
|
return null;
|
|
}
|
|
var newBlock = SpriteMorph.prototype.blockForSelector(selector, true);
|
|
newBlock.isTemplate = true;
|
|
if (isGhosted) {newBlock.ghost(); }
|
|
return newBlock;
|
|
}
|
|
|
|
function variableBlock(varName, isLocal) {
|
|
var newBlock = SpriteMorph.prototype.variableBlock(varName, isLocal);
|
|
newBlock.isDraggable = false;
|
|
newBlock.isTemplate = true;
|
|
if (contains(inheritedVars, varName)) {
|
|
newBlock.ghost();
|
|
}
|
|
return newBlock;
|
|
}
|
|
|
|
function watcherToggle(selector) {
|
|
if (StageMorph.prototype.hiddenPrimitives[selector]) {
|
|
return null;
|
|
}
|
|
var info = SpriteMorph.prototype.blocks[selector];
|
|
return new ToggleMorph(
|
|
'checkbox',
|
|
this,
|
|
function () {
|
|
myself.toggleWatcher(
|
|
selector,
|
|
localize(info.spec),
|
|
myself.blockColor[info.category]
|
|
);
|
|
},
|
|
null,
|
|
function () {
|
|
return myself.showingWatcher(selector);
|
|
},
|
|
null
|
|
);
|
|
}
|
|
|
|
function variableWatcherToggle(varName) {
|
|
return new ToggleMorph(
|
|
'checkbox',
|
|
this,
|
|
function () {
|
|
myself.toggleVariableWatcher(varName);
|
|
},
|
|
null,
|
|
function () {
|
|
return myself.showingVariableWatcher(varName);
|
|
},
|
|
null
|
|
);
|
|
}
|
|
|
|
if (category === 'motion') {
|
|
|
|
blocks.push(block('forward'));
|
|
blocks.push(block('turn'));
|
|
blocks.push(block('turnLeft'));
|
|
blocks.push('-');
|
|
blocks.push(block('setHeading'));
|
|
blocks.push(block('doFaceTowards'));
|
|
blocks.push('-');
|
|
blocks.push(block('gotoXY'));
|
|
blocks.push(block('doGotoObject'));
|
|
blocks.push(block('doGlide'));
|
|
blocks.push('-');
|
|
blocks.push(block('changeXPosition'));
|
|
blocks.push(block('setXPosition'));
|
|
blocks.push(block('changeYPosition'));
|
|
blocks.push(block('setYPosition'));
|
|
blocks.push('-');
|
|
blocks.push(block('bounceOffEdge'));
|
|
blocks.push('-');
|
|
blocks.push(watcherToggle('xPosition'));
|
|
blocks.push(block('xPosition', this.inheritsAttribute('x position')));
|
|
blocks.push(watcherToggle('yPosition'));
|
|
blocks.push(block('yPosition', this.inheritsAttribute('y position')));
|
|
blocks.push(watcherToggle('direction'));
|
|
blocks.push(block('direction', this.inheritsAttribute('direction')));
|
|
|
|
} else if (category === 'looks') {
|
|
|
|
blocks.push(block('doSwitchToCostume'));
|
|
blocks.push(block('doWearNextCostume'));
|
|
blocks.push(watcherToggle('getCostumeIdx'));
|
|
blocks.push(block('getCostumeIdx', this.inheritsAttribute('costume #')));
|
|
blocks.push('-');
|
|
blocks.push(block('doSayFor'));
|
|
blocks.push(block('bubble'));
|
|
blocks.push(block('doThinkFor'));
|
|
blocks.push(block('doThink'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportGetImageAttribute'));
|
|
blocks.push(block('reportNewCostumeStretched'));
|
|
blocks.push(block('reportNewCostume'));
|
|
blocks.push('-');
|
|
blocks.push(block('changeEffect'));
|
|
blocks.push(block('setEffect'));
|
|
blocks.push(block('clearEffects'));
|
|
blocks.push(block('getEffect'));
|
|
blocks.push('-');
|
|
blocks.push(block('changeScale'));
|
|
blocks.push(block('setScale'));
|
|
blocks.push(watcherToggle('getScale'));
|
|
blocks.push(block('getScale', this.inheritsAttribute('size')));
|
|
blocks.push('-');
|
|
blocks.push(block('show'));
|
|
blocks.push(block('hide'));
|
|
blocks.push(watcherToggle('reportShown'));
|
|
blocks.push(block('reportShown', this.inheritsAttribute('shown?')));
|
|
blocks.push('-');
|
|
blocks.push(block('goToLayer'));
|
|
blocks.push(block('goBack'));
|
|
|
|
// for debugging: ///////////////
|
|
if (devMode) {
|
|
blocks.push('-');
|
|
blocks.push(this.devModeText());
|
|
blocks.push('-');
|
|
blocks.push(block('log'));
|
|
blocks.push(block('alert'));
|
|
blocks.push('-');
|
|
blocks.push(block('doScreenshot'));
|
|
}
|
|
|
|
} else if (category === 'sound') {
|
|
|
|
blocks.push(block('playSound'));
|
|
blocks.push(block('doPlaySoundUntilDone'));
|
|
blocks.push(block('doStopAllSounds'));
|
|
blocks.push('-');
|
|
blocks.push(block('doPlaySoundAtRate'));
|
|
blocks.push(block('reportGetSoundAttribute'));
|
|
blocks.push(block('reportNewSoundFromSamples'));
|
|
blocks.push('-');
|
|
blocks.push(block('doRest'));
|
|
blocks.push(block('doPlayNote'));
|
|
blocks.push(block('doSetInstrument'));
|
|
blocks.push('-');
|
|
blocks.push(block('doChangeTempo'));
|
|
blocks.push(block('doSetTempo'));
|
|
blocks.push(watcherToggle('getTempo'));
|
|
blocks.push(block('getTempo'));
|
|
blocks.push('-');
|
|
blocks.push(block('changeVolume'));
|
|
blocks.push(block('setVolume'));
|
|
blocks.push(watcherToggle('getVolume'));
|
|
blocks.push(block('getVolume', this.inheritsAttribute('volume')));
|
|
blocks.push('-');
|
|
blocks.push(block('changePan'));
|
|
blocks.push(block('setPan'));
|
|
blocks.push(watcherToggle('getPan'));
|
|
blocks.push(block('getPan', this.inheritsAttribute('balance')));
|
|
blocks.push('-');
|
|
blocks.push(block('playFreq'));
|
|
blocks.push(block('stopFreq'));
|
|
|
|
// for debugging: ///////////////
|
|
if (devMode) {
|
|
blocks.push('-');
|
|
blocks.push(this.devModeText());
|
|
blocks.push('-');
|
|
blocks.push(block('doPlayFrequency'));
|
|
}
|
|
|
|
} else if (category === 'pen') {
|
|
|
|
blocks.push(block('clear'));
|
|
blocks.push('-');
|
|
blocks.push(block('down'));
|
|
blocks.push(block('up'));
|
|
blocks.push(watcherToggle('getPenDown'));
|
|
blocks.push(block('getPenDown', this.inheritsAttribute('pen down?')));
|
|
blocks.push('-');
|
|
blocks.push(block('setColor'));
|
|
blocks.push(block('changePenColorDimension'));
|
|
blocks.push(block('setPenColorDimension'));
|
|
blocks.push(block('getPenAttribute'));
|
|
blocks.push('-');
|
|
blocks.push(block('changeSize'));
|
|
blocks.push(block('setSize'));
|
|
blocks.push('-');
|
|
blocks.push(block('doStamp'));
|
|
blocks.push(block('floodFill'));
|
|
blocks.push(block('write'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportPenTrailsAsCostume'));
|
|
blocks.push('-');
|
|
blocks.push(block('doPasteOn'));
|
|
blocks.push(block('doCutFrom'));
|
|
|
|
} else if (category === 'control') {
|
|
|
|
blocks.push(block('receiveGo'));
|
|
blocks.push(block('receiveKey'));
|
|
blocks.push(block('receiveInteraction'));
|
|
blocks.push(block('receiveCondition'));
|
|
blocks.push('-');
|
|
blocks.push(block('receiveMessage'));
|
|
blocks.push(block('doBroadcast'));
|
|
blocks.push(block('doBroadcastAndWait'));
|
|
blocks.push('-');
|
|
blocks.push(block('doWarp'));
|
|
blocks.push('-');
|
|
blocks.push(block('doWait'));
|
|
blocks.push(block('doWaitUntil'));
|
|
blocks.push('-');
|
|
blocks.push(block('doForever'));
|
|
blocks.push(block('doRepeat'));
|
|
blocks.push(block('doUntil'));
|
|
blocks.push(block('doFor'));
|
|
blocks.push('-');
|
|
blocks.push(block('doIf'));
|
|
blocks.push(block('doIfElse'));
|
|
blocks.push(block('reportIfElse'));
|
|
blocks.push('-');
|
|
blocks.push(block('doReport'));
|
|
blocks.push(block('doStopThis'));
|
|
blocks.push('-');
|
|
blocks.push(block('doRun'));
|
|
blocks.push(block('fork'));
|
|
blocks.push(block('evaluate'));
|
|
blocks.push('-');
|
|
blocks.push(block('doTellTo'));
|
|
blocks.push(block('reportAskFor'));
|
|
blocks.push('-');
|
|
blocks.push(block('doCallCC'));
|
|
blocks.push(block('reportCallCC'));
|
|
blocks.push('-');
|
|
blocks.push(block('receiveOnClone'));
|
|
blocks.push(block('createClone'));
|
|
blocks.push(block('newClone'));
|
|
blocks.push(block('removeClone'));
|
|
blocks.push('-');
|
|
blocks.push(block('doPauseAll'));
|
|
blocks.push(block('doSwitchToScene'));
|
|
|
|
// for debugging: ///////////////
|
|
if (devMode) {
|
|
blocks.push('-');
|
|
blocks.push(this.devModeText());
|
|
blocks.push('-');
|
|
blocks.push(watcherToggle('getLastMessage'));
|
|
blocks.push(block('getLastMessage'));
|
|
}
|
|
|
|
} else if (category === 'sensing') {
|
|
|
|
blocks.push(block('reportTouchingObject'));
|
|
blocks.push(block('reportTouchingColor'));
|
|
blocks.push(block('reportColorIsTouchingColor'));
|
|
blocks.push('-');
|
|
blocks.push(block('doAsk'));
|
|
blocks.push(watcherToggle('getLastAnswer'));
|
|
blocks.push(block('getLastAnswer'));
|
|
blocks.push('-');
|
|
blocks.push(watcherToggle('reportMouseX'));
|
|
blocks.push(block('reportMouseX'));
|
|
blocks.push(watcherToggle('reportMouseY'));
|
|
blocks.push(block('reportMouseY'));
|
|
blocks.push(block('reportMouseDown'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportKeyPressed'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportRelationTo'));
|
|
blocks.push(block('reportAspect'));
|
|
blocks.push('-');
|
|
blocks.push(block('doResetTimer'));
|
|
blocks.push(watcherToggle('getTimer'));
|
|
blocks.push(block('getTimer'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportAttributeOf'));
|
|
|
|
if (SpriteMorph.prototype.enableFirstClass) {
|
|
blocks.push(block('reportGet'));
|
|
}
|
|
|
|
blocks.push(block('reportObject'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportURL'));
|
|
blocks.push(block('reportAudio'));
|
|
blocks.push(block('reportVideo'));
|
|
blocks.push(block('doSetVideoTransparency'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportGlobalFlag'));
|
|
blocks.push(block('doSetGlobalFlag'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportDate'));
|
|
blocks.push(block('reportBlockAttribute'));
|
|
|
|
// for debugging: ///////////////
|
|
if (devMode) {
|
|
blocks.push('-');
|
|
blocks.push(this.devModeText());
|
|
blocks.push('-');
|
|
blocks.push(watcherToggle('reportThreadCount'));
|
|
blocks.push(block('reportThreadCount'));
|
|
blocks.push(block('reportStackSize'));
|
|
blocks.push(block('reportFrameCount'));
|
|
blocks.push(block('reportYieldCount'));
|
|
}
|
|
} else if (category === 'operators') {
|
|
|
|
blocks.push(block('reifyScript'));
|
|
blocks.push(block('reifyReporter'));
|
|
blocks.push(block('reifyPredicate'));
|
|
blocks.push('#');
|
|
blocks.push('-');
|
|
blocks.push(block('reportSum'));
|
|
blocks.push(block('reportDifference'));
|
|
blocks.push(block('reportProduct'));
|
|
blocks.push(block('reportQuotient'));
|
|
blocks.push(block('reportPower'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportModulus'));
|
|
blocks.push(block('reportRound'));
|
|
blocks.push(block('reportMonadic'));
|
|
blocks.push(block('reportRandom'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportLessThan'));
|
|
blocks.push(block('reportEquals'));
|
|
blocks.push(block('reportGreaterThan'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportAnd'));
|
|
blocks.push(block('reportOr'));
|
|
blocks.push(block('reportNot'));
|
|
blocks.push(block('reportBoolean'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportJoinWords'));
|
|
blocks.push(block('reportTextSplit'));
|
|
blocks.push(block('reportLetter'));
|
|
blocks.push(block('reportStringSize'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportUnicode'));
|
|
blocks.push(block('reportUnicodeAsLetter'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportIsA'));
|
|
blocks.push(block('reportIsIdentical'));
|
|
|
|
if (Process.prototype.enableJS) {
|
|
blocks.push('-');
|
|
blocks.push(block('reportJSFunction'));
|
|
if (Process.prototype.enableCompiling) {
|
|
blocks.push(block('reportCompiled'));
|
|
}
|
|
}
|
|
// for debugging: ///////////////
|
|
if (devMode) {
|
|
blocks.push('-');
|
|
blocks.push(this.devModeText());
|
|
blocks.push('-');
|
|
blocks.push(block('reportTypeOf'));
|
|
blocks.push(block('reportTextFunction'));
|
|
}
|
|
|
|
} else if (category === 'variables') {
|
|
|
|
blocks.push(this.makeVariableButton());
|
|
if (this.deletableVariableNames().length > 0) {
|
|
blocks.push(this.deleteVariableButton());
|
|
}
|
|
blocks.push('-');
|
|
|
|
varNames = this.reachableGlobalVariableNames(true, all);
|
|
if (varNames.length > 0) {
|
|
varNames.forEach(name => {
|
|
blocks.push(variableWatcherToggle(name));
|
|
blocks.push(variableBlock(name));
|
|
});
|
|
blocks.push('-');
|
|
}
|
|
|
|
varNames = this.allLocalVariableNames(true, all);
|
|
if (varNames.length > 0) {
|
|
varNames.forEach(name => {
|
|
blocks.push(variableWatcherToggle(name));
|
|
blocks.push(variableBlock(name, true));
|
|
});
|
|
blocks.push('-');
|
|
}
|
|
|
|
blocks.push(block('doSetVar'));
|
|
blocks.push(block('doChangeVar'));
|
|
blocks.push(block('doShowVar'));
|
|
blocks.push(block('doHideVar'));
|
|
blocks.push(block('doDeclareVariables'));
|
|
|
|
// inheritance:
|
|
|
|
if (StageMorph.prototype.enableInheritance) {
|
|
blocks.push('-');
|
|
blocks.push(block('doDeleteAttr'));
|
|
}
|
|
|
|
blocks.push('=');
|
|
blocks.push(block('reportNewList'));
|
|
blocks.push(block('reportNumbers'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportCONS'));
|
|
blocks.push(block('reportListItem'));
|
|
blocks.push(block('reportCDR'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportListAttribute'));
|
|
blocks.push(block('reportListIndex'));
|
|
blocks.push(block('reportListContainsItem'));
|
|
blocks.push(block('reportListIsEmpty'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportMap'));
|
|
blocks.push(block('reportKeep'));
|
|
blocks.push(block('reportFindFirst'));
|
|
blocks.push(block('reportCombine'));
|
|
blocks.push('-');
|
|
blocks.push(block('doForEach'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportConcatenatedLists'));
|
|
blocks.push(block('reportReshape'));
|
|
blocks.push('-');
|
|
blocks.push(block('doAddToList'));
|
|
blocks.push(block('doDeleteFromList'));
|
|
blocks.push(block('doInsertInList'));
|
|
blocks.push(block('doReplaceInList'));
|
|
|
|
if (SpriteMorph.prototype.showingExtensions) {
|
|
blocks.push('=');
|
|
blocks.push(block('doApplyExtension'));
|
|
blocks.push(block('reportApplyExtension'));
|
|
}
|
|
|
|
if (StageMorph.prototype.enableCodeMapping) {
|
|
blocks.push('=');
|
|
blocks.push(block('doMapCodeOrHeader'));
|
|
blocks.push(block('doMapValueCode'));
|
|
blocks.push(block('doMapListCode'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportMappedCode'));
|
|
}
|
|
|
|
// for debugging: ///////////////
|
|
if (this.world().isDevMode) {
|
|
blocks.push('-');
|
|
blocks.push(this.devModeText());
|
|
blocks.push('-');
|
|
blocks.push(block('doShowTable'));
|
|
}
|
|
}
|
|
|
|
return blocks;
|
|
};
|
|
|
|
// Utitlies displayed in the palette
|
|
SpriteMorph.prototype.makeVariableButton = function () {
|
|
var button, myself = this;
|
|
|
|
function addVar(pair) {
|
|
var ide;
|
|
if (pair) {
|
|
if (myself.isVariableNameInUse(pair[0], pair[1])) {
|
|
myself.inform('that name is already in use');
|
|
} else {
|
|
ide = myself.parentThatIsA(IDE_Morph);
|
|
myself.addVariable(pair[0], pair[1]);
|
|
myself.toggleVariableWatcher(pair[0], pair[1]);
|
|
ide.flushBlocksCache('variables'); // b/c of inheritance
|
|
ide.refreshPalette();
|
|
ide.recordUnsavedChanges();
|
|
}
|
|
}
|
|
}
|
|
|
|
button = new PushButtonMorph(
|
|
null,
|
|
function () {
|
|
new VariableDialogMorph(
|
|
null,
|
|
addVar,
|
|
myself
|
|
).prompt(
|
|
'Variable name',
|
|
null,
|
|
myself.world()
|
|
);
|
|
},
|
|
'Make a variable'
|
|
);
|
|
button.userMenu = this.helpMenu;
|
|
button.selector = 'addVariable';
|
|
button.showHelp = BlockMorph.prototype.showHelp;
|
|
return button;
|
|
};
|
|
|
|
SpriteMorph.prototype.deleteVariableButton = function () {
|
|
var button, myself = this;
|
|
button = new PushButtonMorph(
|
|
null,
|
|
function () {
|
|
var menu = new MenuMorph(
|
|
myself.deleteVariable,
|
|
null,
|
|
myself
|
|
);
|
|
myself.deletableVariableNames().forEach(name =>
|
|
menu.addItem(
|
|
name,
|
|
name,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
true // verbatim - don't translate
|
|
)
|
|
);
|
|
menu.popUpAtHand(myself.world());
|
|
},
|
|
'Delete a variable'
|
|
);
|
|
button.userMenu = this.helpMenu;
|
|
button.selector = 'deleteVariable';
|
|
button.showHelp = BlockMorph.prototype.showHelp;
|
|
return button;
|
|
};
|
|
|
|
SpriteMorph.prototype.categoryText = function (category) {
|
|
var txt = new StringMorph(
|
|
localize(category[0].toUpperCase().concat(category.slice(1))),
|
|
11,
|
|
null,
|
|
true
|
|
);
|
|
txt.setColor(this.paletteTextColor);
|
|
txt.category = category;
|
|
return txt;
|
|
};
|
|
|
|
SpriteMorph.prototype.devModeText = function () {
|
|
var txt = new TextMorph(
|
|
localize('development mode \ndebugging primitives:')
|
|
);
|
|
txt.fontSize = 9;
|
|
txt.setColor(this.paletteTextColor);
|
|
return txt;
|
|
};
|
|
|
|
SpriteMorph.prototype.helpMenu = function () {
|
|
// return a 1 item context menu for anything that implements
|
|
// a 'showHelp' method.
|
|
var menu = new MenuMorph(this);
|
|
menu.addItem('help...', 'showHelp');
|
|
return menu;
|
|
};
|
|
|
|
SpriteMorph.prototype.customBlockTemplatesForCategory = function (
|
|
category,
|
|
includeHidden
|
|
) {
|
|
// returns an array of block templates for a selected category.
|
|
var ide = this.parentThatIsA(IDE_Morph), blocks = [],
|
|
isInherited = false, block, inheritedBlocks;
|
|
|
|
function addCustomBlock(definition) {
|
|
if ((!definition.isHelper || includeHidden) &&
|
|
definition.category === category)
|
|
{
|
|
block = definition.templateInstance();
|
|
if (isInherited) {block.ghost(); }
|
|
blocks.push(block);
|
|
}
|
|
}
|
|
|
|
// global custom blocks:
|
|
if (ide && ide.stage) {
|
|
ide.stage.globalBlocks.forEach(addCustomBlock);
|
|
if (this.customBlocks.length) {blocks.push('='); }
|
|
}
|
|
|
|
// local custom blocks:
|
|
this.customBlocks.forEach(addCustomBlock);
|
|
|
|
// inherited custom blocks:
|
|
if (this.exemplar) {
|
|
inheritedBlocks = this.inheritedBlocks(true);
|
|
if (this.customBlocks.length && inheritedBlocks.length) {
|
|
blocks.push('=');
|
|
}
|
|
isInherited = true;
|
|
inheritedBlocks.forEach(addCustomBlock);
|
|
}
|
|
|
|
return blocks;
|
|
};
|
|
|
|
SpriteMorph.prototype.makeBlockButton = function (category) {
|
|
// answer a button that prompts the user to make a new block
|
|
var button = new PushButtonMorph(
|
|
this,
|
|
'makeBlock',
|
|
'Make a block'
|
|
);
|
|
|
|
button.userMenu = this.helpMenu;
|
|
button.selector = 'addCustomBlock';
|
|
button.showHelp = BlockMorph.prototype.showHelp;
|
|
return button;
|
|
};
|
|
|
|
SpriteMorph.prototype.makeBlock = function () {
|
|
// prompt the user to make a new block
|
|
var ide = this.parentThatIsA(IDE_Morph),
|
|
stage = this.parentThatIsA(StageMorph),
|
|
category = ide.currentCategory === 'unified' ?
|
|
ide.topVisibleCategoryInPalette()
|
|
: ide.currentCategory,
|
|
clr = SpriteMorph.prototype.blockColorFor(category),
|
|
dlg;
|
|
dlg = new BlockDialogMorph(
|
|
null,
|
|
definition => {
|
|
if (definition.spec !== '') {
|
|
if (definition.isGlobal) {
|
|
stage.globalBlocks.push(definition);
|
|
} else {
|
|
this.customBlocks.push(definition);
|
|
}
|
|
ide.flushPaletteCache();
|
|
ide.categories.refreshEmpty();
|
|
ide.refreshPalette();
|
|
ide.recordUnsavedChanges();
|
|
new BlockEditorMorph(definition, this).popUp();
|
|
}
|
|
},
|
|
this
|
|
);
|
|
if (category !== 'variables' || category !== 'unified') {
|
|
dlg.category = category;
|
|
dlg.categories.refresh();
|
|
dlg.types.children.forEach(each => {
|
|
each.setColor(clr);
|
|
each.refresh();
|
|
});
|
|
}
|
|
dlg.prompt(
|
|
'Make a block',
|
|
null,
|
|
this.world()
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.getPrimitiveTemplates = function (category) {
|
|
var blocks = this.primitivesCache[category];
|
|
if (!blocks) {
|
|
blocks = this.blockTemplates(category);
|
|
if (this.isCachingPrimitives) {
|
|
this.primitivesCache[category] = blocks;
|
|
}
|
|
}
|
|
return blocks;
|
|
};
|
|
|
|
SpriteMorph.prototype.palette = function (category) {
|
|
if (!this.paletteCache[category]) {
|
|
this.paletteCache[category] = this.freshPalette(category);
|
|
}
|
|
return this.paletteCache[category];
|
|
};
|
|
|
|
SpriteMorph.prototype.freshPalette = function (category) {
|
|
var myself = this,
|
|
palette = new ScrollFrameMorph(null, null, this.sliderColor),
|
|
unit = SyntaxElementMorph.prototype.fontSize,
|
|
ide,
|
|
showCategories,
|
|
showButtons,
|
|
x = 0,
|
|
y = 5,
|
|
ry = 0,
|
|
blocks,
|
|
hideNextSpace = false,
|
|
shade = new Color(140, 140, 140),
|
|
searchButton,
|
|
makeButton;
|
|
|
|
palette.owner = this;
|
|
palette.padding = unit / 2;
|
|
palette.color = this.paletteColor;
|
|
palette.growth = new Point(0, MorphicPreferences.scrollBarSize);
|
|
|
|
// toolbar:
|
|
|
|
palette.toolBar = new AlignmentMorph('column');
|
|
|
|
searchButton = new PushButtonMorph(
|
|
this,
|
|
"searchBlocks",
|
|
new SymbolMorph("magnifierOutline", 16)
|
|
);
|
|
searchButton.alpha = 0.2;
|
|
searchButton.padding = 1;
|
|
searchButton.hint = localize('find blocks') + '...';
|
|
searchButton.labelShadowColor = shade;
|
|
searchButton.edge = 0;
|
|
searchButton.padding = 3;
|
|
searchButton.fixLayout();
|
|
palette.toolBar.add(searchButton);
|
|
|
|
makeButton = new PushButtonMorph(
|
|
this,
|
|
"makeBlock",
|
|
new SymbolMorph("cross", 16)
|
|
);
|
|
makeButton.alpha = 0.2;
|
|
makeButton.padding = 1;
|
|
makeButton.hint = localize('Make a block') + '...';
|
|
makeButton.labelShadowColor = shade;
|
|
makeButton.edge = 0;
|
|
makeButton.padding = 3;
|
|
makeButton.fixLayout();
|
|
palette.toolBar.add(makeButton);
|
|
|
|
palette.toolBar.fixLayout();
|
|
palette.add(palette.toolBar);
|
|
|
|
// menu:
|
|
palette.userMenu = function () {
|
|
var menu = new MenuMorph();
|
|
|
|
menu.addPair(
|
|
[
|
|
new SymbolMorph(
|
|
'magnifyingGlass',
|
|
MorphicPreferences.menuFontSize
|
|
),
|
|
localize('find blocks') + '...'
|
|
],
|
|
() => myself.searchBlocks(),
|
|
'^F'
|
|
);
|
|
menu.addItem(
|
|
'hide blocks...',
|
|
() => new BlockVisibilityDialogMorph(myself).popUp(myself.world())
|
|
);
|
|
menu.addLine();
|
|
menu.addItem(
|
|
'make a category...',
|
|
() => this.parentThatIsA(IDE_Morph).createNewCategory()
|
|
);
|
|
if (SpriteMorph.prototype.customCategories.size) {
|
|
menu.addItem(
|
|
'delete a category...',
|
|
() => this.parentThatIsA(IDE_Morph).deleteUserCategory()
|
|
);
|
|
}
|
|
return menu;
|
|
};
|
|
|
|
if (category === 'unified') {
|
|
// In a Unified Palette custom blocks appear following each category,
|
|
// but there is only 1 make a block button (at the end).
|
|
ide = this.parentThatIsA(IDE_Morph);
|
|
showCategories = ide.scene.showCategories;
|
|
showButtons = ide.scene.showPaletteButtons;
|
|
blocks = SpriteMorph.prototype.allCategories().reduce(
|
|
(blocks, category) => {
|
|
let header = [this.categoryText(category), '-'],
|
|
primitives = this.getPrimitiveTemplates(category),
|
|
customs = this.customBlockTemplatesForCategory(category),
|
|
showHeader = showCategories &&
|
|
!['lists', 'other'].includes(category) &&
|
|
(primitives.some(item =>
|
|
item instanceof BlockMorph) || customs.length);
|
|
|
|
// hide category names
|
|
if (!showCategories && category !== 'variables') {
|
|
primitives = primitives.filter(each =>
|
|
each !== '-' && each !== '=');
|
|
}
|
|
|
|
// hide "make / delete a variable" buttons
|
|
if (!showButtons && category === 'variables') {
|
|
primitives = primitives.filter(each =>
|
|
!(each instanceof PushButtonMorph &&
|
|
!(each instanceof ToggleMorph)));
|
|
}
|
|
|
|
return blocks.concat(
|
|
showHeader ? header : [],
|
|
primitives,
|
|
showHeader ? '=' : null,
|
|
customs,
|
|
showHeader ? '=' : '-'
|
|
);
|
|
},
|
|
[]
|
|
);
|
|
} else {
|
|
// ensure we do not modify the cached array
|
|
blocks = this.getPrimitiveTemplates(category).slice();
|
|
}
|
|
|
|
if (category !== 'unified' || showButtons) {
|
|
blocks.push('=');
|
|
blocks.push(this.makeBlockButton(category));
|
|
}
|
|
|
|
if (category !== 'unified') {
|
|
blocks.push('=');
|
|
blocks.push(...this.customBlockTemplatesForCategory(category));
|
|
}
|
|
if (category === 'variables') {
|
|
blocks.push(...this.customBlockTemplatesForCategory('lists'));
|
|
blocks.push(...this.customBlockTemplatesForCategory('other'));
|
|
}
|
|
|
|
blocks.forEach(block => {
|
|
if (block === null) {
|
|
return;
|
|
}
|
|
if (block === '-') {
|
|
if (hideNextSpace) {return; }
|
|
y += unit * 0.8;
|
|
hideNextSpace = true;
|
|
} else if (block === '=') {
|
|
if (hideNextSpace) {return; }
|
|
y += unit * 1.6;
|
|
hideNextSpace = true;
|
|
} else if (block === '#') {
|
|
x = 0;
|
|
y = (ry === 0 ? y : ry);
|
|
} else {
|
|
hideNextSpace = false;
|
|
if (x === 0) {
|
|
y += unit * 0.3;
|
|
}
|
|
block.setPosition(new Point(x, y));
|
|
palette.addContents(block);
|
|
if (block instanceof ToggleMorph) {
|
|
x = block.right() + unit / 2;
|
|
} else if (block instanceof RingMorph) {
|
|
x = block.right() + unit / 2;
|
|
ry = block.bottom();
|
|
} else {
|
|
x = 0;
|
|
y += block.height();
|
|
}
|
|
}
|
|
});
|
|
|
|
palette.scrollX(palette.padding);
|
|
palette.scrollY(palette.padding);
|
|
return palette;
|
|
};
|
|
|
|
// SpriteMorph utilities for showing & hiding blocks in the palette
|
|
|
|
SpriteMorph.prototype.allPaletteBlocks = function () {
|
|
// private - only to be used for showing & hiding blocks in the palette
|
|
var blocks = SpriteMorph.prototype.allCategories().reduce(
|
|
(blocks, category) => {
|
|
let primitives = this.blockTemplates(category, true),
|
|
customs = this.customBlockTemplatesForCategory(category, true);
|
|
return blocks.concat(
|
|
primitives,
|
|
customs
|
|
);
|
|
},
|
|
[]
|
|
);
|
|
return blocks.filter(each => each instanceof BlockMorph);
|
|
};
|
|
|
|
SpriteMorph.prototype.isHidingBlock = function (aBlock) {
|
|
var frame;
|
|
if (aBlock.isCustomBlock) {
|
|
return (
|
|
aBlock.isGlobal ? aBlock.definition
|
|
: this.getMethod(aBlock.semanticSpec)
|
|
).isHelper;
|
|
}
|
|
if (aBlock.selector === 'reportGetVar') {
|
|
frame = this.variables.silentFind(aBlock.blockSpec);
|
|
if (!frame) {
|
|
return false;
|
|
}
|
|
return frame.vars[aBlock.blockSpec].isHidden;
|
|
}
|
|
return StageMorph.prototype.hiddenPrimitives[aBlock.selector] === true;
|
|
};
|
|
|
|
SpriteMorph.prototype.isDisablingBlock = function (aBlock) {
|
|
// show or hide certain kinds of blocks in search results only
|
|
// if they are enabled
|
|
var sel = aBlock.selector;
|
|
if (sel === 'reportJSFunction') {
|
|
return !Process.prototype.enableJS;
|
|
}
|
|
if (
|
|
sel === 'doApplyExtension' ||
|
|
sel === 'reportApplyExtension'
|
|
) {
|
|
return !SpriteMorph.prototype.showingExtensions;
|
|
}
|
|
if (
|
|
sel === 'doMapCodeOrHeader' ||
|
|
sel === 'doMapValueCode' ||
|
|
sel === 'doMapListCode' ||
|
|
sel === 'reportMappedCode'
|
|
) {
|
|
return !StageMorph.prototype.enableCodeMapping;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
SpriteMorph.prototype.changeBlockVisibility = function (aBlock, hideIt, quick) {
|
|
var ide = this.parentThatIsA(IDE_Morph),
|
|
dict, cat;
|
|
if (aBlock.isCustomBlock) {
|
|
(aBlock.isGlobal ? aBlock.definition
|
|
: this.getMethod(aBlock.semanticSpec)
|
|
).isHelper = !!hideIt;
|
|
} else if (aBlock.selector === 'reportGetVar') {
|
|
this.variables.find(
|
|
aBlock.blockSpec
|
|
).vars[aBlock.blockSpec].isHidden = !!hideIt;
|
|
} else {
|
|
if (hideIt) {
|
|
StageMorph.prototype.hiddenPrimitives[aBlock.selector] = true;
|
|
} else {
|
|
delete StageMorph.prototype.hiddenPrimitives[aBlock.selector];
|
|
}
|
|
}
|
|
if (quick) {return; }
|
|
dict = {
|
|
doWarp: 'control',
|
|
reifyScript: 'operators',
|
|
reifyReporter: 'operators',
|
|
reifyPredicate: 'operators',
|
|
doDeclareVariables: 'variables'
|
|
};
|
|
cat = dict[aBlock.selector] || aBlock.category;
|
|
if (cat === 'lists') {cat = 'variables'; }
|
|
ide.flushBlocksCache(cat);
|
|
ide.refreshPalette();
|
|
};
|
|
|
|
SpriteMorph.prototype.emptyCategories = function () {
|
|
// return a dictionary that indicates for each category whether
|
|
// it has any shown blocks in it (true) or is empty (false)
|
|
var hasBlocks = (any) => any instanceof BlockMorph &&
|
|
!this.isHidingBlock(any);
|
|
if (this.categoriesCache === null) {
|
|
this.categoriesCache = {};
|
|
SpriteMorph.prototype.allCategories().forEach(category =>
|
|
this.categoriesCache[category] =
|
|
this.getPrimitiveTemplates(category).some(hasBlocks) ||
|
|
this.customBlockTemplatesForCategory(category).some(hasBlocks));
|
|
}
|
|
return this.categoriesCache;
|
|
};
|
|
|
|
// SpriteMorph blocks searching
|
|
|
|
SpriteMorph.prototype.blocksMatching = function (
|
|
searchString,
|
|
strictly,
|
|
types, // optional, ['hat', 'command', 'reporter', 'predicate']
|
|
varNames // optional, list of reachable unique variable names
|
|
) {
|
|
// answer an array of block templates whose spec contains
|
|
// the given search string, ordered by descending relevance
|
|
// types is an optional array containing block types the search
|
|
// is limited to, e.g. "command", "hat", "reporter", "predicate".
|
|
// Note that "predicate" is not subsumed by "reporter" and has
|
|
// to be specified explicitly.
|
|
// if no types are specified all blocks are searched
|
|
var blocks = [],
|
|
blocksDict,
|
|
search = searchString.toLowerCase(),
|
|
stage = this.parentThatIsA(StageMorph),
|
|
reporterized;
|
|
|
|
if (!types || !types.length) {
|
|
types = ['hat', 'command', 'reporter', 'predicate', 'ring'];
|
|
}
|
|
if (!varNames) {varNames = []; }
|
|
|
|
function labelOf(aBlockSpec) {
|
|
var words = (BlockMorph.prototype.parseSpec(aBlockSpec)),
|
|
filtered = words.filter(each =>
|
|
each.indexOf('%') !== 0 || each.length === 1
|
|
),
|
|
slots = words.filter(each =>
|
|
each.length > 1 && each.indexOf('%') === 0
|
|
).map(spec => menuOf(spec));
|
|
return filtered.join(' ') + ' ' + slots.join(' ');
|
|
}
|
|
|
|
function menuOf(aSlotSpec) {
|
|
var info = BlockMorph.prototype.labelParts[aSlotSpec] || {},
|
|
menu = info.menu;
|
|
if (!menu) {return ''; }
|
|
if (isString(menu)) {
|
|
menu = InputSlotMorph.prototype[menu](true);
|
|
}
|
|
return Object.values(menu).map(entry => {
|
|
if (isNil(entry)) {return ''; }
|
|
if (entry instanceof Array) {
|
|
return localize(entry[0]);
|
|
}
|
|
return entry.toString();
|
|
}).join(' ');
|
|
}
|
|
|
|
function fillDigits(anInt, totalDigits, fillChar) {
|
|
var ans = String(anInt);
|
|
while (ans.length < totalDigits) {ans = fillChar + ans; }
|
|
return ans;
|
|
}
|
|
|
|
function relevance(aBlockLabel, aSearchString) {
|
|
var lbl = ' ' + aBlockLabel.toLowerCase(),
|
|
idx = lbl.indexOf(aSearchString),
|
|
atWord;
|
|
if (idx === -1) {return -1; }
|
|
atWord = (lbl.charAt(idx - 1) === ' ');
|
|
if (strictly && !atWord) {return -1; }
|
|
return (atWord ? '1' : '2') + fillDigits(idx, 4, '0');
|
|
}
|
|
|
|
function primitive(selector) {
|
|
var newBlock = SpriteMorph.prototype.blockForSelector(selector, true);
|
|
newBlock.isTemplate = true;
|
|
return newBlock;
|
|
}
|
|
|
|
// variable getters
|
|
varNames.forEach(vName => {
|
|
var rel = relevance(vName, search);
|
|
if (rel !== -1) {
|
|
blocks.push([this.variableBlock(vName), rel + '1']);
|
|
}
|
|
});
|
|
// custom blocks
|
|
[this.customBlocks, stage.globalBlocks].forEach(blocksList =>
|
|
blocksList.forEach(definition => {
|
|
if (contains(types, definition.type)) {
|
|
var spec = definition.localizedSpec(),
|
|
rel = relevance(labelOf(
|
|
spec) + ' ' + definition.menuSearchWords(),
|
|
search
|
|
);
|
|
if (rel !== -1) {
|
|
blocks.push([definition.templateInstance(), rel + '2']);
|
|
}
|
|
}
|
|
})
|
|
);
|
|
// primitives
|
|
blocksDict = SpriteMorph.prototype.blocks;
|
|
Object.keys(blocksDict).forEach(selector => {
|
|
if (!StageMorph.prototype.hiddenPrimitives[selector] &&
|
|
contains(types, blocksDict[selector].type)) {
|
|
var block = blocksDict[selector],
|
|
spec = localize(block.alias || block.spec),
|
|
rel = relevance(labelOf(spec), search);
|
|
if (
|
|
(rel !== -1) &&
|
|
(!block.dev) &&
|
|
(!block.only || (block.only === this.constructor))
|
|
) {
|
|
blocks.push([primitive(selector), rel + '3']);
|
|
}
|
|
}
|
|
});
|
|
// infix arithmetic expression
|
|
if (contains(types, 'reporter')) {
|
|
reporterized = this.reporterize(searchString);
|
|
if (reporterized) {
|
|
// reporterized.isTemplate = true;
|
|
// reporterized.isDraggable = false;
|
|
blocks.push([reporterized, '']);
|
|
}
|
|
}
|
|
blocks.sort((x, y) => x[1] < y[1] ? -1 : 1);
|
|
blocks = blocks.map(each => each[0]);
|
|
return blocks.filter(each =>
|
|
!this.isHidingBlock(each) &&
|
|
!this.isDisablingBlock(each)
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.searchBlocks = function (
|
|
searchString,
|
|
types,
|
|
varNames,
|
|
scriptFocus
|
|
) {
|
|
var myself = this,
|
|
unit = SyntaxElementMorph.prototype.fontSize,
|
|
ide = this.parentThatIsA(IDE_Morph),
|
|
oldTop = ide.palette.contents.top(),
|
|
oldSearch = '',
|
|
searchBar = new InputFieldMorph(searchString || ''),
|
|
searchPane = ide.createPalette('forSearch'),
|
|
blocksList = [],
|
|
selection,
|
|
focus;
|
|
|
|
function showSelection() {
|
|
if (focus) {focus.destroy(); }
|
|
if (!selection || !scriptFocus) {return; }
|
|
focus = selection.outline(
|
|
MorphicPreferences.isFlat ? new Color(150, 200, 255) : WHITE,
|
|
2
|
|
);
|
|
searchPane.contents.add(focus);
|
|
focus.scrollIntoView();
|
|
}
|
|
|
|
function show(blocks) {
|
|
var x = searchPane.contents.left() + 5,
|
|
y = (searchBar.bottom() + unit);
|
|
blocksList = blocks;
|
|
selection = null;
|
|
if (blocks.length && scriptFocus) {
|
|
selection = blocks[0];
|
|
}
|
|
searchPane.contents.children = [searchPane.contents.children[0]];
|
|
blocks.forEach(block => {
|
|
block.setPosition(new Point(x, y));
|
|
searchPane.addContents(block);
|
|
y += block.height();
|
|
y += unit * 0.3;
|
|
});
|
|
showSelection();
|
|
searchPane.changed();
|
|
}
|
|
|
|
searchPane.owner = this;
|
|
searchPane.color = this.paletteColor;
|
|
searchPane.contents.color = this.paletteColor;
|
|
searchPane.addContents(searchBar);
|
|
searchBar.setWidth(ide.logo.width() - 30);
|
|
searchBar.contrast = 90;
|
|
searchBar.setPosition(
|
|
searchPane.contents.topLeft().add(new Point(10, 10))
|
|
);
|
|
searchBar.fixLayout();
|
|
|
|
searchPane.accept = function () {
|
|
var search;
|
|
if (scriptFocus) {
|
|
searchBar.cancel();
|
|
if (selection) {
|
|
scriptFocus.insertBlock(selection);
|
|
}
|
|
if (ide) {
|
|
ide.recordUnsavedChanges();
|
|
}
|
|
} else {
|
|
search = searchBar.getValue();
|
|
if (search.length > 0) {
|
|
show(myself.blocksMatching(search));
|
|
}
|
|
}
|
|
};
|
|
|
|
searchPane.reactToKeystroke = function (evt) {
|
|
var idx, code = evt ? evt.keyCode : 0;
|
|
switch (code) {
|
|
case 38: // up arrow
|
|
if (!scriptFocus || !selection) {return; }
|
|
idx = blocksList.indexOf(selection) - 1;
|
|
if (idx < 0) {
|
|
idx = blocksList.length - 1;
|
|
}
|
|
selection = blocksList[idx];
|
|
showSelection();
|
|
return;
|
|
case 40: // down arrow
|
|
if (!scriptFocus || !selection) {return; }
|
|
idx = blocksList.indexOf(selection) + 1;
|
|
if (idx >= blocksList.length) {
|
|
idx = 0;
|
|
}
|
|
selection = blocksList[idx];
|
|
showSelection();
|
|
return;
|
|
default:
|
|
nop();
|
|
}
|
|
};
|
|
|
|
searchPane.reactToInput = function (evt) {
|
|
var search = searchBar.getValue();
|
|
if (search !== oldSearch) {
|
|
oldSearch = search;
|
|
show(myself.blocksMatching(
|
|
search,
|
|
search.length < 2,
|
|
types,
|
|
varNames
|
|
));
|
|
}
|
|
};
|
|
|
|
searchBar.cancel = function () {
|
|
ide.refreshPalette();
|
|
ide.palette.contents.setTop(oldTop);
|
|
ide.palette.adjustScrollBars();
|
|
};
|
|
|
|
ide.fixLayout('refreshPalette');
|
|
searchBar.edit();
|
|
if (searchString) {searchPane.reactToKeystroke(); }
|
|
};
|
|
|
|
// SpritMorph parsing simple arithmetic expressions to reporter blocks
|
|
|
|
SpriteMorph.prototype.reporterize = function (expressionString) {
|
|
// highly experimental Christmas Easter Egg 2016 :-)
|
|
var ast;
|
|
|
|
function parseInfix(expression, operator, already) {
|
|
// very basic diadic infix parser for arithmetic expressions
|
|
// with strict left-to-right operator precedence (as in Smalltalk)
|
|
// which can be overriden by - nested - parentheses.
|
|
// assumes well-formed expressions, no graceful error handling yet.
|
|
|
|
var inputs = ['', ''],
|
|
idx = 0,
|
|
ch;
|
|
|
|
function format(value) {
|
|
return value instanceof Array || isNaN(+value) ? value : +value;
|
|
}
|
|
|
|
function nested() {
|
|
var level = 1,
|
|
expr = '';
|
|
while (idx < expression.length) {
|
|
ch = expression[idx];
|
|
idx += 1;
|
|
switch (ch) {
|
|
case '(':
|
|
level += 1;
|
|
break;
|
|
case ')':
|
|
level -= 1;
|
|
if (!level) {
|
|
return expr;
|
|
}
|
|
break;
|
|
}
|
|
expr += ch;
|
|
}
|
|
}
|
|
|
|
while (idx < expression.length) {
|
|
ch = expression[idx];
|
|
idx += 1;
|
|
switch (ch) {
|
|
case ' ':
|
|
break;
|
|
case '(':
|
|
if (inputs[operator ? 1 : 0].length) {
|
|
inputs[operator ? 1 : 0] = [
|
|
inputs[operator ? 1 : 0],
|
|
parseInfix(nested())
|
|
];
|
|
} else {
|
|
inputs[operator ? 1 : 0] = parseInfix(nested());
|
|
}
|
|
break;
|
|
case '-':
|
|
case '+':
|
|
case '*':
|
|
case '/':
|
|
case '%':
|
|
case '^':
|
|
case '=':
|
|
case '<':
|
|
case '>':
|
|
case '&':
|
|
case '|':
|
|
if (!operator && !inputs[0].length) {
|
|
inputs[0] = ch;
|
|
} else if (operator) {
|
|
if (!inputs[1].length) {
|
|
inputs[1] = ch;
|
|
} else {
|
|
return parseInfix(
|
|
expression.slice(idx),
|
|
ch,
|
|
[operator, already, format(inputs[1])]
|
|
);
|
|
}
|
|
} else {
|
|
operator = ch;
|
|
already = format(inputs[0]);
|
|
}
|
|
break;
|
|
default:
|
|
inputs[operator ? 1 : 0] += ch;
|
|
}
|
|
}
|
|
if (operator) {
|
|
return [operator, already, format(inputs[1])];
|
|
}
|
|
return format(inputs[0]);
|
|
}
|
|
|
|
function blockFromAST(ast) {
|
|
var block, selectors, monads, alias, key, sel, i, inps,
|
|
off = 1,
|
|
reverseDict = {};
|
|
selectors = {
|
|
'+': 'reportSum',
|
|
'-': 'reportDifference',
|
|
'*': 'reportProduct',
|
|
'/': 'reportQuotient',
|
|
'%': 'reportModulus',
|
|
'^': 'reportPower',
|
|
'=': 'reportEquals',
|
|
'<': 'reportLessThan',
|
|
'>': 'reportGreaterThan',
|
|
'&': 'reportAnd',
|
|
'|': 'reportOr',
|
|
round: 'reportRound',
|
|
not: 'reportNot'
|
|
};
|
|
monads = ['abs', 'neg', 'ceiling', 'floor', 'sqrt', 'sin', 'cos',
|
|
'tan', 'asin', 'acos', 'atan', 'ln', 'log', 'lg', 'id', 'round',
|
|
'not'];
|
|
alias = {
|
|
ceil: 'ceiling',
|
|
'!' : 'not'
|
|
};
|
|
monads.concat(['true', 'false']).forEach(word =>
|
|
reverseDict[localize(word).toLowerCase()] = word
|
|
);
|
|
key = alias[ast[0]] || reverseDict[ast[0].toLowerCase()] || ast[0];
|
|
if (contains(monads, key)) { // monadic
|
|
sel = selectors[key];
|
|
if (sel) { // single input
|
|
block = SpriteMorph.prototype.blockForSelector(sel);
|
|
inps = block.inputs();
|
|
} else { // two inputs, first is function name
|
|
block = SpriteMorph.prototype.blockForSelector('reportMonadic');
|
|
inps = block.inputs();
|
|
inps[0].setContents([key]);
|
|
off = 0;
|
|
}
|
|
} else { // dyadic
|
|
block = SpriteMorph.prototype.blockForSelector(selectors[key]);
|
|
inps = block.inputs();
|
|
}
|
|
for (i = 1; i < ast.length; i += 1) {
|
|
if (ast[i] instanceof Array) {
|
|
block.replaceInput(inps[i - off], blockFromAST(ast[i]));
|
|
} else if (isString(ast[i])) {
|
|
if (contains(
|
|
['true', 'false'], reverseDict[ast[i]] || ast[i])
|
|
) {
|
|
block.replaceInput(
|
|
inps[i - off],
|
|
SpriteMorph.prototype.blockForSelector(
|
|
(reverseDict[ast[i]] || ast[i]) === 'true' ?
|
|
'reportTrue' : 'reportFalse'
|
|
)
|
|
);
|
|
} else if (ast[i] !== '_') {
|
|
block.replaceInput(
|
|
inps[i - off],
|
|
SpriteMorph.prototype.variableBlock(ast[i])
|
|
);
|
|
}
|
|
} else { // number
|
|
inps[i - off].setContents(ast[i]);
|
|
}
|
|
}
|
|
block.isDraggable = true;
|
|
block.fixLayout();
|
|
block.fixBlockColor(null, true);
|
|
return block;
|
|
}
|
|
|
|
if (expressionString.length > 100) {return null; }
|
|
try {
|
|
ast = parseInfix(expressionString);
|
|
return ast instanceof Array ? blockFromAST(ast) : null;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// SpriteMorph variable management
|
|
|
|
SpriteMorph.prototype.addVariable = function (name, isGlobal) {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
if (isGlobal) {
|
|
this.globalVariables().addVar(name);
|
|
if (ide) {
|
|
ide.flushBlocksCache('variables');
|
|
}
|
|
} else {
|
|
this.variables.addVar(name);
|
|
this.primitivesCache.variables = null;
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.deleteVariable = function (varName) {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
if (!contains(this.inheritedVariableNames(true), varName)) {
|
|
// check only shadowed variables
|
|
this.deleteVariableWatcher(varName);
|
|
}
|
|
this.variables.deleteVar(varName);
|
|
if (ide) {
|
|
ide.flushBlocksCache('variables'); // b/c the var could be global
|
|
ide.refreshPalette();
|
|
ide.recordUnsavedChanges();
|
|
}
|
|
};
|
|
|
|
// SpriteMorph costume management
|
|
|
|
SpriteMorph.prototype.addCostume = function (costume) {
|
|
if (!costume.name) {
|
|
costume.name = 'costume' + (this.costumes.length() + 1);
|
|
}
|
|
this.shadowAttribute('costumes');
|
|
this.costumes.add(costume);
|
|
};
|
|
|
|
SpriteMorph.prototype.wearCostume = function (costume, noShadow) {
|
|
var x = this.xPosition ? this.xPosition() : null,
|
|
y = this.yPosition ? this.yPosition() : null,
|
|
idx = isNil(costume) ? null : this.costumes.asArray().indexOf(costume);
|
|
|
|
this.changed();
|
|
this.costume = costume;
|
|
this.fixLayout();
|
|
this.rerender();
|
|
if (x !== null) {
|
|
this.silentGotoXY(x, y, true); // just me
|
|
}
|
|
if (this.positionTalkBubble) { // the stage doesn't talk
|
|
this.positionTalkBubble();
|
|
}
|
|
this.version = Date.now();
|
|
|
|
// propagate to children that inherit my costume #
|
|
if (!noShadow) {
|
|
this.shadowAttribute('costume #');
|
|
}
|
|
this.specimens().forEach(instance => {
|
|
if (instance.cachedPropagation) {
|
|
if (instance.inheritsAttribute('costume #')) {
|
|
if (idx === null) {
|
|
instance.wearCostume(null, true);
|
|
} else if (idx === -1) {
|
|
instance.wearCostume(costume, true);
|
|
} else {
|
|
instance.doSwitchToCostume(idx + 1, true);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
SpriteMorph.prototype.getCostumeIdx = function () {
|
|
if (this.inheritsAttribute('costume #')) {
|
|
return this.exemplar.getCostumeIdx();
|
|
}
|
|
return this.costumes.asArray().indexOf(this.costume) + 1;
|
|
};
|
|
|
|
SpriteMorph.prototype.doWearNextCostume = function () {
|
|
var arr = this.costumes.asArray(),
|
|
idx;
|
|
if (arr.length > 1) {
|
|
idx = arr.indexOf(this.costume);
|
|
if (idx > -1) {
|
|
idx += 1;
|
|
if (idx > (arr.length - 1)) {
|
|
idx = 0;
|
|
}
|
|
this.wearCostume(arr[idx]);
|
|
}
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.doWearPreviousCostume = function () {
|
|
var arr = this.costumes.asArray(),
|
|
idx;
|
|
if (arr.length > 1) {
|
|
idx = arr.indexOf(this.costume);
|
|
if (idx > -1) {
|
|
idx -= 1;
|
|
if (idx < 0) {
|
|
idx = arr.length - 1;
|
|
}
|
|
this.wearCostume(arr[idx]);
|
|
}
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.doSwitchToCostume = function (id, noShadow) {
|
|
var w = 0,
|
|
h = 0,
|
|
stage;
|
|
if (id instanceof List) { // try to turn a list of pixels into a costume
|
|
if (this.costume) {
|
|
// recycle dimensions of current costume
|
|
w = this.costume.width();
|
|
h = this.costume.height();
|
|
}
|
|
if (w * h !== id.length()) {
|
|
// assume stage's dimensions
|
|
stage = this.parentThatIsA(StageMorph);
|
|
w = stage.dimensions.x;
|
|
h = stage.dimensions.y;
|
|
}
|
|
id = Process.prototype.reportNewCostume(
|
|
id,
|
|
w,
|
|
h,
|
|
this.newCostumeName(localize('snap'))
|
|
);
|
|
}
|
|
if (id instanceof Costume) { // allow first-class costumes
|
|
this.wearCostume(id, noShadow);
|
|
return;
|
|
}
|
|
if (id instanceof Array && (id[0] === 'current')) {
|
|
return;
|
|
}
|
|
|
|
var num,
|
|
arr = this.costumes.asArray(),
|
|
costume;
|
|
if (
|
|
contains(
|
|
[localize('Turtle'), localize('Empty')],
|
|
(id instanceof Array ? id[0] : null)
|
|
)
|
|
) {
|
|
costume = null;
|
|
} else {
|
|
if (id === -1) {
|
|
this.doWearPreviousCostume();
|
|
return;
|
|
}
|
|
costume = detect(arr, cst => cst.name === id);
|
|
if (costume === null) {
|
|
num = parseFloat(id);
|
|
if (num === 0) {
|
|
costume = null;
|
|
} else {
|
|
costume = arr[num - 1] || null;
|
|
}
|
|
}
|
|
}
|
|
this.wearCostume(costume, noShadow);
|
|
};
|
|
|
|
SpriteMorph.prototype.reportCostumes = function () {
|
|
return this.costumes;
|
|
};
|
|
|
|
// SpriteMorph sound management
|
|
|
|
SpriteMorph.prototype.addSound = function (audio, name) {
|
|
var sound = new Sound(audio, name);
|
|
this.shadowAttribute('sounds');
|
|
this.sounds.add(sound);
|
|
return sound;
|
|
};
|
|
|
|
SpriteMorph.prototype.doPlaySound = function (name) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
sound = name instanceof Sound ? name
|
|
: (typeof name === 'number' ? this.sounds.at(name)
|
|
: detect(
|
|
this.sounds.asArray(),
|
|
s => s.name === name.toString()
|
|
)),
|
|
ctx = this.audioContext(),
|
|
gain = this.getGainNode(),
|
|
pan = this.getPannerNode(),
|
|
aud,
|
|
source;
|
|
if (sound) {
|
|
aud = document.createElement('audio');
|
|
aud.src = sound.audio.src;
|
|
ctx.resume(); // needed to fix tainted context in case of autoplay
|
|
source = ctx.createMediaElementSource(aud);
|
|
source.connect(gain);
|
|
if (pan) {
|
|
gain.connect(pan);
|
|
pan.connect(ctx.destination); // perhaps redundant
|
|
this.setPan(this.getPan()); // yep, should be redundant
|
|
} else {
|
|
gain.connect(ctx.destination);
|
|
}
|
|
this.setVolume(this.getVolume()); // probably redundant as well
|
|
aud.play();
|
|
if (stage) {
|
|
stage.activeSounds.push(aud);
|
|
stage.activeSounds = stage.activeSounds.filter(snd =>
|
|
!snd.ended && !snd.terminated
|
|
);
|
|
}
|
|
return aud;
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.reportSounds = function () {
|
|
return this.sounds;
|
|
};
|
|
|
|
// SpriteMorph volume
|
|
|
|
SpriteMorph.prototype.setVolume = function (num, noShadow) {
|
|
this.volume = Math.max(Math.min(+num || 0, 100), 0);
|
|
this.getGainNode().gain.setValueAtTime(
|
|
1 / Math.pow(10, Math.log2(100 / this.volume)),
|
|
this.audioContext().currentTime
|
|
);
|
|
if (this instanceof StageMorph) {
|
|
return;
|
|
}
|
|
// propagate to children that inherit my volume
|
|
if (!noShadow) {
|
|
this.shadowAttribute('volume');
|
|
}
|
|
this.instances.forEach(instance => {
|
|
if (instance.cachedPropagation) {
|
|
if (instance.inheritsAttribute('volume')) {
|
|
instance.setVolume(num, true);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
SpriteMorph.prototype.changeVolume = function (delta) {
|
|
this.setVolume(this.getVolume() + (+delta || 0));
|
|
};
|
|
|
|
SpriteMorph.prototype.getVolume = function () {
|
|
if (this.inheritsAttribute('volume')) {
|
|
return this.exemplar.getVolume();
|
|
}
|
|
return this.volume;
|
|
};
|
|
|
|
SpriteMorph.prototype.getGainNode = function () {
|
|
if (!this.gainNode) {
|
|
this.gainNode = this.audioContext().createGain();
|
|
}
|
|
return this.gainNode;
|
|
};
|
|
|
|
SpriteMorph.prototype.audioContext = function () {
|
|
return Note.prototype.getAudioContext();
|
|
};
|
|
|
|
// SpriteMorph stero panning
|
|
|
|
SpriteMorph.prototype.setPan = function (num, noShadow) {
|
|
var panner = this.getPannerNode();
|
|
if (!panner) {return; }
|
|
this.pan = Math.max(Math.min((+num || 0), 100), -100);
|
|
panner.pan.setValueAtTime(
|
|
this.pan / 100,
|
|
this.audioContext().currentTime
|
|
);
|
|
if (this instanceof StageMorph) {
|
|
return;
|
|
}
|
|
// propagate to children that inherit my balance
|
|
if (!noShadow) {
|
|
this.shadowAttribute('balance');
|
|
}
|
|
this.instances.forEach(instance => {
|
|
if (instance.cachedPropagation) {
|
|
if (instance.inheritsAttribute('balance')) {
|
|
instance.setPan(num, true);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
SpriteMorph.prototype.changePan = function (delta) {
|
|
this.setPan(this.getPan() + (+delta || 0));
|
|
};
|
|
|
|
SpriteMorph.prototype.getPan = function () {
|
|
if (this.inheritsAttribute('balance')) {
|
|
return this.exemplar.getPan();
|
|
}
|
|
return this.pan;
|
|
};
|
|
|
|
SpriteMorph.prototype.getPannerNode = function () {
|
|
var ctx;
|
|
if (!this.pannerNode) {
|
|
ctx = this.audioContext();
|
|
if (ctx.createStereoPanner) {
|
|
this.pannerNode = this.audioContext().createStereoPanner();
|
|
}
|
|
}
|
|
return this.pannerNode;
|
|
};
|
|
|
|
// SpriteMorph frequency player
|
|
|
|
SpriteMorph.prototype.playFreq = function (hz) {
|
|
// start playing the given frequency until stopped
|
|
var note,
|
|
ctx = this.audioContext(),
|
|
gain = this.getGainNode(),
|
|
pan = this.getPannerNode(),
|
|
stage = this.parentThatIsA(StageMorph);
|
|
if (!this.freqPlayer) {
|
|
this.freqPlayer = new Note();
|
|
}
|
|
note = this.freqPlayer;
|
|
note.fader = ctx.createGain();
|
|
if (note.oscillator) {
|
|
note.oscillator.frequency.value = hz;
|
|
} else {
|
|
note.oscillator = ctx.createOscillator();
|
|
if (!note.oscillator.start) {
|
|
note.oscillator.start = note.oscillator.noteOn;
|
|
}
|
|
if (!note.oscillator.stop) {
|
|
note.oscillator.stop = note.oscillator.noteOff;
|
|
}
|
|
note.setInstrument(this.instrument);
|
|
note.oscillator.frequency.value = hz;
|
|
this.setVolume(this.getVolume());
|
|
note.oscillator.connect(note.fader);
|
|
note.fader.connect(gain);
|
|
if (pan) {
|
|
gain.connect(pan);
|
|
pan.connect(ctx.destination);
|
|
this.setPan(this.pan);
|
|
} else {
|
|
gain.connect(ctx.destination);
|
|
}
|
|
note.ended = false;
|
|
if (stage) {
|
|
stage.activeSounds.push(note);
|
|
stage.activeSounds = stage.activeSounds.filter(snd =>
|
|
!snd.ended && !snd.terminated
|
|
);
|
|
}
|
|
note.fader.gain.setValueCurveAtTime(
|
|
note.fadeIn,
|
|
ctx.currentTime,
|
|
note.fadeTime
|
|
);
|
|
note.oscillator.start(0);
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.stopFreq = function () {
|
|
if (this.freqPlayer) {
|
|
this.freqPlayer.stop();
|
|
}
|
|
};
|
|
|
|
// SpriteMorph user menu
|
|
|
|
SpriteMorph.prototype.userMenu = function () {
|
|
var ide = this.parentThatIsA(IDE_Morph),
|
|
menu = new MenuMorph(this),
|
|
allParts,
|
|
anchors;
|
|
|
|
if (ide && ide.isAppMode) {
|
|
// menu.addItem('help', 'nop');
|
|
return menu;
|
|
}
|
|
if (!this.isTemporary) {
|
|
menu.addItem("duplicate", 'duplicate');
|
|
if (StageMorph.prototype.enableInheritance) {
|
|
menu.addItem("clone", 'instantiate');
|
|
menu.addLine();
|
|
}
|
|
}
|
|
menu.addItem("delete", 'remove');
|
|
menu.addItem("move", 'moveCenter');
|
|
menu.addItem("rotate", 'setRotation');
|
|
if (this.costume) {
|
|
menu.addItem(
|
|
"pivot",
|
|
'moveRotationCenter',
|
|
'edit the costume\'s\nrotation center'
|
|
);
|
|
}
|
|
if (this.isTemporary) {
|
|
if (StageMorph.prototype.enableInheritance) {
|
|
menu.addItem(
|
|
"edit",
|
|
'perpetuateAndEdit',
|
|
'make permanent and\nshow in the sprite corral'
|
|
);
|
|
}
|
|
} else {
|
|
menu.addItem("edit", 'edit');
|
|
}
|
|
menu.addLine();
|
|
if (this.anchor) {
|
|
menu.addItem(
|
|
localize('detach from') + ' ' + this.anchor.name,
|
|
'detachFromAnchor'
|
|
);
|
|
} else {
|
|
allParts = this.allParts();
|
|
anchors = this.parent.children.filter(morph =>
|
|
morph instanceof SpriteMorph && !contains(allParts, morph)
|
|
);
|
|
if (anchors.length) {
|
|
menu.addMenu('stick to', this.anchorsMenu(anchors));
|
|
}
|
|
}
|
|
if (this.parts.length) {
|
|
menu.addItem('detach all parts', 'detachAllParts');
|
|
}
|
|
menu.addItem("export...", 'exportSprite');
|
|
return menu;
|
|
};
|
|
|
|
SpriteMorph.prototype.anchorsMenu = function (targets) {
|
|
var menu = new MenuMorph(this.attachTo, null, this);
|
|
targets.forEach(sprite =>
|
|
menu.addItem(
|
|
[
|
|
sprite.thumbnail(new Point(24, 24)),
|
|
sprite.name,
|
|
],
|
|
sprite
|
|
)
|
|
);
|
|
return menu;
|
|
};
|
|
|
|
SpriteMorph.prototype.exportSprite = function () {
|
|
if (this.isTemporary) {return; }
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
if (ide) {
|
|
ide.exportSprite(this);
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.edit = function () {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
if (ide && !ide.isAppMode) {
|
|
ide.selectSprite(this);
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.showOnStage = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (stage) {
|
|
this.keepWithin(stage);
|
|
stage.add(this);
|
|
}
|
|
this.show();
|
|
};
|
|
|
|
SpriteMorph.prototype.duplicate = function () {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
if (ide) {
|
|
ide.duplicateSprite(this);
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.instantiate = function () {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
if (ide) {
|
|
ide.instantiateSprite(this);
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.remove = function () {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
if (ide) {
|
|
ide.removeSprite(this);
|
|
}
|
|
};
|
|
|
|
// SpriteMorph cloning
|
|
|
|
/*
|
|
clones are temporary, partially shallow copies of sprites that don't
|
|
appear as icons in the corral. Clones get deleted when the red stop button
|
|
is pressed. Shallow-copying clones' scripts and costumes makes spawning
|
|
very fast, so they can be used for particle system simulations.
|
|
This speed-up, however, comes at the cost of some detrimental side
|
|
effects: Changes to a costume or a script of the original sprite are
|
|
in some cases shared with all of its clones, however such shared changes
|
|
are hard to predict for users and not actively propagated, so they don't
|
|
offer any reliable feature, and will not be supported as such.
|
|
Changes to the original sprite's scripts affect all of its clones, unless
|
|
the script contains any custom block whose definition contains one or more
|
|
block variables (in which case the script does get deep-copied).
|
|
The original sprite's scripting area, costumes wardrobe or sounds jukebox
|
|
are also not shared. therefore adding or deleting a script, sound or
|
|
costume in the original sprite has no effect on any of its clones.
|
|
*/
|
|
|
|
SpriteMorph.prototype.createClone = function (immediately) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
clone;
|
|
if (stage && stage.cloneCount <= 5000) {
|
|
clone = this.fullCopy(true);
|
|
clone.clonify(stage, immediately);
|
|
}
|
|
return clone;
|
|
};
|
|
|
|
SpriteMorph.prototype.newClone = function (immediately) {
|
|
var clone = this.createClone(immediately);
|
|
if (isNil(clone)) {
|
|
throw new Error('exceeding maximum number of clones');
|
|
}
|
|
return clone;
|
|
};
|
|
|
|
SpriteMorph.prototype.clonify = function (stage, immediately) {
|
|
var hats;
|
|
this.parts.forEach(part => part.clonify(stage));
|
|
stage.cloneCount += 1;
|
|
this.cloneOriginName = this.isTemporary ?
|
|
this.cloneOriginName : this.name;
|
|
this.isTemporary = true;
|
|
this.name = '';
|
|
stage.add(this);
|
|
hats = this.allHatBlocksFor('__clone__init__');
|
|
hats.forEach(block =>
|
|
stage.threads.startProcess(
|
|
block,
|
|
this,
|
|
stage.isThreadSafe,
|
|
null, // export result
|
|
null, // callback
|
|
null, // is clicked
|
|
immediately // without yielding
|
|
)
|
|
);
|
|
this.endWarp();
|
|
};
|
|
|
|
SpriteMorph.prototype.initClone = function (hats) {
|
|
// used when manually instantiating a sprite in the IDE
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (stage) {
|
|
hats.forEach(block =>
|
|
stage.threads.startProcess(
|
|
block,
|
|
this,
|
|
stage.isThreadSafe
|
|
)
|
|
);
|
|
this.endWarp();
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.removeClone = function () {
|
|
var exemplar = this.exemplar;
|
|
if (this.isTemporary) {
|
|
// this.stopTalking();
|
|
this.parent.threads.stopAllForReceiver(this);
|
|
this.parts.slice().forEach(part => {
|
|
this.detachPart(part);
|
|
part.removeClone();
|
|
});
|
|
this.corpsify();
|
|
this.instances.forEach(child => {
|
|
if (child.isTemporary) {
|
|
child.setExemplar(exemplar);
|
|
}
|
|
});
|
|
this.destroy();
|
|
this.parent.cloneCount -= 1;
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.perpetuate = function () {
|
|
// make a temporary sprite (clone) permanent
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
ide = this.parentThatIsA(IDE_Morph);
|
|
|
|
// make sure my exemplar-chain is fully perpetuated
|
|
if (this.exemplar) {
|
|
this.exemplar.perpetuate();
|
|
}
|
|
if (!this.isTemporary || !stage || !ide) {
|
|
return;
|
|
}
|
|
this.isTemporary = false;
|
|
this.name = ide.newSpriteName(this.cloneOriginName);
|
|
this.cloneOriginName = '';
|
|
stage.cloneCount -= 1;
|
|
ide.corral.addSprite(this);
|
|
ide.sprites.add(this);
|
|
this.parts.forEach(part => part.perpetuate());
|
|
};
|
|
|
|
SpriteMorph.prototype.perpetuateAndEdit = function () {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
if (ide) {
|
|
this.perpetuate();
|
|
ide.selectSprite(this);
|
|
ide.recordUnsavedChanges();
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.release = function () {
|
|
// turn a permenent sprite that's an instance of another one
|
|
// into a temporary one (clone), that will vanish either when
|
|
// the "delete this clone" operation is executed or when the user
|
|
// hits the red stop sign button in the IDE
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
ide = this.parentThatIsA(IDE_Morph),
|
|
idx;
|
|
|
|
if (this.isTemporary || !this.exemplar || !stage || !ide) {
|
|
return;
|
|
}
|
|
|
|
// make sure all parts and instances are also released
|
|
this.parts.forEach(part => part.release());
|
|
this.instances.forEach(inst => inst.release());
|
|
this.isTemporary = true;
|
|
this.name = '';
|
|
this.cloneOriginName = this.exemplar.name;
|
|
stage.cloneCount += 1;
|
|
idx = ide.sprites.asArray().indexOf(this) + 1;
|
|
stage.watchers().forEach(watcher => {
|
|
if (watcher.object() === this) {
|
|
watcher.destroy();
|
|
}
|
|
});
|
|
if (idx > 0) {
|
|
ide.sprites.remove(idx);
|
|
}
|
|
ide.createCorral();
|
|
ide.fixLayout();
|
|
if (ide.currentSprite === this) {
|
|
ide.currentSprite = detect(
|
|
stage.children,
|
|
morph => morph instanceof SpriteMorph && !morph.isTemporary
|
|
) || this.stage;
|
|
}
|
|
ide.selectSprite(ide.currentSprite);
|
|
if (ide.isAppMode) {
|
|
ide.toggleAppMode(true);
|
|
}
|
|
};
|
|
|
|
// SpriteMorph deleting
|
|
|
|
SpriteMorph.prototype.corpsify = function () {
|
|
this.isCorpse = true;
|
|
this.version = Date.now();
|
|
};
|
|
|
|
// SpriteMorph primitives
|
|
|
|
// SpriteMorph hiding and showing:
|
|
|
|
/*
|
|
override the inherited behavior to also hide/show all
|
|
nested parts.
|
|
*/
|
|
|
|
SpriteMorph.prototype.show = function () {
|
|
this.setVisibility(true);
|
|
};
|
|
|
|
SpriteMorph.prototype.hide = function () {
|
|
this.setVisibility(false);
|
|
};
|
|
|
|
SpriteMorph.prototype.setVisibility = function (bool, noShadow) {
|
|
var bubble = this.talkBubble();
|
|
|
|
if (bool) {
|
|
SpriteMorph.uber.show.call(this);
|
|
} else {
|
|
SpriteMorph.uber.hide.call(this);
|
|
}
|
|
|
|
// propagate to speech bubble, if any
|
|
if (bubble) {
|
|
if (bool) {
|
|
bubble.show();
|
|
} else {
|
|
bubble.hide();
|
|
}
|
|
}
|
|
|
|
// progagate to parts
|
|
this.parts.forEach(part => part.setVisibility(bool));
|
|
|
|
// propagate to children that inherit my visibility
|
|
if (!noShadow) {
|
|
this.shadowAttribute('shown?');
|
|
}
|
|
this.instances.forEach(instance => {
|
|
if (instance.cachedPropagation) {
|
|
if (instance.inheritsAttribute('shown?')) {
|
|
instance.setVisibility(bool, true);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
SpriteMorph.prototype.reportShown = function () {
|
|
if (this.inheritsAttribute('shown?')) {
|
|
return this.exemplar.reportShown();
|
|
}
|
|
return this.isVisible;
|
|
};
|
|
|
|
// SpriteMorph pen color
|
|
|
|
SpriteMorph.prototype.setColorDimension = function (idx, num) {
|
|
var x = this.xPosition(),
|
|
y = this.yPosition(),
|
|
n = +num;
|
|
|
|
idx = +idx;
|
|
if (idx < 0 || idx > 3) {return; }
|
|
if (idx === 0) {
|
|
if (n < 0 || n > 100) { // wrap the hue
|
|
n = (n < 0 ? 100 : 0) + n % 100;
|
|
}
|
|
} else {
|
|
n = Math.min(100, Math.max(0, n));
|
|
}
|
|
if (idx === 3) {
|
|
this.color.a = 1 - n / 100;
|
|
} else {
|
|
this.cachedColorDimensions[idx] = n / 100;
|
|
this.color['set_' + this.penColorModel].apply(
|
|
this.color,
|
|
this.cachedColorDimensions
|
|
);
|
|
}
|
|
if (!this.costume) {
|
|
this.rerender();
|
|
}
|
|
this.gotoXY(x, y);
|
|
};
|
|
|
|
SpriteMorph.prototype.getColorDimension = function (idx) {
|
|
idx = +idx;
|
|
if (idx === 3) {
|
|
return (1 - this.color.a) * 100;
|
|
}
|
|
return (this.cachedColorDimensions[idx] || 0) * 100;
|
|
};
|
|
|
|
SpriteMorph.prototype.changeColorDimension = function (idx, delta) {
|
|
this.setColorDimension(
|
|
idx,
|
|
this.getColorDimension(idx) + (+delta || 0)
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.setColorRGBA = function (dta) {
|
|
// dta can be one of the following:
|
|
// - a 4 item list representing r-g-b-a each on a scale of 0-255
|
|
// - a 3 item list representing r-g-b leaving a unchanged
|
|
// - a 1 item list representing greyscale from 0-255 leaving alpha unchanged
|
|
// - a 2 item list representing greyscale and alpha each from 0-255
|
|
// - a number representing greyscale from 0-255 leaving alpha unchanged
|
|
var clr = this.color.copy(),
|
|
num;
|
|
if (dta instanceof List) {
|
|
switch (dta.length()) {
|
|
case 1:
|
|
num = Math.max(0, Math.min(+(dta.at(1)), 255));
|
|
if (isNaN(num)) {return; }
|
|
clr.r = num;
|
|
clr.g = num;
|
|
clr.b = num;
|
|
break;
|
|
case 2:
|
|
num = Math.max(0, Math.min(+(dta.at(1)), 255));
|
|
if (isNaN(num)) {return; }
|
|
clr.r = num;
|
|
clr.g = num;
|
|
clr.b = num;
|
|
num = Math.max(0, Math.min(+(dta.at(2)), 255));
|
|
if (isNaN(num)) {return; }
|
|
clr.a = num / 255;
|
|
break;
|
|
case 3:
|
|
num = Math.max(0, Math.min(+(dta.at(1)), 255));
|
|
if (isNaN(num)) {return; }
|
|
clr.r = num;
|
|
num = Math.max(0, Math.min(+(dta.at(2)), 255));
|
|
if (isNaN(num)) {return; }
|
|
clr.g = num;
|
|
num = Math.max(0, Math.min(+(dta.at(3)), 255));
|
|
if (isNaN(num)) {return; }
|
|
clr.b = num;
|
|
break;
|
|
case 4:
|
|
num = Math.max(0, Math.min(+(dta.at(1)), 255));
|
|
if (isNaN(num)) {return; }
|
|
clr.r = num;
|
|
num = Math.max(0, Math.min(+(dta.at(2)), 255));
|
|
if (isNaN(num)) {return; }
|
|
clr.g = num;
|
|
num = Math.max(0, Math.min(+(dta.at(3)), 255));
|
|
if (isNaN(num)) {return; }
|
|
clr.b = num;
|
|
num = Math.max(0, Math.min(+(dta.at(4)), 255));
|
|
if (isNaN(num)) {return; }
|
|
clr.a = num / 255;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
} else {
|
|
num = Math.max(0, Math.min(+dta, 255));
|
|
if (isNaN(num)) {return; }
|
|
clr.r = num;
|
|
clr.g = num;
|
|
clr.b = num;
|
|
}
|
|
this.setColor(clr);
|
|
};
|
|
|
|
SpriteMorph.prototype.changeColorRGBA = function (dta) {
|
|
// dta can be one of the following:
|
|
// - a 4 item list representing r-g-b-a each on a scale of 0-255
|
|
// - a 3 item list representing r-g-b leaving a unchanged
|
|
// - a 1 item list representing greyscale from 0-255 leaving alpha unchanged
|
|
// - a 2 item list representing greyscale and alpha each from 0-255
|
|
// - a number representing greyscale from 0-255 leaving alpha unchanged
|
|
var clr = this.color.copy(),
|
|
num;
|
|
if (dta instanceof List) {
|
|
switch (dta.length()) {
|
|
case 1:
|
|
num = +(dta.at(1));
|
|
if (isNaN(num)) {return; }
|
|
clr.r = Math.max(0, Math.min(clr.r + num, 255));
|
|
clr.g = Math.max(0, Math.min(clr.g + num, 255));
|
|
clr.b = Math.max(0, Math.min(clr.b + num, 255));
|
|
break;
|
|
case 2:
|
|
num = +(dta.at(1));
|
|
if (isNaN(num)) {return; }
|
|
clr.r = Math.max(0, Math.min(clr.r + num, 255));
|
|
clr.g = Math.max(0, Math.min(clr.g + num, 255));
|
|
clr.b = Math.max(0, Math.min(clr.b + num, 255));
|
|
num = +(dta.at(2));
|
|
if (isNaN(num)) {return; }
|
|
clr.a = Math.max(0, Math.min((clr.a * 255) + num, 255)) / 255;
|
|
break;
|
|
case 3:
|
|
num = +(dta.at(1));
|
|
if (isNaN(num)) {return; }
|
|
clr.r = Math.max(0, Math.min(clr.r + num, 255));
|
|
num = +(dta.at(2));
|
|
if (isNaN(num)) {return; }
|
|
clr.g = Math.max(0, Math.min(clr.g + num, 255));
|
|
num = +(dta.at(3));
|
|
if (isNaN(num)) {return; }
|
|
clr.b = Math.max(0, Math.min(clr.b + num, 255));
|
|
break;
|
|
case 4:
|
|
num = +(dta.at(1));
|
|
if (isNaN(num)) {return; }
|
|
clr.r = Math.max(0, Math.min(clr.r + num, 255));
|
|
num = +(dta.at(2));
|
|
if (isNaN(num)) {return; }
|
|
clr.g = Math.max(0, Math.min(clr.g + num, 255));
|
|
num = +(dta.at(3));
|
|
if (isNaN(num)) {return; }
|
|
clr.b = Math.max(0, Math.min(clr.b + num, 255));
|
|
num = +(dta.at(4));
|
|
if (isNaN(num)) {return; }
|
|
clr.a = Math.max(0, Math.min((clr.a * 255) + num, 255)) / 255;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
} else {
|
|
num = +dta;
|
|
if (isNaN(num)) {return; }
|
|
clr.r = Math.max(0, Math.min(clr.r + num, 255));
|
|
clr.g = Math.max(0, Math.min(clr.g + num, 255));
|
|
clr.b = Math.max(0, Math.min(clr.b + num, 255));
|
|
}
|
|
this.setColor(clr);
|
|
};
|
|
|
|
SpriteMorph.prototype.setColor = function (aColor) {
|
|
var x = this.xPosition(),
|
|
y = this.yPosition();
|
|
if (!this.color.eq(aColor, true)) { // observeAlpha
|
|
this.color = aColor.copy();
|
|
if (!this.costume) {
|
|
this.rerender();
|
|
this.silentGotoXY(x, y);
|
|
}
|
|
this.cachedColorDimensions = this.color[this.penColorModel]();
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.setBackgroundColor = SpriteMorph.prototype.setColor;
|
|
|
|
SpriteMorph.prototype.getPenAttribute = function (attrib) {
|
|
var name = attrib instanceof Array ? attrib[0] : attrib.toString(),
|
|
options = ['hue', 'saturation', 'brightness', 'transparency'];
|
|
if (name === 'size') {
|
|
return this.size || 0;
|
|
}
|
|
if (name === 'r-g-b-a') {
|
|
return new List([
|
|
this.color.r,
|
|
this.color.g,
|
|
this.color.b,
|
|
Math.round(this.color.a * 255)
|
|
]);
|
|
}
|
|
return this.getColorDimension(options.indexOf(name));
|
|
};
|
|
|
|
// SpriteMorph layers
|
|
|
|
SpriteMorph.prototype.comeToFront = function () {
|
|
if (this.parent) {
|
|
this.parent.add(this);
|
|
this.changed();
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.goToBack = function () {
|
|
if (this.parent) {
|
|
this.parent.addBack(this);
|
|
this.changed();
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.goBack = function (layers) {
|
|
var layer,
|
|
newLayer = +layers,
|
|
targetLayer;
|
|
|
|
if (!this.parent) {return null; }
|
|
layer = this.parent.children.indexOf(this);
|
|
this.parent.removeChild(this);
|
|
targetLayer = Math.max(layer - newLayer, 0);
|
|
this.parent.children.splice(targetLayer, null, this);
|
|
this.parent.changed();
|
|
};
|
|
|
|
// SpriteMorph collision detection
|
|
|
|
// attempted optimized collision detection using video buffer
|
|
// turns out it's actually slower to partially enumerate 2 sets of pixels
|
|
// than to create a new masked canvas.
|
|
// commented out and kept for reference
|
|
|
|
/*
|
|
SpriteMorph.prototype.isTouching = function (other) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
inter, off, src, trg, sw, tw, x, y;
|
|
|
|
function isOpaque(imageData, width, x, y) {
|
|
return (imageData[y * width + x] && 0x000000FF) > 0; // alpha
|
|
}
|
|
|
|
if (!(other instanceof SpriteMorph)) {
|
|
return SpriteMorph.uber.isTouching.call(this, other);
|
|
}
|
|
|
|
// determine the intersection of both bounding boxes
|
|
// unscaled and translated to local coordinates
|
|
|
|
inter = this.bounds.intersect(other.bounds).translateBy(
|
|
this.position().neg()
|
|
).scaleBy(1 / stage.scale); // .floor(); ?
|
|
|
|
if (inter.width() < 1 || inter.height() < 1) {
|
|
return false;
|
|
}
|
|
|
|
off = this.position().subtract(
|
|
other.position()
|
|
).scaleBy(1 / stage.scale).floor();
|
|
|
|
src = this.getImageData();
|
|
sw = Math.floor(this.width() / stage.scale);
|
|
trg = other.getImageData();
|
|
tw = Math.floor(other.width() / stage.scale);
|
|
|
|
for (y = inter.origin.y; y <= inter.corner.y; y += 1) {
|
|
for (x = inter.origin.x; x <= inter.corner.x; x += 1) {
|
|
if (isOpaque(src, sw, x, y) &&
|
|
isOpaque(trg, tw, x + off.x, y + off.y)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
*/
|
|
|
|
SpriteMorph.prototype.reportTouchingColor = function (aColor) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
data, len, i;
|
|
|
|
if (stage) {
|
|
data = this.overlappingPixels(stage);
|
|
if (!data) {return false; }
|
|
len = data[0].length;
|
|
for (i = 3; i < len; i += 4) {
|
|
if (data[0][i] && data[1][i]) {
|
|
if (
|
|
data[1][i - 3] === aColor.r &&
|
|
data[1][i - 2] === aColor.g &&
|
|
data[1][i - 1] === aColor.b
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
SpriteMorph.prototype.reportColorIsTouchingColor = function (
|
|
thisColor,
|
|
thatColor
|
|
) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
data, len, i;
|
|
|
|
if (stage) {
|
|
data = this.overlappingPixels(stage);
|
|
if (!data) {return false; }
|
|
len = data[0].length;
|
|
for (i = 3; i < len; i += 4) {
|
|
if (data[0][i] && data[1][i]) {
|
|
if (
|
|
data[0][i - 3] === thisColor.r &&
|
|
data[0][i - 2] === thisColor.g &&
|
|
data[0][i - 1] === thisColor.b &&
|
|
data[1][i - 3] === thatColor.r &&
|
|
data[1][i - 2] === thatColor.g &&
|
|
data[1][i - 1] === thatColor.b
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
SpriteMorph.prototype.overlappingPixels = function (otherSprite) {
|
|
// overrides method from Morph because Sprites aren't nested Morphs
|
|
// the same applies for speech balloons, where it's enough to
|
|
// test the encompassing shape only
|
|
var oRect = this.bounds.intersect(otherSprite.bounds),
|
|
thisImg = this.getImage(),
|
|
thatImg = otherSprite.getImage();
|
|
|
|
if (otherSprite instanceof StageMorph) {
|
|
// only check for color collision
|
|
thatImg = otherSprite.fancyThumbnail(otherSprite.extent(), this, true);
|
|
}
|
|
if (oRect.width() < 1 || oRect.height() < 1 || !thisImg || !thatImg ||
|
|
!thisImg.width || !thisImg.height || !thatImg.width || !thatImg.height
|
|
) {
|
|
return false;
|
|
}
|
|
if (thisImg.isRetinaEnabled !== thatImg.isRetinaEnabled) {
|
|
thisImg = normalizeCanvas(thisImg, true);
|
|
thatImg = normalizeCanvas(thatImg, true);
|
|
}
|
|
return [
|
|
thisImg.getContext("2d").getImageData(
|
|
oRect.left() - this.left(),
|
|
oRect.top() - this.top(),
|
|
oRect.width(),
|
|
oRect.height()
|
|
).data,
|
|
thatImg.getContext("2d").getImageData(
|
|
oRect.left() - otherSprite.left(),
|
|
oRect.top() - otherSprite.top(),
|
|
oRect.width(),
|
|
oRect.height()
|
|
).data
|
|
];
|
|
};
|
|
|
|
// SpriteMorph neighbor detection
|
|
|
|
SpriteMorph.prototype.neighbors = function (aStage) {
|
|
var stage = aStage || this.parentThatIsA(StageMorph),
|
|
myPerimeter = this.perimeter(stage);
|
|
return new List(
|
|
stage.children.filter(each => {
|
|
var eachPerimeter,
|
|
distance;
|
|
if (each instanceof SpriteMorph &&
|
|
each.isVisible &&
|
|
(each !== this)
|
|
) {
|
|
eachPerimeter = each.perimeter(stage);
|
|
distance = myPerimeter.center.distanceTo(eachPerimeter.center);
|
|
return (distance - myPerimeter.radius - eachPerimeter.radius)
|
|
< 0;
|
|
}
|
|
return false;
|
|
})
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.perimeter = function (aStage) {
|
|
var stage = aStage || this.parentThatIsA(StageMorph),
|
|
stageScale = this instanceof StageMorph ? 1 : stage.scale,
|
|
radius;
|
|
if (this.costume) {
|
|
radius = Math.max(
|
|
this.costume.width(),
|
|
this.costume.height()
|
|
) * this.scale * stageScale;
|
|
} else {
|
|
radius = Math.max(this.width(), this.height());
|
|
}
|
|
return {
|
|
center: this.center(), // geometric center rather than position
|
|
radius: radius
|
|
};
|
|
};
|
|
|
|
// SpriteMorph pen ops
|
|
|
|
SpriteMorph.prototype.doStamp = function () {
|
|
var stage = this.parent,
|
|
ctx = stage.penTrails().getContext('2d'),
|
|
img = this.getImage();
|
|
|
|
if (img.width < 1 || (img.height < 1)) {
|
|
// too small to draw
|
|
return;
|
|
}
|
|
ctx.save();
|
|
ctx.scale(1 / stage.scale, 1 / stage.scale);
|
|
ctx.globalAlpha = this.alpha;
|
|
ctx.drawImage(
|
|
img,
|
|
this.left() - stage.left(),
|
|
this.top() - stage.top()
|
|
);
|
|
ctx.restore();
|
|
this.changed();
|
|
stage.cachedPenTrailsMorph = null;
|
|
};
|
|
|
|
SpriteMorph.prototype.clear = function () {
|
|
this.parent.clearPenTrails();
|
|
};
|
|
|
|
SpriteMorph.prototype.write = function (text, size) {
|
|
// thanks to Michael Ball for contributing this code!
|
|
if (typeof text !== 'string' && typeof text !== 'number') {
|
|
throw new Error(
|
|
localize('can only write text or numbers, not a') + ' ' +
|
|
typeof text
|
|
);
|
|
}
|
|
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
context = stage.penTrails().getContext('2d'),
|
|
rotation = radians(this.direction() - 90),
|
|
trans = new Point(
|
|
this.rotationCenter().x - stage.left(),
|
|
this.rotationCenter().y - stage.top()
|
|
),
|
|
len,
|
|
pos;
|
|
|
|
context.save();
|
|
context.font = size + 'px monospace';
|
|
context.textAlign = 'left';
|
|
context.textBaseline = 'alphabetic';
|
|
context.fillStyle = this.color.toString();
|
|
len = context.measureText(text).width;
|
|
trans = trans.multiplyBy(1 / stage.scale);
|
|
context.translate(trans.x, trans.y);
|
|
context.rotate(rotation);
|
|
context.fillText(text, 0, 0);
|
|
context.translate(-trans.x, -trans.y);
|
|
context.restore();
|
|
pos = new Point(
|
|
len * Math.sin(radians(this.direction())),
|
|
len * Math.cos(radians(this.direction()))
|
|
);
|
|
pos = pos.add(new Point(this.xPosition(), this.yPosition()));
|
|
this.gotoXY(pos.x, pos.y, false);
|
|
this.changed();
|
|
stage.changed();
|
|
};
|
|
|
|
SpriteMorph.prototype.setSize = function (size) {
|
|
// pen size
|
|
if (!isNaN(size)) {
|
|
this.size = Math.min(Math.max(+size, 0.0001), 1000);
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.changeSize = function (delta) {
|
|
this.setSize(this.size + (+delta || 0));
|
|
};
|
|
|
|
// SpriteMorph printing on another sprite:
|
|
|
|
SpriteMorph.prototype.blitOn = function (target, mask = 'source-atop') {
|
|
// draw my costume onto a copy of the target's costume scaled and rotated
|
|
// so it appears as though I'm "stamped" onto it.
|
|
|
|
var sourceHeading = (this.rotationStyle === 1) ? this.heading : 90,
|
|
targetHeading = (target.rotationStyle === 1) ? target.heading : 90,
|
|
sourceCostume, targetCostume, ctx,
|
|
relRot, relScale, stageScale,
|
|
centerDist, centerDelta, centerAngleRadians, center,
|
|
originDist, originAngleRadians,
|
|
spriteCenter, thisCenter, relPos, pos;
|
|
|
|
// prevent pasting an object onto itself
|
|
if (this === target) {return; }
|
|
|
|
// check if both source and target have costumes,
|
|
// rasterize copy of target costume if it's an SVG
|
|
if (this.costume && target.costume) {
|
|
sourceCostume = this.costume;
|
|
if (sourceCostume instanceof SVG_Costume) {
|
|
sourceCostume = sourceCostume.rasterized();
|
|
}
|
|
if (target.costume instanceof SVG_Costume) {
|
|
targetCostume = target.costume.rasterized();
|
|
} else {
|
|
targetCostume = target.costume.copy();
|
|
}
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
// do the math:
|
|
if (target instanceof SpriteMorph) {
|
|
if (this instanceof SpriteMorph) {
|
|
// stamp a sprite on a sprite:
|
|
relRot = sourceHeading - targetHeading;
|
|
relScale = this.scale / target.scale;
|
|
stageScale = this.parentThatIsA(StageMorph).scale;
|
|
centerDist = target.center().distanceTo(this.center());
|
|
centerDelta = this.center().subtract(target.center());
|
|
centerAngleRadians = Math.atan2(centerDelta.y, centerDelta.x);
|
|
center = new Point(
|
|
sourceCostume.width(),
|
|
sourceCostume.height()
|
|
).multiplyBy(0.5 * this.scale * stageScale);
|
|
originDist = center.distanceTo(ZERO);
|
|
originAngleRadians = Math.atan2(center.y, center.x);
|
|
spriteCenter = new Point(
|
|
target.costume.width(),
|
|
target.costume.height()
|
|
).multiplyBy(0.5 * target.scale * stageScale)
|
|
.rotateBy(radians(relRot));
|
|
thisCenter = spriteCenter.distanceAngle(
|
|
centerDist,
|
|
degrees(centerAngleRadians) - sourceHeading + 180
|
|
);
|
|
relPos = thisCenter.distanceAngle(
|
|
originDist,
|
|
degrees(originAngleRadians) -90
|
|
);
|
|
pos = relPos.divideBy(stageScale)
|
|
.divideBy(relScale)
|
|
.divideBy(target.scale);
|
|
} else { // if the stage is the source
|
|
// stamp the stage on a sprite:
|
|
relRot = 90 - targetHeading;
|
|
relScale = 1 / target.scale;
|
|
centerDist = target.center().distanceTo(this.position());
|
|
centerDelta = this.position().subtract(target.center());
|
|
centerAngleRadians = Math.atan2(centerDelta.y, centerDelta.x);
|
|
center = new Point(
|
|
target.costume.width(),
|
|
target.costume.height()
|
|
).multiplyBy(0.5 * target.scale)
|
|
.rotateBy(radians(90 - targetHeading));
|
|
pos = center.distanceAngle(
|
|
centerDist / this.scale,
|
|
degrees(centerAngleRadians) + 90
|
|
);
|
|
}
|
|
} else { // if the stage is the target
|
|
// stamp a sprite on the stage:
|
|
relRot = sourceHeading - 90;
|
|
relScale = this.scale;
|
|
center = this.center().subtract(target.position())
|
|
.divideBy(this.scale * target.scale)
|
|
.rotateBy(radians(sourceHeading - 90));
|
|
pos = center.subtract(
|
|
new Point(
|
|
sourceCostume.width(),
|
|
sourceCostume.height()
|
|
).multiplyBy(0.5)
|
|
);
|
|
}
|
|
|
|
// draw my costume onto the target's costume copy:
|
|
ctx = targetCostume.contents.getContext('2d');
|
|
ctx.rotate(radians(relRot));
|
|
ctx.scale(relScale, relScale);
|
|
ctx.globalCompositeOperation = mask;
|
|
ctx.drawImage(sourceCostume.contents, pos.x, pos.y);
|
|
|
|
// make the target wear the new costume
|
|
target.doSwitchToCostume(targetCostume);
|
|
};
|
|
|
|
// SpriteMorph pen up and down:
|
|
|
|
SpriteMorph.prototype.down = function () {
|
|
this.setPenDown(true);
|
|
};
|
|
|
|
SpriteMorph.prototype.up = function () {
|
|
this.setPenDown(false);
|
|
};
|
|
|
|
SpriteMorph.prototype.setPenDown = function (bool, noShadow) {
|
|
if (bool) {
|
|
SpriteMorph.uber.down.call(this);
|
|
} else {
|
|
SpriteMorph.uber.up.call(this);
|
|
}
|
|
|
|
// propagate to children that inherit my visibility
|
|
if (!noShadow) {
|
|
this.shadowAttribute('pen down?');
|
|
}
|
|
this.instances.forEach(instance => {
|
|
if (instance.cachedPropagation) {
|
|
if (instance.inheritsAttribute('pen down?')) {
|
|
instance.setPenDown(bool, true);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
SpriteMorph.prototype.getPenDown = function () {
|
|
if (this.inheritsAttribute('pen down?')) {
|
|
return this.exemplar.getPenDown();
|
|
}
|
|
return this.isDown;
|
|
};
|
|
|
|
// SpriteMorph scale
|
|
|
|
SpriteMorph.prototype.getScale = function () {
|
|
// answer my scale in percent
|
|
if (this.inheritsAttribute('size')) {
|
|
return this.exemplar.getScale();
|
|
}
|
|
return this.scale * 100;
|
|
};
|
|
|
|
SpriteMorph.prototype.setScale = function (percentage, noShadow) {
|
|
// set my (absolute) scale in percent
|
|
var x = this.xPosition(),
|
|
y = this.yPosition(),
|
|
realScale,
|
|
growth;
|
|
|
|
realScale = (+percentage || 0) / 100;
|
|
growth = realScale / this.nestingScale;
|
|
this.nestingScale = realScale;
|
|
this.scale = Math.max(realScale, 0.01);
|
|
|
|
// apply to myself
|
|
this.changed();
|
|
this.fixLayout();
|
|
this.rerender();
|
|
|
|
this.silentGotoXY(x, y, true); // just me
|
|
this.positionTalkBubble();
|
|
|
|
// propagate to nested parts
|
|
this.parts.forEach(part => {
|
|
var xDist = part.xPosition() - x,
|
|
yDist = part.yPosition() - y;
|
|
part.setScale(part.scale * 100 * growth);
|
|
part.silentGotoXY(
|
|
x + (xDist * growth),
|
|
y + (yDist * growth)
|
|
);
|
|
});
|
|
|
|
// propagate to children that inherit my scale
|
|
if (!noShadow) {
|
|
this.shadowAttribute('size');
|
|
}
|
|
this.instances.forEach(instance => {
|
|
if (instance.cachedPropagation) {
|
|
if (instance.inheritsAttribute('size')) {
|
|
instance.setScale(percentage, true);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
SpriteMorph.prototype.changeScale = function (delta) {
|
|
this.setScale(this.getScale() + (+delta || 0));
|
|
};
|
|
|
|
// Spritemorph graphic effects
|
|
|
|
SpriteMorph.prototype.graphicsChanged = function () {
|
|
return Object.keys(this.graphicsValues).some(any =>
|
|
this.graphicsValues[any] < 0 ||
|
|
this.graphicsValues[any] > 0
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.applyGraphicsEffects = function (canvas) {
|
|
// For every effect: apply transform of that effect(canvas, stored value)
|
|
// Graphic effects from Scratch are heavily based on ScratchPlugin.c
|
|
|
|
var ctx, imagedata, w, h;
|
|
|
|
function transform_fisheye(imagedata, value) {
|
|
var pixels, newImageData, newPixels, centerX, centerY,
|
|
w, h, x, y, dx, dy, r, angle, srcX, srcY, i, srcI;
|
|
|
|
w = imagedata.width;
|
|
h = imagedata.height;
|
|
pixels = imagedata.data;
|
|
newImageData = ctx.createImageData(w, h);
|
|
newPixels = newImageData.data;
|
|
|
|
centerX = w / 2;
|
|
centerY = h / 2;
|
|
value = Math.max(0, (value + 100) / 100);
|
|
for (y = 0; y < h; y++) {
|
|
for (x = 0; x < w; x++) {
|
|
dx = (x - centerX) / centerX;
|
|
dy = (y - centerY) / centerY;
|
|
r = Math.pow(Math.sqrt(dx * dx + dy * dy), value);
|
|
if (r <= 1) {
|
|
angle = Math.atan2(dy, dx);
|
|
srcX = Math.floor(
|
|
centerX + (r * Math.cos(angle) * centerX)
|
|
);
|
|
srcY = Math.floor(
|
|
centerY + (r * Math.sin(angle) * centerY)
|
|
);
|
|
} else {
|
|
srcX = x;
|
|
srcY = y;
|
|
}
|
|
i = (y * w + x) * 4;
|
|
srcI = (srcY * w + srcX) * 4;
|
|
newPixels[i] = pixels[srcI];
|
|
newPixels[i + 1] = pixels[srcI + 1];
|
|
newPixels[i + 2] = pixels[srcI + 2];
|
|
newPixels[i + 3] = pixels[srcI + 3];
|
|
}
|
|
}
|
|
return newImageData;
|
|
}
|
|
|
|
function transform_whirl(imagedata, value) {
|
|
var pixels, newImageData, newPixels, w, h, centerX, centerY,
|
|
x, y, radius, scaleX, scaleY, whirlRadians, radiusSquared,
|
|
dx, dy, d, factor, angle, srcX, srcY, i, srcI, sina, cosa;
|
|
|
|
w = imagedata.width;
|
|
h = imagedata.height;
|
|
pixels = imagedata.data;
|
|
newImageData = ctx.createImageData(w, h);
|
|
newPixels = newImageData.data;
|
|
|
|
centerX = w / 2;
|
|
centerY = h / 2;
|
|
radius = Math.min(centerX, centerY);
|
|
if (w < h) {
|
|
scaleX = h / w;
|
|
scaleY = 1;
|
|
} else {
|
|
scaleX = 1;
|
|
scaleY = w / h;
|
|
}
|
|
whirlRadians = -radians(value);
|
|
radiusSquared = radius * radius;
|
|
for (y = 0; y < h; y++) {
|
|
for (x = 0; x < w; x++) {
|
|
dx = scaleX * (x - centerX);
|
|
dy = scaleY * (y - centerY);
|
|
d = dx * dx + dy * dy;
|
|
if (d < radiusSquared) {
|
|
factor = 1 - (Math.sqrt(d) / radius);
|
|
angle = whirlRadians * (factor * factor);
|
|
sina = Math.sin(angle);
|
|
cosa = Math.cos(angle);
|
|
srcX = Math.floor(
|
|
(cosa * dx - sina * dy) / scaleX + centerX
|
|
);
|
|
srcY = Math.floor(
|
|
(sina * dx + cosa * dy) / scaleY + centerY
|
|
);
|
|
} else {
|
|
srcX = x;
|
|
srcY = y;
|
|
}
|
|
i = (y * w + x) * 4;
|
|
srcI = (srcY * w + srcX) * 4;
|
|
newPixels[i] = pixels[srcI];
|
|
newPixels[i + 1] = pixels[srcI + 1];
|
|
newPixels[i + 2] = pixels[srcI + 2];
|
|
newPixels[i + 3] = pixels[srcI + 3];
|
|
}
|
|
}
|
|
return newImageData;
|
|
}
|
|
|
|
function transform_pixelate(imagedata, value) {
|
|
var pixels, newImageData, newPixels, w, h,
|
|
x, y, srcX, srcY, i, srcI;
|
|
|
|
w = imagedata.width;
|
|
h = imagedata.height;
|
|
pixels = imagedata.data;
|
|
newImageData = ctx.createImageData(w, h);
|
|
newPixels = newImageData.data;
|
|
|
|
value = Math.floor(Math.abs(value / 10) + 1);
|
|
for (y = 0; y < h; y++) {
|
|
for (x = 0; x < w; x++) {
|
|
srcX = Math.floor(x / value) * value;
|
|
srcY = Math.floor(y / value) * value;
|
|
i = (y * w + x) * 4;
|
|
srcI = (srcY * w + srcX) * 4;
|
|
newPixels[i] = pixels[srcI];
|
|
newPixels[i + 1] = pixels[srcI + 1];
|
|
newPixels[i + 2] = pixels[srcI + 2];
|
|
newPixels[i + 3] = pixels[srcI + 3];
|
|
}
|
|
}
|
|
return newImageData;
|
|
}
|
|
|
|
function transform_mosaic(imagedata, value) {
|
|
var pixels, i, l, newImageData, newPixels, srcI;
|
|
pixels = imagedata.data;
|
|
newImageData = ctx.createImageData(imagedata.width, imagedata.height);
|
|
newPixels = newImageData.data;
|
|
|
|
value = Math.round((Math.abs(value) + 10) / 10);
|
|
value = Math.max(
|
|
0,
|
|
Math.min(value, Math.min(imagedata.width, imagedata.height))
|
|
);
|
|
for (i = 0, l = pixels.length; i < l; i += 4) {
|
|
srcI = i * value % l;
|
|
newPixels[i] = pixels[srcI];
|
|
newPixels[i + 1] = pixels[srcI + 1];
|
|
newPixels[i + 2] = pixels[srcI + 2];
|
|
newPixels[i + 3] = pixels[srcI + 3];
|
|
}
|
|
return newImageData;
|
|
}
|
|
|
|
function transform_duplicate(imagedata, value) {
|
|
var pixels, i;
|
|
pixels = imagedata.data;
|
|
for (i = 0; i < pixels.length; i += 4) {
|
|
pixels[i] = pixels[i * value];
|
|
pixels[i + 1] = pixels[i * value + 1];
|
|
pixels[i + 2] = pixels[i * value + 2];
|
|
pixels[i + 3] = pixels[i * value + 3];
|
|
}
|
|
return imagedata;
|
|
}
|
|
|
|
function transform_colorDimensions(
|
|
imagedata,
|
|
hueShift,
|
|
saturationShift,
|
|
brightnessShift
|
|
) {
|
|
var pixels = imagedata.data,
|
|
l = pixels.length,
|
|
clr = new Color(),
|
|
index, dim;
|
|
|
|
for (index = 0; index < l; index += 4) {
|
|
|
|
clr.r = pixels[index];
|
|
clr.g = pixels[index + 1];
|
|
clr.b = pixels[index + 2];
|
|
|
|
dim = clr[SpriteMorph.prototype.penColorModel]();
|
|
dim[0] = dim[0] * 100 + hueShift;
|
|
if (dim[0] < 0 || dim[0] > 100) { // wrap the hue
|
|
dim[0] = (dim[0] < 0 ? 100 : 0) + dim[0] % 100;
|
|
}
|
|
dim[0] = dim[0] / 100;
|
|
dim[1] = dim[1] + saturationShift / 100;
|
|
dim[2] = dim[2] + brightnessShift / 100;
|
|
|
|
clr['set_' + SpriteMorph.prototype.penColorModel].apply(clr, dim);
|
|
pixels[index] = clr.r;
|
|
pixels[index + 1] = clr.g;
|
|
pixels[index + 2] = clr.b;
|
|
}
|
|
return imagedata;
|
|
}
|
|
|
|
function transform_negative (imagedata, value) {
|
|
var pixels, i, l, rcom, gcom, bcom;
|
|
pixels = imagedata.data;
|
|
for (i = 0, l = pixels.length; i < l; i += 4) {
|
|
rcom = 255 - pixels[i];
|
|
gcom = 255 - pixels[i + 1];
|
|
bcom = 255 - pixels[i + 2];
|
|
|
|
if (pixels[i] < rcom) { //compare to the complement
|
|
pixels[i] += value;
|
|
} else if (pixels[i] > rcom) {
|
|
pixels[i] -= value;
|
|
}
|
|
if (pixels[i + 1] < gcom) {
|
|
pixels[i + 1] += value;
|
|
} else if (pixels[i + 1] > gcom) {
|
|
pixels[i + 1] -= value;
|
|
}
|
|
if (pixels[i + 2] < bcom) {
|
|
pixels[i + 2] += value;
|
|
} else if (pixels[i + 2] > bcom) {
|
|
pixels[i + 2] -= value;
|
|
}
|
|
}
|
|
return imagedata;
|
|
}
|
|
|
|
function transform_comic (imagedata, value) {
|
|
var pixels, i, l;
|
|
pixels = imagedata.data;
|
|
for (i = 0, l = pixels.length; i < l; i += 4) {
|
|
pixels[i] += Math.sin(i * value) * 127 + 128;
|
|
pixels[i + 1] += Math.sin(i * value) * 127 + 128;
|
|
pixels[i + 2] += Math.sin(i * value) * 127 + 128;
|
|
}
|
|
return imagedata;
|
|
}
|
|
|
|
function transform_confetti (imagedata, value) {
|
|
var pixels, i, l;
|
|
pixels = imagedata.data;
|
|
for (i = 0, l = pixels.length; i < l; i += 1) {
|
|
pixels[i] = Math.sin(value * pixels[i]) * 127 + pixels[i];
|
|
}
|
|
return imagedata;
|
|
}
|
|
|
|
if (this.graphicsChanged()) {
|
|
w = Math.ceil(this.width());
|
|
h = Math.ceil(this.height());
|
|
if (!canvas.width || !canvas.height || !w || !h) {
|
|
// too small to get image data, abort
|
|
return canvas;
|
|
}
|
|
ctx = canvas.getContext("2d");
|
|
imagedata = ctx.getImageData(0, 0, w, h);
|
|
|
|
if (this.graphicsValues.fisheye) {
|
|
imagedata = transform_fisheye(
|
|
imagedata,
|
|
this.graphicsValues.fisheye
|
|
);
|
|
}
|
|
if (this.graphicsValues.whirl) {
|
|
imagedata = transform_whirl(
|
|
imagedata,
|
|
this.graphicsValues.whirl
|
|
);
|
|
}
|
|
if (this.graphicsValues.pixelate) {
|
|
imagedata = transform_pixelate(
|
|
imagedata,
|
|
this.graphicsValues.pixelate
|
|
);
|
|
}
|
|
if (this.graphicsValues.mosaic) {
|
|
imagedata = transform_mosaic(
|
|
imagedata,
|
|
this.graphicsValues.mosaic
|
|
);
|
|
}
|
|
if (this.graphicsValues.duplicate) {
|
|
imagedata = transform_duplicate(
|
|
imagedata,
|
|
this.graphicsValues.duplicate
|
|
);
|
|
}
|
|
if (this.graphicsValues.color ||
|
|
this.graphicsValues.saturation ||
|
|
this.graphicsValues.brightness) {
|
|
imagedata = transform_colorDimensions(
|
|
imagedata,
|
|
this.graphicsValues.color,
|
|
this.graphicsValues.saturation,
|
|
this.graphicsValues.brightness
|
|
);
|
|
}
|
|
if (this.graphicsValues.negative) {
|
|
imagedata = transform_negative(
|
|
imagedata,
|
|
this.graphicsValues.negative
|
|
);
|
|
}
|
|
if (this.graphicsValues.comic) {
|
|
imagedata = transform_comic(
|
|
imagedata,
|
|
this.graphicsValues.comic
|
|
);
|
|
}
|
|
if (this.graphicsValues.confetti) {
|
|
imagedata = transform_confetti(
|
|
imagedata,
|
|
this.graphicsValues.confetti
|
|
);
|
|
}
|
|
|
|
ctx.putImageData(imagedata, 0, 0);
|
|
}
|
|
|
|
return canvas;
|
|
};
|
|
|
|
SpriteMorph.prototype.setEffect = function (effect, value) {
|
|
var eff = effect instanceof Array ? effect[0] : effect.toString();
|
|
if (!contains(
|
|
[
|
|
'color',
|
|
'saturation',
|
|
'brightness',
|
|
'ghost',
|
|
'fisheye',
|
|
'whirl',
|
|
'pixelate',
|
|
'mosaic',
|
|
'negative',
|
|
// depracated, but still supported in legacy projects:
|
|
'duplicate',
|
|
'comic',
|
|
'confetti'
|
|
],
|
|
eff
|
|
)) {
|
|
throw new Error(localize('unsupported graphic effect') + ': "' + eff + '"');
|
|
}
|
|
if (eff === 'ghost') {
|
|
this.alpha = 1 - Math.min(Math.max(+value || 0, 0), 100) / 100;
|
|
} else {
|
|
this.graphicsValues[eff] = +value;
|
|
}
|
|
this.rerender();
|
|
};
|
|
|
|
SpriteMorph.prototype.getEffect = function (effect) {
|
|
var eff = effect instanceof Array ? effect[0] : effect.toString();
|
|
if (eff === 'ghost') {
|
|
return this.getGhostEffect();
|
|
}
|
|
return this.graphicsValues[eff] || 0;
|
|
};
|
|
|
|
SpriteMorph.prototype.getGhostEffect = function () {
|
|
return (1 - this.alpha) * 100;
|
|
};
|
|
|
|
SpriteMorph.prototype.changeEffect = function (effect, value) {
|
|
var eff = effect instanceof Array ? effect[0] : effect.toString();
|
|
if (eff === 'ghost') {
|
|
this.setEffect(effect, this.getGhostEffect() + (+value || 0));
|
|
} else {
|
|
this.setEffect(effect, +this.graphicsValues[eff] + (+value));
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.clearEffects = function () {
|
|
var effect;
|
|
for (effect in this.graphicsValues) {
|
|
if (this.graphicsValues.hasOwnProperty(effect)) {
|
|
this.setEffect([effect], 0);
|
|
}
|
|
}
|
|
this.setEffect(['ghost'], 0);
|
|
};
|
|
|
|
// SpriteMorph talk bubble
|
|
|
|
SpriteMorph.prototype.stopTalking = function () {
|
|
var bubble = this.talkBubble();
|
|
if (bubble) {bubble.destroy(); }
|
|
};
|
|
|
|
SpriteMorph.prototype.doThink = function (data) {
|
|
this.bubble(data, true);
|
|
};
|
|
|
|
SpriteMorph.prototype.bubble = function (data, isThought, isQuestion) {
|
|
var bubble,
|
|
stage = this.parentThatIsA(StageMorph);
|
|
|
|
this.stopTalking();
|
|
if (data === '' || isNil(data)) {return; }
|
|
bubble = new SpriteBubbleMorph(
|
|
data,
|
|
stage,
|
|
isThought,
|
|
isQuestion
|
|
);
|
|
this.add(bubble);
|
|
this.positionTalkBubble();
|
|
};
|
|
|
|
SpriteMorph.prototype.talkBubble = function () {
|
|
return detect(
|
|
this.children,
|
|
morph => morph instanceof SpeechBubbleMorph
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.positionTalkBubble = function () {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
stageScale = stage ? stage.scale : 1,
|
|
bubble = this.talkBubble(),
|
|
bottom = this.bottom(),
|
|
step = this.extent().divideBy(10)
|
|
.max(new Point(5, 5).scaleBy(stageScale))
|
|
.multiplyBy(new Point(-1, 1));
|
|
|
|
if (!bubble) {return null; }
|
|
bubble.show();
|
|
if (!bubble.isPointingRight) {
|
|
bubble.isPointingRight = true;
|
|
bubble.fixLayout();
|
|
bubble.rerender();
|
|
}
|
|
bubble.setLeft(this.right());
|
|
bubble.setBottom(this.top());
|
|
while (!this.isTouching(bubble) && bubble.bottom() < bottom) {
|
|
bubble.moveBy(step);
|
|
}
|
|
bubble.moveBy(step.mirror());
|
|
if (!stage) {return null; }
|
|
if (bubble.right() > stage.right()) {
|
|
bubble.isPointingRight = false;
|
|
bubble.fixLayout();
|
|
bubble.rerender();
|
|
bubble.setRight(this.center().x);
|
|
}
|
|
bubble.keepWithin(stage);
|
|
};
|
|
|
|
// dragging and dropping adjustments b/c of talk bubbles and parts
|
|
|
|
SpriteMorph.prototype.prepareToBeGrabbed = function (hand) {
|
|
this.recordLayers();
|
|
this.shadowAttribute('x position');
|
|
this.shadowAttribute('y position');
|
|
if (!this.bounds.containsPoint(hand.position()) &&
|
|
this.isCorrectingOutsideDrag()) {
|
|
this.setCenter(hand.position());
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.isCorrectingOutsideDrag = function () {
|
|
// make sure I don't "trail behind" the hand when dragged
|
|
// override for morphs that you want to be dragged outside
|
|
// their full bounds
|
|
return !this.parts.length;
|
|
};
|
|
|
|
SpriteMorph.prototype.justDropped = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (stage) {
|
|
stage.enableCustomHatBlocks = true;
|
|
}
|
|
if (this.exemplar) {
|
|
this.inheritedAttributes.forEach(att => {
|
|
if (contains(['direction', 'size', 'costume #'], att)) {
|
|
// only refresh certain propagated attributes
|
|
this.refreshInheritedAttribute(att);
|
|
}
|
|
});
|
|
}
|
|
this.restoreLayers();
|
|
this.positionTalkBubble();
|
|
this.receiveUserInteraction('dropped');
|
|
};
|
|
|
|
// SpriteMorph drawing:
|
|
|
|
SpriteMorph.prototype.drawLine = function (start, dest) {
|
|
var stagePos = this.parent.bounds.origin,
|
|
stageScale = this.parent.scale,
|
|
context = this.parent.penTrails().getContext('2d'),
|
|
from = start.subtract(stagePos).divideBy(stageScale),
|
|
to = dest.subtract(stagePos).divideBy(stageScale),
|
|
damagedFrom = from.multiplyBy(stageScale).add(stagePos),
|
|
damagedTo = to.multiplyBy(stageScale).add(stagePos),
|
|
damaged = damagedFrom.rectangle(damagedTo).expandBy(
|
|
Math.max(this.size * stageScale / 2, 1)
|
|
).intersect(this.parent.visibleBounds()).spread();
|
|
|
|
if (this.isDown) {
|
|
// record for later svg conversion
|
|
if (StageMorph.prototype.enablePenLogging) {
|
|
this.parent.trailsLog.push(
|
|
[
|
|
this.snapPoint(start),
|
|
this.snapPoint(dest),
|
|
this.color.copy(),
|
|
this.size,
|
|
this.useFlatLineEnds ? 'butt' : 'round'
|
|
]
|
|
);
|
|
}
|
|
|
|
// draw on the pen-trails layer
|
|
context.lineWidth = this.size;
|
|
context.strokeStyle = this.color.toString();
|
|
if (this.useFlatLineEnds) {
|
|
context.lineCap = 'butt';
|
|
context.lineJoin = 'miter';
|
|
} else {
|
|
context.lineCap = 'round';
|
|
context.lineJoin = 'round';
|
|
}
|
|
context.beginPath();
|
|
context.moveTo(from.x, from.y);
|
|
context.lineTo(to.x, to.y);
|
|
context.stroke();
|
|
if (this.isWarped === false) {
|
|
this.world().broken.push(damaged);
|
|
}
|
|
this.parent.cachedPenTrailsMorph = null;
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.floodFill = function () {
|
|
if (!this.parent.bounds.containsPoint(this.rotationCenter())) {
|
|
return;
|
|
}
|
|
this.parent.cachedPenTrailsMorph = null;
|
|
if (this.color.a > 1) {
|
|
// fix a legacy bug in Morphic color detection
|
|
this.color.a = this.color.a / 255;
|
|
}
|
|
var layer = normalizeCanvas(this.parent.penTrails()),
|
|
width = layer.width,
|
|
height = layer.height,
|
|
ctx = layer.getContext('2d'),
|
|
img = ctx.getImageData(0, 0, width, height),
|
|
dta = img.data,
|
|
stack = [
|
|
Math.floor((height / 2) - this.yPosition()) * width +
|
|
Math.floor(this.xPosition() + (width / 2))
|
|
],
|
|
clr = new Color(
|
|
Math.round(Math.min(Math.max(this.color.r, 0), 255)),
|
|
Math.round(Math.min(Math.max(this.color.g, 0), 255)),
|
|
Math.round(Math.min(Math.max(this.color.b, 0), 255)),
|
|
this.color.a
|
|
),
|
|
current,
|
|
src;
|
|
|
|
function read(p) {
|
|
var d = p * 4;
|
|
return [dta[d], dta[d + 1], dta[d + 2], dta[d + 3]];
|
|
}
|
|
|
|
function check(p) {
|
|
return p[0] === src[0] &&
|
|
p[1] === src[1] &&
|
|
p[2] === src[2] &&
|
|
p[3] === src[3];
|
|
}
|
|
|
|
src = read(stack[0]);
|
|
if (src[0] === clr.r &&
|
|
src[1] === clr.g &&
|
|
src[2] === clr.b &&
|
|
src[3] === Math.round(clr.a * 255)) {
|
|
return;
|
|
}
|
|
while (stack.length > 0) {
|
|
current = stack.pop();
|
|
if (check(read(current))) {
|
|
if (current % width > 1) {
|
|
stack.push(current + 1);
|
|
stack.push(current - 1);
|
|
}
|
|
if (current > 0 && current < height * width) {
|
|
stack.push(current + width);
|
|
stack.push(current - width);
|
|
}
|
|
}
|
|
dta[current * 4] = clr.r;
|
|
dta[current * 4 + 1] = clr.g;
|
|
dta[current * 4 + 2] = clr.b;
|
|
dta[current * 4 + 3] = Math.round(clr.a * 255);
|
|
}
|
|
ctx.putImageData(img, 0, 0);
|
|
this.parent.changed();
|
|
};
|
|
|
|
// SpriteMorph pen trails as costume
|
|
|
|
SpriteMorph.prototype.reportPenTrailsAsCostume = function () {
|
|
var cst = new Costume(
|
|
this.parentThatIsA(StageMorph).trailsCanvas,
|
|
this.newCostumeName(localize('Costume'))
|
|
);
|
|
cst.shrinkWrap();
|
|
cst.rotationCenter = cst.rotationCenter.translateBy(
|
|
new Point(this.xPosition(), -this.yPosition())
|
|
);
|
|
return cst;
|
|
};
|
|
|
|
// SpriteMorph motion - adjustments due to nesting
|
|
|
|
SpriteMorph.prototype.moveBy = function (delta, justMe) {
|
|
// override the inherited default to make sure my parts follow
|
|
// unless it's justMe (a correction)
|
|
var start = this.isDown && !justMe && this.parent ?
|
|
this.rotationCenter() : null;
|
|
SpriteMorph.uber.moveBy.call(this, delta);
|
|
if (start) {
|
|
this.drawLine(start, this.rotationCenter());
|
|
}
|
|
if (!justMe) {
|
|
this.parts.forEach(part => part.moveBy(delta));
|
|
this.instances.forEach(instance => {
|
|
if (instance.cachedPropagation) {
|
|
var inheritsX = instance.inheritsAttribute('x position'),
|
|
inheritsY = instance.inheritsAttribute('y position');
|
|
if (inheritsX && inheritsY) {
|
|
instance.moveBy(delta);
|
|
} else if (inheritsX) {
|
|
instance.moveBy(new Point(delta.x, 0));
|
|
} else if (inheritsY) {
|
|
instance.moveBy(new Point(0, delta.y));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.rootForGrab = function () {
|
|
if (this.anchor) {
|
|
return this.anchor.rootForGrab();
|
|
}
|
|
return SpriteMorph.uber.rootForGrab.call(this);
|
|
};
|
|
|
|
SpriteMorph.prototype.setCenter = function (aPoint, justMe) {
|
|
// override the inherited default to make sure my parts follow
|
|
// unless it's justMe
|
|
var delta = aPoint.subtract(this.center());
|
|
this.moveBy(delta, justMe);
|
|
};
|
|
|
|
SpriteMorph.prototype.nestingBounds = function () {
|
|
// same as fullBounds(), except that it uses "parts" instead of children
|
|
// and special cases the costume-less "arrow" shape's bounding box
|
|
var result = this.bounds;
|
|
if (!this.costume && this.penBounds) {
|
|
result = this.penBounds.translateBy(this.position());
|
|
}
|
|
this.parts.forEach(part => {
|
|
if (part.isVisible) {
|
|
result = result.merge(part.nestingBounds());
|
|
}
|
|
});
|
|
return result;
|
|
};
|
|
|
|
// SpriteMorph motion primitives
|
|
|
|
SpriteMorph.prototype.setPosition = function (aPoint, justMe) {
|
|
// override the inherited default to make sure my parts follow
|
|
// unless it's justMe
|
|
var delta = aPoint.subtract(this.topLeft());
|
|
if ((delta.x !== 0) || (delta.y !== 0)) {
|
|
this.moveBy(delta, justMe);
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.forward = function (steps) {
|
|
var dest,
|
|
dist = steps * this.parent.scale || 0,
|
|
dot = 0.1;
|
|
|
|
if (dist === 0 && this.isDown) { // draw a dot
|
|
// dot = Math.min(this.size, 1);
|
|
this.isDown = false;
|
|
this.forward(dot * -0.5);
|
|
this.isDown = true;
|
|
this.forward(dot);
|
|
this.isDown = false;
|
|
this.forward(dot * -0.5);
|
|
this.isDown = true;
|
|
return;
|
|
} else if (dist >= 0) {
|
|
dest = this.position().distanceAngle(dist, this.heading);
|
|
} else {
|
|
dest = this.position().distanceAngle(
|
|
Math.abs(dist),
|
|
(this.heading - 180)
|
|
);
|
|
}
|
|
|
|
this.shadowAttribute('x position');
|
|
this.shadowAttribute('y position');
|
|
|
|
this.setPosition(dest);
|
|
this.positionTalkBubble();
|
|
};
|
|
|
|
SpriteMorph.prototype.setHeading = function (degrees, noShadow) {
|
|
var x = this.xPosition(),
|
|
y = this.yPosition(),
|
|
dir = !isFinite(degrees) ? 0 : +degrees,
|
|
turn = dir - this.heading;
|
|
|
|
// apply to myself
|
|
if (this.rotationStyle) { // optimization, only redraw if rotatable
|
|
this.changed();
|
|
SpriteMorph.uber.setHeading.call(this, dir);
|
|
this.silentGotoXY(x, y, true); // just me
|
|
this.positionTalkBubble();
|
|
} else {
|
|
this.heading = ((+degrees % 360) + 360) % 360;
|
|
}
|
|
|
|
// propagate to my parts
|
|
this.parts.forEach(part => {
|
|
var pos = new Point(part.xPosition(), part.yPosition()),
|
|
trg = pos.rotateBy(radians(turn), new Point(x, y));
|
|
if (part.rotatesWithAnchor) {
|
|
part.turn(turn);
|
|
}
|
|
part.gotoXY(trg.x, trg.y);
|
|
});
|
|
|
|
// propagate to children that inherit my direction
|
|
if (!noShadow) {
|
|
this.shadowAttribute('direction');
|
|
}
|
|
this.instances.forEach(instance => {
|
|
if (instance.cachedPropagation) {
|
|
if (instance.inheritsAttribute('direction')) {
|
|
instance.setHeading(degrees, true);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
SpriteMorph.prototype.faceToXY = function (x, y) {
|
|
this.setHeading(this.angleToXY(x, y));
|
|
};
|
|
|
|
SpriteMorph.prototype.angleToXY = function (x, y) {
|
|
var deltaX = (x - this.xPosition()) * this.parent.scale,
|
|
deltaY = (y - this.yPosition()) * this.parent.scale,
|
|
angle = Math.abs(deltaX) < 0.001 ? (deltaY < 0 ? 90 : 270)
|
|
: Math.round(
|
|
(deltaX >= 0 ? 0 : 180)
|
|
- (Math.atan(deltaY / deltaX) * 57.2957795131)
|
|
);
|
|
return angle + 90;
|
|
};
|
|
|
|
SpriteMorph.prototype.turn = function (degrees) {
|
|
this.setHeading(this.heading + (+degrees || 0));
|
|
};
|
|
|
|
SpriteMorph.prototype.turnLeft = function (degrees) {
|
|
this.setHeading(this.heading - (+degrees || 0));
|
|
};
|
|
|
|
SpriteMorph.prototype.xPosition = function () {
|
|
if (this.inheritsAttribute('x position')) {
|
|
return this.exemplar.xPosition();
|
|
}
|
|
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
|
|
if (!stage && this.parent.grabOrigin) { // I'm currently being dragged
|
|
stage = this.parent.grabOrigin.origin;
|
|
}
|
|
if (stage) {
|
|
return (this.rotationCenter().x - stage.center().x) / stage.scale;
|
|
}
|
|
return this.rotationCenter().x;
|
|
};
|
|
|
|
SpriteMorph.prototype.yPosition = function () {
|
|
if (this.inheritsAttribute('y position')) {
|
|
return this.exemplar.yPosition();
|
|
}
|
|
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
|
|
if (!stage && this.parent.grabOrigin) { // I'm currently being dragged
|
|
stage = this.parent.grabOrigin.origin;
|
|
}
|
|
if (stage) {
|
|
return (stage.center().y - this.rotationCenter().y) / stage.scale;
|
|
}
|
|
return this.rotationCenter().y;
|
|
};
|
|
|
|
SpriteMorph.prototype.direction = function () {
|
|
if (this.inheritsAttribute('direction')) {
|
|
return this.exemplar.direction();
|
|
}
|
|
return this.heading;
|
|
};
|
|
|
|
SpriteMorph.prototype.penSize = function () {
|
|
return this.size;
|
|
};
|
|
|
|
SpriteMorph.prototype.gotoXY = function (x, y, justMe, noShadow) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
newX,
|
|
newY,
|
|
dest;
|
|
|
|
if (!stage) {return; }
|
|
if (!noShadow) {
|
|
this.shadowAttribute('x position');
|
|
this.shadowAttribute('y position');
|
|
}
|
|
x = !isFinite(+x) ? 0 : +x;
|
|
y = !isFinite(+y) ? 0 : +y;
|
|
newX = stage.center().x + x * stage.scale;
|
|
newY = stage.center().y - y * stage.scale;
|
|
if (this.costume) {
|
|
dest = new Point(newX, newY).subtract(this.rotationOffset);
|
|
} else {
|
|
dest = new Point(newX, newY).subtract(this.extent().divideBy(2));
|
|
}
|
|
this.setPosition(dest, justMe);
|
|
this.positionTalkBubble();
|
|
};
|
|
|
|
SpriteMorph.prototype.silentGotoXY = function (x, y, justMe) {
|
|
// move without drawing
|
|
// don't shadow coordinate attributes
|
|
var penState = this.isDown;
|
|
this.isDown = false;
|
|
this.gotoXY(x, y, justMe, true); // don't shadow coordinates
|
|
this.isDown = penState;
|
|
};
|
|
|
|
SpriteMorph.prototype.setXPosition = function (num) {
|
|
this.shadowAttribute('x position');
|
|
this.gotoXY(+num || 0, this.yPosition(), false, true);
|
|
};
|
|
|
|
SpriteMorph.prototype.changeXPosition = function (delta) {
|
|
this.setXPosition(this.xPosition() + (+delta || 0));
|
|
};
|
|
|
|
SpriteMorph.prototype.setYPosition = function (num) {
|
|
this.shadowAttribute('y position');
|
|
this.gotoXY(this.xPosition(), +num || 0, false, true);
|
|
};
|
|
|
|
SpriteMorph.prototype.changeYPosition = function (delta) {
|
|
this.setYPosition(this.yPosition() + (+delta || 0));
|
|
};
|
|
|
|
SpriteMorph.prototype.glide = function (
|
|
duration,
|
|
endX,
|
|
endY,
|
|
elapsed,
|
|
startPoint
|
|
) {
|
|
var fraction, endPoint, rPos;
|
|
endPoint = new Point(endX, endY);
|
|
fraction = Math.max(Math.min(elapsed / duration, 1), 0);
|
|
rPos = startPoint.add(
|
|
endPoint.subtract(startPoint).multiplyBy(fraction)
|
|
);
|
|
this.gotoXY(rPos.x, rPos.y);
|
|
};
|
|
|
|
SpriteMorph.prototype.bounceOffEdge = function () {
|
|
// taking nested parts into account
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
fb = this.nestingBounds(),
|
|
dirX,
|
|
dirY;
|
|
|
|
if (!stage) {return null; }
|
|
if (stage.bounds.containsRectangle(fb)) {return null; }
|
|
|
|
dirX = Math.cos(radians(this.heading - 90));
|
|
dirY = -(Math.sin(radians(this.heading - 90)));
|
|
|
|
if (fb.left() < stage.left()) {
|
|
dirX = Math.abs(dirX);
|
|
}
|
|
if (fb.right() > stage.right()) {
|
|
dirX = -(Math.abs(dirX));
|
|
}
|
|
if (fb.top() < stage.top()) {
|
|
dirY = -(Math.abs(dirY));
|
|
}
|
|
if (fb.bottom() > stage.bottom()) {
|
|
dirY = Math.abs(dirY);
|
|
}
|
|
|
|
this.shadowAttribute('x position');
|
|
this.shadowAttribute('y position');
|
|
this.shadowAttribute('direction');
|
|
|
|
this.setHeading(degrees(Math.atan2(-dirY, dirX)) + 90);
|
|
this.setPosition(this.position().add(
|
|
fb.amountToTranslateWithin(stage.bounds)
|
|
));
|
|
this.positionTalkBubble();
|
|
};
|
|
|
|
// SpriteMorph coordinate conversion
|
|
|
|
SpriteMorph.prototype.snapPoint = function(aPoint) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
origin = stage.center();
|
|
return new Point(
|
|
(aPoint.x - origin.x) / stage.scale,
|
|
(origin.y - aPoint.y) / stage.scale
|
|
);
|
|
};
|
|
|
|
// SpriteMorph rotation center / fixation point manipulation
|
|
|
|
SpriteMorph.prototype.setRotationX = function (absoluteX) {
|
|
this.setRotationCenter(new Point(absoluteX, this.yPosition()));
|
|
alert(absoluteX);
|
|
};
|
|
|
|
SpriteMorph.prototype.setRotationY = function (absoluteY) {
|
|
this.setRotationCenter(new Point(this.xPosition(), absoluteY));
|
|
alert(absolutexy);
|
|
};
|
|
|
|
SpriteMorph.prototype.setRotationCenter = function (absoluteCoordinate) {
|
|
var delta, normal;
|
|
if (!this.costume) {
|
|
throw new Error('setting the rotation center requires a costume');
|
|
}
|
|
this.shadowAttribute('costumes');
|
|
delta = absoluteCoordinate.subtract(
|
|
new Point(this.xPosition(), this.yPosition())
|
|
).divideBy(this.scale).rotateBy(radians(90 - this.heading));
|
|
normal = this.costume.rotationCenter.add(new Point(delta.x, -delta.y));
|
|
this.costume.rotationCenter = normal;
|
|
this.changed();
|
|
this.fixLayout();
|
|
this.changed();
|
|
};
|
|
|
|
SpriteMorph.prototype.moveRotationCenter = function () {
|
|
// make this a method of Snap >> SpriteMorph
|
|
this.world().activeHandle = new HandleMorph(
|
|
this,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
'movePivot'
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.setPivot = function (worldCoordinate) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
ide = this.parentThatIsA(IDE_Morph),
|
|
cntr;
|
|
if (stage) {
|
|
cntr = stage.center();
|
|
this.setRotationCenter(
|
|
new Point(
|
|
(worldCoordinate.x - cntr.x) / stage.scale,
|
|
(cntr.y - worldCoordinate.y) / stage.scale
|
|
)
|
|
);
|
|
if (ide) {
|
|
ide.recordUnsavedChanges();
|
|
}
|
|
}
|
|
};
|
|
|
|
// SpriteMorph dimension getters
|
|
|
|
SpriteMorph.prototype.xCenter = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
|
|
if (!stage && this.parent.grabOrigin) { // I'm currently being dragged
|
|
stage = this.parent.grabOrigin.origin;
|
|
}
|
|
if (stage) {
|
|
return (this.center().x - stage.center().x) / stage.scale;
|
|
}
|
|
return this.center().x;
|
|
};
|
|
|
|
SpriteMorph.prototype.yCenter = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
|
|
if (!stage && this.parent.grabOrigin) { // I'm currently being dragged
|
|
stage = this.parent.grabOrigin.origin;
|
|
}
|
|
if (stage) {
|
|
return (stage.center().y - this.center().y) / stage.scale;
|
|
}
|
|
return this.center().y;
|
|
};
|
|
|
|
SpriteMorph.prototype.xLeft = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
|
|
if (!stage && this.parent.grabOrigin) { // I'm currently being dragged
|
|
stage = this.parent.grabOrigin.origin;
|
|
}
|
|
if (stage) {
|
|
return (this.left() - stage.center().x) / stage.scale;
|
|
}
|
|
return this.left();
|
|
};
|
|
|
|
SpriteMorph.prototype.xRight = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
|
|
if (!stage && this.parent.grabOrigin) { // I'm currently being dragged
|
|
stage = this.parent.grabOrigin.origin;
|
|
}
|
|
if (stage) {
|
|
return (this.right() - stage.center().x) / stage.scale;
|
|
}
|
|
return this.right();
|
|
};
|
|
|
|
SpriteMorph.prototype.yTop = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
|
|
if (!stage && this.parent.grabOrigin) { // I'm currently being dragged
|
|
stage = this.parent.grabOrigin.origin;
|
|
}
|
|
if (stage) {
|
|
return (stage.center().y - this.top()) / stage.scale;
|
|
}
|
|
return this.top();
|
|
};
|
|
|
|
SpriteMorph.prototype.yBottom = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
|
|
if (!stage && this.parent.grabOrigin) { // I'm currently being dragged
|
|
stage = this.parent.grabOrigin.origin;
|
|
}
|
|
if (stage) {
|
|
return (stage.center().y - this.bottom()) / stage.scale;
|
|
}
|
|
return this.bottom();
|
|
};
|
|
|
|
// SpriteMorph message broadcasting
|
|
|
|
SpriteMorph.prototype.allMessageNames = function () {
|
|
var msgs = [];
|
|
this.allScripts().forEach(script => {
|
|
script.allChildren().forEach(morph => {
|
|
var txt;
|
|
if (morph instanceof InputSlotMorph && morph.choices && contains(
|
|
['messagesMenu', 'messagesReceivedMenu'],
|
|
morph.choices
|
|
)) {
|
|
txt = morph.evaluate();
|
|
if (isString(txt) && txt !== '') {
|
|
if (!contains(msgs, txt)) {
|
|
msgs.push(txt);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
return msgs;
|
|
};
|
|
|
|
SpriteMorph.prototype.allSendersOf = function (message, receiverName, known) {
|
|
return this.allScripts().filter(script =>
|
|
script.isSending && script.isSending(message, receiverName, known)
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.allHatBlocksFor = function (message) {
|
|
if (typeof message === 'number') { message = message.toString(); }
|
|
return this.scripts.children.filter(morph => {
|
|
var sel = morph.selector,
|
|
event;
|
|
if (sel) {
|
|
if (sel === 'receiveMessage') {
|
|
event = morph.inputs()[0].evaluate();
|
|
return event === message
|
|
|| (event instanceof Array
|
|
&& message !== '__shout__go__'
|
|
&& message !== '__clone__init__');
|
|
}
|
|
if (sel === 'receiveGo') {
|
|
return message === '__shout__go__';
|
|
}
|
|
if (sel === 'receiveOnClone') {
|
|
return message === '__clone__init__';
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
};
|
|
|
|
SpriteMorph.prototype.allHatBlocksForKey = function (key) {
|
|
return this.scripts.children.filter(morph => {
|
|
if (morph.selector) {
|
|
if (morph.selector === 'receiveKey') {
|
|
var choice = morph.inputs()[0].evaluate(),
|
|
evt = choice instanceof Array ? choice[0] : choice;
|
|
return evt === key || evt === 'any key';
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
};
|
|
|
|
SpriteMorph.prototype.allHatBlocksForInteraction = function (interaction) {
|
|
return this.scripts.children.filter(morph => {
|
|
if (morph.selector) {
|
|
if (morph.selector === 'receiveInteraction') {
|
|
return morph.inputs()[0].evaluate()[0] === interaction;
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
};
|
|
|
|
SpriteMorph.prototype.hasGenericHatBlocks = function () {
|
|
return this.scripts.children.some(morph =>
|
|
morph.selector === 'receiveCondition'
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.allGenericHatBlocks = function () {
|
|
return this.scripts.children.filter(morph => {
|
|
if (morph.selector) {
|
|
return morph.selector === 'receiveCondition';
|
|
}
|
|
return false;
|
|
});
|
|
};
|
|
|
|
SpriteMorph.prototype.allScripts = function () {
|
|
var all = this.scripts.children.slice();
|
|
this.customBlocks.forEach(def => {
|
|
if (def.body) {
|
|
all.push(def.body.expression);
|
|
}
|
|
def.scripts.forEach(scr => all.push(scr));
|
|
});
|
|
if (this.globalBlocks) {
|
|
this.globalBlocks.forEach(def => {
|
|
if (def.body) {
|
|
all.push(def.body.expression);
|
|
}
|
|
def.scripts.forEach(scr => all.push(scr));
|
|
});
|
|
}
|
|
return all;
|
|
};
|
|
|
|
// SpriteMorph events
|
|
|
|
SpriteMorph.prototype.mouseClickLeft = function () {
|
|
return this.receiveUserInteraction('clicked');
|
|
};
|
|
|
|
SpriteMorph.prototype.mouseEnter = function () {
|
|
return this.receiveUserInteraction('mouse-entered');
|
|
};
|
|
|
|
SpriteMorph.prototype.mouseDownLeft = function () {
|
|
return this.receiveUserInteraction('pressed');
|
|
};
|
|
|
|
SpriteMorph.prototype.mouseScroll = function (y) {
|
|
return this.receiveUserInteraction('scrolled-' + (y > 0 ? 'up' : 'down'));
|
|
};
|
|
|
|
SpriteMorph.prototype.receiveUserInteraction = function (
|
|
interaction,
|
|
rightAway,
|
|
threadSafe
|
|
) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
procs = [],
|
|
hats;
|
|
if (!stage) {return; } // currently dragged
|
|
hats = this.allHatBlocksForInteraction(interaction);
|
|
hats.forEach(block =>
|
|
procs.push(stage.threads.startProcess(
|
|
block,
|
|
this,
|
|
threadSafe || stage.isThreadSafe,
|
|
null, // export result
|
|
null, // callback
|
|
null, // is clicked
|
|
rightAway, // immediately
|
|
interaction === 'stopped' // atomic
|
|
))
|
|
);
|
|
return procs;
|
|
};
|
|
|
|
SpriteMorph.prototype.mouseDoubleClick = function () {
|
|
if (this.isTemporary) {return; }
|
|
this.edit();
|
|
};
|
|
|
|
// SpriteMorph timer
|
|
|
|
SpriteMorph.prototype.getTimer = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (stage) {
|
|
return stage.getTimer();
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
// SpriteMorph tempo
|
|
|
|
SpriteMorph.prototype.getTempo = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (stage) {
|
|
return stage.getTempo();
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
// SpriteMorph last message
|
|
|
|
SpriteMorph.prototype.getLastMessage = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (stage) {
|
|
return stage.getLastMessage();
|
|
}
|
|
return '';
|
|
};
|
|
|
|
// SpriteMorph user prompting
|
|
|
|
SpriteMorph.prototype.getLastAnswer = function () {
|
|
return this.parentThatIsA(StageMorph).lastAnswer;
|
|
};
|
|
|
|
// SpriteMorph mouse coordinates
|
|
|
|
SpriteMorph.prototype.reportMouseX = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (stage) {
|
|
return stage.reportMouseX();
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
SpriteMorph.prototype.reportMouseY = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (stage) {
|
|
return stage.reportMouseY();
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
// SpriteMorph thread count (for debugging)
|
|
|
|
SpriteMorph.prototype.reportThreadCount = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (stage) {
|
|
return stage.threads.processes.length;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
// SpriteMorph variable refactoring
|
|
|
|
SpriteMorph.prototype.refactorVariableInstances = function (
|
|
oldName,
|
|
newName,
|
|
isGlobal
|
|
) {
|
|
if (isGlobal && this.hasSpriteVariable(oldName)) {
|
|
return;
|
|
}
|
|
|
|
this.scripts.children.forEach(child => {
|
|
if (child instanceof BlockMorph) {
|
|
child.refactorVarInStack(oldName, newName);
|
|
}
|
|
});
|
|
|
|
};
|
|
|
|
// SpriteMorph variable watchers (for palette checkbox toggling)
|
|
|
|
SpriteMorph.prototype.findVariableWatcher = function (varName) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
globals = this.globalVariables();
|
|
if (stage === null) {
|
|
return null;
|
|
}
|
|
return detect(
|
|
stage.children,
|
|
morph => morph instanceof WatcherMorph &&
|
|
(morph.target === this.variables || morph.target === globals) &&
|
|
morph.getter === varName
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.toggleVariableWatcher = function (varName, isGlobal) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
ide = this.parentThatIsA(IDE_Morph),
|
|
globals = this.globalVariables(),
|
|
watcher,
|
|
others;
|
|
|
|
if (stage === null) {
|
|
return null;
|
|
}
|
|
if (isNil(isGlobal)) {
|
|
isGlobal = contains(globals.names(), varName);
|
|
}
|
|
watcher = this.findVariableWatcher(varName);
|
|
if (watcher !== null) {
|
|
if (watcher.isVisible) {
|
|
watcher.hide();
|
|
} else {
|
|
watcher.show();
|
|
watcher.fixLayout(); // re-hide hidden parts
|
|
watcher.keepWithin(stage);
|
|
}
|
|
if (isGlobal) {
|
|
ide.flushBlocksCache('variables');
|
|
ide.refreshPalette();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// if no watcher exists, create a new one
|
|
watcher = new WatcherMorph(
|
|
varName,
|
|
this.blockColor.variables,
|
|
isGlobal ? globals : this.variables,
|
|
varName
|
|
);
|
|
watcher.setPosition(stage.position().add(10));
|
|
others = stage.watchers(watcher.left());
|
|
if (others.length > 0) {
|
|
watcher.setTop(others[others.length - 1].bottom());
|
|
}
|
|
watcher.fixLayout();
|
|
watcher.keepWithin(stage);
|
|
stage.add(watcher);
|
|
watcher.changed();
|
|
return watcher;
|
|
};
|
|
|
|
SpriteMorph.prototype.showingVariableWatcher = function (varName) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
watcher;
|
|
if (stage === null) {
|
|
return false;
|
|
}
|
|
watcher = this.findVariableWatcher(varName);
|
|
if (watcher) {
|
|
return watcher.isVisible;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
SpriteMorph.prototype.deleteVariableWatcher = function (varName) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
watcher;
|
|
if (stage === null) {
|
|
return null;
|
|
}
|
|
watcher = this.findVariableWatcher(varName);
|
|
if (watcher !== null) {
|
|
watcher.destroy();
|
|
}
|
|
};
|
|
|
|
// SpriteMorph non-variable watchers
|
|
|
|
SpriteMorph.prototype.toggleWatcher = function (selector, label, color) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
ide = this.parentThatIsA(IDE_Morph),
|
|
watcher,
|
|
others;
|
|
if (!stage) { return; }
|
|
watcher = this.watcherFor(stage, selector);
|
|
if (watcher) {
|
|
if (watcher.isVisible) {
|
|
watcher.hide();
|
|
} else {
|
|
watcher.show();
|
|
watcher.fixLayout(); // re-hide hidden parts
|
|
watcher.keepWithin(stage);
|
|
}
|
|
if (watcher.isGlobal(selector)) {
|
|
ide.flushBlocksCache();
|
|
ide.refreshPalette();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// if no watcher exists, create a new one
|
|
watcher = new WatcherMorph(
|
|
label,
|
|
color,
|
|
WatcherMorph.prototype.isGlobal(selector) ? stage : this,
|
|
selector
|
|
);
|
|
watcher.setPosition(stage.position().add(10));
|
|
others = stage.watchers(watcher.left());
|
|
if (others.length > 0) {
|
|
watcher.setTop(others[others.length - 1].bottom());
|
|
}
|
|
stage.add(watcher);
|
|
watcher.fixLayout();
|
|
watcher.keepWithin(stage);
|
|
watcher.changed();
|
|
if (watcher.isGlobal(selector)) {
|
|
ide.flushBlocksCache();
|
|
ide.refreshPalette();
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.showingWatcher = function (selector) {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
watcher;
|
|
if (stage === null) {
|
|
return false;
|
|
}
|
|
watcher = this.watcherFor(stage, selector);
|
|
if (watcher) {
|
|
return watcher.isVisible;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
SpriteMorph.prototype.watcherFor = function (stage, selector) {
|
|
return detect(
|
|
stage.children,
|
|
morph => morph instanceof WatcherMorph &&
|
|
morph.getter === selector &&
|
|
morph.target === (morph.isGlobal(selector) ? stage : this)
|
|
);
|
|
};
|
|
|
|
// SpriteMorph custom blocks
|
|
|
|
SpriteMorph.prototype.deleteAllBlockInstances = function (definition) {
|
|
var stage,
|
|
blocks = definition.isGlobal ? this.allBlockInstances(definition)
|
|
: this.allIndependentInvocationsOf(definition.blockSpec());
|
|
|
|
blocks.forEach(each => each.deleteBlock());
|
|
|
|
// purge custom block definitions of "corpses"
|
|
// i.e. blocks that have been marked for deletion
|
|
if (definition.isGlobal) {
|
|
stage = this.parentThatIsA(StageMorph);
|
|
if (stage) {
|
|
stage.globalBlocks.forEach(def => def.purgeCorpses());
|
|
stage.children.concat(stage).forEach(sprite => {
|
|
if (sprite.isSnapObject) {
|
|
sprite.customBlocks.forEach(def => def.purgeCorpses());
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
this.allSpecimens().concat(this).forEach(sprite =>
|
|
sprite.customBlocks.forEach(def => def.purgeCorpses())
|
|
);
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.allBlockInstances = function (definition) {
|
|
var stage, objects, blocks = [], inDefinitions;
|
|
if (definition.isGlobal) {
|
|
stage = this.parentThatIsA(StageMorph);
|
|
objects = stage.children.filter(morph =>
|
|
morph instanceof SpriteMorph
|
|
);
|
|
objects.push(stage);
|
|
objects.forEach(sprite =>
|
|
blocks = blocks.concat(
|
|
sprite.allLocalBlockInstances(definition)
|
|
)
|
|
);
|
|
inDefinitions = [];
|
|
stage.globalBlocks.forEach(def => {
|
|
def.scripts.forEach(eachScript =>
|
|
eachScript.allChildren().forEach(c => {
|
|
if (c.isCustomBlock && (c.definition === definition)) {
|
|
inDefinitions.push(c);
|
|
}
|
|
})
|
|
);
|
|
if (def.body) {
|
|
def.body.expression.allChildren().forEach(c => {
|
|
if (c.isCustomBlock && (c.definition === definition)) {
|
|
inDefinitions.push(c);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
return blocks.concat(inDefinitions);
|
|
}
|
|
return this.allLocalBlockInstances(definition);
|
|
};
|
|
|
|
SpriteMorph.prototype.allIndependentInvocationsOf = function (aSpec) {
|
|
var blocks;
|
|
if (this.exemplar && this.exemplar.getMethod(aSpec)) {
|
|
// shadows an inherited method, don't delete
|
|
return [];
|
|
}
|
|
blocks = this.allInvocationsOf(aSpec);
|
|
this.instances.forEach(sprite =>
|
|
sprite.addAllInvocationsOf(aSpec, blocks)
|
|
);
|
|
return blocks;
|
|
};
|
|
|
|
SpriteMorph.prototype.allDependentInvocationsOf = function (aSpec) {
|
|
var blocks;
|
|
blocks = this.allInvocationsOf(aSpec);
|
|
this.instances.forEach(sprite =>
|
|
sprite.addAllInvocationsOf(aSpec, blocks)
|
|
);
|
|
return blocks;
|
|
};
|
|
|
|
SpriteMorph.prototype.allInvocationsOf = function (aSpec) {
|
|
// only inside the receiver, without the inheritance branches
|
|
var inScripts, inDefinitions, inBlockEditors, blocks;
|
|
|
|
inScripts = this.scripts.allChildren().filter(c =>
|
|
c.isCustomBlock && !c.isGlobal && (c.blockSpec === aSpec)
|
|
);
|
|
|
|
inDefinitions = [];
|
|
this.customBlocks.forEach(def => {
|
|
def.scripts.forEach(eachScript =>
|
|
eachScript.allChildren().forEach(c => {
|
|
if (c.isCustomBlock && !c.isGlobal &&
|
|
(c.blockSpec === aSpec)
|
|
) {
|
|
inDefinitions.push(c);
|
|
}
|
|
})
|
|
);
|
|
if (def.body) {
|
|
def.body.expression.allChildren().forEach(c => {
|
|
if (c.isCustomBlock && !c.isGlobal &&
|
|
(c.blockSpec === aSpec)
|
|
) {
|
|
inDefinitions.push(c);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
inBlockEditors = this.allEditorBlockInstances(null, aSpec);
|
|
blocks = inScripts.concat(inDefinitions).concat(inBlockEditors);
|
|
return blocks;
|
|
};
|
|
|
|
SpriteMorph.prototype.addAllInvocationsOf = function (aSpec, anArray) {
|
|
if (!this.getLocalMethod(aSpec)) {
|
|
this.allInvocationsOf(aSpec).forEach(block =>
|
|
anArray.push(block)
|
|
);
|
|
this.instances.forEach(sprite =>
|
|
sprite.addAllInvocationsOf(aSpec, anArray)
|
|
);
|
|
}
|
|
};
|
|
|
|
|
|
SpriteMorph.prototype.allLocalBlockInstances = function (definition) {
|
|
var inScripts, inDefinitions, inBlockEditors, inPalette, result;
|
|
|
|
inScripts = this.scripts.allChildren().filter(c =>
|
|
c.isCustomBlock && (c.definition === definition)
|
|
);
|
|
|
|
inDefinitions = [];
|
|
this.customBlocks.forEach(def => {
|
|
if (def.body) {
|
|
def.body.expression.allChildren().forEach(c => {
|
|
if (c.isCustomBlock && (c.definition === definition)) {
|
|
inDefinitions.push(c);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
inBlockEditors = this.allEditorBlockInstances(definition);
|
|
inPalette = this.paletteBlockInstance(definition);
|
|
|
|
result = inScripts.concat(inDefinitions).concat(inBlockEditors);
|
|
if (inPalette) {
|
|
result.push(inPalette);
|
|
}
|
|
return result;
|
|
};
|
|
|
|
SpriteMorph.prototype.allEditorBlockInstances = function (definition, spec) {
|
|
// either pass a definition for global custom blocks
|
|
// or a spec for local ones
|
|
var inBlockEditors = [],
|
|
world = this.world();
|
|
|
|
if (!world) {return []; } // when copying a sprite
|
|
|
|
this.world().children.forEach(morph => {
|
|
if (morph instanceof BlockEditorMorph) {
|
|
morph.body.contents.allChildren().forEach(block => {
|
|
if (definition) { // global
|
|
if (!block.isPrototype
|
|
&& !(block instanceof PrototypeHatBlockMorph)
|
|
&& (block.definition === definition)) {
|
|
inBlockEditors.push(block);
|
|
}
|
|
} else { // local
|
|
if (block.isCustomBlock
|
|
&& !block.isGlobal
|
|
&& !block.isPrototype
|
|
&& !(block instanceof PrototypeHatBlockMorph)
|
|
&& (block.blockSpec === spec)) {
|
|
inBlockEditors.push(block);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
return inBlockEditors;
|
|
};
|
|
|
|
SpriteMorph.prototype.paletteBlockInstance = function (definition) {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
if (!ide) {return null; }
|
|
return detect(
|
|
ide.palette.contents.children,
|
|
block => block.isCustomBlock && (block.definition === definition)
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.usesBlockInstance = function (
|
|
definition,
|
|
forRemoval, // optional bool
|
|
skipGlobals, // optional bool
|
|
skipBlocks // optional array with ignorable definitions
|
|
) {
|
|
var inDefinitions,
|
|
inScripts = detect(
|
|
this.scripts.allChildren(),
|
|
c => c.isCustomBlock && (c.definition === definition)
|
|
);
|
|
|
|
if (inScripts) {return true; }
|
|
|
|
if (definition.isGlobal && !skipGlobals) {
|
|
inDefinitions = [];
|
|
this.parentThatIsA(StageMorph).globalBlocks.forEach(def => {
|
|
if (forRemoval && (definition === def)) {return; }
|
|
if (skipBlocks && contains(skipBlocks, def)) {return; }
|
|
if (def.body) {
|
|
def.body.expression.allChildren().forEach(c => {
|
|
if (c.isCustomBlock && (c.definition === definition)) {
|
|
inDefinitions.push(c);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
if (inDefinitions.length > 0) {return true; }
|
|
}
|
|
|
|
inDefinitions = [];
|
|
this.customBlocks.forEach(def => {
|
|
if (def.body) {
|
|
def.body.expression.allChildren().forEach(c => {
|
|
if (c.isCustomBlock && (c.definition === definition)) {
|
|
inDefinitions.push(c);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
return (inDefinitions.length > 0);
|
|
};
|
|
|
|
SpriteMorph.prototype.doubleDefinitionsFor = function (definition) {
|
|
var spec = definition.blockSpec(),
|
|
blockList,
|
|
idx,
|
|
stage;
|
|
|
|
if (definition.isGlobal) {
|
|
stage = this.parentThatIsA(StageMorph);
|
|
if (!stage) {return []; }
|
|
blockList = stage.globalBlocks;
|
|
} else {
|
|
blockList = this.customBlocks;
|
|
}
|
|
idx = blockList.indexOf(definition);
|
|
if (idx === -1) {return []; }
|
|
return blockList.filter((def, i) =>
|
|
def.blockSpec() === spec && (i !== idx)
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.replaceDoubleDefinitionsFor = function (definition) {
|
|
var doubles = this.doubleDefinitionsFor(definition),
|
|
stage,
|
|
ide;
|
|
doubles.forEach(double =>
|
|
this.allBlockInstances(double).forEach(block => {
|
|
block.definition = definition;
|
|
block.refresh();
|
|
})
|
|
);
|
|
if (definition.isGlobal) {
|
|
stage = this.parentThatIsA(StageMorph);
|
|
stage.globalBlocks = stage.globalBlocks.filter(def =>
|
|
!contains(doubles, def)
|
|
);
|
|
} else {
|
|
this.customBlocks = this.customBlocks.filter(def =>
|
|
!contains(doubles, def)
|
|
);
|
|
}
|
|
ide = this.parentThatIsA(IDE_Morph);
|
|
if (ide) {
|
|
ide.flushPaletteCache();
|
|
ide.refreshPalette();
|
|
}
|
|
};
|
|
|
|
// SpriteMorph controlling generic WHEN hats
|
|
|
|
SpriteMorph.prototype.pauseGenericHatBlocks = function () {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
ide = this.parentThatIsA(IDE_Morph);
|
|
if (this.hasGenericHatBlocks()) {
|
|
stage.enableCustomHatBlocks = true;
|
|
stage.threads.pauseCustomHatBlocks = true;
|
|
ide.controlBar.stopButton.refresh();
|
|
}
|
|
};
|
|
|
|
// SpriteMorph inheritance - general
|
|
|
|
SpriteMorph.prototype.chooseExemplar = function () {
|
|
var stage = this.parentThatIsA(StageMorph),
|
|
other = stage.children.filter(m =>
|
|
m instanceof SpriteMorph &&
|
|
!m.isTemporary &&
|
|
(!contains(m.allExemplars(), this))
|
|
),
|
|
menu;
|
|
menu = new MenuMorph(
|
|
aSprite => this.setExemplar(aSprite),
|
|
localize('current parent') +
|
|
':\n' +
|
|
(this.exemplar ? this.exemplar.name : localize('none'))
|
|
);
|
|
other.forEach(eachSprite =>
|
|
menu.addItem(
|
|
eachSprite.name,
|
|
eachSprite,
|
|
null, // hint
|
|
null, // color
|
|
null, // bold
|
|
null, // italic
|
|
null, // doubleClickAction
|
|
null, // shortcut
|
|
true // verbatim
|
|
)
|
|
);
|
|
menu.addLine();
|
|
menu.addItem(localize('none'), null);
|
|
menu.popUpAtHand(this.world());
|
|
};
|
|
|
|
SpriteMorph.prototype.setExemplar = function (another, enableError) {
|
|
var ide;
|
|
|
|
// check for circularity
|
|
if (another instanceof SpriteMorph &&
|
|
contains(another.allExemplars(), this)) {
|
|
if (enableError) {
|
|
throw new Error(
|
|
localize('unable to inherit\n(disabled or circular?)')
|
|
);
|
|
}
|
|
return; // silently fail so stored projects can still be loaded
|
|
}
|
|
|
|
this.emancipate();
|
|
this.exemplar = another;
|
|
if (another) {
|
|
this.variables.parentFrame = another.variables;
|
|
another.addSpecimen(this);
|
|
} else {
|
|
this.variables.parentFrame = this.globalVariables();
|
|
}
|
|
if (this.isTemporary) {
|
|
this.cloneOriginName = another.cloneOriginName || another.name;
|
|
} else {
|
|
ide = this.parentThatIsA(IDE_Morph);
|
|
if (ide) {
|
|
ide.flushBlocksCache();
|
|
ide.refreshPalette();
|
|
}
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.prune = function () {
|
|
// sever ties with all my specimen, if any,
|
|
this.instances.forEach(child => {
|
|
child.shadowAllAttributes();
|
|
child.shadowAllMethods();
|
|
child.shadowAllVars();
|
|
child.exemplar = null;
|
|
});
|
|
this.instances = [];
|
|
};
|
|
|
|
SpriteMorph.prototype.emancipate = function () {
|
|
// sever all relations with my exemplar, if any,
|
|
// and make sure I am the root of my specimen
|
|
if (this.exemplar) {
|
|
if (!this.isTemporary) {
|
|
this.shadowAllAttributes();
|
|
this.shadowAllMethods();
|
|
this.shadowAllVars();
|
|
}
|
|
this.exemplar.removeSpecimen(this); // optimization
|
|
this.exemplar = null;
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.allExemplars = function () {
|
|
// including myself
|
|
var all = [],
|
|
current = this;
|
|
while (!isNil(current)) {
|
|
all.push(current);
|
|
current = current.exemplar;
|
|
}
|
|
return all;
|
|
};
|
|
|
|
SpriteMorph.prototype.specimens = function () {
|
|
// without myself
|
|
return this.instances;
|
|
};
|
|
|
|
SpriteMorph.prototype.allSpecimens = function () {
|
|
// without myself
|
|
var all = this.instances.slice();
|
|
this.instances.forEach(child =>
|
|
all.push.apply(all, child.allSpecimens())
|
|
);
|
|
return all;
|
|
};
|
|
|
|
SpriteMorph.prototype.addSpecimen = function (another) {
|
|
// private - use setExemplar() to establish an inheritance relationship
|
|
this.instances.push(another);
|
|
};
|
|
|
|
SpriteMorph.prototype.removeSpecimen = function(another) {
|
|
// private - use setExemplar(null) to cancel an inheritance relationship
|
|
var idx = this.instances.indexOf(another);
|
|
if (idx !== -1) {
|
|
this.instances.splice(idx, 1);
|
|
}
|
|
};
|
|
|
|
// SpriteMorph inheritance - attributes
|
|
|
|
SpriteMorph.prototype.inheritsAttribute = function (aName) {
|
|
return !isNil(this.exemplar) && contains(this.inheritedAttributes, aName);
|
|
};
|
|
|
|
SpriteMorph.prototype.updatePropagationCache = function () {
|
|
// private - indicate whether one of my inherited attributes is technically
|
|
// propagated down from my exemplar, instead of truly shared.
|
|
// (only) needed for internal optimization caching
|
|
this.cachedPropagation = !isNil(this.exemplar) && detect(
|
|
[
|
|
'x position',
|
|
'y position',
|
|
'direction',
|
|
'size',
|
|
'costume #',
|
|
'volume',
|
|
'balance',
|
|
'shown?',
|
|
'pen down?'
|
|
],
|
|
att => contains(this.inheritedAttributes, att)
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.shadowedAttributes = function () {
|
|
// answer an array of attribute names that can be deleted/shared
|
|
var inherited = this.inheritedAttributes;
|
|
return this.attributes.filter(each => !contains(inherited, each));
|
|
};
|
|
|
|
SpriteMorph.prototype.shadowAllAttributes = function () {
|
|
this.attributes.forEach(att =>
|
|
this.shadowAttribute(att)
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.shadowAttribute = function (aName) {
|
|
var ide, wardrobe, jukebox,
|
|
pos;
|
|
if (!this.inheritsAttribute(aName)) {
|
|
return;
|
|
}
|
|
ide = this.parentThatIsA(IDE_Morph);
|
|
this.inheritedAttributes = this.inheritedAttributes.filter(each =>
|
|
each !== aName
|
|
);
|
|
if (aName === 'costumes') {
|
|
wardrobe = new List();
|
|
this.costumes.asArray().forEach(costume => {
|
|
var cst = costume.copy();
|
|
wardrobe.add(cst);
|
|
if (costume === this.costume) {
|
|
this.wearCostume(cst);
|
|
}
|
|
});
|
|
this.costumes = wardrobe;
|
|
this.instances.forEach(obj => {
|
|
if (obj.inheritsAttribute('costumes')) {
|
|
obj.refreshInheritedAttribute('costumes');
|
|
}
|
|
});
|
|
} else if (aName === 'sounds') {
|
|
jukebox = new List();
|
|
this.sounds.asArray().forEach(sound => jukebox.add(sound.copy()));
|
|
this.sounds = jukebox;
|
|
this.instances.forEach(obj => {
|
|
if (obj.inheritsAttribute('sounds')) {
|
|
obj.refreshInheritedAttribute('sounds');
|
|
}
|
|
});
|
|
} else if (aName === 'scripts') {
|
|
ide.stage.threads.stopAllForReceiver(this);
|
|
pos = this.scripts.position();
|
|
this.scripts = this.exemplar.scripts.fullCopy();
|
|
if (ide && (contains(ide.currentSprite.allExemplars(), this))) {
|
|
ide.createSpriteEditor();
|
|
ide.fixLayout('selectSprite');
|
|
this.scripts.fixMultiArgs();
|
|
this.scripts.setPosition(pos);
|
|
ide.spriteEditor.adjustScrollBars();
|
|
}
|
|
this.instances.forEach(obj => {
|
|
if (obj.inheritsAttribute('scripts')) {
|
|
obj.refreshInheritedAttribute('scripts');
|
|
}
|
|
});
|
|
} else {
|
|
this.updatePropagationCache();
|
|
if (ide && !this.isTemporary) {
|
|
ide.flushBlocksCache(); // optimization: specify category if known
|
|
ide.refreshPalette();
|
|
}
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.inheritAttribute = function (aName) {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
if (!this.exemplar || !contains(this.attributes, aName)) {
|
|
return;
|
|
}
|
|
if (!this.inheritsAttribute(aName)) {
|
|
this.inheritedAttributes.push(aName);
|
|
this.refreshInheritedAttribute(aName);
|
|
if (ide) {
|
|
ide.flushBlocksCache(); // optimization: specify category
|
|
ide.refreshPalette();
|
|
}
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.refreshInheritedAttribute = function (aName) {
|
|
var ide, idx;
|
|
switch (aName) {
|
|
case 'x position':
|
|
case 'y position':
|
|
this.cachedPropagation = true;
|
|
this.gotoXY(this.xPosition(), this.yPosition(), false, true);
|
|
break;
|
|
case 'direction':
|
|
this.cachedPropagation = true;
|
|
this.setHeading(this.direction(), true);
|
|
break;
|
|
case 'size':
|
|
this.cachedPropagation = true;
|
|
this.setScale(this.getScale(), true);
|
|
break;
|
|
case 'costume #':
|
|
this.cachedPropagation = true;
|
|
if (this.inheritsAttribute('costumes')) {
|
|
// if inheriting the whole wardrobe,
|
|
// just switch to the exemplar's costume
|
|
this.wearCostume(this.exemplar.costume, true);
|
|
} else {
|
|
// otherwise switch to the own costume of the
|
|
// corresponing number
|
|
this.doSwitchToCostume(this.getCostumeIdx(), true);
|
|
}
|
|
break;
|
|
case 'volume':
|
|
this.cachedPropagation = true;
|
|
this.setVolume(this.getVolume(), true);
|
|
break;
|
|
case 'shown?':
|
|
this.cachedPropagation = true;
|
|
this.setVisibility(this.reportShown(), true);
|
|
break;
|
|
case 'pen down?':
|
|
this.cachedPropagation = true;
|
|
this.setPenDown(this.getPenDown(), true);
|
|
break;
|
|
case 'balance':
|
|
this.cachedPropagation = true;
|
|
this.setPan(this.getPan(), true);
|
|
break;
|
|
case 'costumes':
|
|
idx = this.getCostumeIdx();
|
|
this.costumes = this.exemplar.costumes;
|
|
this.doSwitchToCostume(idx, true);
|
|
this.instances.forEach(sprite => {
|
|
if (sprite.inheritsAttribute('costumes')) {
|
|
sprite.refreshInheritedAttribute('costumes');
|
|
}
|
|
});
|
|
break;
|
|
case 'sounds':
|
|
this.sounds = this.exemplar.sounds;
|
|
this.instances.forEach(sprite => {
|
|
if (sprite.inheritsAttribute('sounds')) {
|
|
sprite.refreshInheritedAttribute('sounds');
|
|
}
|
|
});
|
|
break;
|
|
case 'scripts':
|
|
this.scripts = this.exemplar.scripts;
|
|
ide = this.parentThatIsA(IDE_Morph);
|
|
if (ide) {
|
|
ide.stage.threads.stopAllForReceiver(this);
|
|
if (contains(ide.currentSprite.allExemplars(), this)) {
|
|
ide.createSpriteEditor();
|
|
ide.fixLayout('selectSprite');
|
|
}
|
|
}
|
|
this.instances.forEach(sprite => {
|
|
if (sprite.inheritsAttribute('scripts')) {
|
|
sprite.refreshInheritedAttribute('scripts');
|
|
}
|
|
});
|
|
break;
|
|
default:
|
|
nop();
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.toggleInheritanceForAttribute = function (aName) {
|
|
if (this.inheritsAttribute(aName)) {
|
|
this.shadowAttribute(aName);
|
|
} else {
|
|
this.inheritAttribute(aName);
|
|
}
|
|
};
|
|
|
|
// SpriteMorph inheritance - variables
|
|
|
|
SpriteMorph.prototype.isVariableNameInUse = function (vName, isGlobal) {
|
|
if (isGlobal) {
|
|
return contains(this.variables.allNames(), vName);
|
|
}
|
|
if (contains(this.variables.names(), vName)) {return true; }
|
|
return contains(this.globalVariables().names(), vName);
|
|
};
|
|
|
|
SpriteMorph.prototype.globalVariables = function () {
|
|
var current = this.variables.parentFrame;
|
|
while (current.owner) {
|
|
current = current.parentFrame;
|
|
}
|
|
return current;
|
|
};
|
|
|
|
SpriteMorph.prototype.shadowAllVars = function () {
|
|
this.inheritedVariableNames().forEach(name =>
|
|
this.shadowVar(name, this.variables.getVar(name))
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.shadowVar = function (name, value) {
|
|
var ide;
|
|
this.variables.addVar(name, value);
|
|
if (!this.isTemporary) {
|
|
ide = this.parentThatIsA(IDE_Morph);
|
|
if (ide) {
|
|
ide.flushBlocksCache('variables');
|
|
ide.refreshPalette();
|
|
}
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.toggleInheritedVariable = function (vName) {
|
|
if (contains(this.inheritedVariableNames(true), vName)) { // is shadowed
|
|
this.deleteVariable(vName);
|
|
} else if (contains(this.inheritedVariableNames(), vName)) { // inherited
|
|
this.shadowVar(vName, this.variables.getVar(vName));
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.inheritedVariableNames = function (shadowedOnly) {
|
|
var names = [],
|
|
own = this.variables.names(),
|
|
current = this.variables.parentFrame;
|
|
|
|
function test(each) {
|
|
return shadowedOnly ? contains(own, each) : !contains(own, each);
|
|
}
|
|
|
|
while (current.owner instanceof SpriteMorph) {
|
|
names.push.apply(
|
|
names,
|
|
current.names().filter(test)
|
|
);
|
|
current = current.parentFrame;
|
|
}
|
|
return names;
|
|
};
|
|
|
|
SpriteMorph.prototype.deletableVariableNames = function () {
|
|
var locals = this.variables.names(),
|
|
inherited = this.inheritedVariableNames();
|
|
return locals.concat(
|
|
this.globalVariables().names().filter(each =>
|
|
!contains(locals, each) && !contains(inherited, each)
|
|
)
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.hasSpriteVariable = function (varName) {
|
|
return contains(this.variables.names(), varName);
|
|
};
|
|
|
|
SpriteMorph.prototype.allLocalVariableNames = function (sorted, all) {
|
|
// "all" includes hidden ones in the palette
|
|
var exceptGlobals = this.globalVariables(),
|
|
globalNames = exceptGlobals.names(all),
|
|
data;
|
|
|
|
function alphabetically(x, y) {
|
|
return x.toLowerCase() < y.toLowerCase() ? -1 : 1;
|
|
}
|
|
|
|
data = this.variables.allNames(exceptGlobals, all).filter(each =>
|
|
!contains(globalNames, each)
|
|
);
|
|
if (sorted) {
|
|
data.sort(alphabetically);
|
|
}
|
|
return data;
|
|
};
|
|
|
|
SpriteMorph.prototype.reachableGlobalVariableNames = function (sorted, all) {
|
|
// "all" includes hidden ones in the palette
|
|
var locals = this.allLocalVariableNames(null, all),
|
|
data;
|
|
|
|
function alphabetically(x, y) {
|
|
return x.toLowerCase() < y.toLowerCase() ? -1 : 1;
|
|
}
|
|
|
|
data = this.globalVariables().names(all).filter(each =>
|
|
!contains(locals, each)
|
|
);
|
|
if (sorted) {
|
|
data.sort(alphabetically);
|
|
}
|
|
return data;
|
|
};
|
|
|
|
// SpriteMorph inheritance - custom blocks
|
|
|
|
SpriteMorph.prototype.getMethod = function (spec) {
|
|
return this.allBlocks()[spec];
|
|
};
|
|
|
|
SpriteMorph.prototype.getLocalMethod = function (spec) {
|
|
return this.ownBlocks()[spec];
|
|
};
|
|
|
|
SpriteMorph.prototype.ownBlocks = function () {
|
|
var dict = {};
|
|
this.customBlocks.forEach(def =>
|
|
dict[def.blockSpec()] = def
|
|
);
|
|
return dict;
|
|
};
|
|
|
|
SpriteMorph.prototype.allBlocks = function (valuesOnly) {
|
|
var dict = {};
|
|
this.allExemplars().reverse().forEach(sprite =>
|
|
sprite.customBlocks.forEach(def =>
|
|
dict[def.blockSpec()] = def
|
|
)
|
|
);
|
|
if (valuesOnly) {
|
|
return Object.keys(dict).map(key => dict[key]);
|
|
}
|
|
return dict;
|
|
};
|
|
|
|
SpriteMorph.prototype.inheritedBlocks = function (valuesOnly) {
|
|
var dict = {},
|
|
own = Object.keys(this.ownBlocks()),
|
|
others = this.allExemplars().reverse();
|
|
others.pop();
|
|
others.forEach(sprite =>
|
|
sprite.customBlocks.forEach(def => {
|
|
var spec = def.blockSpec();
|
|
if (!contains(own, spec)) {
|
|
dict[spec] = def;
|
|
}
|
|
})
|
|
);
|
|
if (valuesOnly) {
|
|
return Object.keys(dict).map(key => dict[key]);
|
|
}
|
|
return dict;
|
|
};
|
|
|
|
SpriteMorph.prototype.shadowAllMethods = function () {
|
|
var ide;
|
|
this.inheritedMethods().forEach(dup =>
|
|
this.customBlocks.push(dup)
|
|
);
|
|
if (!this.isTemporary) {
|
|
ide = this.parentThatIsA(IDE_Morph);
|
|
if (ide) {
|
|
ide.flushPaletteCache();
|
|
ide.refreshPalette();
|
|
}
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.inheritedMethods = function () {
|
|
// private - pre-serialization preparation
|
|
return this.inheritedBlocks(true).map(def =>
|
|
def.copyAndBindTo(this, true) // header only
|
|
);
|
|
};
|
|
|
|
// SpriteMorph thumbnail
|
|
|
|
SpriteMorph.prototype.thumbnail = function (extentPoint, recycleMe, noCorpse) {
|
|
// answer a new Canvas of extentPoint dimensions containing
|
|
// my thumbnail representation keeping the originial aspect ratio
|
|
// a "recycleMe canvas can be passed for re-use
|
|
var src = this.getImage(), // at this time sprites aren't composite morphs
|
|
w = this.width(),
|
|
h = this.height(),
|
|
scale = Math.min(
|
|
(extentPoint.x / w),
|
|
(extentPoint.y / h)
|
|
),
|
|
xOffset = (extentPoint.x - (w * scale)) / 2,
|
|
yOffset = (extentPoint.y - (h * scale)) / 2,
|
|
trg = newCanvas(extentPoint, false, recycleMe),
|
|
ctx = trg.getContext('2d');
|
|
|
|
function xOut(style, alpha, width) {
|
|
var inset = Math.min(extentPoint.x, extentPoint.y) / 10;
|
|
ctx.strokeStyle = style;
|
|
ctx.globalAlpha = alpha;
|
|
ctx.compositeOperation = 'lighter';
|
|
ctx.lineWidth = width || 1;
|
|
ctx.moveTo(inset, inset);
|
|
ctx.lineTo(trg.width - inset, trg.height - inset);
|
|
ctx.moveTo(inset, trg.height - inset);
|
|
ctx.lineTo(trg.width - inset, inset);
|
|
ctx.stroke();
|
|
}
|
|
|
|
ctx.save();
|
|
if (this.isCorpse && !noCorpse) {
|
|
ctx.globalAlpha = 0.3;
|
|
}
|
|
if (w && h && src.width && src.height) {
|
|
ctx.scale(scale, scale);
|
|
ctx.drawImage(
|
|
src,
|
|
Math.floor(xOffset / scale),
|
|
Math.floor(yOffset / scale)
|
|
);
|
|
}
|
|
if (this.isCorpse && !noCorpse) {
|
|
ctx.restore();
|
|
xOut('white', 0.8, 6);
|
|
xOut('black', 0.8, 1);
|
|
}
|
|
ctx.restore();
|
|
return trg;
|
|
};
|
|
|
|
SpriteMorph.prototype.fullThumbnail = function (extentPoint, recycleMe) {
|
|
// containing parts and anchor symbols, if any
|
|
var thumb = this.thumbnail(extentPoint, recycleMe),
|
|
ctx = thumb.getContext('2d'),
|
|
ext = extentPoint.divideBy(3),
|
|
i = 0;
|
|
|
|
ctx.restore();
|
|
if (this.anchor) {
|
|
ctx.drawImage(
|
|
this.anchor.thumbnail(ext),
|
|
0,
|
|
0
|
|
);
|
|
}
|
|
for (i = 0; i < 3; i += 1) {
|
|
if (this.parts[i]) {
|
|
ctx.drawImage(
|
|
this.parts[i].thumbnail(ext),
|
|
i * ext.x,
|
|
extentPoint.y - ext.y
|
|
);
|
|
}
|
|
}
|
|
return thumb;
|
|
};
|
|
|
|
// SpriteMorph Boolean visual representation
|
|
|
|
SpriteMorph.prototype.booleanMorph = function (bool) {
|
|
var sym = new BooleanSlotMorph(bool);
|
|
sym.isStatic = true;
|
|
sym.fixLayout();
|
|
return sym;
|
|
};
|
|
|
|
// SpriteMorph nesting
|
|
/*
|
|
simulate Morphic trees
|
|
*/
|
|
|
|
SpriteMorph.prototype.attachTo = function (aSprite) {
|
|
aSprite.attachPart(this);
|
|
};
|
|
|
|
SpriteMorph.prototype.attachPart = function (aSprite) {
|
|
var v = Date.now();
|
|
if (aSprite.anchor) {
|
|
aSprite.anchor.detachPart(aSprite);
|
|
}
|
|
this.parts.push(aSprite);
|
|
this.version = v;
|
|
aSprite.anchor = this;
|
|
this.allParts().forEach(part =>
|
|
part.nestingScale = part.scale
|
|
);
|
|
aSprite.version = v;
|
|
};
|
|
|
|
SpriteMorph.prototype.detachPart = function (aSprite) {
|
|
var idx = this.parts.indexOf(aSprite),
|
|
v;
|
|
if (idx !== -1) {
|
|
v = Date.now();
|
|
this.parts.splice(idx, 1);
|
|
this.version = v;
|
|
aSprite.anchor = null;
|
|
aSprite.version = v;
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.detachAllParts = function () {
|
|
var v = Date.now();
|
|
|
|
this.parts.forEach(part => {
|
|
part.anchor = null;
|
|
part.version = v;
|
|
});
|
|
this.parts = [];
|
|
this.version = v;
|
|
};
|
|
|
|
SpriteMorph.prototype.detachFromAnchor = function () {
|
|
if (this.anchor) {
|
|
this.anchor.detachPart(this);
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.allParts = function () {
|
|
// includes myself
|
|
var result = [this];
|
|
this.parts.forEach(part =>
|
|
result = result.concat(part.allParts())
|
|
);
|
|
return result;
|
|
};
|
|
|
|
SpriteMorph.prototype.allAnchors = function () {
|
|
// includes myself
|
|
var result = [this];
|
|
if (this.anchor !== null) {
|
|
result = result.concat(this.anchor.allAnchors());
|
|
}
|
|
return result;
|
|
};
|
|
|
|
SpriteMorph.prototype.recordLayers = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (!stage) {
|
|
this.layerCache = null;
|
|
return;
|
|
}
|
|
this.layers = this.allParts();
|
|
this.layers.forEach(part => {
|
|
var bubble = part.talkBubble();
|
|
if (bubble) {bubble.hide(); }
|
|
});
|
|
this.layers.sort((x, y) =>
|
|
stage.children.indexOf(x) < stage.children.indexOf(y) ?
|
|
-1 : 1
|
|
);
|
|
};
|
|
|
|
SpriteMorph.prototype.restoreLayers = function () {
|
|
if (this.layers && this.layers.length > 1) {
|
|
this.layers.forEach(sprite => {
|
|
sprite.comeToFront();
|
|
sprite.positionTalkBubble();
|
|
});
|
|
}
|
|
this.layers = null;
|
|
};
|
|
|
|
// SpriteMorph destroying
|
|
|
|
SpriteMorph.prototype.destroy = function () {
|
|
// make sure to sever all inheritance ties to other sprites
|
|
if (this.anchor) {
|
|
this.anchor.detachPart(this);
|
|
}
|
|
this.emancipate();
|
|
if (!this.isTemporary) {
|
|
this.prune();
|
|
}
|
|
SpriteMorph.uber.destroy.call(this);
|
|
};
|
|
|
|
// SpriteMorph highlighting
|
|
|
|
SpriteMorph.prototype.flash = function () {
|
|
var world = this.world();
|
|
this.addHighlight();
|
|
world.animations.push(new Animation(
|
|
nop,
|
|
nop,
|
|
0,
|
|
800,
|
|
nop,
|
|
() => this.removeHighlight()
|
|
));
|
|
};
|
|
|
|
SpriteMorph.prototype.addHighlight = function (oldHighlight) {
|
|
var isHidden = !this.isVisible,
|
|
highlight;
|
|
|
|
if (isHidden) {this.show(); }
|
|
highlight = this.highlight(
|
|
oldHighlight ? oldHighlight.color : this.highlightColor,
|
|
this.highlightBorder
|
|
);
|
|
this.addBack(highlight);
|
|
this.fullChanged();
|
|
if (isHidden) {this.hide(); }
|
|
return highlight;
|
|
};
|
|
|
|
SpriteMorph.prototype.removeHighlight = function () {
|
|
var highlight = this.getHighlight();
|
|
if (highlight !== null) {
|
|
this.fullChanged();
|
|
this.removeChild(highlight);
|
|
}
|
|
return highlight;
|
|
};
|
|
|
|
SpriteMorph.prototype.toggleHighlight = function () {
|
|
if (this.getHighlight()) {
|
|
this.removeHighlight();
|
|
} else {
|
|
this.addHighlight();
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.highlight = function (color, border) {
|
|
var highlight = new SpriteHighlightMorph(),
|
|
fb = this.bounds, // sprites are not nested in a Morphic way
|
|
edge = border,
|
|
ctx;
|
|
|
|
highlight.bounds.setExtent(fb.extent().add(edge * 2));
|
|
highlight.color = color;
|
|
highlight.cachedImage = this.highlightImage(color, border);
|
|
ctx = highlight.cachedImage.getContext('2d');
|
|
ctx.drawImage(
|
|
this.highlightImage(WHITE, 4),
|
|
border - 4,
|
|
border - 4
|
|
);
|
|
ctx.drawImage(
|
|
this.highlightImage(new Color(50, 50, 50), 2),
|
|
border - 2,
|
|
border - 2
|
|
);
|
|
ctx.drawImage(
|
|
this.highlightImage(WHITE, 1),
|
|
border - 1,
|
|
border - 1
|
|
);
|
|
highlight.setPosition(fb.origin.subtract(new Point(edge, edge)));
|
|
return highlight;
|
|
};
|
|
|
|
SpriteMorph.prototype.highlightImage = function (color, border) {
|
|
var fb, img, hi, ctx, out;
|
|
fb = this.extent();
|
|
img = this.getImage();
|
|
|
|
hi = newCanvas(fb.add(border * 2));
|
|
ctx = hi.getContext('2d');
|
|
|
|
ctx.drawImage(img, 0, 0);
|
|
ctx.drawImage(img, border, 0);
|
|
ctx.drawImage(img, border * 2, 0);
|
|
ctx.drawImage(img, border * 2, border);
|
|
ctx.drawImage(img, border * 2, border * 2);
|
|
ctx.drawImage(img, border, border * 2);
|
|
ctx.drawImage(img, 0, border * 2);
|
|
ctx.drawImage(img, 0, border);
|
|
|
|
ctx.globalCompositeOperation = 'destination-out';
|
|
ctx.drawImage(img, border, border);
|
|
|
|
out = newCanvas(fb.add(border * 2));
|
|
ctx = out.getContext('2d');
|
|
ctx.drawImage(hi, 0, 0);
|
|
ctx.globalCompositeOperation = 'source-atop';
|
|
ctx.fillStyle = color.toString();
|
|
ctx.fillRect(0, 0, out.width, out.height);
|
|
|
|
return out;
|
|
};
|
|
|
|
SpriteMorph.prototype.getHighlight = function () {
|
|
var highlights = this.children.slice(0).reverse().filter(child =>
|
|
child instanceof SpriteHighlightMorph
|
|
);
|
|
if (highlights.length !== 0) {
|
|
return highlights[0];
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// SpriteMorph nesting events
|
|
|
|
SpriteMorph.prototype.mouseEnterDragging = function () {
|
|
var obj;
|
|
if (!this.enableNesting) {return; }
|
|
obj = this.world().hand.children[0];
|
|
if (this.wantsDropOf(obj)) {
|
|
this.addHighlight();
|
|
}
|
|
};
|
|
|
|
SpriteMorph.prototype.mouseLeave = function () {
|
|
this.receiveUserInteraction('mouse-departed');
|
|
if (!this.enableNesting) {return; }
|
|
this.removeHighlight();
|
|
};
|
|
|
|
SpriteMorph.prototype.wantsDropOf = function (morph) {
|
|
// allow myself to be the anchor of another sprite
|
|
// by drag & drop
|
|
return this.enableNesting
|
|
&& morph instanceof SpriteIconMorph
|
|
&& !contains(morph.object.allParts(), this);
|
|
};
|
|
|
|
SpriteMorph.prototype.reactToDropOf = function (morph, hand) {
|
|
this.removeHighlight();
|
|
this.attachPart(morph.object);
|
|
this.world().add(morph);
|
|
morph.slideBackTo(hand.grabOrigin);
|
|
};
|
|
|
|
// SpriteMorph screenshots
|
|
|
|
SpriteMorph.prototype.newCostumeName = function (name, ignoredCostume) {
|
|
var ix = name.indexOf('('),
|
|
stem = (ix < 0) ? name : name.substring(0, ix),
|
|
count = 1,
|
|
newName = stem,
|
|
all = this.costumes.asArray().filter(each =>
|
|
each !== ignoredCostume
|
|
).map(each => each.name);
|
|
while (contains(all, newName)) {
|
|
count += 1;
|
|
newName = stem + '(' + count + ')';
|
|
}
|
|
return newName;
|
|
};
|
|
|
|
SpriteMorph.prototype.doScreenshot = function (imgSource, data) {
|
|
var canvas,
|
|
stage = this.parentThatIsA(StageMorph),
|
|
costume;
|
|
data = this.newCostumeName(data);
|
|
if (imgSource[0] === undefined) {
|
|
return;
|
|
}
|
|
if (imgSource[0] === "pen trails") {
|
|
canvas = stage.trailsCanvas;
|
|
costume = new Costume(canvas, data).copy(); // prevent mutation
|
|
} else if (imgSource[0] === "stage image") {
|
|
canvas = stage.fullImage();
|
|
costume = new Costume(canvas, data);
|
|
}
|
|
this.addCostume(costume);
|
|
};
|
|
|
|
// SpriteMorph adding sounds
|
|
|
|
SpriteMorph.prototype.newSoundName = function (name, ignoredSound) {
|
|
var ix = name.indexOf('('),
|
|
stem = (ix < 0) ? name : name.substring(0, ix),
|
|
count = 1,
|
|
newName = stem,
|
|
all = this.sounds.asArray().filter(each =>
|
|
each !== ignoredSound
|
|
).map(each => each.name);
|
|
while (contains(all, newName)) {
|
|
count += 1;
|
|
newName = stem + '(' + count + ')';
|
|
}
|
|
return newName;
|
|
};
|
|
|
|
// SpriteHighlightMorph /////////////////////////////////////////////////
|
|
|
|
// SpriteHighlightMorph inherits from Morph:
|
|
|
|
SpriteHighlightMorph.prototype = new Morph();
|
|
SpriteHighlightMorph.prototype.constructor = SpriteHighlightMorph;
|
|
SpriteHighlightMorph.uber = Morph.prototype;
|
|
|
|
// SpriteHighlightMorph instance creation:
|
|
|
|
function SpriteHighlightMorph() {
|
|
this.init();
|
|
}
|
|
|
|
SpriteHighlightMorph.prototype.init = function () {
|
|
SpriteHighlightMorph.uber.init.call(this);
|
|
this.isCachingImage = true;
|
|
};
|
|
|
|
// StageMorph /////////////////////////////////////////////////////////
|
|
|
|
/*
|
|
I inherit from FrameMorph and copy from SpriteMorph.
|
|
*/
|
|
|
|
// StageMorph inherits from FrameMorph:
|
|
|
|
StageMorph.prototype = new FrameMorph();
|
|
StageMorph.prototype.constructor = StageMorph;
|
|
StageMorph.uber = FrameMorph.prototype;
|
|
|
|
// StageMorph preferences settings
|
|
|
|
StageMorph.prototype.dimensions = new Point(480, 360); // fallback unscaled ext
|
|
|
|
StageMorph.prototype.isCachingPrimitives
|
|
= SpriteMorph.prototype.isCachingPrimitives;
|
|
|
|
StageMorph.prototype.sliderColor
|
|
= SpriteMorph.prototype.sliderColor;
|
|
|
|
StageMorph.prototype.paletteTextColor
|
|
= SpriteMorph.prototype.paletteTextColor;
|
|
|
|
StageMorph.prototype.hiddenPrimitives = {};
|
|
StageMorph.prototype.codeMappings = {};
|
|
StageMorph.prototype.codeHeaders = {};
|
|
StageMorph.prototype.enableCodeMapping = false;
|
|
StageMorph.prototype.enableInheritance = true;
|
|
StageMorph.prototype.enableSublistIDs = false;
|
|
StageMorph.prototype.enablePenLogging = false; // for SVG generation
|
|
|
|
// StageMorph instance creation
|
|
|
|
function StageMorph(globals) {
|
|
this.init(globals);
|
|
}
|
|
|
|
StageMorph.prototype.init = function (globals) {
|
|
this.name = localize('Stage');
|
|
this.dimensions = new Point(480, 360); // unscaled extent
|
|
this.instrument = null;
|
|
this.threads = new ThreadManager();
|
|
this.variables = new VariableFrame(globals || null, this);
|
|
this.scripts = new ScriptsMorph();
|
|
this.customBlocks = [];
|
|
this.globalBlocks = [];
|
|
this.costumes = new List();
|
|
this.costumes.type = 'costume';
|
|
this.costume = null;
|
|
this.sounds = new List();
|
|
this.sounds.type = 'sound';
|
|
this.version = Date.now(); // for observers
|
|
this.isFastTracked = false;
|
|
this.enableCustomHatBlocks = true;
|
|
this.cloneCount = 0;
|
|
|
|
this.timerStart = Date.now();
|
|
this.tempo = 60; // bpm
|
|
this.lastMessage = '';
|
|
|
|
// volume and stereo-pan support, experimental:
|
|
this.volume = 100;
|
|
this.gainNode = null; // must be lazily initialized in Chrome, sigh...
|
|
this.pan = 0;
|
|
this.pannerNode = null; // must be lazily initialized in Chrome, sigh...
|
|
|
|
// frequency player, experimental
|
|
this.freqPlayer = null; // Note, to be lazily initialized
|
|
|
|
this.watcherUpdateFrequency = 2;
|
|
this.lastWatcherUpdate = Date.now();
|
|
|
|
this.scale = 1; // for display modes, do not persist
|
|
|
|
this.cachedColorDimensions = [0, 0, 0]; // bg color support, not serialized
|
|
|
|
this.keysPressed = {}; // for handling keyboard events, do not persist
|
|
this.primitivesCache = {}; // not to be serialized (!)
|
|
this.paletteCache = {}; // not to be serialized (!)
|
|
this.categoriesCache = null; // not to be serialized (!)
|
|
this.lastAnswer = ''; // last user input, do not persist
|
|
this.activeSounds = []; // do not persist
|
|
|
|
this.trailsCanvas = null;
|
|
this.trailsLog = []; // each line being [p1, p2, color, width, cap]
|
|
this.isThreadSafe = false;
|
|
|
|
this.microphone = new Microphone(); // audio input, do not persist
|
|
|
|
this.graphicsValues = {
|
|
'color': 0,
|
|
'fisheye': 0,
|
|
'whirl': 0,
|
|
'pixelate': 0,
|
|
'mosaic': 0,
|
|
'duplicate': 0,
|
|
'negative': 0,
|
|
'comic': 0,
|
|
'confetti': 0,
|
|
'saturation': 0,
|
|
'brightness': 0
|
|
};
|
|
|
|
this.cachedPenTrailsMorph = null; // optimization, do not persist
|
|
|
|
this.remixID = null;
|
|
|
|
// projection layer - for video, maps, 3D extensions etc., transient
|
|
this.projectionSource = null; // offscreen DOM element for video, maps, 3D
|
|
this.getProjectionImage = null; // function to return a blittable image
|
|
this.stopProjectionSource = null; // function to turn off video stream etc.
|
|
this.continuousProjection = false; // turn ON for video
|
|
this.projectionCanvas = null;
|
|
this.projectionTransparency = 50;
|
|
|
|
// video motion detection, transient
|
|
this.mirrorVideo = true;
|
|
this.videoMotion = null;
|
|
|
|
// world map client - experimental, transient
|
|
this.worldMap = new WorldMap();
|
|
|
|
// Snap! API event listeners - experimental, transient
|
|
this.messageCallbacks = {}; // name : [functions]
|
|
|
|
StageMorph.uber.init.call(this);
|
|
|
|
this.setExtent(this.dimensions);
|
|
this.isCachingImage = true;
|
|
this.cachedColorDimensions = this.color[
|
|
SpriteMorph.prototype.penColorModel
|
|
]();
|
|
this.acceptsDrops = false;
|
|
this.setColor(new Color(255, 255, 255));
|
|
};
|
|
|
|
// StageMorph scaling
|
|
|
|
StageMorph.prototype.setScale = function (number) {
|
|
var delta = number / this.scale,
|
|
pos = this.position(),
|
|
relativePos,
|
|
bubble;
|
|
|
|
if (delta === 1) {return; }
|
|
this.cachedPenTrailsMorph = null;
|
|
this.scale = number;
|
|
this.setExtent(this.dimensions.multiplyBy(number));
|
|
|
|
// now move and resize all children - sprites, bubbles, watchers etc..
|
|
this.children.forEach(morph => {
|
|
relativePos = morph.position().subtract(pos);
|
|
morph.fixLayout();
|
|
morph.setPosition(
|
|
relativePos.multiplyBy(delta).add(pos),
|
|
true // just me (for nested sprites)
|
|
);
|
|
if (morph instanceof SpriteMorph) {
|
|
morph.rerender();
|
|
bubble = morph.talkBubble();
|
|
if (bubble) {
|
|
bubble.setScale(number);
|
|
morph.positionTalkBubble();
|
|
}
|
|
} else if (morph instanceof StagePrompterMorph) {
|
|
if (this.scale < 1) {
|
|
morph.setWidth(this.width() - 10);
|
|
} else {
|
|
morph.setWidth(this.dimensions.x - 20);
|
|
}
|
|
morph.setCenter(this.center());
|
|
morph.setBottom(this.bottom());
|
|
}
|
|
});
|
|
};
|
|
|
|
StageMorph.prototype.moveBy = function (delta) {
|
|
// override the inherited method to skip attached sprite parts,
|
|
// because they are also level-1 children of the stage and thus
|
|
// will be moved individually
|
|
var children = this.children,
|
|
i = children.length;
|
|
this.changed();
|
|
this.bounds = this.bounds.translateBy(delta);
|
|
this.changed();
|
|
for (i; i > 0; i -= 1) {
|
|
children[i - 1].moveBy(delta, true); // justMe - skip sprite parts
|
|
}
|
|
};
|
|
|
|
// StageMorph rendering
|
|
|
|
StageMorph.prototype.render = function (ctx) {
|
|
ctx.save();
|
|
ctx.fillStyle = this.color.toString();
|
|
ctx.fillRect(0, 0, this.width(), this.height());
|
|
if (this.costume && !(this.costume.loaded instanceof Function)) {
|
|
ctx.scale(this.scale, this.scale);
|
|
ctx.drawImage(
|
|
this.costume.contents,
|
|
(this.width() / this.scale - this.costume.width()) / 2,
|
|
(this.height() / this.scale - this.costume.height()) / 2
|
|
);
|
|
this.cachedImage = this.applyGraphicsEffects(this.cachedImage);
|
|
}
|
|
ctx.restore();
|
|
this.version = Date.now(); // for observer optimization
|
|
};
|
|
|
|
StageMorph.prototype.drawOn = function (ctx, rect) {
|
|
// draw pen trails and webcam layers
|
|
var clipped = rect.intersect(this.bounds),
|
|
pos, src, w, h, sl, st, ws, hs;
|
|
|
|
if (!this.isVisible || !clipped.extent().gt(ZERO)) {
|
|
return;
|
|
}
|
|
|
|
// costume, if any, and background color
|
|
StageMorph.uber.drawOn.call(this, ctx, rect);
|
|
|
|
pos = this.position();
|
|
src = clipped.translateBy(pos.neg());
|
|
sl = src.left();
|
|
st = src.top();
|
|
w = src.width();
|
|
h = src.height();
|
|
ws = w / this.scale;
|
|
hs = h / this.scale;
|
|
|
|
ctx.save();
|
|
ctx.scale(this.scale, this.scale);
|
|
|
|
// projection layer (e.g. webcam)
|
|
if (this.projectionSource) {
|
|
ctx.globalAlpha = 1 - (this.projectionTransparency / 100);
|
|
ctx.drawImage(
|
|
this.projectionLayer(),
|
|
sl / this.scale,
|
|
st / this.scale,
|
|
ws,
|
|
hs,
|
|
clipped.left() / this.scale,
|
|
clipped.top() / this.scale,
|
|
ws,
|
|
hs
|
|
);
|
|
this.version = Date.now(); // update watcher icons
|
|
}
|
|
|
|
// pen trails
|
|
ctx.globalAlpha = 1;
|
|
ctx.drawImage(
|
|
this.penTrails(),
|
|
sl / this.scale,
|
|
st / this.scale,
|
|
ws,
|
|
hs,
|
|
clipped.left() / this.scale,
|
|
clipped.top() / this.scale,
|
|
ws,
|
|
hs
|
|
);
|
|
|
|
ctx.restore();
|
|
};
|
|
|
|
StageMorph.prototype.clearPenTrails = function () {
|
|
this.cachedPenTrailsMorph = null;
|
|
this.trailsCanvas = newCanvas(this.dimensions, null, this.trailsCanvas);
|
|
this.trailsLog = [];
|
|
this.changed();
|
|
};
|
|
|
|
StageMorph.prototype.penTrails = function () {
|
|
if (!this.trailsCanvas) {
|
|
this.trailsCanvas = newCanvas(this.dimensions);
|
|
}
|
|
return this.trailsCanvas;
|
|
};
|
|
|
|
StageMorph.prototype.penTrailsMorph = function () {
|
|
// for collision detection purposes
|
|
var morph, trails, ctx;
|
|
|
|
if (this.cachedPenTrailsMorph) {
|
|
return this.cachedPenTrailsMorph;
|
|
}
|
|
morph = new Morph();
|
|
morph.isCachingImage = true;
|
|
trails = this.penTrails();
|
|
morph.bounds = this.bounds.copy();
|
|
morph.cachedImage = newCanvas(this.extent(), true);
|
|
ctx = morph.cachedImage.getContext('2d');
|
|
ctx.drawImage(
|
|
trails,
|
|
0,
|
|
0,
|
|
trails.width,
|
|
trails.height,
|
|
0,
|
|
0,
|
|
this.width(),
|
|
this.height()
|
|
);
|
|
this.cachedPenTrailsMorph = morph;
|
|
return morph;
|
|
};
|
|
|
|
StageMorph.prototype.projectionLayer = function () {
|
|
if (!this.projectionCanvas) {
|
|
this.projectionCanvas = newCanvas(this.dimensions, true);
|
|
}
|
|
return this.projectionCanvas;
|
|
};
|
|
|
|
StageMorph.prototype.clearProjectionLayer = function () {
|
|
this.projectionCanvas = null;
|
|
this.changed();
|
|
};
|
|
|
|
// StageMorph video capture
|
|
|
|
StageMorph.prototype.startVideo = function() {
|
|
var myself = this;
|
|
|
|
function noCameraSupport() {
|
|
var dialog = new DialogBoxMorph();
|
|
dialog.inform(
|
|
localize('Camera not supported'),
|
|
localize('Please make sure your web browser is up to date\n' +
|
|
'and your camera is properly configured. \n\n' +
|
|
'Some browsers also require you to access Snap!\n' +
|
|
'through HTTPS to use the camera.\n\n' +
|
|
'Please replace the "http://" part of the address\n' +
|
|
'in your browser by "https://" and try again.'),
|
|
this.world
|
|
);
|
|
dialog.fixLayout();
|
|
if (myself.projectionSource) {
|
|
myself.projectionSource.remove();
|
|
myself.projectionSource = null;
|
|
}
|
|
}
|
|
if (this.projectionSource) { // video capture has already been started
|
|
return;
|
|
}
|
|
|
|
this.projectionSource = document.createElement('video');
|
|
this.projectionSource.width = this.dimensions.x;
|
|
this.projectionSource.height = this.dimensions.y;
|
|
this.projectionSource.hidden = true;
|
|
document.body.appendChild(this.projectionSource);
|
|
if (!this.videoMotion) {
|
|
this.videoMotion = new VideoMotion(
|
|
this.dimensions.x,
|
|
this.dimensions.y
|
|
);
|
|
}
|
|
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
navigator.mediaDevices.getUserMedia({ video: true })
|
|
.then(function(stream) {
|
|
myself.getProjectionImage = myself.getVideoImage;
|
|
myself.stopProjectionSource = myself.stopVideo;
|
|
myself.continuousProjection = true;
|
|
myself.projectionSource.srcObject = stream;
|
|
myself.projectionSource.play().catch(noCameraSupport);
|
|
myself.projectionSource.stream = stream;
|
|
})
|
|
.catch(noCameraSupport);
|
|
}
|
|
};
|
|
|
|
StageMorph.prototype.getVideoImage = function () {
|
|
return this.projectionSource;
|
|
};
|
|
|
|
StageMorph.prototype.stopVideo = function() {
|
|
if (this.projectionSource && this.projectionSource.stream) {
|
|
this.projectionSource.stream.getTracks().forEach(track =>
|
|
track.stop()
|
|
);
|
|
}
|
|
this.videoMotion = null;
|
|
};
|
|
|
|
StageMorph.prototype.stopProjection = function() {
|
|
if (this.projectionSource) {
|
|
this.stopProjectionSource();
|
|
this.projectionSource.remove();
|
|
this.projectionSource = null;
|
|
this.continuousProjection = false;
|
|
}
|
|
this.clearProjectionLayer();
|
|
};
|
|
|
|
StageMorph.prototype.projectionSnap = function() {
|
|
var snap = newCanvas(this.dimensions, true),
|
|
ctx = snap.getContext('2d');
|
|
ctx.drawImage(this.projectionLayer(), 0, 0);
|
|
return new Costume(snap, this.newCostumeName(localize('snap')));
|
|
};
|
|
|
|
// StageMorph pixel access:
|
|
|
|
StageMorph.prototype.getPixelColor = function (aPoint) {
|
|
var point, context, data;
|
|
if (this.trailsCanvas) {
|
|
point = aPoint.subtract(this.bounds.origin);
|
|
context = this.penTrailsMorph().getImage().getContext('2d');
|
|
data = context.getImageData(point.x, point.y, 1, 1);
|
|
if (data.data[3] === 0) {
|
|
if (this.projectionCanvas) {
|
|
point = point.divideBy(this.scale);
|
|
context = this.projectionCanvas.getContext('2d');
|
|
data = context.getImageData(point.x, point.y, 1, 1);
|
|
return new Color(
|
|
data.data[0],
|
|
data.data[1],
|
|
data.data[2],
|
|
data.data[3] / 255
|
|
);
|
|
}
|
|
return StageMorph.uber.getPixelColor.call(this, aPoint);
|
|
}
|
|
return new Color(
|
|
data.data[0],
|
|
data.data[1],
|
|
data.data[2],
|
|
data.data[3] / 255
|
|
);
|
|
}
|
|
};
|
|
|
|
// StageMorph accessing
|
|
|
|
StageMorph.prototype.watchers = function (leftPos) {
|
|
/*
|
|
answer an array of all currently visible watchers.
|
|
If leftPos is specified, filter the list for all
|
|
shown or hidden watchers whose left side equals
|
|
the given border (for automatic positioning)
|
|
*/
|
|
return this.children.filter(morph => {
|
|
if (morph instanceof WatcherMorph) {
|
|
if (leftPos) {
|
|
return morph.left() === leftPos;
|
|
}
|
|
return morph.isVisible;
|
|
}
|
|
return false;
|
|
});
|
|
};
|
|
|
|
// StageMorph timer
|
|
|
|
StageMorph.prototype.resetTimer = function () {
|
|
this.timerStart = Date.now();
|
|
};
|
|
|
|
StageMorph.prototype.getTimer = function () {
|
|
var elapsed = Math.floor((Date.now() - this.timerStart) / 100);
|
|
return elapsed / 10;
|
|
};
|
|
|
|
// StageMorph tempo
|
|
|
|
StageMorph.prototype.setTempo = function (bpm) {
|
|
this.tempo = Math.max(20, (+bpm || 0));
|
|
};
|
|
|
|
StageMorph.prototype.changeTempo = function (delta) {
|
|
this.setTempo(this.getTempo() + (+delta || 0));
|
|
};
|
|
|
|
StageMorph.prototype.getTempo = function () {
|
|
return +this.tempo;
|
|
};
|
|
|
|
// StageMorph messages
|
|
|
|
StageMorph.prototype.getLastMessage = function () {
|
|
return this.lastMessage || '';
|
|
};
|
|
|
|
// StageMorph Mouse Coordinates
|
|
|
|
StageMorph.prototype.reportMouseX = function () {
|
|
var world = this.world();
|
|
if (world) {
|
|
return (world.hand.position().x - this.center().x) / this.scale;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
StageMorph.prototype.reportMouseY = function () {
|
|
var world = this.world();
|
|
if (world) {
|
|
return (this.center().y - world.hand.position().y) / this.scale;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
// StageMorph drag & drop
|
|
|
|
StageMorph.prototype.wantsDropOf = function (aMorph) {
|
|
return aMorph instanceof SpriteMorph ||
|
|
aMorph instanceof WatcherMorph ||
|
|
aMorph instanceof ListWatcherMorph ||
|
|
aMorph instanceof SpriteIconMorph;
|
|
};
|
|
|
|
StageMorph.prototype.reactToDropOf = function (morph, hand) {
|
|
if (morph instanceof SpriteIconMorph) { // detach sprite from anchor
|
|
if (morph.object.anchor) {
|
|
morph.object.anchor.detachPart(morph.object);
|
|
}
|
|
this.world().add(morph);
|
|
morph.slideBackTo(hand.grabOrigin);
|
|
}
|
|
};
|
|
|
|
// StageMorph stepping
|
|
|
|
StageMorph.prototype.step = function () {
|
|
var current, elapsed, leftover, ide, world = this.world();
|
|
|
|
// handle keyboard events
|
|
if (world.keyboardFocus === null) {
|
|
world.keyboardFocus = this;
|
|
}
|
|
if (world.currentKey === null) {
|
|
this.keyPressed = null;
|
|
}
|
|
|
|
// manage threads
|
|
if (this.enableCustomHatBlocks) {
|
|
this.stepGenericConditions();
|
|
}
|
|
if (this.isFastTracked && this.threads.processes.length) {
|
|
while (this.isFastTracked && (Date.now() - this.lastTime) < 15) {
|
|
this.threads.step(); // approx. 67 fps
|
|
}
|
|
this.changed();
|
|
} else {
|
|
this.threads.step();
|
|
|
|
// single-stepping hook:
|
|
if (this.threads.wantsToPause) {
|
|
ide = this.parentThatIsA(IDE_Morph);
|
|
if (ide) {
|
|
ide.controlBar.pauseButton.refresh();
|
|
}
|
|
}
|
|
}
|
|
|
|
// update watchers
|
|
current = Date.now();
|
|
elapsed = current - this.lastWatcherUpdate;
|
|
leftover = (1000 / this.watcherUpdateFrequency) - elapsed;
|
|
if (leftover < 1) {
|
|
this.watchers().forEach(w => w.update());
|
|
this.lastWatcherUpdate = Date.now();
|
|
}
|
|
|
|
// projection layer update (e.g. video frame capture)
|
|
if (this.continuousProjection && this.projectionSource) {
|
|
this.updateProjection();
|
|
}
|
|
};
|
|
|
|
StageMorph.prototype.updateProjection = function () {
|
|
var context = this.projectionLayer().getContext('2d');
|
|
context.save();
|
|
if (this.mirrorVideo) {
|
|
context.translate(this.dimensions.x, 0);
|
|
context.scale(-1, 1);
|
|
}
|
|
context.drawImage(
|
|
this.getProjectionImage(),
|
|
0,
|
|
0,
|
|
this.projectionSource.width,
|
|
this.projectionSource.height
|
|
);
|
|
if (this.videoMotion) {
|
|
this.videoMotion.addFrame(
|
|
context.getImageData(
|
|
0,
|
|
0,
|
|
this.projectionSource.width,
|
|
this.projectionSource.height
|
|
).data
|
|
);
|
|
}
|
|
context.restore();
|
|
this.changed();
|
|
};
|
|
|
|
StageMorph.prototype.stepGenericConditions = function (stopAll) {
|
|
var hatCount = 0,
|
|
ide;
|
|
this.children.concat(this).forEach(morph => {
|
|
if (isSnapObject(morph)) {
|
|
morph.allGenericHatBlocks().forEach(block => {
|
|
hatCount += 1;
|
|
this.threads.doWhen(block, morph, stopAll);
|
|
});
|
|
}
|
|
});
|
|
if (!hatCount) {
|
|
this.enableCustomHatBlocks = false;
|
|
ide = this.parentThatIsA(IDE_Morph);
|
|
if (ide) {
|
|
ide.controlBar.stopButton.refresh();
|
|
}
|
|
}
|
|
};
|
|
|
|
StageMorph.prototype.developersMenu = function () {
|
|
var menu = StageMorph.uber.developersMenu.call(this);
|
|
menu.addItem(
|
|
"stop",
|
|
() => this.threads.stopAll(),
|
|
'terminate all running threads'
|
|
);
|
|
return menu;
|
|
};
|
|
|
|
// StageMorph keyboard events
|
|
|
|
StageMorph.prototype.processKeyDown = function (event) {
|
|
this.processKeyEvent(
|
|
event,
|
|
this.fireKeyEvent
|
|
);
|
|
};
|
|
|
|
StageMorph.prototype.processKeyUp = function (event) {
|
|
this.processKeyEvent(
|
|
event,
|
|
this.removePressedKey
|
|
);
|
|
};
|
|
|
|
StageMorph.prototype.processKeyEvent = function (event, action) {
|
|
var keyName;
|
|
|
|
// this.inspectKeyEvent(event);
|
|
switch (event.keyCode) {
|
|
case 13:
|
|
keyName = 'enter';
|
|
if (event.ctrlKey || event.metaKey) {
|
|
keyName = 'ctrl enter';
|
|
} else if (event.shiftKey) {
|
|
keyName = 'shift enter';
|
|
}
|
|
break;
|
|
case 27:
|
|
keyName = 'esc';
|
|
break;
|
|
case 32:
|
|
keyName = 'space';
|
|
break;
|
|
case 37:
|
|
keyName = 'left arrow';
|
|
break;
|
|
case 39:
|
|
keyName = 'right arrow';
|
|
break;
|
|
case 38:
|
|
keyName = 'up arrow';
|
|
break;
|
|
case 40:
|
|
keyName = 'down arrow';
|
|
break;
|
|
default:
|
|
keyName = event.key || String.fromCharCode(
|
|
event.keyCode || event.charCode
|
|
);
|
|
if (event.ctrlKey || event.metaKey) {
|
|
keyName =
|
|
(keyName === 'Control' || keyName === 'Meta' ? '' : 'ctrl ') +
|
|
(event.shiftKey ? 'shift ' : '') + keyName;
|
|
}
|
|
}
|
|
action.call(this, keyName);
|
|
};
|
|
|
|
StageMorph.prototype.fireKeyEvent = function (key) {
|
|
var evt = key.toLowerCase(),
|
|
procs = [],
|
|
ide = this.parentThatIsA(IDE_Morph);
|
|
|
|
this.keysPressed[evt] = true;
|
|
if (evt === 'ctrl enter' && !ide.isAppMode) {
|
|
return this.fireGreenFlagEvent();
|
|
}
|
|
if (evt === 'shift enter') {
|
|
return this.editScripts();
|
|
}
|
|
if (evt === 'ctrl f') {
|
|
if (!ide.isAppMode) {ide.currentSprite.searchBlocks(); }
|
|
return;
|
|
}
|
|
if (evt === 'ctrl z') {
|
|
if (!ide.isAppMode) {ide.currentSprite.scripts.undrop(); }
|
|
return;
|
|
}
|
|
if (evt === 'ctrl shift z' || (evt === 'ctrl y')) {
|
|
if (!ide.isAppMode) {ide.currentSprite.scripts.redrop(); }
|
|
return;
|
|
}
|
|
if (evt === 'ctrl n') {
|
|
if (!ide.isAppMode) {ide.createNewProject(); }
|
|
return;
|
|
}
|
|
if (evt === 'ctrl o') {
|
|
if (!ide.isAppMode) {ide.openProjectsBrowser(); }
|
|
return;
|
|
}
|
|
if (evt === 'ctrl s') {
|
|
if (!ide.isAppMode) {ide.save(); }
|
|
return;
|
|
}
|
|
if (evt === 'ctrl shift s') {
|
|
if (!ide.isAppMode) {return ide.saveProjectsBrowser(); }
|
|
return;
|
|
}
|
|
if (evt === 'esc' && !ide.isAppMode) {
|
|
return this.fireStopAllEvent();
|
|
}
|
|
this.children.concat(this).forEach(morph => {
|
|
if (isSnapObject(morph)) {
|
|
morph.allHatBlocksForKey(evt).forEach(block => {
|
|
var varName = block.inputs()[1].evaluate()[0],
|
|
varFrame;
|
|
if (varName) {
|
|
varFrame = new VariableFrame();
|
|
varFrame.addVar(
|
|
varName,
|
|
key === 'space' ? ' ' : key // not lowercased
|
|
);
|
|
}
|
|
procs.push(this.threads.startProcess(
|
|
block,
|
|
morph,
|
|
true, // ignore running scripts, was: myself.isThreadSafe
|
|
null, // exportResult (bool)
|
|
null, // callback
|
|
null, // isClicked
|
|
null, // rightAway
|
|
null, // atomic
|
|
varFrame
|
|
));
|
|
});
|
|
}
|
|
});
|
|
return procs;
|
|
};
|
|
|
|
StageMorph.prototype.removePressedKey = function (key) {
|
|
delete this.keysPressed[key.toLowerCase()];
|
|
};
|
|
|
|
StageMorph.prototype.processKeyPress = function (event) {
|
|
nop(event);
|
|
};
|
|
|
|
StageMorph.prototype.inspectKeyEvent
|
|
= CursorMorph.prototype.inspectKeyEvent;
|
|
|
|
StageMorph.prototype.fireChangeOfSceneEvent = function (message) {
|
|
var procs = [];
|
|
|
|
// remove all clones when the green flag event is broadcast
|
|
if (message === '__shout__go__') {
|
|
this.removeAllClones();
|
|
}
|
|
|
|
this.children.concat(this).forEach(morph => {
|
|
if (isSnapObject(morph)) {
|
|
morph.allHatBlocksFor(message).forEach(block => {
|
|
var varName, varFrame;
|
|
if (block.selector === 'receiveMessage') {
|
|
varName = block.inputs()[1].evaluate()[0];
|
|
if (varName) {
|
|
varFrame = new VariableFrame();
|
|
varFrame.addVar(varName, message);
|
|
}
|
|
procs.push(this.threads.startProcess(
|
|
block,
|
|
morph,
|
|
this.isThreadSafe || // make "any msg" threadsafe
|
|
block.inputs()[0].evaluate() instanceof Array,
|
|
null, // exportResult (bool)
|
|
null, // callback
|
|
null, // isClicked
|
|
null, // rightAway
|
|
null, // atomic
|
|
varFrame
|
|
));
|
|
} else {
|
|
procs.push(this.threads.startProcess(
|
|
block,
|
|
morph,
|
|
this.isThreadSafe
|
|
));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
return procs;
|
|
};
|
|
|
|
StageMorph.prototype.fireGreenFlagEvent = function () {
|
|
var procs = [],
|
|
ide = this.parentThatIsA(IDE_Morph);
|
|
|
|
this.removeAllClones();
|
|
this.children.concat(this).forEach(morph => {
|
|
if (isSnapObject(morph)) {
|
|
morph.allHatBlocksFor('__shout__go__').forEach(block =>
|
|
procs.push(this.threads.startProcess(
|
|
block,
|
|
morph,
|
|
this.isThreadSafe
|
|
))
|
|
);
|
|
}
|
|
});
|
|
if (ide) {
|
|
ide.controlBar.pauseButton.refresh();
|
|
}
|
|
return procs;
|
|
};
|
|
|
|
StageMorph.prototype.fireStopAllEvent = function () {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
|
|
this.threads.resumeAll(this.stage);
|
|
|
|
// experimental: run one step of a user-defined script
|
|
this.runStopScripts();
|
|
|
|
this.keysPressed = {};
|
|
this.threads.stopAll();
|
|
this.stopAllActiveSounds();
|
|
this.children.forEach(morph => {
|
|
if (morph.stopTalking) {
|
|
morph.stopTalking();
|
|
}
|
|
});
|
|
this.removeAllClones();
|
|
if (ide) {
|
|
ide.nextSteps([
|
|
nop,
|
|
() => this.stopAllActiveSounds(), // catch forever loops
|
|
() => this.stopProjection(),
|
|
() => ide.controlBar.pauseButton.refresh()
|
|
]);
|
|
}
|
|
};
|
|
|
|
StageMorph.prototype.runStopScripts = function () {
|
|
// experimental: Allow each sprite to run one last step before termination
|
|
// usage example: Stop a robot or device associated with the sprite
|
|
this.receiveUserInteraction('stopped', true, true);
|
|
this.children.forEach(morph => {
|
|
if (morph instanceof SpriteMorph) {
|
|
morph.receiveUserInteraction('stopped', true, true);
|
|
}
|
|
});
|
|
};
|
|
|
|
StageMorph.prototype.removeAllClones = function () {
|
|
var clones = this.children.filter(morph =>
|
|
morph instanceof SpriteMorph && morph.isTemporary
|
|
);
|
|
clones.forEach(clone => {
|
|
this.threads.stopAllForReceiver(clone);
|
|
clone.detachFromAnchor();
|
|
clone.corpsify();
|
|
clone.destroy();
|
|
});
|
|
this.cloneCount = 0;
|
|
};
|
|
|
|
StageMorph.prototype.editScripts = function () {
|
|
var ide = this.parentThatIsA(IDE_Morph),
|
|
scripts,
|
|
sorted;
|
|
if (ide.isAppMode || !ScriptsMorph.prototype.enableKeyboard) {return; }
|
|
scripts = this.parentThatIsA(
|
|
IDE_Morph
|
|
).currentSprite.scripts.selectForEdit(); // shadow on edit, if inherited
|
|
scripts.edit(scripts.position());
|
|
sorted = scripts.focus.sortedScripts();
|
|
if (sorted.length) {
|
|
scripts.focus.element = sorted[0];
|
|
if (scripts.focus.element instanceof HatBlockMorph) {
|
|
scripts.focus.nextCommand();
|
|
}
|
|
} else {
|
|
scripts.focus.moveBy(new Point(50, 50));
|
|
}
|
|
scripts.focus.fixLayout();
|
|
};
|
|
|
|
// StageMorph controlling generic WHEN hats
|
|
|
|
StageMorph.prototype.pauseGenericHatBlocks = function () {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
if (this.hasGenericHatBlocks() ||
|
|
ide.sprites.asArray().some(any => any.hasGenericHatBlocks())) {
|
|
this.enableCustomHatBlocks = true;
|
|
this.threads.pauseCustomHatBlocks = true;
|
|
ide.controlBar.stopButton.refresh();
|
|
}
|
|
};
|
|
|
|
// StageMorph block templates
|
|
|
|
StageMorph.prototype.blockTemplates = function (
|
|
category = 'motion',
|
|
all = false // include hidden blocks
|
|
) {
|
|
var blocks = [], myself = this, varNames, txt;
|
|
|
|
function block(selector) {
|
|
if (myself.hiddenPrimitives[selector] && !all) {
|
|
return null;
|
|
}
|
|
var newBlock = SpriteMorph.prototype.blockForSelector(selector, true);
|
|
newBlock.isTemplate = true;
|
|
return newBlock;
|
|
}
|
|
|
|
function variableBlock(varName, isLocal) {
|
|
var newBlock = SpriteMorph.prototype.variableBlock(varName, isLocal);
|
|
newBlock.isDraggable = false;
|
|
newBlock.isTemplate = true;
|
|
return newBlock;
|
|
}
|
|
|
|
|
|
function watcherToggle(selector) {
|
|
if (myself.hiddenPrimitives[selector]) {
|
|
return null;
|
|
}
|
|
var info = SpriteMorph.prototype.blocks[selector];
|
|
return new ToggleMorph(
|
|
'checkbox',
|
|
this,
|
|
function () {
|
|
myself.toggleWatcher(
|
|
selector,
|
|
localize(info.spec),
|
|
myself.blockColor[info.category]
|
|
);
|
|
},
|
|
null,
|
|
function () {
|
|
return myself.showingWatcher(selector);
|
|
},
|
|
null
|
|
);
|
|
}
|
|
|
|
function variableWatcherToggle(varName) {
|
|
return new ToggleMorph(
|
|
'checkbox',
|
|
this,
|
|
function () {
|
|
myself.toggleVariableWatcher(varName);
|
|
},
|
|
null,
|
|
function () {
|
|
return myself.showingVariableWatcher(varName);
|
|
},
|
|
null
|
|
);
|
|
}
|
|
|
|
if (category === 'motion') {
|
|
|
|
txt = new TextMorph(localize('Stage selected:\nno motion primitives'));
|
|
txt.fontSize = 9;
|
|
txt.setColor(this.paletteTextColor);
|
|
blocks.push(txt);
|
|
|
|
} else if (category === 'looks') {
|
|
|
|
blocks.push(block('doSwitchToCostume'));
|
|
blocks.push(block('doWearNextCostume'));
|
|
blocks.push(watcherToggle('getCostumeIdx'));
|
|
blocks.push(block('getCostumeIdx'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportGetImageAttribute'));
|
|
blocks.push(block('reportNewCostumeStretched'));
|
|
blocks.push(block('reportNewCostume'));
|
|
blocks.push('-');
|
|
blocks.push(block('changeEffect'));
|
|
blocks.push(block('setEffect'));
|
|
blocks.push(block('clearEffects'));
|
|
blocks.push(block('getEffect'));
|
|
blocks.push('-');
|
|
blocks.push(block('show'));
|
|
blocks.push(block('hide'));
|
|
blocks.push(watcherToggle('reportShown'));
|
|
blocks.push(block('reportShown'));
|
|
|
|
// for debugging: ///////////////
|
|
if (this.world().isDevMode) {
|
|
blocks.push('-');
|
|
blocks.push(this.devModeText());
|
|
blocks.push('-');
|
|
blocks.push(block('log'));
|
|
blocks.push(block('alert'));
|
|
blocks.push('-');
|
|
blocks.push(block('doScreenshot'));
|
|
}
|
|
|
|
} else if (category === 'sound') {
|
|
|
|
blocks.push(block('playSound'));
|
|
blocks.push(block('doPlaySoundUntilDone'));
|
|
blocks.push(block('doStopAllSounds'));
|
|
blocks.push('-');
|
|
blocks.push(block('doPlaySoundAtRate'));
|
|
blocks.push(block('reportGetSoundAttribute'));
|
|
blocks.push(block('reportNewSoundFromSamples'));
|
|
blocks.push('-');
|
|
blocks.push(block('doRest'));
|
|
blocks.push(block('doPlayNote'));
|
|
blocks.push(block('doSetInstrument'));
|
|
blocks.push('-');
|
|
blocks.push(block('doChangeTempo'));
|
|
blocks.push(block('doSetTempo'));
|
|
blocks.push(watcherToggle('getTempo'));
|
|
blocks.push(block('getTempo'));
|
|
blocks.push('-');
|
|
blocks.push(block('changeVolume'));
|
|
blocks.push(block('setVolume'));
|
|
blocks.push(watcherToggle('getVolume'));
|
|
blocks.push(block('getVolume'));
|
|
blocks.push('-');
|
|
blocks.push(block('changePan'));
|
|
blocks.push(block('setPan'));
|
|
blocks.push(watcherToggle('getPan'));
|
|
blocks.push(block('getPan'));
|
|
blocks.push('-');
|
|
blocks.push(block('playFreq'));
|
|
blocks.push(block('stopFreq'));
|
|
|
|
// for debugging: ///////////////
|
|
if (this.world().isDevMode) {
|
|
blocks.push('-');
|
|
blocks.push(this.devModeText());
|
|
blocks.push('-');
|
|
blocks.push(block('doPlayFrequency'));
|
|
}
|
|
|
|
} else if (category === 'pen') {
|
|
|
|
blocks.push(block('clear'));
|
|
blocks.push('-');
|
|
blocks.push(block('setBackgroundColor'));
|
|
blocks.push(block('changeBackgroundColorDimension'));
|
|
blocks.push(block('setBackgroundColorDimension'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportPenTrailsAsCostume'));
|
|
blocks.push('-');
|
|
blocks.push(block('doPasteOn'));
|
|
blocks.push(block('doCutFrom'));
|
|
|
|
} else if (category === 'control') {
|
|
|
|
blocks.push(block('receiveGo'));
|
|
blocks.push(block('receiveKey'));
|
|
blocks.push(block('receiveInteraction'));
|
|
blocks.push(block('receiveCondition'));
|
|
blocks.push('-');
|
|
blocks.push(block('receiveMessage'));
|
|
blocks.push(block('doBroadcast'));
|
|
blocks.push(block('doBroadcastAndWait'));
|
|
blocks.push('-');
|
|
blocks.push(block('doWarp'));
|
|
blocks.push('-');
|
|
blocks.push(block('doWait'));
|
|
blocks.push(block('doWaitUntil'));
|
|
blocks.push('-');
|
|
blocks.push(block('doForever'));
|
|
blocks.push(block('doRepeat'));
|
|
blocks.push(block('doUntil'));
|
|
blocks.push(block('doFor'));
|
|
blocks.push('-');
|
|
blocks.push(block('doIf'));
|
|
blocks.push(block('doIfElse'));
|
|
blocks.push(block('reportIfElse'));
|
|
blocks.push('-');
|
|
blocks.push(block('doReport'));
|
|
blocks.push(block('doStopThis'));
|
|
blocks.push('-');
|
|
blocks.push(block('doRun'));
|
|
blocks.push(block('fork'));
|
|
blocks.push(block('evaluate'));
|
|
blocks.push('-');
|
|
blocks.push(block('doTellTo'));
|
|
blocks.push(block('reportAskFor'));
|
|
blocks.push('-');
|
|
blocks.push(block('doCallCC'));
|
|
blocks.push(block('reportCallCC'));
|
|
blocks.push('-');
|
|
blocks.push(block('createClone'));
|
|
blocks.push(block('newClone'));
|
|
blocks.push('-');
|
|
blocks.push(block('doPauseAll'));
|
|
blocks.push(block('doSwitchToScene'));
|
|
|
|
// for debugging: ///////////////
|
|
if (this.world().isDevMode) {
|
|
blocks.push('-');
|
|
blocks.push(this.devModeText());
|
|
blocks.push('-');
|
|
blocks.push(watcherToggle('getLastMessage'));
|
|
blocks.push(block('getLastMessage'));
|
|
}
|
|
|
|
} else if (category === 'sensing') {
|
|
|
|
blocks.push(block('doAsk'));
|
|
blocks.push(watcherToggle('getLastAnswer'));
|
|
blocks.push(block('getLastAnswer'));
|
|
blocks.push('-');
|
|
blocks.push(watcherToggle('reportMouseX'));
|
|
blocks.push(block('reportMouseX'));
|
|
blocks.push(watcherToggle('reportMouseY'));
|
|
blocks.push(block('reportMouseY'));
|
|
blocks.push(block('reportMouseDown'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportKeyPressed'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportAspect'));
|
|
blocks.push('-');
|
|
blocks.push(block('doResetTimer'));
|
|
blocks.push(watcherToggle('getTimer'));
|
|
blocks.push(block('getTimer'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportAttributeOf'));
|
|
|
|
if (SpriteMorph.prototype.enableFirstClass) {
|
|
blocks.push(block('reportGet'));
|
|
}
|
|
|
|
blocks.push(block('reportObject'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportURL'));
|
|
blocks.push(block('reportAudio'));
|
|
blocks.push(block('reportVideo'));
|
|
blocks.push(block('doSetVideoTransparency'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportGlobalFlag'));
|
|
blocks.push(block('doSetGlobalFlag'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportDate'));
|
|
blocks.push(block('reportBlockAttribute'));
|
|
|
|
// for debugging: ///////////////
|
|
if (this.world().isDevMode) {
|
|
blocks.push('-');
|
|
blocks.push(this.devModeText());
|
|
blocks.push('-');
|
|
blocks.push(watcherToggle('reportThreadCount'));
|
|
blocks.push(block('reportThreadCount'));
|
|
blocks.push(block('reportStackSize'));
|
|
blocks.push(block('reportFrameCount'));
|
|
blocks.push(block('reportYieldCount'));
|
|
}
|
|
}
|
|
if (category === 'operators') {
|
|
|
|
blocks.push(block('reifyScript'));
|
|
blocks.push(block('reifyReporter'));
|
|
blocks.push(block('reifyPredicate'));
|
|
blocks.push('#');
|
|
blocks.push('-');
|
|
blocks.push(block('reportSum'));
|
|
blocks.push(block('reportDifference'));
|
|
blocks.push(block('reportProduct'));
|
|
blocks.push(block('reportQuotient'));
|
|
blocks.push(block('reportPower'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportModulus'));
|
|
blocks.push(block('reportRound'));
|
|
blocks.push(block('reportMonadic'));
|
|
blocks.push(block('reportRandom'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportLessThan'));
|
|
blocks.push(block('reportEquals'));
|
|
blocks.push(block('reportGreaterThan'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportAnd'));
|
|
blocks.push(block('reportOr'));
|
|
blocks.push(block('reportNot'));
|
|
blocks.push(block('reportBoolean'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportJoinWords'));
|
|
blocks.push(block('reportTextSplit'));
|
|
blocks.push(block('reportLetter'));
|
|
blocks.push(block('reportStringSize'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportUnicode'));
|
|
blocks.push(block('reportUnicodeAsLetter'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportIsA'));
|
|
blocks.push(block('reportIsIdentical'));
|
|
|
|
if (Process.prototype.enableJS) { // (Process.prototype.enableJS) {
|
|
blocks.push('-');
|
|
blocks.push(block('reportJSFunction'));
|
|
if (Process.prototype.enableCompiling) {
|
|
blocks.push(block('reportCompiled'));
|
|
}
|
|
}
|
|
|
|
// for debugging: ///////////////
|
|
if (this.world().isDevMode) {
|
|
blocks.push('-');
|
|
blocks.push(this.devModeText());
|
|
blocks.push('-');
|
|
blocks.push(block('reportTypeOf'));
|
|
blocks.push(block('reportTextFunction'));
|
|
}
|
|
|
|
}
|
|
if (category === 'variables') {
|
|
|
|
blocks.push(this.makeVariableButton());
|
|
if (this.variables.allNames().length > 0) {
|
|
blocks.push(this.deleteVariableButton());
|
|
}
|
|
blocks.push('-');
|
|
|
|
varNames = this.reachableGlobalVariableNames(true, all);
|
|
if (varNames.length > 0) {
|
|
varNames.forEach(name => {
|
|
blocks.push(variableWatcherToggle(name));
|
|
blocks.push(variableBlock(name));
|
|
});
|
|
blocks.push('-');
|
|
}
|
|
|
|
varNames = this.allLocalVariableNames(true, all);
|
|
if (varNames.length > 0) {
|
|
varNames.forEach(name => {
|
|
blocks.push(variableWatcherToggle(name));
|
|
blocks.push(variableBlock(name, true));
|
|
});
|
|
blocks.push('-');
|
|
}
|
|
|
|
blocks.push(block('doSetVar'));
|
|
blocks.push(block('doChangeVar'));
|
|
blocks.push(block('doShowVar'));
|
|
blocks.push(block('doHideVar'));
|
|
blocks.push(block('doDeclareVariables'));
|
|
blocks.push('=');
|
|
blocks.push(block('reportNewList'));
|
|
blocks.push(block('reportNumbers'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportCONS'));
|
|
blocks.push(block('reportListItem'));
|
|
blocks.push(block('reportCDR'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportListAttribute'));
|
|
blocks.push(block('reportListIndex'));
|
|
blocks.push(block('reportListContainsItem'));
|
|
blocks.push(block('reportListIsEmpty'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportMap'));
|
|
blocks.push(block('reportKeep'));
|
|
blocks.push(block('reportFindFirst'));
|
|
blocks.push(block('reportCombine'));
|
|
blocks.push('-');
|
|
blocks.push(block('doForEach'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportConcatenatedLists'));
|
|
blocks.push(block('reportReshape'));
|
|
blocks.push('-');
|
|
blocks.push(block('doAddToList'));
|
|
blocks.push(block('doDeleteFromList'));
|
|
blocks.push(block('doInsertInList'));
|
|
blocks.push(block('doReplaceInList'));
|
|
|
|
if (SpriteMorph.prototype.showingExtensions) {
|
|
blocks.push('=');
|
|
blocks.push(block('doApplyExtension'));
|
|
blocks.push(block('reportApplyExtension'));
|
|
}
|
|
|
|
if (StageMorph.prototype.enableCodeMapping) {
|
|
blocks.push('=');
|
|
blocks.push(block('doMapCodeOrHeader'));
|
|
blocks.push(block('doMapValueCode'));
|
|
blocks.push(block('doMapListCode'));
|
|
blocks.push('-');
|
|
blocks.push(block('reportMappedCode'));
|
|
}
|
|
|
|
// for debugging: ///////////////
|
|
if (this.world().isDevMode) {
|
|
blocks.push('-');
|
|
blocks.push(this.devModeText());
|
|
blocks.push('-');
|
|
blocks.push(block('doShowTable'));
|
|
}
|
|
}
|
|
|
|
return blocks;
|
|
};
|
|
|
|
// StageMorph primitives
|
|
|
|
StageMorph.prototype.clear = function () {
|
|
this.clearPenTrails();
|
|
};
|
|
|
|
// StageMorph user menu
|
|
|
|
StageMorph.prototype.userMenu = function () {
|
|
var ide = this.parentThatIsA(IDE_Morph),
|
|
menu = new MenuMorph(this);
|
|
|
|
if (ide && ide.isAppMode) {
|
|
// menu.addItem('help', 'nop');
|
|
return menu;
|
|
}
|
|
menu.addItem("edit", 'edit');
|
|
menu.addItem("show all", 'showAll');
|
|
menu.addItem(
|
|
"pic...",
|
|
() => ide.saveCanvasAs(this.fullImage(), this.name),
|
|
'save a picture\nof the stage'
|
|
);
|
|
menu.addLine();
|
|
menu.addItem(
|
|
'pen trails',
|
|
() => {
|
|
var costume = ide.currentSprite.reportPenTrailsAsCostume().copy();
|
|
ide.currentSprite.addCostume(costume);
|
|
ide.currentSprite.wearCostume(costume);
|
|
ide.hasChangedMedia = true;
|
|
ide.spriteBar.tabBar.tabTo('costumes');
|
|
},
|
|
ide.currentSprite instanceof SpriteMorph ?
|
|
'turn all pen trails and stamps\n' +
|
|
'into a new costume for the\ncurrently selected sprite'
|
|
: 'turn all pen trails and stamps\n' +
|
|
'into a new background for the stage'
|
|
);
|
|
if (this.trailsLog.length) {
|
|
menu.addItem(
|
|
'svg...',
|
|
'exportTrailsLogAsSVG',
|
|
'export pen trails\nline segments as SVG'
|
|
);
|
|
}
|
|
return menu;
|
|
};
|
|
|
|
StageMorph.prototype.showAll = function () {
|
|
this.children.forEach(m => {
|
|
if (m instanceof SpriteMorph) {
|
|
if (!m.anchor) {
|
|
m.show();
|
|
m.keepWithin(this);
|
|
}
|
|
} else {
|
|
m.show();
|
|
m.keepWithin(this);
|
|
if (m.fixLayout) {m.fixLayout(); }
|
|
}
|
|
});
|
|
};
|
|
|
|
StageMorph.prototype.edit = SpriteMorph.prototype.edit;
|
|
|
|
StageMorph.prototype.fullImage = Morph.prototype.fullImage;
|
|
|
|
// StageMorph thumbnail
|
|
|
|
StageMorph.prototype.thumbnail = function (extentPoint, recycleMe, noWatchers) {
|
|
// answer a new Canvas of extentPoint dimensions containing
|
|
// my thumbnail representation keeping the originial aspect ratio
|
|
// a "recycleMe canvas can be passed for re-use
|
|
return this.fancyThumbnail(extentPoint, null, false, recycleMe, noWatchers);
|
|
};
|
|
|
|
StageMorph.prototype.fancyThumbnail = function (
|
|
extentPoint,
|
|
excludedSprite,
|
|
nonRetina,
|
|
recycleMe,
|
|
noWatchers
|
|
) {
|
|
var src = this.getImage(),
|
|
scale = Math.min(
|
|
(extentPoint.x / src.width),
|
|
(extentPoint.y / src.height)
|
|
),
|
|
trg = newCanvas(extentPoint, nonRetina, recycleMe),
|
|
ctx = trg.getContext('2d'),
|
|
fb,
|
|
fimg;
|
|
|
|
ctx.save();
|
|
ctx.scale(scale, scale);
|
|
ctx.drawImage(
|
|
src,
|
|
0,
|
|
0
|
|
);
|
|
ctx.drawImage(
|
|
this.penTrails(),
|
|
0,
|
|
0,
|
|
this.dimensions.x * this.scale,
|
|
this.dimensions.y * this.scale
|
|
);
|
|
if (this.projectionSource) {
|
|
ctx.save();
|
|
ctx.globalAlpha = 1 - (this.projectionTransparency / 100);
|
|
ctx.drawImage(
|
|
this.projectionLayer(),
|
|
0,
|
|
0,
|
|
this.dimensions.x * this.scale,
|
|
this.dimensions.y * this.scale
|
|
);
|
|
ctx.restore();
|
|
}
|
|
this.children.forEach(morph => {
|
|
if ((isSnapObject(morph) || !noWatchers) &&
|
|
morph.isVisible && (morph !== excludedSprite)
|
|
) {
|
|
fb = morph.fullBounds();
|
|
fimg = morph.fullImage();
|
|
if (fimg.width && fimg.height) {
|
|
ctx.drawImage(
|
|
morph.fullImage(),
|
|
fb.origin.x - this.bounds.origin.x,
|
|
fb.origin.y - this.bounds.origin.y
|
|
);
|
|
}
|
|
}
|
|
});
|
|
ctx.restore();
|
|
return trg;
|
|
};
|
|
|
|
// StageMorph - exporting the pen trails as SVG
|
|
|
|
StageMorph.prototype.exportTrailsLogAsSVG = function () {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
|
|
ide.saveFileAs(
|
|
this.trailsLogAsSVG().src,
|
|
'image/svg',
|
|
ide.projectName || this.name
|
|
);
|
|
};
|
|
|
|
StageMorph.prototype.trailsLogAsSVG = function () {
|
|
var bottomLeft = this.trailsLog[0][0],
|
|
topRight = bottomLeft,
|
|
maxWidth = this.trailsLog[0][3],
|
|
shift,
|
|
box,
|
|
p1, p2,
|
|
svg;
|
|
|
|
// determine bounding box and max line width
|
|
this.trailsLog.forEach(line => {
|
|
bottomLeft = bottomLeft.min(line[0]);
|
|
bottomLeft = bottomLeft.min(line[1]);
|
|
topRight = topRight.max(line[0]);
|
|
topRight = topRight.max(line[1]);
|
|
maxWidth = Math.max(maxWidth, line[3]);
|
|
});
|
|
box = bottomLeft.corner(topRight).expandBy(maxWidth / 2);
|
|
shift = new Point(-bottomLeft.x, topRight.y).translateBy(maxWidth / 2);
|
|
svg = '<svg xmlns="http://www.w3.org/2000/svg" ' +
|
|
'preserveAspectRatio="none" ' +
|
|
'viewBox="0 0 ' + box.width() + ' ' + box.height() + '" ' +
|
|
'width="' + box.width() + '" height="' + box.height() + '" ' +
|
|
// 'style="background-color:black" ' + // for supporting backgrounds
|
|
'>';
|
|
svg += '<!-- Generated by Snap! - http://snap.berkeley.edu/ -->';
|
|
|
|
// for debugging the viewBox:
|
|
// svg += '<rect width="100%" height="100%" fill="black"/>'
|
|
|
|
this.trailsLog.forEach(line => {
|
|
p1 = this.normalizePoint(line[0]).translateBy(shift);
|
|
p2 = this.normalizePoint(line[1]).translateBy(shift);
|
|
svg += '<line x1="' + p1.x + '" y1="' + p1.y +
|
|
'" x2="' + p2.x + '" y2="' + p2.y + '" ' +
|
|
'style="stroke:' + line[2].toRGBstring() + ';' +
|
|
'stroke-opacity:' + line[2].a + ';' +
|
|
'stroke-width:' + line[3] +
|
|
';stroke-linecap:' + line[4] +
|
|
'" />';
|
|
});
|
|
svg += '</svg>';
|
|
return {
|
|
src : svg,
|
|
rot : new Point(-box.origin.x, box.corner.y)
|
|
};
|
|
};
|
|
|
|
StageMorph.prototype.normalizePoint = function (snapPoint) {
|
|
return new Point(snapPoint.x, -snapPoint.y);
|
|
};
|
|
|
|
// StageMorph hiding and showing:
|
|
|
|
/*
|
|
override the inherited behavior to recursively hide/show all
|
|
children.
|
|
*/
|
|
|
|
StageMorph.prototype.hide = function () {
|
|
this.isVisible = false;
|
|
this.changed();
|
|
};
|
|
|
|
StageMorph.prototype.show = function () {
|
|
this.isVisible = true;
|
|
this.changed();
|
|
};
|
|
|
|
StageMorph.prototype.reportShown = SpriteMorph.prototype.reportShown;
|
|
|
|
// StageMorph cloning override
|
|
|
|
StageMorph.prototype.createClone = nop;
|
|
StageMorph.prototype.newClone = nop;
|
|
|
|
// StageMorph background color setting
|
|
|
|
StageMorph.prototype.setColorDimension = function (idx, num) {
|
|
var n = +num;
|
|
|
|
idx = +idx;
|
|
if (idx < 0 || idx > 3) {return; }
|
|
if (idx === 0) {
|
|
if (n < 0 || n > 100) { // wrap the hue
|
|
n = (n < 0 ? 100 : 0) + n % 100;
|
|
}
|
|
} else {
|
|
n = Math.min(100, Math.max(0, n));
|
|
}
|
|
if (idx === 3) {
|
|
this.color.a = 1 - n / 100;
|
|
} else {
|
|
this.cachedColorDimensions[idx] = n / 100;
|
|
this.color['set_' + SpriteMorph.prototype.penColorModel].apply(
|
|
this.color,
|
|
this.cachedColorDimensions
|
|
);
|
|
}
|
|
this.rerender();
|
|
};
|
|
|
|
StageMorph.prototype.getColorDimension =
|
|
SpriteMorph.prototype.getColorDimension;
|
|
|
|
StageMorph.prototype.changeColorDimension =
|
|
SpriteMorph.prototype.changeColorDimension;
|
|
|
|
StageMorph.prototype.setColorRGBA =
|
|
SpriteMorph.prototype.setColorRGBA;
|
|
|
|
StageMorph.prototype.changeColorRGBA =
|
|
SpriteMorph.prototype.changeColorRGBA;
|
|
|
|
StageMorph.prototype.setColor = function (aColor) {
|
|
if (!this.color.eq(aColor, true)) { // observeAlpha
|
|
this.color = aColor.copy();
|
|
this.rerender();
|
|
this.cachedColorDimensions = this.color[
|
|
SpriteMorph.prototype.penColorModel
|
|
]();
|
|
}
|
|
};
|
|
|
|
StageMorph.prototype.setBackgroundColor = StageMorph.prototype.setColor;
|
|
|
|
StageMorph.prototype.getPenAttribute
|
|
= SpriteMorph.prototype.getPenAttribute;
|
|
|
|
// StageMorph printing on another sprite:
|
|
|
|
StageMorph.prototype.blitOn = SpriteMorph.prototype.blitOn;
|
|
|
|
// StageMorph pseudo-inherited behavior
|
|
|
|
StageMorph.prototype.categories = SpriteMorph.prototype.categories;
|
|
StageMorph.prototype.blockColor = SpriteMorph.prototype.blockColor;
|
|
StageMorph.prototype.paletteColor = SpriteMorph.prototype.paletteColor;
|
|
StageMorph.prototype.setName = SpriteMorph.prototype.setName;
|
|
StageMorph.prototype.reporterize = SpriteMorph.prototype.reporterize;
|
|
StageMorph.prototype.variableBlock = SpriteMorph.prototype.variableBlock;
|
|
StageMorph.prototype.showingWatcher = SpriteMorph.prototype.showingWatcher;
|
|
StageMorph.prototype.addVariable = SpriteMorph.prototype.addVariable;
|
|
StageMorph.prototype.deleteVariable = SpriteMorph.prototype.deleteVariable;
|
|
|
|
// StageMorph Palette Utilities
|
|
|
|
StageMorph.prototype.makeBlock = SpriteMorph.prototype.makeBlock;
|
|
StageMorph.prototype.helpMenu = SpriteMorph.prototype.helpMenu;
|
|
StageMorph.prototype.makeBlockButton = SpriteMorph.prototype.makeBlockButton;
|
|
|
|
StageMorph.prototype.makeVariableButton
|
|
= SpriteMorph.prototype.makeVariableButton;
|
|
|
|
StageMorph.prototype.categoryText = SpriteMorph.prototype.categoryText;
|
|
StageMorph.prototype.devModeText = SpriteMorph.prototype.devModeText;
|
|
|
|
StageMorph.prototype.deleteVariableButton
|
|
= SpriteMorph.prototype.deleteVariableButton;
|
|
|
|
StageMorph.prototype.customBlockTemplatesForCategory
|
|
= SpriteMorph.prototype.customBlockTemplatesForCategory;
|
|
|
|
StageMorph.prototype.getPrimitiveTemplates
|
|
= SpriteMorph.prototype.getPrimitiveTemplates;
|
|
|
|
StageMorph.prototype.palette = SpriteMorph.prototype.palette;
|
|
StageMorph.prototype.freshPalette = SpriteMorph.prototype.freshPalette;
|
|
StageMorph.prototype.blocksMatching = SpriteMorph.prototype.blocksMatching;
|
|
StageMorph.prototype.searchBlocks = SpriteMorph.prototype.searchBlocks;
|
|
|
|
// StageMorph utilities for showing & hiding blocks in the palette
|
|
|
|
StageMorph.prototype.allPaletteBlocks
|
|
= SpriteMorph.prototype.allPaletteBlocks;
|
|
|
|
StageMorph.prototype.isHidingBlock = SpriteMorph.prototype.isHidingBlock;
|
|
|
|
StageMorph.prototype.changeBlockVisibility
|
|
= SpriteMorph.prototype.changeBlockVisibility;
|
|
|
|
StageMorph.prototype.changePrimitiveVisibility
|
|
= SpriteMorph.prototype.changePrimitiveVisibility;
|
|
|
|
StageMorph.prototype.changeCustomBlockVisibility
|
|
= SpriteMorph.prototype.changeCustomBlockVisibility;
|
|
|
|
StageMorph.prototype.changeVarBlockVisibility
|
|
= SpriteMorph.prototype.changeVarBlockVisibility;
|
|
|
|
StageMorph.prototype.emptyCategories =
|
|
SpriteMorph.prototype.emptyCategories;
|
|
|
|
// StageMorph neighbor detection
|
|
|
|
StageMorph.prototype.neighbors = SpriteMorph.prototype.neighbors;
|
|
StageMorph.prototype.perimeter = SpriteMorph.prototype.perimeter;
|
|
|
|
// StageMorph block rendering
|
|
|
|
StageMorph.prototype.doScreenshot = SpriteMorph.prototype.doScreenshot;
|
|
StageMorph.prototype.newCostumeName = SpriteMorph.prototype.newCostumeName;
|
|
StageMorph.prototype.blockForSelector = SpriteMorph.prototype.blockForSelector;
|
|
|
|
// StageMorph variable watchers (for palette checkbox toggling)
|
|
|
|
StageMorph.prototype.findVariableWatcher
|
|
= SpriteMorph.prototype.findVariableWatcher;
|
|
|
|
StageMorph.prototype.toggleVariableWatcher
|
|
= SpriteMorph.prototype.toggleVariableWatcher;
|
|
|
|
StageMorph.prototype.showingVariableWatcher
|
|
= SpriteMorph.prototype.showingVariableWatcher;
|
|
|
|
StageMorph.prototype.deleteVariableWatcher
|
|
= SpriteMorph.prototype.deleteVariableWatcher;
|
|
|
|
// StageMorph background management
|
|
|
|
StageMorph.prototype.addCostume
|
|
= SpriteMorph.prototype.addCostume;
|
|
|
|
StageMorph.prototype.wearCostume
|
|
= SpriteMorph.prototype.wearCostume;
|
|
|
|
StageMorph.prototype.getCostumeIdx
|
|
= SpriteMorph.prototype.getCostumeIdx;
|
|
|
|
StageMorph.prototype.doWearNextCostume
|
|
= SpriteMorph.prototype.doWearNextCostume;
|
|
|
|
StageMorph.prototype.doWearPreviousCostume
|
|
= SpriteMorph.prototype.doWearPreviousCostume;
|
|
|
|
StageMorph.prototype.doSwitchToCostume
|
|
= SpriteMorph.prototype.doSwitchToCostume;
|
|
|
|
StageMorph.prototype.reportCostumes
|
|
= SpriteMorph.prototype.reportCostumes;
|
|
|
|
// StageMorph graphic effects
|
|
|
|
StageMorph.prototype.graphicsChanged
|
|
= SpriteMorph.prototype.graphicsChanged;
|
|
|
|
StageMorph.prototype.applyGraphicsEffects
|
|
= SpriteMorph.prototype.applyGraphicsEffects;
|
|
|
|
StageMorph.prototype.setEffect
|
|
= SpriteMorph.prototype.setEffect;
|
|
|
|
StageMorph.prototype.getEffect
|
|
= SpriteMorph.prototype.getEffect;
|
|
|
|
StageMorph.prototype.getGhostEffect
|
|
= SpriteMorph.prototype.getGhostEffect;
|
|
|
|
StageMorph.prototype.changeEffect
|
|
= SpriteMorph.prototype.changeEffect;
|
|
|
|
StageMorph.prototype.clearEffects
|
|
= SpriteMorph.prototype.clearEffects;
|
|
|
|
// StageMorph sound management
|
|
|
|
StageMorph.prototype.addSound
|
|
= SpriteMorph.prototype.addSound;
|
|
|
|
StageMorph.prototype.doPlaySound
|
|
= SpriteMorph.prototype.doPlaySound;
|
|
|
|
StageMorph.prototype.stopAllActiveSounds = function () {
|
|
this.activeSounds.forEach(audio => audio.pause());
|
|
this.activeSounds = [];
|
|
if (this.microphone.modifier && this.microphone.isReady) {
|
|
this.microphone.stop();
|
|
}
|
|
};
|
|
|
|
StageMorph.prototype.pauseAllActiveSounds = function () {
|
|
this.activeSounds.forEach(audio => audio.pause());
|
|
};
|
|
|
|
StageMorph.prototype.resumeAllActiveSounds = function () {
|
|
this.activeSounds.forEach(audio => audio.play());
|
|
};
|
|
|
|
StageMorph.prototype.reportSounds
|
|
= SpriteMorph.prototype.reportSounds;
|
|
|
|
StageMorph.prototype.newSoundName
|
|
= SpriteMorph.prototype.newSoundName;
|
|
|
|
// StageMorph volume
|
|
|
|
StageMorph.prototype.setVolume
|
|
= SpriteMorph.prototype.setVolume;
|
|
|
|
StageMorph.prototype.changeVolume
|
|
= SpriteMorph.prototype.changeVolume;
|
|
|
|
StageMorph.prototype.getVolume
|
|
= SpriteMorph.prototype.getVolume;
|
|
|
|
StageMorph.prototype.getGainNode
|
|
= SpriteMorph.prototype.getGainNode;
|
|
|
|
StageMorph.prototype.audioContext
|
|
= SpriteMorph.prototype.audioContext;
|
|
|
|
// StageMorph stereo panning
|
|
|
|
StageMorph.prototype.setPan
|
|
= SpriteMorph.prototype.setPan;
|
|
|
|
StageMorph.prototype.changePan
|
|
= SpriteMorph.prototype.changePan;
|
|
|
|
StageMorph.prototype.getPan
|
|
= SpriteMorph.prototype.getPan;
|
|
|
|
StageMorph.prototype.getPannerNode
|
|
= SpriteMorph.prototype.getPannerNode;
|
|
|
|
// StageMorph frequency player
|
|
|
|
StageMorph.prototype.playFreq
|
|
= SpriteMorph.prototype.playFreq;
|
|
|
|
StageMorph.prototype.stopFreq
|
|
= SpriteMorph.prototype.stopFreq;
|
|
|
|
// StageMorph non-variable watchers
|
|
|
|
StageMorph.prototype.toggleWatcher
|
|
= SpriteMorph.prototype.toggleWatcher;
|
|
|
|
StageMorph.prototype.showingWatcher
|
|
= SpriteMorph.prototype.showingWatcher;
|
|
|
|
StageMorph.prototype.watcherFor =
|
|
SpriteMorph.prototype.watcherFor;
|
|
|
|
StageMorph.prototype.getLastAnswer
|
|
= SpriteMorph.prototype.getLastAnswer;
|
|
|
|
StageMorph.prototype.reportThreadCount
|
|
= SpriteMorph.prototype.reportThreadCount;
|
|
|
|
// StageMorph coordinate conversion
|
|
|
|
StageMorph.prototype.snapPoint
|
|
= SpriteMorph.prototype.snapPoint;
|
|
|
|
// StageMorph dimension getters
|
|
|
|
StageMorph.prototype.xCenter = function () {
|
|
return 0;
|
|
};
|
|
|
|
StageMorph.prototype.yCenter = function () {
|
|
return 0;
|
|
};
|
|
|
|
StageMorph.prototype.xLeft = function () {
|
|
return this.dimensions.x * -0.5;
|
|
};
|
|
|
|
StageMorph.prototype.xRight = function () {
|
|
return this.dimensions.x / 2;
|
|
};
|
|
|
|
StageMorph.prototype.yTop = function () {
|
|
return this.dimensions.y / 2;
|
|
};
|
|
|
|
StageMorph.prototype.yBottom = function () {
|
|
return this.dimensions.y * -0.5;
|
|
};
|
|
|
|
// StageMorph message broadcasting
|
|
|
|
StageMorph.prototype.allMessageNames
|
|
= SpriteMorph.prototype.allMessageNames;
|
|
|
|
StageMorph.prototype.allSendersOf
|
|
= SpriteMorph.prototype.allSendersOf;
|
|
|
|
StageMorph.prototype.allHatBlocksFor
|
|
= SpriteMorph.prototype.allHatBlocksFor;
|
|
|
|
StageMorph.prototype.allHatBlocksForKey
|
|
= SpriteMorph.prototype.allHatBlocksForKey;
|
|
|
|
StageMorph.prototype.allHatBlocksForInteraction
|
|
= SpriteMorph.prototype.allHatBlocksForInteraction;
|
|
|
|
StageMorph.prototype.hasGenericHatBlocks
|
|
= SpriteMorph.prototype.hasGenericHatBlocks;
|
|
|
|
StageMorph.prototype.allGenericHatBlocks
|
|
= SpriteMorph.prototype.allGenericHatBlocks;
|
|
|
|
StageMorph.prototype.allScripts
|
|
= SpriteMorph.prototype.allScripts;
|
|
|
|
// StageMorph events
|
|
|
|
StageMorph.prototype.mouseClickLeft
|
|
= SpriteMorph.prototype.mouseClickLeft;
|
|
|
|
StageMorph.prototype.mouseEnter
|
|
= SpriteMorph.prototype.mouseEnter;
|
|
|
|
StageMorph.prototype.mouseLeave = function () {
|
|
this.receiveUserInteraction('mouse-departed');
|
|
};
|
|
|
|
StageMorph.prototype.mouseDownLeft
|
|
= SpriteMorph.prototype.mouseDownLeft;
|
|
|
|
StageMorph.prototype.mouseScroll
|
|
= SpriteMorph.prototype.mouseScroll;
|
|
|
|
StageMorph.prototype.receiveUserInteraction
|
|
= SpriteMorph.prototype.receiveUserInteraction;
|
|
|
|
// StageMorph custom blocks
|
|
|
|
StageMorph.prototype.deleteAllBlockInstances
|
|
= SpriteMorph.prototype.deleteAllBlockInstances;
|
|
|
|
StageMorph.prototype.allBlockInstances
|
|
= SpriteMorph.prototype.allBlockInstances;
|
|
|
|
StageMorph.prototype.allLocalBlockInstances
|
|
= SpriteMorph.prototype.allLocalBlockInstances;
|
|
|
|
StageMorph.prototype.allEditorBlockInstances
|
|
= SpriteMorph.prototype.allEditorBlockInstances;
|
|
|
|
StageMorph.prototype.paletteBlockInstance
|
|
= SpriteMorph.prototype.paletteBlockInstance;
|
|
|
|
StageMorph.prototype.usesBlockInstance
|
|
= SpriteMorph.prototype.usesBlockInstance;
|
|
|
|
StageMorph.prototype.doubleDefinitionsFor
|
|
= SpriteMorph.prototype.doubleDefinitionsFor;
|
|
|
|
StageMorph.prototype.replaceDoubleDefinitionsFor
|
|
= SpriteMorph.prototype.replaceDoubleDefinitionsFor;
|
|
|
|
StageMorph.prototype.allInvocationsOf
|
|
= SpriteMorph.prototype.allInvocationsOf;
|
|
|
|
StageMorph.prototype.allIndependentInvocationsOf
|
|
= SpriteMorph.prototype.allInvocationsOf;
|
|
|
|
StageMorph.prototype.allDependentInvocationsOf
|
|
= SpriteMorph.prototype.allInvocationsOf;
|
|
|
|
// StageMorph inheritance support - general
|
|
|
|
StageMorph.prototype.specimens = function () {
|
|
return [];
|
|
};
|
|
|
|
StageMorph.prototype.allSpecimens = function () {
|
|
return [];
|
|
};
|
|
|
|
StageMorph.prototype.shadowAttribute = nop;
|
|
|
|
// StageMorph inheritance support - attributes
|
|
|
|
StageMorph.prototype.inheritsAttribute = function () {
|
|
return false;
|
|
};
|
|
|
|
// StageMorph inheritance support - variables
|
|
|
|
StageMorph.prototype.isVariableNameInUse
|
|
= SpriteMorph.prototype.isVariableNameInUse;
|
|
|
|
StageMorph.prototype.globalVariables
|
|
= SpriteMorph.prototype.globalVariables;
|
|
|
|
StageMorph.prototype.inheritedVariableNames = function () {
|
|
return [];
|
|
};
|
|
|
|
StageMorph.prototype.deletableVariableNames = function () {
|
|
return this.variables.allNames();
|
|
};
|
|
|
|
StageMorph.prototype.allLocalVariableNames
|
|
= SpriteMorph.prototype.allLocalVariableNames;
|
|
|
|
StageMorph.prototype.reachableGlobalVariableNames
|
|
= SpriteMorph.prototype.reachableGlobalVariableNames;
|
|
|
|
// StageMorph inheritance - custom blocks
|
|
|
|
StageMorph.prototype.getMethod
|
|
= SpriteMorph.prototype.getMethod;
|
|
|
|
StageMorph.prototype.getLocalMethod
|
|
= SpriteMorph.prototype.getLocalMethod;
|
|
|
|
StageMorph.prototype.ownBlocks
|
|
= SpriteMorph.prototype.ownBlocks;
|
|
|
|
StageMorph.prototype.allBlocks = function (valuesOnly) {
|
|
var dict = this.ownBlocks();
|
|
if (valuesOnly) {
|
|
return Object.keys(dict).map(key => dict[key]);
|
|
}
|
|
return dict;
|
|
};
|
|
|
|
StageMorph.prototype.inheritedBlocks = function () {
|
|
return [];
|
|
};
|
|
|
|
// StageMorph variable refactoring
|
|
|
|
StageMorph.prototype.hasSpriteVariable
|
|
= SpriteMorph.prototype.hasSpriteVariable;
|
|
|
|
StageMorph.prototype.refactorVariableInstances
|
|
= SpriteMorph.prototype.refactorVariableInstances;
|
|
|
|
// StageMorph pen trails as costume
|
|
|
|
StageMorph.prototype.reportPenTrailsAsCostume = function () {
|
|
return new Costume(
|
|
this.trailsCanvas,
|
|
this.newCostumeName(localize('Background'))
|
|
);
|
|
};
|
|
|
|
// StageMorph scanning global custom blocks for message sends
|
|
|
|
StageMorph.prototype.globalBlocksSending = function (message, receiverName) {
|
|
// "transitive hull"
|
|
var all = this.globalBlocks.filter(
|
|
def => def.isSending(message, receiverName)
|
|
);
|
|
this.globalBlocks.forEach(def => {
|
|
if (def.collectDependencies().some(dep => contains(all, dep))) {
|
|
all.push(def);
|
|
}
|
|
});
|
|
return all;
|
|
};
|
|
|
|
// SpriteBubbleMorph ////////////////////////////////////////////////////////
|
|
|
|
/*
|
|
I am a sprite's scaleable speech bubble. I rely on SpriteMorph
|
|
for my preferences settings
|
|
*/
|
|
|
|
// SpriteBubbleMorph inherits from SpeechBubbleMorph:
|
|
|
|
SpriteBubbleMorph.prototype = new SpeechBubbleMorph();
|
|
SpriteBubbleMorph.prototype.constructor = SpriteBubbleMorph;
|
|
SpriteBubbleMorph.uber = SpeechBubbleMorph.prototype;
|
|
|
|
// SpriteBubbleMorph instance creation:
|
|
|
|
function SpriteBubbleMorph(data, stage, isThought, isQuestion) {
|
|
this.init(data, stage, isThought, isQuestion);
|
|
}
|
|
|
|
SpriteBubbleMorph.prototype.init = function (
|
|
data,
|
|
stage,
|
|
isThought,
|
|
isQuestion
|
|
) {
|
|
var sprite = SpriteMorph.prototype;
|
|
this.stage = stage;
|
|
this.scale = stage ? stage.scale : 1;
|
|
this.data = data;
|
|
this.isQuestion = isQuestion;
|
|
|
|
SpriteBubbleMorph.uber.init.call(
|
|
this,
|
|
this.data,
|
|
sprite.bubbleColor,
|
|
null,
|
|
null,
|
|
isQuestion ? sprite.blockColor.sensing : sprite.bubbleBorderColor,
|
|
null,
|
|
isThought,
|
|
true // no shadow
|
|
);
|
|
|
|
this.isCachingImage = true;
|
|
this.rerender();
|
|
};
|
|
|
|
// SpriteBubbleMorph contents formatting
|
|
|
|
SpriteBubbleMorph.prototype.dataAsMorph = function (data) {
|
|
var contents,
|
|
sprite = SpriteMorph.prototype,
|
|
isText,
|
|
img,
|
|
scaledImg,
|
|
width;
|
|
if (data instanceof Morph) {
|
|
if (isSnapObject(data)) {
|
|
img = data.thumbnail(new Point(40, 40));
|
|
contents = new Morph();
|
|
contents.isCachingImage = true;
|
|
contents.bounds.setWidth(img.width);
|
|
contents.bounds.setHeight(img.height);
|
|
contents.cachedImage = img;
|
|
contents.version = data.version;
|
|
contents.step = function () {
|
|
if (this.version !== data.version) {
|
|
img = data.thumbnail(new Point(40, 40), this.cachedImage);
|
|
this.cachedImage = img;
|
|
this.version = data.version;
|
|
this.changed();
|
|
}
|
|
};
|
|
} else {
|
|
contents = data;
|
|
}
|
|
} else if (isString(data)) {
|
|
isText = true;
|
|
contents = new TextMorph(
|
|
data,
|
|
sprite.bubbleFontSize * this.scale,
|
|
null, // fontStyle
|
|
sprite.bubbleFontIsBold,
|
|
false, // italic
|
|
'center'
|
|
);
|
|
|
|
// support exporting text / numbers directly from speech balloons:
|
|
contents.userMenu = function () {
|
|
var menu = new MenuMorph(this),
|
|
ide = this.parentThatIsA(IDE_Morph)||
|
|
this.world().childThatIsA(IDE_Morph);
|
|
|
|
if (ide.isAppMode) {return; }
|
|
menu.addItem(
|
|
'export',
|
|
() => ide.saveFileAs(
|
|
data,
|
|
'text/plain;charset=utf-8',
|
|
localize('data')
|
|
)
|
|
);
|
|
return menu;
|
|
};
|
|
|
|
} else if (typeof data === 'boolean') {
|
|
img = sprite.booleanMorph(data).fullImage();
|
|
contents = new Morph();
|
|
contents.isCachingImage = true;
|
|
contents.bounds.setWidth(img.width);
|
|
contents.bounds.setHeight(img.height);
|
|
contents.cachedImage = img;
|
|
} else if (data instanceof Costume) {
|
|
img = data.thumbnail(new Point(40, 40));
|
|
contents = new Morph();
|
|
contents.isCachingImage = true;
|
|
contents.bounds.setWidth(img.width);
|
|
contents.bounds.setHeight(img.height);
|
|
contents.cachedImage = img;
|
|
|
|
// support costumes to be dragged out of speech balloons:
|
|
contents.isDraggable = true;
|
|
|
|
contents.selectForEdit = function () {
|
|
var cst = data.copy(),
|
|
icon,
|
|
prepare,
|
|
ide = this.parentThatIsA(IDE_Morph)||
|
|
this.world().childThatIsA(IDE_Morph);
|
|
|
|
cst.name = ide.currentSprite.newCostumeName(cst.name);
|
|
icon = new CostumeIconMorph(cst);
|
|
prepare = icon.prepareToBeGrabbed;
|
|
|
|
icon.prepareToBeGrabbed = function (hand) {
|
|
hand.grabOrigin = {
|
|
origin: ide.palette,
|
|
position: ide.palette.center()
|
|
};
|
|
this.prepareToBeGrabbed = prepare;
|
|
};
|
|
|
|
if (ide.isAppMode) {return; }
|
|
icon.setCenter(this.center());
|
|
return icon;
|
|
};
|
|
|
|
// support exporting costumes directly from speech balloons:
|
|
contents.userMenu = function () {
|
|
var menu = new MenuMorph(this),
|
|
ide = this.parentThatIsA(IDE_Morph)||
|
|
this.world().childThatIsA(IDE_Morph);
|
|
|
|
if (ide.isAppMode) {return; }
|
|
menu.addItem(
|
|
'export',
|
|
() => {
|
|
if (data instanceof SVG_Costume) {
|
|
// don't show SVG costumes in a new tab (shows text)
|
|
ide.saveFileAs(
|
|
data.contents.src,
|
|
'text/svg',
|
|
data.name
|
|
);
|
|
} else { // rasterized Costume
|
|
ide.saveCanvasAs(data.contents, data.name);
|
|
}
|
|
}
|
|
);
|
|
return menu;
|
|
};
|
|
|
|
} else if (data instanceof Sound) {
|
|
contents = new SymbolMorph('notes', 30);
|
|
|
|
// support sounds to be dragged out of speech balloons:
|
|
contents.isDraggable = true;
|
|
|
|
contents.selectForEdit = function () {
|
|
var snd = data.copy(),
|
|
icon,
|
|
prepare,
|
|
ide = this.parentThatIsA(IDE_Morph)||
|
|
this.world().childThatIsA(IDE_Morph);
|
|
|
|
snd.name = ide.currentSprite.newSoundName(snd.name);
|
|
icon = new SoundIconMorph(snd);
|
|
prepare = icon.prepareToBeGrabbed;
|
|
|
|
icon.prepareToBeGrabbed = function (hand) {
|
|
hand.grabOrigin = {
|
|
origin: ide.palette,
|
|
position: ide.palette.center()
|
|
};
|
|
this.prepareToBeGrabbed = prepare;
|
|
};
|
|
|
|
if (ide.isAppMode) {return; }
|
|
icon.setCenter(this.center());
|
|
return icon;
|
|
};
|
|
|
|
// support exporting sounds directly from speech balloons:
|
|
contents.userMenu = function () {
|
|
var menu = new MenuMorph(this),
|
|
ide = this.parentThatIsA(IDE_Morph)||
|
|
this.world().childThatIsA(IDE_Morph);
|
|
|
|
if (ide.isAppMode) {return; }
|
|
menu.addItem(
|
|
'export',
|
|
() => ide.saveAudioAs(data.audio, data.name)
|
|
);
|
|
return menu;
|
|
};
|
|
|
|
} else if (data instanceof HTMLCanvasElement) {
|
|
img = data;
|
|
contents = new Morph();
|
|
contents.isCachingImage = true;
|
|
contents.bounds.setWidth(img.width);
|
|
contents.bounds.setHeight(img.height);
|
|
contents.cachedImage = img;
|
|
} else if (data instanceof List) {
|
|
if (data.isTable()) {
|
|
contents = new TableFrameMorph(new TableMorph(data, 10));
|
|
if (this.stage) {
|
|
contents.expand(this.stage.extent().translateBy(
|
|
-2 * (this.edge + this.border + this.padding)
|
|
));
|
|
}
|
|
} else {
|
|
contents = new ListWatcherMorph(data);
|
|
contents.update(true);
|
|
contents.step = contents.update;
|
|
if (this.stage) {
|
|
contents.expand(this.stage.extent().translateBy(
|
|
-2 * (this.edge + this.border + this.padding)
|
|
));
|
|
}
|
|
}
|
|
contents.isDraggable = false;
|
|
} else if (data instanceof Context) {
|
|
img = data.image();
|
|
contents = new Morph();
|
|
contents.isCachingImage = true;
|
|
contents.bounds.setWidth(img.width);
|
|
contents.bounds.setHeight(img.height);
|
|
contents.cachedImage = img;
|
|
|
|
// support blocks to be dragged out of speech balloons:
|
|
contents.isDraggable = true;
|
|
|
|
contents.selectForEdit = function () {
|
|
var script = data.toBlock(),
|
|
prepare = script.prepareToBeGrabbed,
|
|
ide = this.parentThatIsA(IDE_Morph)||
|
|
this.world().childThatIsA(IDE_Morph);
|
|
|
|
script.prepareToBeGrabbed = function (hand) {
|
|
prepare.call(this, hand);
|
|
hand.grabOrigin = {
|
|
origin: ide.palette,
|
|
position: ide.palette.center()
|
|
};
|
|
this.prepareToBeGrabbed = prepare;
|
|
};
|
|
|
|
if (ide.isAppMode) {return; }
|
|
script.setPosition(this.position());
|
|
return script;
|
|
};
|
|
} else {
|
|
contents = new TextMorph(
|
|
data.toString(),
|
|
sprite.bubbleFontSize * this.scale,
|
|
null, // fontStyle
|
|
sprite.bubbleFontIsBold,
|
|
false, // italic
|
|
'center'
|
|
);
|
|
|
|
// support exporting text / numbers directly from speech balloons:
|
|
contents.userMenu = function () {
|
|
var menu = new MenuMorph(this),
|
|
ide = this.parentThatIsA(IDE_Morph)||
|
|
this.world().childThatIsA(IDE_Morph);
|
|
|
|
if (ide.isAppMode) {return; }
|
|
menu.addItem(
|
|
'export',
|
|
() => ide.saveFileAs(
|
|
data.toString(),
|
|
'text/plain;charset=utf-8',
|
|
localize('data')
|
|
)
|
|
);
|
|
return menu;
|
|
};
|
|
|
|
}
|
|
if (contents instanceof TextMorph) {
|
|
// reflow text boundaries
|
|
width = Math.max(
|
|
contents.width(),
|
|
sprite.bubbleCorner * 2 * this.scale
|
|
);
|
|
if (isText) {
|
|
width = Math.min(width, sprite.bubbleMaxTextWidth * this.scale);
|
|
}
|
|
contents.setWidth(width);
|
|
} else if (!(data instanceof List)) {
|
|
// scale contents image
|
|
scaledImg = newCanvas(contents.extent().multiplyBy(this.scale));
|
|
scaledImg.getContext('2d').drawImage(
|
|
contents.getImage(),
|
|
0,
|
|
0,
|
|
scaledImg.width,
|
|
scaledImg.height
|
|
);
|
|
contents.cachedImage = scaledImg;
|
|
contents.bounds = contents.bounds.scaleBy(this.scale);
|
|
}
|
|
return contents;
|
|
};
|
|
|
|
// SpriteBubbleMorph scaling
|
|
|
|
SpriteBubbleMorph.prototype.setScale = function (scale) {
|
|
this.scale = scale;
|
|
this.changed();
|
|
this.fixLayout();
|
|
this.rerender();
|
|
};
|
|
|
|
// SpriteBubbleMorph layout:
|
|
|
|
SpriteBubbleMorph.prototype.fixLayout = function () {
|
|
var sprite = SpriteMorph.prototype;
|
|
|
|
// rebuild my contents
|
|
if (!(this.contentsMorph instanceof ListWatcherMorph ||
|
|
this.contentsMorph instanceof TableFrameMorph)) {
|
|
this.contentsMorph.destroy();
|
|
this.contentsMorph = this.dataAsMorph(this.data);
|
|
}
|
|
this.add(this.contentsMorph);
|
|
|
|
// scale my settings
|
|
this.edge = sprite.bubbleCorner * this.scale;
|
|
this.border = sprite.bubbleBorder * this.scale;
|
|
this.padding = sprite.bubbleCorner / 2 * this.scale;
|
|
|
|
// adjust my dimensions
|
|
this.bounds.setWidth(this.contentsMorph.width()
|
|
+ (this.padding ? this.padding * 2 : this.edge * 2));
|
|
this.bounds.setHeight(this.contentsMorph.height()
|
|
+ this.edge
|
|
+ this.border * 2
|
|
+ this.padding * 2
|
|
+ 2);
|
|
|
|
// position my contents
|
|
this.contentsMorph.setPosition(this.position().add(
|
|
new Point(
|
|
this.padding || this.edge,
|
|
this.border + this.padding + 1
|
|
)
|
|
));
|
|
};
|
|
|
|
// Costume /////////////////////////////////////////////////////////////
|
|
|
|
/*
|
|
I am a picture that's "wearable" by a sprite. My rotationCenter is
|
|
relative to my contents position.
|
|
*/
|
|
|
|
// Costume instance creation
|
|
|
|
function Costume(canvas, name, rotationCenter, noFit, maxExtent) {
|
|
this.contents = canvas ? normalizeCanvas(canvas, true)
|
|
: newCanvas(null, true);
|
|
if (!noFit) {this.shrinkToFit(maxExtent || this.maxExtent()); }
|
|
this.name = name || null;
|
|
this.rotationCenter = rotationCenter || this.center();
|
|
this.version = Date.now(); // for observer optimization
|
|
this.loaded = null; // for de-serialization only
|
|
}
|
|
|
|
Costume.prototype.maxDimensions = new Point(480, 360);
|
|
|
|
Costume.prototype.maxExtent = function () {
|
|
// return StageMorph.prototype.dimensions;
|
|
return this.maxDimensions;
|
|
};
|
|
|
|
Costume.prototype.toString = function () {
|
|
return 'a Costume(' + this.name + ')';
|
|
};
|
|
|
|
// Costume dimensions - all relative
|
|
|
|
Costume.prototype.extent = function () {
|
|
return new Point(this.contents.width, this.contents.height);
|
|
};
|
|
|
|
Costume.prototype.center = function () {
|
|
return this.extent().divideBy(2);
|
|
};
|
|
|
|
Costume.prototype.width = function () {
|
|
return this.contents.width;
|
|
};
|
|
|
|
Costume.prototype.height = function () {
|
|
return this.contents.height;
|
|
};
|
|
|
|
Costume.prototype.bounds = function () {
|
|
return new Rectangle(0, 0, this.width(), this.height());
|
|
};
|
|
|
|
// Costume shrink-wrapping
|
|
|
|
Costume.prototype.shrinkWrap = function () {
|
|
// adjust my contents' bounds to my visible bounding box
|
|
var bb = this.boundingBox(),
|
|
ext = bb.extent(),
|
|
pic = newCanvas(ext, true),
|
|
ctx = pic.getContext('2d');
|
|
|
|
ctx.drawImage(
|
|
this.contents,
|
|
bb.origin.x,
|
|
bb.origin.y,
|
|
ext.x,
|
|
ext.y,
|
|
0,
|
|
0,
|
|
ext.x,
|
|
ext.y
|
|
);
|
|
this.rotationCenter = this.rotationCenter.subtract(bb.origin);
|
|
this.contents = pic;
|
|
this.version = Date.now();
|
|
};
|
|
|
|
Costume.prototype.canvasBoundingBox = function (pic) {
|
|
// answer the rectangle surrounding my contents' non-transparent pixels
|
|
var row,
|
|
col,
|
|
w = pic.width,
|
|
h = pic.height,
|
|
ctx = pic.getContext('2d'),
|
|
dta = ctx.getImageData(0, 0, w, h);
|
|
|
|
function getAlpha(x, y) {
|
|
return dta.data[((y * w * 4) + (x * 4)) + 3];
|
|
}
|
|
|
|
function getLeft() {
|
|
for (col = 0; col < w; col += 1) {
|
|
for (row = 0; row < h; row += 1) {
|
|
if (getAlpha(col, row)) {
|
|
return col;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function getTop() {
|
|
for (row = 0; row < h; row += 1) {
|
|
for (col = 0; col < w; col += 1) {
|
|
if (getAlpha(col, row)) {
|
|
return row;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function getRight() {
|
|
for (col = w - 1; col >= 0; col -= 1) {
|
|
for (row = h - 1; row >= 0; row -= 1) {
|
|
if (getAlpha(col, row)) {
|
|
return Math.min(col + 1, w);
|
|
}
|
|
}
|
|
}
|
|
return w;
|
|
}
|
|
|
|
function getBottom() {
|
|
for (row = h - 1; row >= 0; row -= 1) {
|
|
for (col = w - 1; col >= 0; col -= 1) {
|
|
if (getAlpha(col, row)) {
|
|
return Math.min(row + 1, h);
|
|
}
|
|
}
|
|
}
|
|
return h;
|
|
}
|
|
|
|
return new Rectangle(getLeft(), getTop(), getRight(), getBottom());
|
|
};
|
|
|
|
Costume.prototype.boundingBox = function () {
|
|
return this.canvasBoundingBox(this.contents);
|
|
};
|
|
|
|
// Costume duplication
|
|
|
|
Costume.prototype.copy = function () {
|
|
var canvas = newCanvas(this.extent(), true),
|
|
cpy,
|
|
ctx;
|
|
ctx = canvas.getContext('2d');
|
|
ctx.drawImage(this.contents, 0, 0);
|
|
cpy = new Costume(canvas, this.name ? copy(this.name) : null);
|
|
cpy.rotationCenter = this.rotationCenter.copy();
|
|
return cpy;
|
|
};
|
|
|
|
// Costume flipping & stretching
|
|
|
|
Costume.prototype.flipped = function () {
|
|
/*
|
|
answer a copy of myself flipped horizontally
|
|
(mirrored along a vertical axis), used for
|
|
SpriteMorph's rotation style type 2
|
|
*/
|
|
var canvas = newCanvas(this.extent(), true),
|
|
ctx = canvas.getContext('2d'),
|
|
flipped;
|
|
|
|
ctx.translate(this.width(), 0);
|
|
ctx.scale(-1, 1);
|
|
ctx.drawImage(this.contents, 0, 0);
|
|
flipped = new Costume(
|
|
canvas,
|
|
this.name,
|
|
new Point(
|
|
this.width() - this.rotationCenter.x,
|
|
this.rotationCenter.y
|
|
),
|
|
true // no shrink-wrap
|
|
);
|
|
return flipped;
|
|
};
|
|
|
|
Costume.prototype.stretched = function (w, h) {
|
|
w = (Math.sign(w) || 1) * Math.max(1, Math.abs(w));
|
|
h = (Math.sign(h) || 1) * Math.max(1, Math.abs(h));
|
|
|
|
var canvas = newCanvas(new Point(Math.abs(w), Math.abs(h)), true),
|
|
ctx = canvas.getContext('2d'),
|
|
xRatio = w / this.width(),
|
|
yRatio = h / this.height(),
|
|
center = this.rotationCenter.multiplyBy(new Point(xRatio, yRatio)),
|
|
stretched;
|
|
|
|
if (xRatio < 0) {
|
|
center.x = canvas.width - Math.abs(center.x);
|
|
}
|
|
if (yRatio < 0) {
|
|
center.y = canvas.height - Math.abs(center.y);
|
|
}
|
|
|
|
ctx.translate(Math.abs(Math.min(w, 0)), Math.abs(Math.min(h, 0)));
|
|
ctx.scale(xRatio, yRatio);
|
|
// first rasterize in case it's an SVG and in case it's on Firefox
|
|
// because Firefox prevents stretching of SVGs with locked aspect ratios
|
|
ctx.drawImage(this.rasterized().contents, 0, 0);
|
|
stretched = new Costume(
|
|
canvas,
|
|
this.name,
|
|
center,
|
|
true
|
|
);
|
|
return stretched;
|
|
};
|
|
|
|
// Costume actions
|
|
|
|
Costume.prototype.edit = function (aWorld, anIDE, isnew, oncancel, onsubmit) {
|
|
var editor = new PaintEditorMorph();
|
|
editor.oncancel = oncancel || nop;
|
|
editor.openIn(
|
|
aWorld,
|
|
isnew ?
|
|
newCanvas(anIDE.stage.dimensions, true) :
|
|
this.contents,
|
|
isnew ?
|
|
null :
|
|
this.rotationCenter,
|
|
(img, rc) => {
|
|
this.contents = img;
|
|
this.rotationCenter = rc;
|
|
this.version = Date.now();
|
|
aWorld.changed();
|
|
if (anIDE) {
|
|
if (anIDE.currentSprite instanceof SpriteMorph) {
|
|
// don't shrinkwrap stage costumes
|
|
this.shrinkWrap();
|
|
}
|
|
anIDE.currentSprite.wearCostume(this, true); // don't shadow
|
|
anIDE.hasChangedMedia = true;
|
|
}
|
|
(onsubmit || nop)();
|
|
},
|
|
anIDE
|
|
);
|
|
};
|
|
|
|
Costume.prototype.editRotationPointOnly = function (aWorld, anIDE) {
|
|
var editor = new CostumeEditorMorph(this),
|
|
action,
|
|
dialog,
|
|
txt;
|
|
|
|
editor.fixLayout();
|
|
action = () => {
|
|
editor.accept();
|
|
anIDE.currentSprite.wearCostume(this, true); // don't shadow
|
|
};
|
|
dialog = new DialogBoxMorph(this, action);
|
|
txt = new TextMorph(
|
|
localize('click or drag crosshairs to move the rotation center'),
|
|
dialog.fontSize,
|
|
dialog.fontStyle,
|
|
true,
|
|
false,
|
|
'center',
|
|
null,
|
|
null,
|
|
new Point(1, 1),
|
|
WHITE
|
|
);
|
|
|
|
dialog.labelString = 'Costume Editor';
|
|
dialog.createLabel();
|
|
dialog.setPicture(editor);
|
|
dialog.addBody(txt);
|
|
dialog.addButton('ok', 'Ok');
|
|
dialog.addButton('cancel', 'Cancel');
|
|
dialog.fixLayout();
|
|
dialog.popUp(aWorld);
|
|
};
|
|
|
|
// Costume thumbnail
|
|
|
|
Costume.prototype.shrinkToFit = function (extentPoint) {
|
|
if (extentPoint.x < this.width() || (extentPoint.y < this.height())) {
|
|
this.contents = this.thumbnail(extentPoint, null, true);
|
|
}
|
|
};
|
|
|
|
Costume.prototype.thumbnail = function (extentPoint, recycleMe, noPadding) {
|
|
// answer a new Canvas of extentPoint dimensions containing
|
|
// my thumbnail representation keeping the originial aspect ratio
|
|
// a "recycleMe canvas can be passed for re-use
|
|
// if "noPadding" is "true" the resulting thumbnail fits inside the
|
|
// given extentPoint without padding it, i.e. one of the dimensions
|
|
// is likely to be lesser than that of the extentPoint
|
|
var src = this.contents,
|
|
w = src ? src.width : 1, // could be an asynchronously loading SVG
|
|
h = src ? src.height : 1, // could be an asynchronously loading SVG
|
|
scale = Math.min(
|
|
(extentPoint.x / w),
|
|
(extentPoint.y / h)
|
|
),
|
|
xOffset = noPadding ? 0
|
|
: Math.floor((extentPoint.x - (w * scale)) / 2),
|
|
yOffset = noPadding ? 0
|
|
: Math.floor((extentPoint.y - (h * scale)) / 2),
|
|
trg, ctx;
|
|
|
|
trg = newCanvas(
|
|
noPadding ? new Point(this.width() * scale, this.height() * scale)
|
|
: extentPoint,
|
|
true, // non-retina
|
|
recycleMe
|
|
);
|
|
if (!src || src.width + src.height === 0) {return trg; }
|
|
ctx = trg.getContext('2d');
|
|
ctx.save();
|
|
ctx.scale(scale, scale);
|
|
ctx.drawImage(
|
|
src,
|
|
Math.floor(xOffset / scale),
|
|
Math.floor(yOffset / scale)
|
|
);
|
|
ctx.restore();
|
|
return trg;
|
|
};
|
|
|
|
// Costume pixel access
|
|
|
|
Costume.prototype.rasterized = function () {
|
|
return this;
|
|
};
|
|
|
|
Costume.prototype.pixels = function () {
|
|
var pixels = [],
|
|
src,
|
|
i;
|
|
|
|
if (!this.contents.width || !this.contents.height) {
|
|
return pixels;
|
|
}
|
|
src = this.contents.getContext('2d').getImageData(
|
|
0,
|
|
0,
|
|
this.contents.width,
|
|
this.contents.height
|
|
);
|
|
for (i = 0; i < src.data.length; i += 4) {
|
|
pixels.push(new List([
|
|
src.data[i],
|
|
src.data[i + 1],
|
|
src.data[i + 2],
|
|
src.data[i + 3]
|
|
]));
|
|
}
|
|
return new List(pixels);
|
|
};
|
|
|
|
// Costume catching "tainted" canvases
|
|
|
|
Costume.prototype.isTainted = function () {
|
|
// find out whether the canvas has been tainted by cross-origin data
|
|
// assumes that if reading image data throws an error it is tainted
|
|
try {
|
|
this.contents.getContext('2d').getImageData(
|
|
0,
|
|
0,
|
|
this.contents.width,
|
|
this.contents.height
|
|
);
|
|
} catch (err) {
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// SVG_Costume /////////////////////////////////////////////////////////////
|
|
|
|
/*
|
|
I am a costume containing an SVG image.
|
|
*/
|
|
|
|
// SVG_Costume inherits from Costume:
|
|
|
|
SVG_Costume.prototype = new Costume();
|
|
SVG_Costume.prototype.constructor = SVG_Costume;
|
|
SVG_Costume.uber = Costume.prototype;
|
|
|
|
// SVG_Costume instance creation
|
|
|
|
function SVG_Costume(svgImage, name, rotationCenter) {
|
|
this.contents = svgImage;
|
|
this.shapes = [];
|
|
this.shrinkToFit(this.maxExtent());
|
|
this.name = name || null;
|
|
this.rotationCenter = rotationCenter || this.center();
|
|
this.version = Date.now(); // for observer optimization
|
|
this.loaded = null; // for de-serialization only
|
|
}
|
|
|
|
SVG_Costume.prototype.toString = function () {
|
|
return 'an SVG_Costume(' + this.name + ')';
|
|
};
|
|
|
|
// SVG_Costume duplication
|
|
|
|
SVG_Costume.prototype.copy = function () {
|
|
var img = new Image(),
|
|
cpy;
|
|
img.src = this.contents.src;
|
|
cpy = new SVG_Costume(img, this.name ? copy(this.name) : null);
|
|
cpy.rotationCenter = this.rotationCenter.copy();
|
|
cpy.shapes = this.shapes.map(shape => shape.copy());
|
|
return cpy;
|
|
};
|
|
|
|
// SVG_Costume flipping
|
|
|
|
/*
|
|
Flipping is currently inherited from Costume, which rasterizes it.
|
|
Therefore flipped SVG costumes may appear pixelated until we add
|
|
a method to either truly flip SVGs or change the Sprite's render()
|
|
method to scale the costume before flipping it.
|
|
|
|
Stretching, OTOH, is achieved with real scaling and thus produces
|
|
smooth, albeit rasterized results for vector graphics.
|
|
*/
|
|
|
|
// SVG_Costume thumbnail
|
|
|
|
SVG_Costume.prototype.shrinkToFit = function (extentPoint) {
|
|
// overridden for unrasterized SVGs
|
|
nop(extentPoint);
|
|
return;
|
|
};
|
|
|
|
SVG_Costume.prototype.parseShapes = function () {
|
|
// I try to parse my SVG as an editable collection of shapes
|
|
var element = new XML_Element(),
|
|
// remove 'data:image/svg+xml, ' from src
|
|
contents = this.contents.src.replace(/^data:image\/.*?, */, '');
|
|
|
|
if (this.contents.src.indexOf('base64') > -1) {
|
|
contents = atob(contents);
|
|
}
|
|
|
|
element.parseString(contents);
|
|
|
|
if (this.shapes.length === 0 && element.attributes.snap) {
|
|
this.shapes = element.children.map(child =>
|
|
window[child.attributes.prototype].fromSVG(child)
|
|
);
|
|
}
|
|
};
|
|
|
|
SVG_Costume.prototype.edit = function (
|
|
aWorld,
|
|
anIDE,
|
|
isnew,
|
|
oncancel,
|
|
onsubmit
|
|
) {
|
|
var editor = new VectorPaintEditorMorph(),
|
|
myself = this;
|
|
|
|
editor.oncancel = oncancel || nop;
|
|
editor.openIn(
|
|
aWorld,
|
|
isnew ? newCanvas(anIDE.stage.dimensions) : this.contents,
|
|
isnew ? new Point(240, 180) : this.rotationCenter,
|
|
(img, rc, shapes) => {
|
|
myself.contents = img;
|
|
myself.rotationCenter = rc;
|
|
myself.shapes = shapes;
|
|
myself.version = Date.now();
|
|
aWorld.changed();
|
|
if (anIDE) {
|
|
if (isnew) {anIDE.currentSprite.addCostume(myself); }
|
|
anIDE.currentSprite.wearCostume(myself);
|
|
anIDE.hasChangedMedia = true;
|
|
}
|
|
(onsubmit || nop)();
|
|
},
|
|
anIDE,
|
|
this.shapes || []
|
|
);
|
|
};
|
|
|
|
// SVG_Costume pixel access
|
|
|
|
SVG_Costume.prototype.rasterized = function () {
|
|
var canvas = newCanvas(this.extent(), true),
|
|
ctx = canvas.getContext('2d'),
|
|
rasterized;
|
|
|
|
ctx.drawImage(this.contents, 0, 0);
|
|
rasterized = new Costume(
|
|
canvas,
|
|
this.name,
|
|
this.rotationCenter.copy()
|
|
);
|
|
return rasterized;
|
|
};
|
|
|
|
// CostumeEditorMorph ////////////////////////////////////////////////////////
|
|
|
|
// CostumeEditorMorph inherits from Morph:
|
|
|
|
CostumeEditorMorph.prototype = new Morph();
|
|
CostumeEditorMorph.prototype.constructor = CostumeEditorMorph;
|
|
CostumeEditorMorph.uber = Morph.prototype;
|
|
|
|
// CostumeEditorMorph preferences settings:
|
|
CostumeEditorMorph.prototype.size = Costume.prototype.maxExtent();
|
|
|
|
// CostumeEditorMorph instance creation
|
|
|
|
function CostumeEditorMorph(costume) {
|
|
this.init(costume);
|
|
}
|
|
|
|
CostumeEditorMorph.prototype.init = function (costume) {
|
|
this.costume = costume || new Costume();
|
|
this.rotationCenter = this.costume.rotationCenter.copy();
|
|
this.margin = ZERO;
|
|
CostumeEditorMorph.uber.init.call(this);
|
|
};
|
|
|
|
// CostumeEditorMorph edit ops
|
|
|
|
CostumeEditorMorph.prototype.accept = function () {
|
|
this.costume.rotationCenter = this.rotationCenter.copy();
|
|
this.costume.version = Date.now();
|
|
};
|
|
|
|
// CostumeEditorMorph displaying
|
|
|
|
CostumeEditorMorph.prototype.fixLayout = function () {
|
|
this.bounds.setExtent(this.size);
|
|
};
|
|
|
|
CostumeEditorMorph.prototype.render = function (ctx) {
|
|
var rp;
|
|
|
|
this.margin = this.size.subtract(this.costume.extent()).divideBy(2);
|
|
rp = this.rotationCenter.add(this.margin);
|
|
|
|
|
|
// draw the background
|
|
if (!this.cachedTexture) {
|
|
this.cachedTexture = this.createTexture();
|
|
|
|
}
|
|
this.renderCachedTexture(ctx);
|
|
|
|
/*
|
|
pattern = ctx.createPattern(this.background, 'repeat');
|
|
ctx.fillStyle = pattern;
|
|
ctx.fillRect(0, 0, this.size.x, this.size.y);
|
|
*/
|
|
|
|
// draw the costume
|
|
ctx.drawImage(this.costume.contents, this.margin.x, this.margin.y);
|
|
|
|
// draw crosshairs:
|
|
ctx.globalAlpha = 0.5;
|
|
|
|
// circle around center:
|
|
ctx.fillStyle = 'white';
|
|
ctx.beginPath();
|
|
ctx.arc(
|
|
rp.x,
|
|
rp.y,
|
|
20,
|
|
radians(0),
|
|
radians(360),
|
|
false
|
|
);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(
|
|
rp.x,
|
|
rp.y,
|
|
10,
|
|
radians(0),
|
|
radians(360),
|
|
false
|
|
);
|
|
ctx.stroke();
|
|
|
|
// horizontal line:
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, rp.y);
|
|
ctx.lineTo(this.costume.width() + this.margin.x * 2, rp.y);
|
|
ctx.stroke();
|
|
|
|
// vertical line:
|
|
ctx.beginPath();
|
|
ctx.moveTo(rp.x, 0);
|
|
ctx.lineTo(rp.x, this.costume.height() + this.margin.y * 2);
|
|
ctx.stroke();
|
|
};
|
|
|
|
CostumeEditorMorph.prototype.createTexture = function () {
|
|
var size = 5,
|
|
texture = newCanvas(new Point(size * 2, size * 2)),
|
|
ctx = texture.getContext('2d'),
|
|
grey = new Color(230, 230, 230);
|
|
|
|
ctx.fillStyle = 'white';
|
|
ctx.fillRect(0, 0, size * 2, size * 2);
|
|
ctx.fillStyle = grey.toString();
|
|
ctx.fillRect(0, 0, size, size);
|
|
ctx.fillRect(size, size, size, size);
|
|
return texture;
|
|
};
|
|
|
|
|
|
// CostumeEditorMorph events
|
|
|
|
CostumeEditorMorph.prototype.mouseDownLeft = function (pos) {
|
|
this.rotationCenter = pos.subtract(
|
|
this.position().add(this.margin)
|
|
);
|
|
this.rerender();
|
|
};
|
|
|
|
CostumeEditorMorph.prototype.mouseMove
|
|
= CostumeEditorMorph.prototype.mouseDownLeft;
|
|
|
|
// Sound /////////////////////////////////////////////////////////////
|
|
|
|
// Sound instance creation
|
|
|
|
function Sound(audio, name) {
|
|
this.audio = audio; // mandatory
|
|
this.name = name || "Sound";
|
|
|
|
// cached samples, don't persist
|
|
this.cachedSamples = null;
|
|
|
|
// internal for decoding, don't persist
|
|
this.audioBuffer = null; // for decoding ops
|
|
this.isDecoding = false;
|
|
|
|
// internal for deserializing, don't persist
|
|
this.loaded = null; // for de-serialization only
|
|
}
|
|
|
|
Sound.prototype.play = function () {
|
|
// return an instance of an audio element which can be terminated
|
|
// externally (i.e. by the stage)
|
|
// Note: only to be used by the GUI, not by scripts,
|
|
// because no effects like volume or panning are applied
|
|
var aud = document.createElement('audio');
|
|
aud.src = this.audio.src;
|
|
aud.play();
|
|
return aud;
|
|
};
|
|
|
|
Sound.prototype.copy = function () {
|
|
var snd = document.createElement('audio'),
|
|
cpy;
|
|
|
|
snd.src = this.audio.src;
|
|
cpy = new Sound(snd, this.name ? copy(this.name) : null);
|
|
return cpy;
|
|
};
|
|
|
|
Sound.prototype.toDataURL = function () {
|
|
return this.audio.src;
|
|
};
|
|
|
|
// Note /////////////////////////////////////////////////////////
|
|
|
|
// I am a single musical note.
|
|
// alternatively I can be used to play a frequency in hz
|
|
|
|
// Note instance creation
|
|
|
|
function Note(pitch) {
|
|
this.pitch = pitch === 0 ? 0 : pitch || 69;
|
|
this.frequency = null; // alternative for playing a non-note frequency
|
|
this.setupContext();
|
|
this.oscillator = null;
|
|
this.fader = null; // gain node for suppressing clicks
|
|
this.ended = false; // for active sounds management
|
|
}
|
|
|
|
// Note shared properties
|
|
|
|
Note.prototype.audioContext = null;
|
|
|
|
Note.prototype.fadeIn = new Float32Array(2);
|
|
Note.prototype.fadeIn[0] = [0.0];
|
|
Note.prototype.fadeIn[1] = [0.2];
|
|
|
|
Note.prototype.fadeOut = new Float32Array(2);
|
|
Note.prototype.fadeOut[0] = [0.2];
|
|
Note.prototype.fadeOut[1] = [0.0];
|
|
|
|
Note.prototype.fadeTime = 0.01;
|
|
|
|
// Note audio context
|
|
|
|
Note.prototype.setupContext = function () {
|
|
if (this.audioContext) { return; }
|
|
var AudioContext = (function () {
|
|
// cross browser some day?
|
|
var ctx = window.AudioContext ||
|
|
window.mozAudioContext ||
|
|
window.msAudioContext ||
|
|
window.oAudioContext ||
|
|
window.webkitAudioContext;
|
|
if (!ctx.prototype.createGain) {
|
|
ctx.prototype.createGain = ctx.prototype.createGainNode;
|
|
}
|
|
return ctx;
|
|
}());
|
|
if (!AudioContext) {
|
|
throw new Error('Web Audio API is not supported\nin this browser');
|
|
}
|
|
Note.prototype.audioContext = new AudioContext();
|
|
};
|
|
|
|
Note.prototype.getAudioContext = function () {
|
|
// lazily initializes and shares the Note prototype's audio context
|
|
// to be used by all other Snap! objects requiring audio,
|
|
// e.g. the microphone, the sprites, etc.
|
|
if (!this.audioContext) {
|
|
this.setupContext();
|
|
}
|
|
this.audioContext.resume();
|
|
return this.audioContext;
|
|
};
|
|
|
|
// Note playing
|
|
|
|
Note.prototype.play = function (type, gainNode, pannerNode) {
|
|
if (!gainNode) {
|
|
gainNode = this.audioContext.createGain();
|
|
}
|
|
this.fader = this.audioContext.createGain();
|
|
this.oscillator = this.audioContext.createOscillator();
|
|
if (!this.oscillator.start) {
|
|
this.oscillator.start = this.oscillator.noteOn;
|
|
}
|
|
if (!this.oscillator.stop) {
|
|
this.oscillator.stop = this.oscillator.noteOff;
|
|
}
|
|
this.setInstrument(type);
|
|
this.oscillator.frequency.value = isNil(this.frequency) ?
|
|
Math.pow(2, (this.pitch - 69) / 12) * 440 : this.frequency;
|
|
this.oscillator.connect(this.fader);
|
|
this.fader.connect(gainNode);
|
|
if (pannerNode) {
|
|
gainNode.connect(pannerNode);
|
|
pannerNode.connect(this.audioContext.destination);
|
|
} else {
|
|
gainNode.connect(this.audioContext.destination);
|
|
}
|
|
this.ended = false;
|
|
this.fader.gain.setValueCurveAtTime(
|
|
this.fadeIn,
|
|
this.audioContext.currentTime,
|
|
this.fadeTime
|
|
);
|
|
this.oscillator.start(0);
|
|
};
|
|
|
|
Note.prototype.setInstrument = function (type) {
|
|
// private - make sure the oscillator node has been initialized before
|
|
if (this.oscillator) {
|
|
this.oscillator.type = [
|
|
'sine',
|
|
'square',
|
|
'sawtooth',
|
|
'triangle'
|
|
][(type || 1) - 1];
|
|
}
|
|
};
|
|
|
|
Note.prototype.stop = function (immediately) {
|
|
// set "immediately" to true to terminate instantly
|
|
// needed for widgets like the PianoKeyboard
|
|
var fade = !immediately;
|
|
if (immediately && this.oscillator) {
|
|
this.oscillator.stop(0);
|
|
return;
|
|
}
|
|
if (this.fader) {
|
|
try {
|
|
this.fader.gain.setValueCurveAtTime(
|
|
this.fadeOut,
|
|
this.audioContext.currentTime,
|
|
this.fadeTime
|
|
);
|
|
} catch (err) {
|
|
fade = false;
|
|
}
|
|
}
|
|
if (this.oscillator) {
|
|
this.oscillator.stop(
|
|
fade ?
|
|
this.audioContext.currentTime + this.fadeTime
|
|
: 0
|
|
);
|
|
this.oscillator = null;
|
|
}
|
|
this.ended = true;
|
|
};
|
|
|
|
Note.prototype.pause = function () {
|
|
// emulate a sound for active sounds mngmt
|
|
this.stop();
|
|
};
|
|
|
|
// Microphone /////////////////////////////////////////////////////////
|
|
|
|
// I am a microphone and know about volume, note, pitch, as well as
|
|
// signals and frequencies.
|
|
// mostly meant to be a singleton of the stage
|
|
// I stop when I'm not queried something for 5 seconds
|
|
// to free up system resources
|
|
//
|
|
// modifying and metering output is currently experimental
|
|
// and only fully works in Chrome. Modifiers work in Firefox, but only with
|
|
// a significant lag, metering output is currently not supported by Firefox.
|
|
// Safari... well, let's not talk about Safari :-)
|
|
|
|
function Microphone() {
|
|
// web audio components:
|
|
this.audioContext = null; // shared with Note.prototype.audioContext
|
|
this.sourceStream = null;
|
|
this.processor = null;
|
|
this.analyser = null;
|
|
|
|
// parameters:
|
|
this.resolution = 2;
|
|
this.GOOD_ENOUGH_CORRELATION = 0.96;
|
|
|
|
// modifier
|
|
this.modifier = null;
|
|
this.compiledModifier = null;
|
|
this.compilerProcess = null;
|
|
|
|
// memory alloc
|
|
this.correlations = [];
|
|
this.wrapper = new List([0]);
|
|
this.outChannels = [];
|
|
|
|
// metered values:
|
|
this.volume = 0;
|
|
this.signals = [];
|
|
this.output = [];
|
|
this.frequencies = [];
|
|
this.pitch = -1;
|
|
|
|
// asynch control:
|
|
this.isStarted = false;
|
|
this.isReady = false;
|
|
|
|
// idling control:
|
|
this.isAutoStop = (location.protocol !== 'file:');
|
|
this.lastTime = Date.now();
|
|
}
|
|
|
|
Microphone.prototype.isOn = function () {
|
|
if (this.isReady) {
|
|
this.lastTime = Date.now();
|
|
return true;
|
|
}
|
|
this.start();
|
|
return false;
|
|
};
|
|
|
|
// Microphone shared properties
|
|
|
|
Microphone.prototype.binSizes = [256, 512, 1024, 2048, 4096];
|
|
|
|
// Microphone resolution
|
|
|
|
Microphone.prototype.binSize = function () {
|
|
return this.binSizes[this.resolution - 1];
|
|
};
|
|
|
|
Microphone.prototype.setResolution = function (num) {
|
|
if (contains([1, 2, 3, 4], num)) {
|
|
if (this.isReady) {
|
|
this.stop();
|
|
}
|
|
this.resolution = num;
|
|
}
|
|
};
|
|
|
|
// Microphone ops
|
|
|
|
Microphone.prototype.start = function () {
|
|
if (this.isStarted) {return; }
|
|
this.isStarted = true;
|
|
this.isReady = false;
|
|
this.audioContext = Note.prototype.getAudioContext();
|
|
|
|
navigator.mediaDevices.getUserMedia(
|
|
{
|
|
"audio": {
|
|
"mandatory": {
|
|
"googEchoCancellation": "false",
|
|
"googAutoGainControl": "false",
|
|
"googNoiseSuppression": "false",
|
|
"googHighpassFilter": "false"
|
|
},
|
|
"optional": []
|
|
},
|
|
}
|
|
).then(
|
|
stream => this.setupNodes(stream)
|
|
).catch(nop);
|
|
};
|
|
|
|
Microphone.prototype.stop = function () {
|
|
this.processor.onaudioprocess = null;
|
|
this.sourceStream.getTracks().forEach(track => track.stop());
|
|
this.processor.disconnect();
|
|
this.analyser.disconnect();
|
|
this.processor = null;
|
|
this.analyser = null;
|
|
this.audioContext = null;
|
|
this.isReady = false;
|
|
this.isStarted = false;
|
|
};
|
|
|
|
// Microphone initialization
|
|
|
|
Microphone.prototype.setupNodes = function (stream) {
|
|
this.sourceStream = stream;
|
|
this.createProcessor();
|
|
this.createAnalyser();
|
|
this.analyser.connect(this.processor);
|
|
this.processor.connect(this.audioContext.destination);
|
|
this.audioContext.createMediaStreamSource(stream).connect(this.analyser);
|
|
this.lastTime = Date.now();
|
|
};
|
|
|
|
Microphone.prototype.createAnalyser = function () {
|
|
var bufLength;
|
|
this.analyser = this.audioContext.createAnalyser();
|
|
this.analyser.fftSize = this.binSizes[this.resolution];
|
|
bufLength = this.analyser.frequencyBinCount;
|
|
this.frequencies = new Uint8Array(bufLength);
|
|
|
|
// setup pitch detection correlations:
|
|
this.correlations = new Array(Math.floor(bufLength/2));
|
|
};
|
|
|
|
Microphone.prototype.createProcessor = function () {
|
|
var myself = this;
|
|
this.processor = this.audioContext.createScriptProcessor(
|
|
this.binSizes[this.resolution - 1]
|
|
);
|
|
|
|
this.processor.onaudioprocess = function (event) {
|
|
myself.stepAudio(event);
|
|
};
|
|
|
|
this.processor.clipping = false;
|
|
this.processor.lastClip = 0;
|
|
this.processor.clipLevel = 0.98;
|
|
this.processor.averaging = 0.95;
|
|
this.processor.clipLag = 750;
|
|
};
|
|
|
|
// Microphone stepping
|
|
|
|
Microphone.prototype.stepAudio = function (event) {
|
|
var channels, i;
|
|
if (this.isAutoStop &&
|
|
((Date.now() - this.lastTime) > 5000) &&
|
|
!this.modifier
|
|
) {
|
|
this.stop();
|
|
return;
|
|
}
|
|
|
|
// signals:
|
|
this.signals = event.inputBuffer.getChannelData(0);
|
|
|
|
// output:
|
|
if (this.modifier) {
|
|
channels = event.outputBuffer.numberOfChannels;
|
|
if (this.outChannels.length !== channels) {
|
|
this.outChannels = new Array(channels);
|
|
}
|
|
for (i = 0; i < channels; i += 1) {
|
|
this.outChannels[i] = event.outputBuffer.getChannelData(i);
|
|
}
|
|
this.output = this.outChannels[0];
|
|
} else {
|
|
this.output = event.outputBuffer.getChannelData(0);
|
|
}
|
|
|
|
// frequency bins:
|
|
this.analyser.getByteFrequencyData(this.frequencies);
|
|
|
|
// pitch & volume:
|
|
this.pitch = this.detectPitchAndVolume(
|
|
this.signals,
|
|
this.audioContext.sampleRate
|
|
);
|
|
|
|
// note:
|
|
if (this.pitch > 0) {
|
|
this.note = Math.round(
|
|
12 * (Math.log(this.pitch / 440) / Math.log(2))
|
|
) + 69;
|
|
}
|
|
|
|
this.isReady = true;
|
|
this.isStarted = false;
|
|
};
|
|
|
|
Microphone.prototype.detectPitchAndVolume = function (buf, sampleRate) {
|
|
// https://en.wikipedia.org/wiki/Autocorrelation
|
|
// thanks to Chris Wilson:
|
|
// https://plus.google.com/+ChrisWilson/posts/9zHsF9PCDAL
|
|
// https://github.com/cwilso/PitchDetect/
|
|
|
|
var SIZE = buf.length,
|
|
MAX_SAMPLES = Math.floor(SIZE/2),
|
|
best_offset = -1,
|
|
best_correlation = 0,
|
|
rms = 0,
|
|
foundGoodCorrelation = false,
|
|
correlations = this.correlations,
|
|
channels = this.outChannels.length,
|
|
correlation,
|
|
lastCorrelation,
|
|
offset,
|
|
shift,
|
|
i,
|
|
k,
|
|
val,
|
|
modified;
|
|
|
|
for (i = 0; i < SIZE; i += 1) {
|
|
val = buf[i];
|
|
if (Math.abs(val) >= this.processor.clipLevel) {
|
|
this.processor.clipping = true;
|
|
this.processor.lastClip = window.performance.now();
|
|
}
|
|
rms += val * val;
|
|
|
|
// apply modifier, if any
|
|
if (this.modifier) {
|
|
this.wrapper.contents[0] = val;
|
|
modified = invoke(
|
|
this.compiledModifier,
|
|
this.wrapper,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
this.compilerProcess
|
|
);
|
|
for (k = 0; k < channels; k += 1) {
|
|
this.outChannels[k][i] = modified;
|
|
}
|
|
}
|
|
}
|
|
rms = Math.sqrt(rms/SIZE);
|
|
this.volume = Math.max(rms, this.volume * this.processor.averaging);
|
|
if (rms < 0.01)
|
|
return this.pitch;
|
|
|
|
lastCorrelation = 1;
|
|
for (offset = 1; offset < MAX_SAMPLES; offset += 1) {
|
|
correlation = 0;
|
|
|
|
for (i = 0; i < MAX_SAMPLES; i += 1) {
|
|
correlation += Math.abs((buf[i]) - (buf[i + offset]));
|
|
}
|
|
correlation = 1 - (correlation/MAX_SAMPLES);
|
|
correlations[offset] = correlation;
|
|
if ((correlation > this.GOOD_ENOUGH_CORRELATION)
|
|
&& (correlation > lastCorrelation)
|
|
) {
|
|
foundGoodCorrelation = true;
|
|
if (correlation > best_correlation) {
|
|
best_correlation = correlation;
|
|
best_offset = offset;
|
|
}
|
|
} else if (foundGoodCorrelation) {
|
|
shift = (correlations[best_offset + 1] -
|
|
correlations[best_offset - 1]) /
|
|
correlations[best_offset];
|
|
return sampleRate / (best_offset + (8 * shift));
|
|
}
|
|
lastCorrelation = correlation;
|
|
}
|
|
if (best_correlation > 0.01) {
|
|
return sampleRate / best_offset;
|
|
}
|
|
return this.pitch;
|
|
};
|
|
|
|
// CellMorph //////////////////////////////////////////////////////////
|
|
|
|
/*
|
|
I am a spreadsheet style cell that can display either a string,
|
|
a Morph, a Canvas or a toString() representation of anything else.
|
|
I can be used in variable watchers or list view element cells.
|
|
*/
|
|
|
|
// CellMorph inherits from BoxMorph:
|
|
|
|
CellMorph.prototype = new BoxMorph();
|
|
CellMorph.prototype.constructor = CellMorph;
|
|
CellMorph.uber = BoxMorph.prototype;
|
|
|
|
// CellMorph instance creation:
|
|
|
|
function CellMorph(contents, color, idx, parentCell) {
|
|
this.init(contents, color, idx, parentCell);
|
|
}
|
|
|
|
CellMorph.prototype.init = function (contents, color, idx, parentCell) {
|
|
this.contents = (contents === 0 ? 0
|
|
: contents === false ? false
|
|
: contents || '');
|
|
this.isEditable = isNil(idx) ? false : true;
|
|
this.idx = idx || null; // for list watchers
|
|
this.parentCell = parentCell || null; // for list circularity detection
|
|
CellMorph.uber.init.call(
|
|
this,
|
|
SyntaxElementMorph.prototype.corner,
|
|
1,
|
|
WHITE
|
|
);
|
|
this.color = color || new Color(255, 140, 0);
|
|
this.isBig = false;
|
|
this.version = null; // only for observing sprites
|
|
this.fixLayout();
|
|
};
|
|
|
|
// CellMorph accessing:
|
|
|
|
CellMorph.prototype.big = function () {
|
|
this.isBig = true;
|
|
this.changed();
|
|
if (this.contentsMorph instanceof TextMorph) {
|
|
this.contentsMorph.setFontSize(
|
|
SyntaxElementMorph.prototype.fontSize * 1.5
|
|
);
|
|
}
|
|
this.fixLayout(true);
|
|
this.rerender();
|
|
};
|
|
|
|
CellMorph.prototype.normal = function () {
|
|
this.isBig = false;
|
|
this.changed();
|
|
if (this.contentsMorph instanceof TextMorph) {
|
|
this.contentsMorph.setFontSize(
|
|
SyntaxElementMorph.prototype.fontSize
|
|
);
|
|
}
|
|
this.fixLayout(true);
|
|
this.rerender();
|
|
};
|
|
|
|
// CellMorph circularity testing:
|
|
|
|
|
|
CellMorph.prototype.isCircular = function (list) {
|
|
if (!this.parentCell) {return false; }
|
|
if (list instanceof List) {
|
|
return this.contents === list || this.parentCell.isCircular(list);
|
|
}
|
|
return this.parentCell.isCircular(this.contents);
|
|
};
|
|
|
|
// CellMorph layout:
|
|
|
|
CellMorph.prototype.fixLayout = function (justMe) {
|
|
var isSameList = this.contentsMorph instanceof ListWatcherMorph
|
|
&& (this.contentsMorph.list === this.contents),
|
|
isSameTable = this.contentsMorph instanceof TableFrameMorph
|
|
&& (this.contentsMorph.tableMorph.table === this.contents),
|
|
listwatcher;
|
|
|
|
if (justMe) {return; }
|
|
|
|
this.createContents();
|
|
|
|
// adjust my dimensions
|
|
this.bounds.setHeight(this.contentsMorph.height()
|
|
+ this.edge
|
|
+ this.border * 2);
|
|
this.bounds.setWidth(Math.max(
|
|
this.contentsMorph.width() + this.edge * 2,
|
|
(this.contents instanceof Context ||
|
|
this.contents instanceof List ? 0 :
|
|
SyntaxElementMorph.prototype.fontSize * 3.5)
|
|
));
|
|
|
|
// position my contents
|
|
if (!isSameList && !isSameTable) {
|
|
this.contentsMorph.setCenter(this.center());
|
|
}
|
|
|
|
if (this.parent) {
|
|
this.parent.changed();
|
|
this.parent.fixLayout();
|
|
this.parent.rerender();
|
|
listwatcher = this.parentThatIsA(ListWatcherMorph);
|
|
if (listwatcher) {
|
|
listwatcher.changed();
|
|
listwatcher.fixLayout();
|
|
listwatcher.rerender();
|
|
}
|
|
}
|
|
};
|
|
|
|
CellMorph.prototype.createContents = function () {
|
|
// re-build my contents
|
|
var txt,
|
|
img,
|
|
myself = this,
|
|
fontSize = SyntaxElementMorph.prototype.fontSize,
|
|
isSameList = this.contentsMorph instanceof ListWatcherMorph
|
|
&& (this.contentsMorph.list === this.contents),
|
|
isSameTable = this.contentsMorph instanceof TableFrameMorph
|
|
&& (this.contentsMorph.tableMorph.table === this.contents);
|
|
|
|
if (this.isBig) {
|
|
fontSize = fontSize * 1.5;
|
|
}
|
|
|
|
if (this.contentsMorph && !isSameList && !isSameTable) {
|
|
this.contentsMorph.destroy();
|
|
this.version = null;
|
|
}
|
|
|
|
if (!isSameList && !isSameTable) {
|
|
if (this.contents instanceof Morph) {
|
|
if (isSnapObject(this.contents)) {
|
|
img = this.contents.thumbnail(new Point(40, 40));
|
|
this.contentsMorph = new Morph();
|
|
this.contentsMorph.isCachingImage = true;
|
|
this.contentsMorph.bounds.setWidth(img.width);
|
|
this.contentsMorph.bounds.setHeight(img.height);
|
|
this.contentsMorph.cachedImage = img;
|
|
this.version = this.contents.version;
|
|
} else {
|
|
this.contentsMorph = this.contents;
|
|
}
|
|
} else if (isString(this.contents)) {
|
|
txt = this.contents.length > 500 ?
|
|
this.contents.slice(0, 500) + '...' : this.contents;
|
|
this.contentsMorph = new TextMorph(
|
|
txt,
|
|
fontSize,
|
|
null,
|
|
true,
|
|
false,
|
|
'left' // was formerly 'center', reverted b/c of code-mapping
|
|
);
|
|
if (this.isEditable) {
|
|
this.contentsMorph.isEditable = true;
|
|
this.contentsMorph.enableSelecting();
|
|
}
|
|
this.contentsMorph.setColor(WHITE);
|
|
} else if (typeof this.contents === 'boolean') {
|
|
img = SpriteMorph.prototype.booleanMorph.call(
|
|
null,
|
|
this.contents
|
|
).fullImage();
|
|
this.contentsMorph = new Morph();
|
|
this.contentsMorph.isCachingImage = true;
|
|
this.contentsMorph.bounds.setWidth(img.width);
|
|
this.contentsMorph.bounds.setHeight(img.height);
|
|
this.contentsMorph.cachedImage = img;
|
|
} else if (this.contents instanceof HTMLCanvasElement) {
|
|
img = this.contents;
|
|
this.contentsMorph = new Morph();
|
|
this.contentsMorph.isCachingImage = true;
|
|
this.contentsMorph.bounds.setWidth(img.width);
|
|
this.contentsMorph.bounds.setHeight(img.height);
|
|
this.contentsMorph.cachedImage = img;
|
|
} else if (this.contents instanceof Context) {
|
|
img = this.contents.image();
|
|
this.contentsMorph = new Morph();
|
|
this.contentsMorph.isCachingImage = true;
|
|
this.contentsMorph.bounds.setWidth(img.width);
|
|
this.contentsMorph.bounds.setHeight(img.height);
|
|
this.contentsMorph.cachedImage = img;
|
|
|
|
// support blocks to be dragged out of watchers:
|
|
this.contentsMorph.isDraggable = true;
|
|
|
|
this.contentsMorph.selectForEdit = function () {
|
|
var script = myself.contents.toBlock(),
|
|
prepare = script.prepareToBeGrabbed,
|
|
ide = this.parentThatIsA(IDE_Morph) ||
|
|
this.world().childThatIsA(IDE_Morph);
|
|
|
|
script.prepareToBeGrabbed = function (hand) {
|
|
prepare.call(this, hand);
|
|
hand.grabOrigin = {
|
|
origin: ide.palette,
|
|
position: ide.palette.center()
|
|
};
|
|
this.prepareToBeGrabbed = prepare;
|
|
};
|
|
|
|
if (ide.isAppMode) {return; }
|
|
script.setPosition(this.position());
|
|
return script;
|
|
};
|
|
} else if (this.contents instanceof Costume) {
|
|
img = this.contents.thumbnail(new Point(40, 40));
|
|
this.contentsMorph = new Morph();
|
|
this.contentsMorph.isCachingImage = true;
|
|
this.contentsMorph.bounds.setWidth(img.width);
|
|
this.contentsMorph.bounds.setHeight(img.height);
|
|
this.contentsMorph.cachedImage = img;
|
|
|
|
// support costumes to be dragged out of watchers:
|
|
this.contentsMorph.isDraggable = true;
|
|
|
|
this.contentsMorph.selectForEdit = function () {
|
|
var cst = myself.contents.copy(),
|
|
icon,
|
|
prepare,
|
|
ide = this.parentThatIsA(IDE_Morph)||
|
|
this.world().childThatIsA(IDE_Morph);
|
|
|
|
cst.name = ide.currentSprite.newCostumeName(cst.name);
|
|
icon = new CostumeIconMorph(cst);
|
|
prepare = icon.prepareToBeGrabbed;
|
|
|
|
icon.prepareToBeGrabbed = function (hand) {
|
|
hand.grabOrigin = {
|
|
origin: ide.palette,
|
|
position: ide.palette.center()
|
|
};
|
|
this.prepareToBeGrabbed = prepare;
|
|
};
|
|
|
|
if (ide.isAppMode) {return; }
|
|
icon.setCenter(this.center());
|
|
return icon;
|
|
};
|
|
} else if (this.contents instanceof Sound) {
|
|
this.contentsMorph = new SymbolMorph('notes', 30);
|
|
|
|
// support sounds to be dragged out of watchers:
|
|
this.contentsMorph.isDraggable = true;
|
|
|
|
this.contentsMorph.selectForEdit = function () {
|
|
var snd = myself.contents.copy(),
|
|
icon,
|
|
prepare,
|
|
ide = this.parentThatIsA(IDE_Morph)||
|
|
this.world().childThatIsA(IDE_Morph);
|
|
|
|
snd.name = ide.currentSprite.newCostumeName(snd.name);
|
|
icon = new SoundIconMorph(snd);
|
|
prepare = icon.prepareToBeGrabbed;
|
|
|
|
icon.prepareToBeGrabbed = function (hand) {
|
|
hand.grabOrigin = {
|
|
origin: ide.palette,
|
|
position: ide.palette.center()
|
|
};
|
|
this.prepareToBeGrabbed = prepare;
|
|
};
|
|
|
|
if (ide.isAppMode) {return; }
|
|
icon.setCenter(this.center());
|
|
return icon;
|
|
};
|
|
} else if (this.contents instanceof List) {
|
|
if (this.contents.isTable()) {
|
|
this.contentsMorph = new TableFrameMorph(new TableMorph(
|
|
this.contents,
|
|
10
|
|
));
|
|
this.contentsMorph.expand(new Point(200, 150));
|
|
} else {
|
|
if (this.isCircular()) {
|
|
this.contentsMorph = new TextMorph(
|
|
'(...)',
|
|
fontSize,
|
|
null,
|
|
false, // bold
|
|
true, // italic
|
|
'center'
|
|
);
|
|
this.contentsMorph.setColor(WHITE);
|
|
} else {
|
|
this.contentsMorph = new ListWatcherMorph(
|
|
this.contents,
|
|
this
|
|
);
|
|
}
|
|
}
|
|
this.contentsMorph.isDraggable = false;
|
|
} else {
|
|
this.contentsMorph = new TextMorph(
|
|
!isNil(this.contents) ? this.contents.toString() : '',
|
|
fontSize,
|
|
null,
|
|
true,
|
|
false,
|
|
'center'
|
|
);
|
|
if (this.isEditable) {
|
|
this.contentsMorph.isEditable = true;
|
|
this.contentsMorph.enableSelecting();
|
|
}
|
|
this.contentsMorph.setColor(WHITE);
|
|
}
|
|
this.add(this.contentsMorph);
|
|
}
|
|
};
|
|
|
|
// CellMorph drawing:
|
|
|
|
CellMorph.prototype.update = function () {
|
|
// special case for observing sprites
|
|
if (!isSnapObject(this.contents) && !(this.contents instanceof Costume)) {
|
|
return;
|
|
}
|
|
if (this.version !== this.contents.version) {
|
|
this.fixLayout();
|
|
this.rerender();
|
|
this.version = this.contents.version;
|
|
}
|
|
};
|
|
|
|
CellMorph.prototype.render = function (ctx) {
|
|
// draw my outline
|
|
if ((this.edge === 0) && (this.border === 0)) {
|
|
BoxMorph.uber.render.call(this, ctx);
|
|
return null;
|
|
}
|
|
ctx.fillStyle = this.color.toString();
|
|
ctx.beginPath();
|
|
this.outlinePath(
|
|
ctx,
|
|
Math.max(this.edge - this.border, 0),
|
|
this.border
|
|
);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
if (this.border > 0 && !MorphicPreferences.isFlat) {
|
|
ctx.lineWidth = this.border;
|
|
ctx.strokeStyle = this.borderColor.toString();
|
|
ctx.beginPath();
|
|
this.outlinePath(ctx, this.edge, this.border / 2);
|
|
ctx.closePath();
|
|
ctx.stroke();
|
|
|
|
if (useBlurredShadows) {
|
|
ctx.shadowOffsetX = this.border;
|
|
ctx.shadowOffsetY = this.border;
|
|
ctx.shadowBlur = this.border;
|
|
ctx.shadowColor = this.color.darker(80).toString();
|
|
this.drawShadow(ctx, this.edge, 0);
|
|
}
|
|
}
|
|
};
|
|
|
|
CellMorph.prototype.drawShadow = function (context, radius, inset) {
|
|
var offset = radius + inset,
|
|
w = this.width(),
|
|
h = this.height();
|
|
|
|
// bottom left:
|
|
context.beginPath();
|
|
context.moveTo(0, h - offset);
|
|
context.lineTo(0, offset);
|
|
|
|
// top left:
|
|
context.arc(
|
|
offset,
|
|
offset,
|
|
radius,
|
|
radians(-180),
|
|
radians(-90),
|
|
false
|
|
);
|
|
|
|
// top right:
|
|
context.lineTo(w - offset, 0);
|
|
context.stroke();
|
|
};
|
|
|
|
// CellMorph editing (inside list watchers):
|
|
|
|
CellMorph.prototype.layoutChanged = function () {
|
|
var listWatcher = this.parentThatIsA(ListWatcherMorph);
|
|
|
|
// adjust my layout
|
|
this.bounds.setHeight(this.contentsMorph.height()
|
|
+ this.edge
|
|
+ this.border * 2);
|
|
this.bounds.setWidth(Math.max(
|
|
this.contentsMorph.width() + this.edge * 2,
|
|
(this.contents instanceof Context ||
|
|
this.contents instanceof List ? 0 : this.height() * 2)
|
|
));
|
|
|
|
// position my contents
|
|
this.contentsMorph.setCenter(this.center());
|
|
this.rerender();
|
|
|
|
if (listWatcher) {
|
|
listWatcher.fixLayout();
|
|
}
|
|
};
|
|
|
|
CellMorph.prototype.reactToEdit = function (textMorph) {
|
|
var listWatcher;
|
|
if (!isNil(this.idx)) {
|
|
listWatcher = this.parentThatIsA(ListWatcherMorph);
|
|
if (listWatcher) {
|
|
listWatcher.list.put(
|
|
textMorph.text,
|
|
this.idx + listWatcher.start - 1
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
CellMorph.prototype.mouseClickLeft = function (pos) {
|
|
if (this.isEditable && this.contentsMorph instanceof TextMorph) {
|
|
this.contentsMorph.selectAllAndEdit();
|
|
} else {
|
|
this.escalateEvent('mouseClickLeft', pos);
|
|
}
|
|
};
|
|
|
|
CellMorph.prototype.mouseDoubleClick = function (pos) {
|
|
if (List.prototype.enableTables &&
|
|
this.currentValue instanceof List) {
|
|
new TableDialogMorph(this.contents).popUp(this.world());
|
|
} else {
|
|
this.escalateEvent('mouseDoubleClick', pos);
|
|
}
|
|
};
|
|
|
|
// WatcherMorph //////////////////////////////////////////////////////////
|
|
|
|
/*
|
|
I am a little window which observes some value and continuously
|
|
updates itself accordingly.
|
|
|
|
My target can be either a SpriteMorph or a VariableFrame.
|
|
*/
|
|
|
|
// WatcherMorph inherits from BoxMorph:
|
|
|
|
WatcherMorph.prototype = new BoxMorph();
|
|
WatcherMorph.prototype.constructor = WatcherMorph;
|
|
WatcherMorph.uber = BoxMorph.prototype;
|
|
|
|
// WatcherMorph instance creation:
|
|
|
|
function WatcherMorph(label, color, target, getter, isHidden) {
|
|
this.init(label, color, target, getter, isHidden);
|
|
}
|
|
|
|
WatcherMorph.prototype.init = function (
|
|
label,
|
|
color,
|
|
target,
|
|
getter,
|
|
isHidden
|
|
) {
|
|
// additional properties
|
|
this.labelText = label || '';
|
|
this.version = null;
|
|
this.objName = '';
|
|
this.isGhosted = false; // transient, don't persist
|
|
|
|
// initialize inherited properties
|
|
WatcherMorph.uber.init.call(
|
|
this,
|
|
SyntaxElementMorph.prototype.rounding,
|
|
1.000001, // shadow bug in Chrome,
|
|
new Color(120, 120, 120)
|
|
);
|
|
|
|
// override inherited behavior
|
|
this.color = new Color(220, 220, 220);
|
|
this.readoutColor = color;
|
|
this.style = 'normal';
|
|
this.target = target || null; // target obj (Sprite) or VariableFrame
|
|
this.getter = getter || null; // callback or variable name (string)
|
|
this.currentValue = null;
|
|
this.labelMorph = null;
|
|
this.sliderMorph = null;
|
|
this.cellMorph = null;
|
|
this.isDraggable = true;
|
|
this.fixLayout();
|
|
this.update();
|
|
if (isHidden) { // for de-serializing
|
|
this.hide();
|
|
}
|
|
};
|
|
|
|
// WatcherMorph accessing:
|
|
|
|
WatcherMorph.prototype.isTemporary = function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
if (this.target instanceof VariableFrame) {
|
|
if (stage) {
|
|
if (this.target === stage.variables.parentFrame) {
|
|
return false; // global
|
|
}
|
|
}
|
|
return this.target.owner === null;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
WatcherMorph.prototype.object = function () {
|
|
// answer the actual sprite I refer to
|
|
return this.target instanceof VariableFrame ?
|
|
this.target.owner : this.target;
|
|
};
|
|
|
|
WatcherMorph.prototype.isGlobal = function (selector) {
|
|
return contains(
|
|
['getLastAnswer', 'getLastMessage', 'getTempo', 'getTimer',
|
|
'reportMouseX', 'reportMouseY', 'reportThreadCount'],
|
|
selector
|
|
);
|
|
};
|
|
|
|
// WatcherMorph slider accessing:
|
|
|
|
WatcherMorph.prototype.setSliderMin = function (num, noUpdate) {
|
|
if (this.target instanceof VariableFrame) {
|
|
this.sliderMorph.setSize(1, noUpdate);
|
|
this.sliderMorph.setStart(num, noUpdate);
|
|
this.sliderMorph.setSize(this.sliderMorph.rangeSize() / 5, noUpdate);
|
|
}
|
|
};
|
|
|
|
WatcherMorph.prototype.setSliderMax = function (num, noUpdate) {
|
|
if (this.target instanceof VariableFrame) {
|
|
this.sliderMorph.setSize(1, noUpdate);
|
|
this.sliderMorph.setStop(num, noUpdate);
|
|
this.sliderMorph.setSize(this.sliderMorph.rangeSize() / 5, noUpdate);
|
|
}
|
|
};
|
|
|
|
// WatcherMorph updating:
|
|
|
|
WatcherMorph.prototype.update = function () {
|
|
var newValue, sprite, num, att,
|
|
isInherited = false;
|
|
|
|
if (this.target && this.getter) {
|
|
this.updateLabel();
|
|
if (this.target instanceof VariableFrame) {
|
|
newValue = this.target.vars[this.getter] ?
|
|
this.target.vars[this.getter].value : undefined;
|
|
if (newValue === undefined && this.target.owner) {
|
|
sprite = this.target.owner;
|
|
if (contains(sprite.inheritedVariableNames(), this.getter)) {
|
|
newValue = this.target.getVar(this.getter);
|
|
// ghost cell color
|
|
this.cellMorph.setColor(
|
|
SpriteMorph.prototype.blockColor.variables
|
|
.lighter(35)
|
|
);
|
|
} else {
|
|
this.destroy();
|
|
return;
|
|
}
|
|
} else {
|
|
// un-ghost the cell color
|
|
this.cellMorph.setColor(
|
|
SpriteMorph.prototype.blockColor.variables
|
|
);
|
|
}
|
|
} else {
|
|
newValue = this.target[this.getter]();
|
|
|
|
// determine whether my getter is an inherited attribute
|
|
att = {
|
|
xPosition: 'x position',
|
|
yPosition: 'y position',
|
|
direction: 'direction',
|
|
getCostumeIdx: 'costume #',
|
|
getScale: 'size',
|
|
getVolume: 'volume',
|
|
getPan: 'balance',
|
|
reportShown: 'shown?',
|
|
getPenDown: 'pen down?'
|
|
} [this.getter];
|
|
isInherited = att ? this.target.inheritsAttribute(att) : false;
|
|
}
|
|
if (newValue !== '' && !isNil(newValue)) {
|
|
num = +newValue;
|
|
if (typeof newValue !== 'boolean' && !isNaN(num)) {
|
|
newValue = Math.round(newValue * 1000000000) / 1000000000;
|
|
}
|
|
}
|
|
if (newValue === undefined) {
|
|
// console.log('removing watcher for', this.labelText);
|
|
this.destroy();
|
|
return;
|
|
}
|
|
if (newValue !== this.currentValue ||
|
|
isInherited !== this.isGhosted ||
|
|
(!isNil(newValue) &&
|
|
newValue.version &&
|
|
(newValue.version !== this.version)
|
|
)
|
|
) {
|
|
this.changed();
|
|
this.cellMorph.contents = newValue;
|
|
this.isGhosted = isInherited;
|
|
if (isSnapObject(this.target)) {
|
|
if (isInherited) {
|
|
this.cellMorph.setColor(this.readoutColor.lighter(35));
|
|
} else {
|
|
this.cellMorph.setColor(this.readoutColor);
|
|
}
|
|
}
|
|
this.cellMorph.fixLayout();
|
|
if (!isNaN(newValue)) {
|
|
this.sliderMorph.value = newValue;
|
|
this.sliderMorph.fixLayout();
|
|
}
|
|
this.fixLayout();
|
|
if (this.currentValue && this.currentValue.version) {
|
|
this.version = this.currentValue.version;
|
|
} else {
|
|
this.version = Date.now();
|
|
}
|
|
this.currentValue = newValue;
|
|
}
|
|
}
|
|
if (this.cellMorph.contentsMorph instanceof ListWatcherMorph) {
|
|
this.cellMorph.contentsMorph.update();
|
|
} else if (isSnapObject(this.cellMorph.contents)) {
|
|
this.cellMorph.update();
|
|
}
|
|
};
|
|
|
|
WatcherMorph.prototype.updateLabel = function () {
|
|
// check whether the target object's name has been changed
|
|
var obj = this.object();
|
|
|
|
if (!obj || this.isGlobal(this.getter)) { return; }
|
|
if (obj.version !== this.version) {
|
|
this.objName = obj.name ? obj.name + ' ' : ' ';
|
|
if (this.labelMorph) {
|
|
this.labelMorph.destroy();
|
|
this.labelMorph = null;
|
|
this.fixLayout();
|
|
}
|
|
}
|
|
};
|
|
|
|
// WatcherMorph layout:
|
|
|
|
WatcherMorph.prototype.fixLayout = function () {
|
|
var fontSize = SyntaxElementMorph.prototype.fontSize, isList,
|
|
myself = this;
|
|
|
|
// create my parts
|
|
if (this.labelMorph === null) {
|
|
this.labelMorph = new StringMorph(
|
|
this.objName + this.labelText,
|
|
fontSize,
|
|
null,
|
|
true,
|
|
false,
|
|
false,
|
|
MorphicPreferences.isFlat ? new Point() : new Point(1, 1),
|
|
WHITE
|
|
);
|
|
this.add(this.labelMorph);
|
|
}
|
|
if (this.cellMorph === null) {
|
|
this.cellMorph = new CellMorph('', this.readoutColor);
|
|
this.add(this.cellMorph);
|
|
}
|
|
if (this.sliderMorph === null) {
|
|
this.sliderMorph = new SliderMorph(
|
|
0,
|
|
100,
|
|
0,
|
|
20,
|
|
'horizontal'
|
|
);
|
|
this.sliderMorph.alpha = 1;
|
|
this.sliderMorph.button.color = this.color.darker();
|
|
this.sliderMorph.color = this.color.lighter(60);
|
|
this.sliderMorph.button.highlightColor = this.color.darker();
|
|
this.sliderMorph.button.highlightColor.b += 50;
|
|
this.sliderMorph.button.pressColor = this.color.darker();
|
|
this.sliderMorph.button.pressColor.b += 100;
|
|
this.sliderMorph.setHeight(fontSize);
|
|
this.sliderMorph.action = function (num) {
|
|
myself.target.setVar(
|
|
myself.getter,
|
|
Math.round(num),
|
|
myself.target.owner
|
|
);
|
|
};
|
|
this.add(this.sliderMorph);
|
|
}
|
|
|
|
// adjust my layout
|
|
isList = this.cellMorph.contents instanceof List;
|
|
if (isList) { this.style = 'normal'; }
|
|
|
|
if (this.style === 'large') {
|
|
this.labelMorph.hide();
|
|
this.sliderMorph.hide();
|
|
this.cellMorph.big();
|
|
this.cellMorph.setPosition(this.position());
|
|
this.bounds.setExtent(this.cellMorph.extent().subtract(1));
|
|
return;
|
|
}
|
|
|
|
this.labelMorph.show();
|
|
this.sliderMorph.show();
|
|
this.cellMorph.normal();
|
|
this.labelMorph.setPosition(this.position().add(new Point(
|
|
this.edge,
|
|
this.border + SyntaxElementMorph.prototype.typeInPadding
|
|
)));
|
|
|
|
if (isList) {
|
|
this.cellMorph.setPosition(this.labelMorph.bottomLeft().add(
|
|
new Point(0, SyntaxElementMorph.prototype.typeInPadding)
|
|
));
|
|
} else {
|
|
this.cellMorph.setPosition(this.labelMorph.topRight().add(new Point(
|
|
fontSize / 3,
|
|
0
|
|
)));
|
|
this.labelMorph.setTop(
|
|
this.cellMorph.top()
|
|
+ (this.cellMorph.height() - this.labelMorph.height()) / 2
|
|
);
|
|
}
|
|
|
|
if (this.style === 'slider') {
|
|
this.sliderMorph.setPosition(new Point(
|
|
this.labelMorph.left(),
|
|
this.cellMorph.bottom()
|
|
+ SyntaxElementMorph.prototype.typeInPadding
|
|
));
|
|
this.sliderMorph.setWidth(this.cellMorph.right()
|
|
- this.labelMorph.left());
|
|
this.bounds.setHeight(
|
|
this.cellMorph.height()
|
|
+ this.sliderMorph.height()
|
|
+ this.border * 2
|
|
+ SyntaxElementMorph.prototype.typeInPadding * 3
|
|
);
|
|
} else {
|
|
this.sliderMorph.hide();
|
|
this.bounds.corner.y = this.cellMorph.bottom()
|
|
+ this.border
|
|
+ SyntaxElementMorph.prototype.typeInPadding;
|
|
}
|
|
this.bounds.corner.x = Math.max(
|
|
this.cellMorph.right(),
|
|
this.labelMorph.right()
|
|
) + this.edge
|
|
+ SyntaxElementMorph.prototype.typeInPadding;
|
|
};
|
|
|
|
// WatcherMorph events:
|
|
|
|
WatcherMorph.prototype.mouseDoubleClick = function (pos) {
|
|
if (List.prototype.enableTables &&
|
|
this.currentValue instanceof List) {
|
|
new TableDialogMorph(this.currentValue).popUp(this.world());
|
|
} else {
|
|
this.escalateEvent('mouseDoubleClick', pos);
|
|
}
|
|
};
|
|
|
|
// WatcherMorph dragging and dropping:
|
|
|
|
WatcherMorph.prototype.rootForGrab = function () {
|
|
// prevent watchers to be dragged in presentation mode
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
if (ide && ide.isAppMode) {
|
|
return ide;
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/*
|
|
// Scratch-like watcher-toggling, commented out b/c we have a drop-down menu
|
|
|
|
WatcherMorph.prototype.mouseClickLeft = function () {
|
|
if (this.style === 'normal') {
|
|
if (this.target instanceof VariableFrame) {
|
|
this.style = 'slider';
|
|
} else {
|
|
this.style = 'large';
|
|
}
|
|
} else if (this.style === 'slider') {
|
|
this.style = 'large';
|
|
} else {
|
|
this.style = 'normal';
|
|
}
|
|
this.fixLayout();
|
|
};
|
|
*/
|
|
|
|
// WatcherMorph user menu:
|
|
|
|
WatcherMorph.prototype.userMenu = function () {
|
|
var myself = this,
|
|
ide = this.parentThatIsA(IDE_Morph),
|
|
shiftClicked = (this.world().currentKey === 16),
|
|
menu = new MenuMorph(this),
|
|
on = '\u25CF',
|
|
off = '\u25CB',
|
|
vNames;
|
|
|
|
function monitor(vName) {
|
|
var stage = myself.parentThatIsA(StageMorph),
|
|
varFrame = myself.currentValue.outerContext.variables;
|
|
menu.addItem(
|
|
vName + '...',
|
|
function () {
|
|
var watcher = detect(
|
|
stage.children,
|
|
(morph) => morph instanceof WatcherMorph
|
|
&& morph.target === varFrame
|
|
&& morph.getter === vName
|
|
),
|
|
others;
|
|
if (watcher !== null) {
|
|
watcher.show();
|
|
watcher.fixLayout(); // re-hide hidden parts
|
|
return;
|
|
}
|
|
watcher = new WatcherMorph(
|
|
vName + ' ' + localize('(temporary)'),
|
|
SpriteMorph.prototype.blockColor.variables,
|
|
varFrame,
|
|
vName
|
|
);
|
|
watcher.setPosition(stage.position().add(10));
|
|
others = stage.watchers(watcher.left());
|
|
if (others.length > 0) {
|
|
watcher.setTop(others[others.length - 1].bottom());
|
|
}
|
|
stage.add(watcher);
|
|
watcher.fixLayout();
|
|
}
|
|
);
|
|
}
|
|
|
|
if (ide && ide.isAppMode) { // prevent context menu in app mode
|
|
return;
|
|
}
|
|
|
|
menu.addItem(
|
|
(this.style === 'normal' ? on : off) + ' ' + localize('normal'),
|
|
'styleNormal'
|
|
);
|
|
menu.addItem(
|
|
(this.style === 'large' ? on : off) + ' ' + localize('large'),
|
|
'styleLarge'
|
|
);
|
|
if (this.target instanceof VariableFrame) {
|
|
menu.addItem(
|
|
(this.style === 'slider' ? on : off) + ' ' + localize('slider'),
|
|
'styleSlider'
|
|
);
|
|
menu.addLine();
|
|
menu.addItem(
|
|
'slider min...',
|
|
'userSetSliderMin'
|
|
);
|
|
menu.addItem(
|
|
'slider max...',
|
|
'userSetSliderMax'
|
|
);
|
|
menu.addLine();
|
|
menu.addItem(
|
|
'import...',
|
|
'importData'
|
|
);
|
|
menu.addItem(
|
|
'raw data...',
|
|
() => this.importData(true),
|
|
'import without attempting to\nparse or format data'//,
|
|
);
|
|
if (shiftClicked) {
|
|
if (this.currentValue instanceof List &&
|
|
this.currentValue.canBeCSV()) {
|
|
menu.addItem(
|
|
'export as CSV...',
|
|
() => ide.saveFileAs(
|
|
this.currentValue.asCSV(),
|
|
'text/csv;charset=utf-8', // RFC 4180
|
|
this.getter // variable name
|
|
),
|
|
null,
|
|
new Color(100, 0, 0)
|
|
);
|
|
}
|
|
if (this.currentValue instanceof List &&
|
|
this.currentValue.canBeJSON()) {
|
|
menu.addItem(
|
|
'export as JSON...',
|
|
() => ide.saveFileAs(
|
|
this.currentValue.asJSON(true), // guess objects
|
|
'text/json;charset=utf-8',
|
|
this.getter // variable name
|
|
),
|
|
null,
|
|
new Color(100, 0, 0)
|
|
);
|
|
}
|
|
}
|
|
if (isString(this.currentValue) || !isNaN(+this.currentValue)) {
|
|
if (shiftClicked) {
|
|
menu.addItem(
|
|
'parse',
|
|
'parseTxt',
|
|
'try to convert\nraw data into a list',
|
|
new Color(100, 0, 0)
|
|
);
|
|
}
|
|
menu.addItem(
|
|
'export...',
|
|
() => ide.saveFileAs(
|
|
this.currentValue.toString(),
|
|
'text/plain;charset=utf-8',
|
|
this.getter // variable name
|
|
)
|
|
);
|
|
} else if (this.currentValue instanceof Costume) {
|
|
menu.addItem(
|
|
'export...',
|
|
() => {
|
|
if (this.currentValue instanceof SVG_Costume) {
|
|
// don't show SVG costumes in a new tab (shows text)
|
|
ide.saveFileAs(
|
|
this.currentValue.contents.src,
|
|
'text/svg',
|
|
this.currentValue.name
|
|
);
|
|
} else { // rasterized Costume
|
|
ide.saveCanvasAs(
|
|
this.currentValue.contents,
|
|
this.currentValue.name
|
|
);
|
|
}
|
|
}
|
|
);
|
|
} else if (this.currentValue instanceof Sound) {
|
|
menu.addItem(
|
|
'export...',
|
|
() => ide.saveAudioAs(
|
|
this.currentValue.audio,
|
|
this.currentValue.name
|
|
)
|
|
);
|
|
} else if (this.currentValue instanceof List &&
|
|
this.currentValue.canBeCSV()) {
|
|
menu.addItem(
|
|
'export...',
|
|
() => ide.saveFileAs(
|
|
this.currentValue.asCSV(),
|
|
'text/csv;charset=utf-8', // RFC 4180
|
|
this.getter // variable name
|
|
)
|
|
);
|
|
if (this.currentValue.canBeJSON()) {
|
|
menu.addItem(
|
|
'blockify',
|
|
() => {
|
|
var world = ide.world();
|
|
this.currentValue.blockify().pickUp(world);
|
|
world.hand.grabOrigin = {
|
|
origin: ide.palette,
|
|
position: ide.palette.center()
|
|
};
|
|
}
|
|
);
|
|
}
|
|
} else if (this.currentValue instanceof List &&
|
|
this.currentValue.canBeJSON()) {
|
|
menu.addItem(
|
|
'export...',
|
|
() => ide.saveFileAs(
|
|
this.currentValue.asJSON(true), // guessObjects
|
|
'text/json;charset=utf-8',
|
|
this.getter // variable name
|
|
)
|
|
);
|
|
} else if (this.currentValue instanceof Context) {
|
|
vNames = this.currentValue.outerContext.variables.names();
|
|
if (vNames.length) {
|
|
menu.addLine();
|
|
vNames.forEach(vName => monitor(vName));
|
|
}
|
|
}
|
|
}
|
|
return menu;
|
|
};
|
|
|
|
WatcherMorph.prototype.importData = function (raw) {
|
|
// raw is a Boolean flag selecting to keep the data unparsed
|
|
var inp = document.createElement('input'),
|
|
ide = this.parentThatIsA(IDE_Morph),
|
|
myself = this;
|
|
|
|
function userImport() {
|
|
|
|
function txtOnlyMsg(ftype, anyway) {
|
|
ide.confirm(
|
|
localize(
|
|
'Snap! can only import "text" files.\n' +
|
|
'You selected a file of type "' +
|
|
ftype +
|
|
'".'
|
|
) + '\n\n' + localize('Open anyway?'),
|
|
'Unable to import',
|
|
anyway // callback
|
|
);
|
|
}
|
|
|
|
function readText(aFile) {
|
|
var frd = new FileReader(),
|
|
ext = aFile.name.split('.').pop().toLowerCase();
|
|
|
|
function isTextFile(aFile) {
|
|
// special cases for Windows
|
|
// check the file extension for text-like-ness
|
|
return aFile.type.indexOf('text') !== -1 ||
|
|
contains(['txt', 'csv', 'xml', 'json', 'tsv'], ext);
|
|
}
|
|
|
|
function isType(aFile, string) {
|
|
return aFile.type.indexOf(string) !== -1 || (ext === string);
|
|
}
|
|
|
|
frd.onloadend = function (e) {
|
|
if (!raw && isType(aFile, 'csv')) {
|
|
myself.target.setVar(
|
|
myself.getter,
|
|
Process.prototype.parseCSV(e.target.result)
|
|
);
|
|
} else if (!raw && isType(aFile, 'json')) {
|
|
myself.target.setVar(
|
|
myself.getter,
|
|
Process.prototype.parseJSON(e.target.result)
|
|
);
|
|
} else {
|
|
myself.target.setVar(
|
|
myself.getter,
|
|
e.target.result
|
|
);
|
|
}
|
|
};
|
|
|
|
if (raw || isTextFile(aFile)) {
|
|
frd.readAsText(aFile);
|
|
} else {
|
|
// show a warning and an option
|
|
// letting the user load the file anyway
|
|
txtOnlyMsg(
|
|
aFile.type,
|
|
() => frd.readAsText(aFile)
|
|
);
|
|
}
|
|
}
|
|
|
|
document.body.removeChild(inp);
|
|
ide.filePicker = null;
|
|
if (inp.files.length > 0) {
|
|
readText(inp.files[inp.files.length - 1]);
|
|
}
|
|
}
|
|
|
|
if (ide.filePicker) {
|
|
document.body.removeChild(ide.filePicker);
|
|
ide.filePicker = null;
|
|
}
|
|
inp.type = 'file';
|
|
inp.style.color = "transparent";
|
|
inp.style.backgroundColor = "transparent";
|
|
inp.style.border = "none";
|
|
inp.style.outline = "none";
|
|
inp.style.position = "absolute";
|
|
inp.style.top = "0px";
|
|
inp.style.left = "0px";
|
|
inp.style.width = "0px";
|
|
inp.style.height = "0px";
|
|
inp.style.display = "none";
|
|
inp.addEventListener(
|
|
"change",
|
|
userImport,
|
|
false
|
|
);
|
|
document.body.appendChild(inp);
|
|
ide.filePicker = inp;
|
|
inp.click();
|
|
};
|
|
|
|
WatcherMorph.prototype.parseTxt = function () {
|
|
// experimental!
|
|
var src = this.target.vars[this.getter].value;
|
|
this.target.setVar(
|
|
this.getter,
|
|
src.indexOf('\[') === 0 ?
|
|
Process.prototype.parseJSON(src)
|
|
: Process.prototype.parseCSV(src)
|
|
);
|
|
};
|
|
|
|
WatcherMorph.prototype.setStyle = function (style) {
|
|
this.style = style;
|
|
this.changed();
|
|
this.fixLayout();
|
|
this.rerender();
|
|
};
|
|
|
|
WatcherMorph.prototype.styleNormal = function () {
|
|
this.setStyle('normal');
|
|
};
|
|
|
|
WatcherMorph.prototype.styleLarge = function () {
|
|
this.setStyle('large');
|
|
};
|
|
|
|
WatcherMorph.prototype.styleSlider = function () {
|
|
this.setStyle('slider');
|
|
};
|
|
|
|
WatcherMorph.prototype.userSetSliderMin = function () {
|
|
new DialogBoxMorph(
|
|
this,
|
|
this.setSliderMin,
|
|
this
|
|
).prompt(
|
|
"Slider minimum value",
|
|
this.sliderMorph.start.toString(),
|
|
this.world(),
|
|
null, // pic
|
|
null, // choices
|
|
null, // read only
|
|
true // numeric
|
|
);
|
|
};
|
|
|
|
WatcherMorph.prototype.userSetSliderMax = function () {
|
|
new DialogBoxMorph(
|
|
this,
|
|
this.setSliderMax,
|
|
this
|
|
).prompt(
|
|
"Slider maximum value",
|
|
this.sliderMorph.stop.toString(),
|
|
this.world(),
|
|
null, // pic
|
|
null, // choices
|
|
null, // read only
|
|
true // numeric
|
|
);
|
|
};
|
|
|
|
// WatcherMorph drawing:
|
|
|
|
WatcherMorph.prototype.render = function (ctx) {
|
|
var gradient;
|
|
if (MorphicPreferences.isFlat || (this.edge === 0 && this.border === 0)) {
|
|
BoxMorph.uber.render.call(this, ctx);
|
|
return;
|
|
}
|
|
gradient = ctx.createLinearGradient(0, 0, 0, this.height());
|
|
gradient.addColorStop(0, this.color.lighter().toString());
|
|
gradient.addColorStop(1, this.color.darker().toString());
|
|
ctx.fillStyle = gradient;
|
|
ctx.beginPath();
|
|
this.outlinePath(
|
|
ctx,
|
|
Math.max(this.edge - this.border, 0),
|
|
this.border
|
|
);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
if (this.border > 0) {
|
|
gradient = ctx.createLinearGradient(0, 0, 0, this.height());
|
|
gradient.addColorStop(0, this.borderColor.lighter().toString());
|
|
gradient.addColorStop(1, this.borderColor.darker().toString());
|
|
ctx.lineWidth = this.border;
|
|
ctx.strokeStyle = gradient;
|
|
ctx.beginPath();
|
|
this.outlinePath(ctx, this.edge, this.border / 2);
|
|
ctx.closePath();
|
|
ctx.stroke();
|
|
}
|
|
};
|
|
|
|
// StagePrompterMorph ////////////////////////////////////////////////////////
|
|
|
|
/*
|
|
I am a sensor-category-colored input box at the bottom of the stage
|
|
which lets the user answer to a question. If I am opened from within
|
|
the context of a sprite, my question can be anything that is displayable
|
|
in a SpeechBubble and will be, if I am opened from within the stage
|
|
my question will be shown as a single line of text within my label morph.
|
|
*/
|
|
|
|
// StagePrompterMorph inherits from BoxMorph:
|
|
|
|
StagePrompterMorph.prototype = new BoxMorph();
|
|
StagePrompterMorph.prototype.constructor = StagePrompterMorph;
|
|
StagePrompterMorph.uber = BoxMorph.prototype;
|
|
|
|
// StagePrompterMorph instance creation:
|
|
|
|
function StagePrompterMorph(question) {
|
|
this.init(question);
|
|
}
|
|
|
|
StagePrompterMorph.prototype.init = function (question) {
|
|
// question is optional in case the Stage is asking
|
|
|
|
// additional properties
|
|
this.isDone = false;
|
|
if (question) {
|
|
this.label = new StringMorph(
|
|
question,
|
|
SpriteMorph.prototype.bubbleFontSize,
|
|
null, // fontStyle
|
|
SpriteMorph.prototype.bubbleFontIsBold,
|
|
false, // italic
|
|
'left'
|
|
);
|
|
} else {
|
|
this.label = null;
|
|
}
|
|
this.inputField = new InputFieldMorph();
|
|
this.button = new PushButtonMorph(
|
|
null,
|
|
() => this.accept(),
|
|
'\u2713'
|
|
);
|
|
|
|
// initialize inherited properties
|
|
StagePrompterMorph.uber.init.call(
|
|
this,
|
|
SyntaxElementMorph.prototype.rounding,
|
|
SpriteMorph.prototype.bubbleBorder,
|
|
SpriteMorph.prototype.blockColor.sensing
|
|
);
|
|
|
|
// override inherited behavior
|
|
this.color = WHITE;
|
|
if (this.label) {this.add(this.label); }
|
|
this.add(this.inputField);
|
|
this.add(this.button);
|
|
this.fixLayout();
|
|
};
|
|
|
|
// StagePrompterMorph layout:
|
|
|
|
StagePrompterMorph.prototype.fixLayout = function () {
|
|
var y = 0;
|
|
if (this.label) {
|
|
this.label.setPosition(new Point(
|
|
this.left() + this.edge,
|
|
this.top() + this.edge
|
|
));
|
|
y = this.label.bottom() - this.top();
|
|
}
|
|
this.inputField.setPosition(new Point(
|
|
this.left() + this.edge,
|
|
this.top() + y + this.edge
|
|
));
|
|
this.inputField.setWidth(
|
|
this.width()
|
|
- this.edge * 2
|
|
- this.button.width()
|
|
- this.border
|
|
);
|
|
this.button.setCenter(this.inputField.center());
|
|
this.button.setLeft(this.inputField.right() + this.border);
|
|
this.bounds.setHeight(
|
|
this.inputField.bottom()
|
|
- this.top()
|
|
+ this.edge
|
|
);
|
|
};
|
|
|
|
// StagePrompterMorph events:
|
|
|
|
StagePrompterMorph.prototype.mouseClickLeft = function () {
|
|
this.inputField.edit();
|
|
};
|
|
|
|
StagePrompterMorph.prototype.accept = function () {
|
|
this.isDone = true;
|
|
};
|