kopia lustrzana https://github.com/backface/turtlestitch
1266 wiersze
44 KiB
Plaintext
Executable File
1266 wiersze
44 KiB
Plaintext
Executable File
|
|
morphic.js
|
|
|
|
a lively Web-GUI
|
|
inspired by Squeak
|
|
|
|
written by Jens Mönig
|
|
jens@moenig.org
|
|
|
|
Copyright (C) 2010-2020 by Jens Mönig
|
|
|
|
This documentation last changed: June 9, 2020
|
|
|
|
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/>.
|
|
|
|
|
|
documentation contents
|
|
----------------------
|
|
I. inheritance hierarchy
|
|
II. object definition toc
|
|
III. yet to implement
|
|
IV. open issues
|
|
V. browser compatibility
|
|
VI. the big picture
|
|
VII. programming guide
|
|
(1) setting up a web page
|
|
(a) single world
|
|
(b) multiple worlds
|
|
(c) an application
|
|
(2) manipulating morphs
|
|
(3) events
|
|
(a) mouse events
|
|
(b) context menu
|
|
(c) dragging
|
|
(d) dropping
|
|
(e) keyboard events
|
|
(f) resize event
|
|
(g) combined mouse-keyboard events
|
|
(h) text editing events
|
|
(4) stepping
|
|
(5) creating new kinds of morphs
|
|
(a) drawing the shape
|
|
(b) determining extent and arranging submorphs
|
|
(c) pixel-perfect pointing events
|
|
(d) caching the shape
|
|
(e) holes
|
|
(f) updating
|
|
(g) duplicating
|
|
(6) development and user modes
|
|
(7) turtle graphics
|
|
(8) supporting high-resolution "retina" screens
|
|
(9 animations
|
|
(10) minifying morphic.js
|
|
VIII. acknowledgements
|
|
IX. contributors
|
|
|
|
|
|
I. hierarchy
|
|
-------------
|
|
the following tree lists all constructors hierarchically,
|
|
indentation indicating inheritance. Refer to this list to get a
|
|
contextual overview:
|
|
|
|
Animation
|
|
Color
|
|
Node
|
|
Morph
|
|
BlinkerMorph
|
|
CursorMorph
|
|
BouncerMorph*
|
|
BoxMorph
|
|
InspectorMorph
|
|
MenuMorph
|
|
MouseSensorMorph*
|
|
SpeechBubbleMorph
|
|
CircleBoxMorph
|
|
SliderButtonMorph
|
|
SliderMorph
|
|
ColorPaletteMorph
|
|
GrayPaletteMorph
|
|
ColorPickerMorph
|
|
DialMorph
|
|
FrameMorph
|
|
ScrollFrameMorph
|
|
ListMorph
|
|
StringFieldMorph
|
|
WorldMorph
|
|
HandleMorph
|
|
HandMorph
|
|
PenMorph
|
|
ShadowMorph
|
|
StringMorph
|
|
TextMorph
|
|
TriggerMorph
|
|
MenuItemMorph
|
|
Point
|
|
Rectangle
|
|
|
|
|
|
II. toc
|
|
-------
|
|
the following list shows the order in which all constructors are
|
|
defined. Use this list to locate code in this document:
|
|
|
|
Global settings
|
|
Global functions
|
|
|
|
Animation
|
|
Color
|
|
Point
|
|
Rectangle
|
|
Node
|
|
Morph
|
|
ShadowMorph
|
|
HandleMorph
|
|
PenMorph
|
|
ColorPaletteMorph
|
|
GrayPaletteMorph
|
|
ColorPickerMorph
|
|
BlinkerMorph
|
|
CursorMorph
|
|
BoxMorph
|
|
SpeechBubbleMorph
|
|
DialMorph
|
|
CircleBoxMorph
|
|
SliderButtonMorph
|
|
SliderMorph
|
|
MouseSensorMorph*
|
|
InspectorMorph
|
|
MenuMorph
|
|
StringMorph
|
|
TextMorph
|
|
TriggerMorph
|
|
MenuItemMorph
|
|
FrameMorph
|
|
ScrollFrameMorph
|
|
ListMorph
|
|
StringFieldMorph
|
|
BouncerMorph*
|
|
HandMorph
|
|
WorldMorph
|
|
|
|
* included only for demo purposes
|
|
|
|
|
|
III. yet to implement
|
|
---------------------
|
|
- keyboard support for scroll frames and lists
|
|
- virtual keyboard support for Android
|
|
|
|
|
|
IV. open issues
|
|
----------------
|
|
- clipboard support (copy & paste) for non-textual data
|
|
|
|
|
|
V. browser compatibility
|
|
------------------------
|
|
I have taken great care and considerable effort to make morphic.js
|
|
runnable and appearing exactly the same on all current browsers
|
|
available to me:
|
|
|
|
- Firefox for Windows
|
|
- Firefox for Mac
|
|
- Firefox for Android
|
|
- Chrome for Windows
|
|
- Chrome for Mac
|
|
- Chrome for Android
|
|
- Safari for Windows (deprecated)
|
|
- safari for Mac
|
|
- Safari for iOS (mobile)
|
|
- IE for Windows (partial support)
|
|
- Edge for Windows
|
|
- Opera for Windows
|
|
- Opera for Mac
|
|
|
|
|
|
VI. the big picture
|
|
-------------------
|
|
Morphic.js is completely based on Canvas and JavaScript, it is just
|
|
Morphic, nothing else. Morphic.js is very basic and covers only the
|
|
bare essentials:
|
|
|
|
* a stepping mechanism (a time-sharing multiplexer for lively
|
|
user interaction ontop of a single OS/browser thread)
|
|
* progressive display updates (only dirty rectangles are
|
|
redrawn at each display cycle)
|
|
* a tree structure
|
|
* a single World per Canvas element (although you can have
|
|
multiple worlds in multiple Canvas elements on the same web
|
|
page)
|
|
* a single Hand per World (but you can support multi-touch
|
|
events)
|
|
* a single text entry focus per World
|
|
|
|
In its current state morphic.js doesn't support transforms (you
|
|
cannot rotate Morphs), but with PenMorph there already is a simple
|
|
LOGO-like turtle that you can use to draw onto any Morph it is
|
|
attached to. I'm planning to add special Morphs that support these
|
|
operations later on, but not for every Morph in the system.
|
|
Therefore these additions ("sprites" etc.) are likely to be part of
|
|
other libraries ("microworld.js") in separate files.
|
|
|
|
the purpose of morphic.js is to provide a malleable framework that
|
|
will let me experiment with lively GUIs for my hobby horse, which
|
|
is drag-and-drop, blocks based programming languages. Those things
|
|
(BYOB4 - http://byob.berkeley.edu) will be written using morphic.js
|
|
as a library.
|
|
|
|
|
|
VII. programming guide
|
|
----------------------
|
|
Morphic.js provides a library for lively GUIs inside single HTML
|
|
Canvas elements. Each such canvas element functions as a "world" in
|
|
which other visible shapes ("morphs") can be positioned and
|
|
manipulated, often directly and interactively by the user. Morphs
|
|
are tree nodes and may contain any number of submorphs ("children").
|
|
|
|
All things visible in a morphic World are morphs themselves, i.e.
|
|
all text rendering, blinking cursors, entry fields, menus, buttons,
|
|
sliders, windows and dialog boxes etc. are created with morphic.js
|
|
rather than using HTML DOM elements, and as a consequence can be
|
|
changed and adjusted by the programmer regardless of proprietary
|
|
browser behavior.
|
|
|
|
Each World has an - invisible - "Hand" resembling the mouse cursor
|
|
(or the user's finger on touch screens) which handles mouse events,
|
|
and may also have a keyboard focus to handle key events.
|
|
|
|
The basic idea of Morphic is to continuously run display cycles and
|
|
to incrementally update the screen by only redrawing those World
|
|
regions which have been "dirtied" since the last redraw. Before
|
|
each shape is processed for redisplay it gets the chance to perform
|
|
a "step" procedure, thus allowing for an illusion of concurrency.
|
|
|
|
|
|
(1) setting up a web page
|
|
-------------------------
|
|
Setting up a web page for Morphic always involves three steps:
|
|
adding one or more Canvas elements, defining one or more worlds,
|
|
initializing and starting the main loop.
|
|
|
|
|
|
(a) single world
|
|
-----------------
|
|
Most commonly you will want your World to fill the browsers's whole
|
|
client area. This default situation is easiest and most straight
|
|
forward.
|
|
|
|
example html file:
|
|
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
<title>Morphic!</title>
|
|
<script type="text/javascript" src="morphic.js"></script>
|
|
<script type="text/javascript">
|
|
var world;
|
|
|
|
window.onload = function () {
|
|
world = new WorldMorph(document.getElementById('world'));
|
|
world.isDevMode = true;
|
|
loop();
|
|
};
|
|
|
|
function loop() {
|
|
requestAnimationFrame(loop);
|
|
world.doOneCycle();
|
|
}
|
|
</script>
|
|
</head>
|
|
<body style="margin: 0;">
|
|
<canvas id="world" tabindex="1" width="800" height="600"
|
|
style="position: absolute;"></canvas>
|
|
</body>
|
|
</html>
|
|
|
|
if you use ScrollFrames or otherwise plan to support mouse wheel
|
|
scrolling events, make sure to add the following inline-CSS
|
|
attribute to the Canvas element:
|
|
|
|
style="position: absolute;"
|
|
|
|
which will prevent the World to be scrolled around instead of the
|
|
elements inside of it in some browsers.
|
|
|
|
|
|
(b) multiple worlds
|
|
-------------------
|
|
If you wish to create a web page with more than one world, make
|
|
sure to prevent each world from auto-filling the whole page and
|
|
include it in the main loop. It's also a good idea to give each
|
|
world its own tabindex:
|
|
|
|
example html file:
|
|
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
<title>Morphic!</title>
|
|
<script type="text/javascript" src="morphic.js"></script>
|
|
<script type="text/javascript">
|
|
var world1, world2;
|
|
|
|
window.onload = function () {
|
|
disableRetinaSupport();
|
|
world1 = new WorldMorph(
|
|
document.getElementById('world1'), false);
|
|
world2 = new WorldMorph(
|
|
document.getElementById('world2'), false);
|
|
loop();
|
|
};
|
|
|
|
function loop() {
|
|
requestAnimationFrame(loop);
|
|
world1.doOneCycle();
|
|
world2.doOneCycle();
|
|
}
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<p>first world:</p>
|
|
<canvas id="world1" tabindex="1" width="600" height="400"></canvas>
|
|
<p>second world:</p>
|
|
<canvas id="world2" tabindex="2" width="400" height="600"></canvas>
|
|
</body>
|
|
</html>
|
|
|
|
|
|
(c) an application
|
|
-------------------
|
|
Of course, most of the time you don't want to just plain use the
|
|
standard Morphic World "as is" out of the box, but write your own
|
|
application (something like Scratch!) in it. For such an
|
|
application you'll create your own morph prototypes, perhaps
|
|
assemble your own "window frame" and bring it all to life in a
|
|
customized World state. the following example creates a simple
|
|
snake-like mouse drawing game.
|
|
|
|
example html file:
|
|
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
<title>touch me!</title>
|
|
<script type="text/javascript" src="morphic.js"></script>
|
|
<script type="text/javascript">
|
|
var worldCanvas, sensor;
|
|
|
|
window.onload = function () {
|
|
var x, y, w, h;
|
|
|
|
worldCanvas = document.getElementById('world');
|
|
world = new WorldMorph(worldCanvas);
|
|
world.isDevMode = false;
|
|
world.setColor(new Color());
|
|
|
|
w = 100;
|
|
h = 100;
|
|
|
|
x = 0;
|
|
y = 0;
|
|
|
|
while ((y * h) < world.height()) {
|
|
while ((x * w) < world.width()) {
|
|
sensor = new MouseSensorMorph();
|
|
sensor.setPosition(new Point(x * w, y * h));
|
|
sensor.alpha = 0;
|
|
sensor.setExtent(new Point(w, h));
|
|
world.add(sensor);
|
|
x += 1;
|
|
}
|
|
x = 0;
|
|
y += 1;
|
|
}
|
|
loop();
|
|
};
|
|
|
|
function loop() {
|
|
requestAnimationFrame(loop);
|
|
world.doOneCycle();
|
|
}
|
|
</script>
|
|
</head>
|
|
<body bgcolor='black' style="margin: 0;">
|
|
<canvas id="world" width="800" height="600"
|
|
style="position: absolute;"></canvas>
|
|
</body>
|
|
</html>
|
|
|
|
To get an idea how you can craft your own custom morph prototypes
|
|
I've included two examples which should give you an idea how to add
|
|
properties, override inherited methods and use the stepping
|
|
mechanism for "livelyness":
|
|
|
|
BouncerMorph
|
|
MouseSensorMorph
|
|
|
|
For the sake of sharing a single file I've included those examples
|
|
in morphic.js itself. Usually you'll define your additions in a
|
|
separate file and keep morphic.js untouched.
|
|
|
|
|
|
(2) manipulating morphs
|
|
-----------------------
|
|
There are many methods to programmatically manipulate morphs. Among
|
|
the most important and common ones among all morphs are the
|
|
following nine:
|
|
|
|
* hide()
|
|
* show()
|
|
|
|
* setPosition(aPoint)
|
|
* setExtent(aPoint)
|
|
* setColor(aColor)
|
|
|
|
* add(submorph) - attaches submorph ontop
|
|
* addBack(submorph) - attaches submorph underneath
|
|
|
|
* fullCopy() - duplication
|
|
* destroy() - deletion
|
|
|
|
|
|
(3) events
|
|
----------
|
|
All user (and system) interaction is triggered by events, which are
|
|
passed on from the root element - the World - to its submorphs. The
|
|
World contains a list of system (browser) events it reacts to in its
|
|
|
|
initEventListeners()
|
|
|
|
method. Currently there are
|
|
|
|
- mouse
|
|
- drop
|
|
- keyboard
|
|
- (window) resize
|
|
|
|
events.
|
|
|
|
These system events are dispatched within the morphic World by the
|
|
World's Hand and its keyboardFocus (usually the active text
|
|
cursor).
|
|
|
|
|
|
(a) mouse events:
|
|
-----------------
|
|
The Hand dispatches the following mouse events to relevant morphs:
|
|
|
|
mouseDownLeft
|
|
mouseDownRight
|
|
mouseClickLeft
|
|
mouseClickRight
|
|
mouseDoubleClick
|
|
mouseEnter
|
|
mouseLeave
|
|
mouseEnterDragging
|
|
mouseLeaveDragging
|
|
mouseMove
|
|
mouseScroll
|
|
|
|
If you wish your morph to react to any such event, simply add a
|
|
method of the same name as the event, e.g:
|
|
|
|
MyMorph.prototype.mouseMove = function(pos) {};
|
|
|
|
All of these methods have as optional parameter a Point object
|
|
indicating the current position of the Hand inside the World's
|
|
coordinate system. The
|
|
|
|
mouseMove(pos, button)
|
|
|
|
event method has an additional optional parameter indicating the
|
|
currently pressed mouse button, which is either 'left' or 'right'.
|
|
You can use this to let users interact with 3D environments.
|
|
|
|
Events may be "bubbled" up a morph's owner chain by calling
|
|
|
|
this.escalateEvent(functionName, arg)
|
|
|
|
in the event handler method's code.
|
|
|
|
Likewise, removing the event handler method will render your morph
|
|
passive to the event in question.
|
|
|
|
|
|
(b) context menu:
|
|
-----------------
|
|
By default right-clicking (or single-finger tap-and-hold) on a morph
|
|
also invokes its context menu (in addition to firing the
|
|
mouseClickRight event). A morph's context menu can be customized by
|
|
assigning a Menu instance to its
|
|
|
|
customContextMenu
|
|
|
|
property, or altogether suppressed by overriding its inherited
|
|
|
|
contextMenu()
|
|
|
|
method.
|
|
|
|
|
|
(c) dragging:
|
|
-------------
|
|
Dragging a morph is initiated when the left mouse button is pressed,
|
|
held and the mouse is moved.
|
|
|
|
You can control whether a morph is draggable by setting its
|
|
|
|
isDraggable
|
|
|
|
property either to false or true. If a morph isn't draggable itself
|
|
it will pass the pick-up request up its owner chain. This lets you
|
|
create draggable composite morphs like Windows, DialogBoxes,
|
|
Sliders etc.
|
|
|
|
Sometimes it is desireable to make "template" shapes which cannot be
|
|
moved themselves, but from which instead duplicates can be peeled
|
|
off. This is especially useful for building blocks in construction
|
|
kits, e.g. the MIT-Scratch palette. Morphic.js lets you control this
|
|
functionality by setting the
|
|
|
|
isTemplate
|
|
|
|
property flag to true for any morph whose "isDraggable" property is
|
|
turned off. When dragging such a Morph the hand will instead grab
|
|
a duplicate of the template whose "isDraggable" flag is true and
|
|
whose "isTemplate" flag is false, in other words: a non-template.
|
|
|
|
When creating a copy from a template, the copy's
|
|
|
|
reactToTemplateCopy
|
|
|
|
is invoked, if it is present.
|
|
|
|
Dragging is indicated by adding a drop shadow to the morph in hand.
|
|
If a morph follows the hand without displaying a drop shadow it is
|
|
merely being moved about without changing its parent (owner morph),
|
|
e.g. when "dragging" a morph handle to resize its owner, or when
|
|
"dragging" a slider button.
|
|
|
|
Right before a morph is picked up its
|
|
|
|
selectForEdit
|
|
|
|
and
|
|
|
|
prepareToBeGrabbed(handMorph)
|
|
|
|
methods are invoked, each if it is present. the optional
|
|
|
|
selectForEdit
|
|
|
|
if implemented, must return the object that is to be picked up.
|
|
In addition to just returning the original object chosen by the user
|
|
your method can also modify the target's environment and instead return
|
|
a copy of the selected morph if, for example, you would like to implement
|
|
a copy-on-write mechanism such as in Snap.
|
|
|
|
Immediately after the pick-up the former parent's
|
|
|
|
reactToGrabOf(grabbedMorph)
|
|
|
|
method is called, again only if it exists.
|
|
|
|
Similar to events, these methods are optional and don't exist by
|
|
default. For a simple example of how they can be used to adjust
|
|
scroll bars in a scroll frame please have a look at their
|
|
implementation in FrameMorph.
|
|
|
|
|
|
(d) dropping:
|
|
-------------
|
|
Dropping is triggered when the left mouse button is either pressed
|
|
or released while the Hand is dragging a morph.
|
|
|
|
Dropping a morph causes it to become embedded in a new owner morph.
|
|
You can control this embedding behavior by setting the prospective
|
|
drop target's
|
|
|
|
acceptsDrops
|
|
|
|
property to either true or false, or by overriding its inherited
|
|
|
|
wantsDropOf(aMorph)
|
|
|
|
method.
|
|
|
|
Right before dropping a morph the designated new parent's optional
|
|
|
|
selectForEdit
|
|
|
|
method is invoked if it is present. Again, if implemented this method
|
|
must return the new parent for the morph that is about to be dropped.
|
|
Again, in addition to just returning the designeted drop-target
|
|
your method can also modify its environment and instead return
|
|
a copy of the new parent if, for example, you would like to implement
|
|
a copy-on-write mechanism such as in Snap.
|
|
|
|
Right after a morph has been dropped its
|
|
|
|
justDropped(handMorph)
|
|
|
|
method is called, and its new parent's
|
|
|
|
reactToDropOf(droppedMorph, handMorph)
|
|
|
|
method is invoked, again only if each method exists.
|
|
|
|
Similar to events, these methods are optional and by default are
|
|
not present in morphs by default (watch out for inheritance,
|
|
though!). For a simple example of how they can be used to adjust
|
|
scroll bars in a scroll frame please have a look at their
|
|
implementation in FrameMorph.
|
|
|
|
Drops of image elements from outside the world canvas are dispatched as
|
|
|
|
droppedImage(aCanvas, name)
|
|
droppedSVG(anImage, name)
|
|
|
|
events to interested Morphs at the mouse pointer. If you want your Morph
|
|
to e.g. import outside images you can add the droppedImage() and / or the
|
|
droppedSVG() methods to it. The parameter passed to the event handles is
|
|
a new offscreen canvas element representing a copy of the original image
|
|
element which can be directly used, e.g. by assigning it to another
|
|
Morph's cachedImage property. In the case of a dropped SVG it is an image
|
|
element (not a canvas), which has to be rasterized onto a canvas before
|
|
it can be used. The benefit of handling SVGs as image elements is that
|
|
rasterization can be deferred until the destination scale is known, taking
|
|
advantage of SVG's ability for smooth scaling. If instead SVGs are to be
|
|
rasterized right away, you can set the
|
|
|
|
MorphicPreferences.rasterizeSVGs
|
|
|
|
preference to <true>. In this case dropped SVGs also trigger the
|
|
droppedImage() event with a canvas containing a rasterized version of the
|
|
SVG.
|
|
|
|
The same applies to drops of audio or text files from outside the world
|
|
canvas.
|
|
|
|
Those are dispatched as
|
|
|
|
droppedAudio(anAudio, name)
|
|
droppedText(aString, name, type)
|
|
|
|
events to interested Morphs at the mouse pointer.
|
|
|
|
if none of the above content types can be determined, the file contents
|
|
is dispatched as an ArrayBuffer to interested Morphs:
|
|
|
|
droppedBinary(anArrayBuffer, name)
|
|
|
|
|
|
(e) keyboard events
|
|
-------------------
|
|
The World dispatches the following key events to its active
|
|
keyboard focus:
|
|
|
|
keypress
|
|
keydown
|
|
keyup
|
|
|
|
Currently the only morphs which acts as keyboard focus are
|
|
CursorMorph - the basic text editing widget - and MenuMorph elements.
|
|
If you wish to add keyboard support to your morph you need to add event
|
|
handling methods for
|
|
|
|
processKeyPress(event)
|
|
processKeyDown(event)
|
|
processKeyUp(event)
|
|
|
|
and activate them by assigning your morph to the World's
|
|
|
|
keyboardFocus
|
|
|
|
property.
|
|
|
|
Note that processKeyUp() is optional and doesn't have to be present
|
|
if your morph doesn't require it.
|
|
|
|
|
|
(f) resize event
|
|
----------------
|
|
The Window resize event is handled by the World and allows the
|
|
World's extent to be adjusted so that it always completely fills
|
|
the browser's visible page. You can turn off this default behavior
|
|
by setting the World's
|
|
|
|
useFillPage
|
|
|
|
property to false.
|
|
|
|
Alternatively you can also initialize the World with the
|
|
useFillPage switch turned off from the beginning by passing the
|
|
false value as second parameter to the World's constructor:
|
|
|
|
world = new World(aCanvas, false);
|
|
|
|
Use this when creating a web page with multiple Worlds.
|
|
|
|
if "useFillPage" is turned on the World dispatches an
|
|
|
|
reactToWorldResize(newBounds)
|
|
|
|
events to all of its children (toplevel only), allowing each to
|
|
adjust to the new World bounds by implementing a corresponding
|
|
method, the passed argument being the World's new dimensions after
|
|
completing the resize. By default, the "reactToWorldResize" Method
|
|
does not exist.
|
|
|
|
Example:
|
|
|
|
Add the following method to your Morph to let it automatically
|
|
fill the whole World, but leave a 10 pixel border uncovered:
|
|
|
|
MyMorph.prototype.reactToWorldResize = function (rect) {
|
|
this.changed();
|
|
this.bounds = rect.insetBy(10);
|
|
this.rerender();
|
|
};
|
|
|
|
|
|
(g) combined mouse-keyboard events
|
|
----------------------------------
|
|
Occasionally you'll want an object to react differently to a mouse
|
|
click or to some other mouse event while the user holds down a key
|
|
on the keyboard. Such "shift-click", "ctl-click", or "alt-click"
|
|
events can be implemented by querying the World's
|
|
|
|
currentKey
|
|
|
|
property inside the function that reacts to the mouse event. This
|
|
property stores the keyCode of the key that's currently pressed.
|
|
Once the key is released by the user it reverts to null.
|
|
|
|
|
|
(h) text editing events
|
|
-----------------------
|
|
Much of Morphic's "liveliness" comes out of allowing text elements
|
|
(instances of either single-lined StringMorph or multi-lined TextMorph)
|
|
to be directly manipulated and edited by users. This requires other
|
|
objects which may have an interest in the text element's state to react
|
|
appropriately. Therefore text elements and their manipulators emit
|
|
a stream of events, mostly by "bubbling" them up the text element's
|
|
owner chain. Text elements' parents are notified about the following
|
|
events:
|
|
|
|
Whenever the user presses a key on the keyboard while a text element
|
|
is being edited, first a
|
|
|
|
reactToKeystroke(event)
|
|
|
|
is escalated up its parent chain, the "event" parameter being the
|
|
original one received by the World.
|
|
|
|
Whenever the input changes, by adding or removing one or more characters,
|
|
an additional
|
|
|
|
reactToInput(event)
|
|
|
|
is escalated up its parent chain, the "event" parameter again being the
|
|
original one received by the World or by the IME element.
|
|
|
|
Note that the "reactToKeystroke" event gets triggered before the input
|
|
changes, and thus befgore the "reactToInput" event fires.
|
|
|
|
Once the user has completed the edit, the following events are
|
|
dispatched:
|
|
|
|
accept() - <enter> was pressed on a single line of text
|
|
cancel() - <esc> was pressed on any text element
|
|
|
|
Note that "accept" only gets triggered by single-line texte elements,
|
|
as the <enter> key is used to insert line breaks in multi-line
|
|
elements. Therefore, whenever a text edit is terminated by the user
|
|
(accepted, cancelled or otherwise),
|
|
|
|
reactToEdit(StringOrTextMorph)
|
|
|
|
is triggered.
|
|
|
|
If the MorphicPreference's
|
|
|
|
useSliderForInput
|
|
|
|
setting is turned on, a slider is popped up underneath the currently
|
|
edited text element letting the user insert numbers out of the given
|
|
slider range. Whenever this happens, i.e. whenever the slider is moved
|
|
or while the slider button is pressed, a stream of
|
|
|
|
reactToSliderEdit(StringOrTextMorph)
|
|
|
|
events is dispatched, allowing for "Bret-Victor" style "scrubbing"
|
|
applications.
|
|
|
|
In addition to user-initiated events text elements also emit
|
|
change notifications to their direct parents whenever their contents
|
|
changes. That way complex Morphs containing text elements
|
|
get a chance to react if something about the embedded text has been
|
|
modified programmatically. These events are:
|
|
|
|
layoutChanged() - sent only from instances of TextMorph
|
|
fixLayout() - sent from instances of all Morphs, including StringMorphs
|
|
|
|
they are different so that Morphs which contain both multi-line and
|
|
single-line text elements can hold them apart.
|
|
|
|
|
|
(4) stepping
|
|
------------
|
|
Stepping is what makes Morphic "magical". Two properties control
|
|
a morph's stepping behavior: the fps attribute and the step()
|
|
method.
|
|
|
|
By default the
|
|
|
|
step()
|
|
|
|
method does nothing. As you can see in the examples of BouncerMorph
|
|
and MouseSensorMorph you can easily override this inherited method
|
|
to suit your needs.
|
|
|
|
By default the step() method is called once per display cycle.
|
|
Depending on the number of actively stepping morphs and the
|
|
complexity of your step() methods this can cause quite a strain on
|
|
your CPU, and also result in your application behaving differently
|
|
on slower computers than on fast ones.
|
|
|
|
setting
|
|
|
|
myMorph.fps
|
|
|
|
to a number lower than the interval for the main loop lets you free
|
|
system resources (albeit at the cost of a less responsive or slower
|
|
behavior for this particular morph).
|
|
|
|
|
|
(5) creating new kinds of morphs
|
|
--------------------------------
|
|
The real fun begins when you start to create new kinds of morphs
|
|
with customized shapes. Imagine, e.g. jigsaw puzzle pieces or
|
|
musical notes.
|
|
|
|
When you create your own morphs, you'll want to think about how to
|
|
graphically render it, how to determine its size and whether it needs
|
|
to arrange any other parts ("submorphs). There are also ways to specify
|
|
its collision detection behavior and define "untouchable" regions
|
|
("holes").
|
|
|
|
|
|
(a) drawing the shape
|
|
---------------------
|
|
For this you have to override the default
|
|
|
|
render(ctx)
|
|
|
|
method.
|
|
|
|
This method draws the morph's shape using a given 2d graphics context.
|
|
Note that any coordinates used in the render() method must be relative
|
|
to the morph's own position, i.e. you don't need to worry about
|
|
translating the shape yourself.
|
|
|
|
You can use the following template for a start:
|
|
|
|
MyMorph.prototype.render = function(ctx) {
|
|
ctx.fillStyle = this.color.toString();
|
|
ctx.fillRect(0, 0, this.width(), this.height());
|
|
};
|
|
|
|
it renders the morph as a solid rectangle completely filling its
|
|
area with its current color.
|
|
|
|
Notice how the coordinates for the fillRect() call are relative
|
|
to the morph's own position: The rendered rectangle's origin is always
|
|
located at (0, 0) regardless of the morph's actual position in the World.
|
|
|
|
|
|
(b) determining extent and arranging submorphs
|
|
----------------------------------------------
|
|
If your new morph also needs to determine its extent and, e.g. to
|
|
encompass one or several other morphs, or arrange the layout of its
|
|
submorphs, make sure to also override the default
|
|
|
|
fixLayout()
|
|
|
|
method.
|
|
|
|
NOTE: If you need to set the morph's extent inside, in order to avoid
|
|
infinite recursion instead of calling morph.setExtent() - which will
|
|
in turn call morph.fixLayout() again - directly modify the morph's
|
|
|
|
bounds
|
|
|
|
property. Bounds is a rectable on which you can also use the same
|
|
size-setters, e.g. by calling:
|
|
|
|
this.bounds.setExtent()
|
|
|
|
|
|
(c) pixel-perfect pointing events
|
|
---------------------------------
|
|
In case your new morph needs to support pixel-perfect collision detection
|
|
with other morphs or pointing devices such as the mouse or a stylus you
|
|
can set the inherited attribute
|
|
|
|
isFreeForm = bool
|
|
|
|
to "true" (default is "false"). This makes sense the more your morph's
|
|
visual shape diverges from a rectangle. For example, if you create a
|
|
circular filled morph the default setting will register mouse-events
|
|
anywhere within its bounding box, e.g. also in the transparent parts
|
|
between the bounding box's corners outside of the circle's bounds.
|
|
Instead you can specify your irregulary shaped morph to only register
|
|
pointing events (mouse and touch) on solid, non-transparent parts.
|
|
|
|
Notice, however, that such pixel-perfect collision detection might
|
|
strain processing resources, especially if applied liberally.
|
|
|
|
In order to mitigate unfavorable processor loads for pixel-perfect
|
|
collision deteciton of irregularly shaped morphs there are two strategies
|
|
to consider: Caching the shape and specifying "untouchable" regions.
|
|
|
|
|
|
(d) caching the shape
|
|
---------------------
|
|
In case of pixel-perfect free-form collision detection it makes sense to
|
|
cache your morph's current shape, so it doesn't have to be re-drawn onto a
|
|
new Canvas element every time the mouse moves over its bounding box.
|
|
For this you can set then inherited
|
|
|
|
isCachingImage = bool
|
|
|
|
attribute to "true" instead of the default "false" value. This will
|
|
significantly speed up collision detection and smoothen animations that
|
|
continuously perform collision detection. However, it will also consume
|
|
more memory. Therefore it's best to use this setting with caution.
|
|
|
|
Snap! caches the shapes of sprites but not those of blocks. Instead it
|
|
manages the insides of C- and E-shaped blocks through the morphic "holes"
|
|
mechanism.
|
|
|
|
|
|
(e) holes
|
|
---------
|
|
An alternative albeit not as precise and general way for handling
|
|
irregularly shaped morphs with "untouchable" regions is to specify a set
|
|
of rectangular areas in which pointing events (mouse or touch) are not
|
|
registered.
|
|
|
|
By default the inherited
|
|
|
|
holes = []
|
|
|
|
property is an empty array. You can add one or more morphic Rectangle
|
|
objects to this list, representing regions, in which occurring events will
|
|
instead be passed on to the morph underneath.
|
|
|
|
Note that, same with the render() method, the coordinates of these
|
|
rectangular holes must be specified relative to your morph's position.
|
|
|
|
If you specify holes you might find the need to adjust their layout
|
|
depending on the layout of your morph. To accomplish this you can override
|
|
the inherited
|
|
|
|
fixHolesLayout()
|
|
|
|
method.
|
|
|
|
|
|
(f) updating
|
|
------------
|
|
One way for morphs to become alive is form them to literally "morph" their
|
|
shape depending on whicher contest you wish them to react to. For example,
|
|
you might want the user to interactively draw a shape using their fingers
|
|
on a touch screen device, or you want the user to be able to "pinch" or
|
|
otherwise distort a shape interactively. In all of these situations you'll
|
|
want your morph to frequently rerender its shape.
|
|
|
|
You can accomplish this, by calling
|
|
|
|
rerender()
|
|
|
|
after every change to your morph's appearance that requires rerendering.
|
|
|
|
Such changes are usually only happening when the morph's dimensions or
|
|
other visual properties - such as its color - changes.
|
|
|
|
|
|
(g) duplicating
|
|
---------------
|
|
If your new morph stores or references to other morphs outside of
|
|
the submorph tree in other properties, be sure to also override the
|
|
default
|
|
|
|
updateReferences()
|
|
|
|
method if you want it to support duplication.
|
|
|
|
|
|
(6) development and user modes
|
|
------------------------------
|
|
When working with Squeak on Scratch or BYOB among the features I
|
|
like the best and use the most is inspecting what's going on in
|
|
the World while it is up and running. That's what development mode
|
|
is for (you could also call it debug mode). In essence development
|
|
mode controls which context menu shows up. In user mode right
|
|
clicking (or double finger tapping) a morph invokes its
|
|
|
|
customContextMenu
|
|
|
|
property, whereas in development mode only the general
|
|
|
|
developersMenu()
|
|
|
|
method is called and the resulting menu invoked. The developers'
|
|
menu features Gui-Builder-wise functionality to directly inspect,
|
|
take apart, reassamble and otherwise manipulate morphs and their
|
|
contents.
|
|
|
|
Instead of using the "customContextMenu" property you can also
|
|
assign a more dynamic contextMenu by overriding the general
|
|
|
|
userMenu()
|
|
|
|
method with a customized menu constructor. The difference between
|
|
the customContextMenu property and the userMenu() method is that
|
|
the former is also present in development mode and overrides the
|
|
developersMenu() result. For an example of how to use the
|
|
customContextMenu property have a look at TextMorph's evaluation
|
|
menu, which is used for the Inspector's evaluation pane.
|
|
|
|
When in development mode you can inspect every Morph's properties
|
|
with the inspector, including all of its methods. The inspector
|
|
also lets you add, remove and rename properties, and even edit
|
|
their values at runtime. Like in a Smalltalk environment the inspect
|
|
features an evaluation pane into which you can type in arbitrary
|
|
JavaScript code and evaluate it in the context of the inspectee.
|
|
|
|
Use switching between user and development modes while you are
|
|
developing an application and disable switching to development once
|
|
you're done and deploying, because generally you don't want to
|
|
confuse end-users with inspectors and meta-level stuff.
|
|
|
|
|
|
(7) turtle graphics
|
|
-------------------
|
|
|
|
The basic Morphic kernel features a simple LOGO turtle constructor
|
|
called
|
|
|
|
PenMorph
|
|
|
|
which you can use to draw onto its parent Morph. By default every
|
|
Morph in the system (including the World) is able to act as turtle
|
|
canvas and can display pen trails. Pen trails will be lost whenever
|
|
the trails morph (the pen's parent) performs a "render()"
|
|
operation. If you want to create your own pen trails canvas, you
|
|
may wish to modify its
|
|
|
|
penTrails()
|
|
|
|
property, so that it keeps a separate offscreen canvas for pen
|
|
trails (and doesn't loose these on redraw).
|
|
|
|
the following properties of PenMorph are relevant for turtle
|
|
graphics:
|
|
|
|
color - a Color
|
|
size - line width of pen trails
|
|
heading - degrees
|
|
isDown - drawing state
|
|
|
|
the following commands can be used to actually draw something:
|
|
|
|
up() - lift the pen up, further movements leave no trails
|
|
down() - set down, further movements leave trails
|
|
clear() - remove all trails from the current parent
|
|
forward(n) - move n steps in the current direction (heading)
|
|
turn(n) - turn right n degrees
|
|
|
|
Turtle graphics can best be explored interactively by creating a
|
|
new PenMorph object and by manipulating it with the inspector
|
|
widget.
|
|
|
|
NOTE: PenMorph has a special optimization for recursive operations
|
|
called
|
|
|
|
warp(function)
|
|
|
|
You can significantly speed up recursive ops and increase the depth
|
|
of recursion that's displayable by wrapping WARP around your
|
|
recursive function call:
|
|
|
|
example:
|
|
|
|
myPen.warp(function () {
|
|
myPen.tree(12, 120, 20);
|
|
})
|
|
|
|
will be much faster than just invoking the tree function, because it
|
|
prevents the parent's parent from keeping track of every single line
|
|
segment and instead redraws the outcome in a single pass.
|
|
|
|
|
|
(8) supporting high-resolution "retina" screens
|
|
-----------------------------------------------
|
|
By default retina support gets installed when Morphic.js loads. There
|
|
are two global functions that let you test for retina availability:
|
|
|
|
isRetinaSupported() - Bool, answers if retina support is available
|
|
isRetinaEnabled() - Bool, answers if currently in retina mode
|
|
|
|
and two more functions that let you control retina support if it is
|
|
available:
|
|
|
|
enableRetinaSupport()
|
|
disableRetinaSupport()
|
|
|
|
Both of these internally test whether retina is available, so they are
|
|
safe to call directly. For an example how to make retina support
|
|
user-specifiable refer to
|
|
|
|
Snap! >> guis.js >> toggleRetina()
|
|
|
|
Even when in retina mode it often makes sense to use normal-resolution
|
|
canvasses for simple shapes in order to save system resources and
|
|
optimize performance. Examples are costumes and backgrounds in Snap.
|
|
In Morphic you can create new canvas elements using
|
|
|
|
newCanvas(extentPoint [, nonRetinaFlag])
|
|
|
|
If retina support is enabled such new canvasses will automatically be
|
|
high-resolution canvasses, unless the newCanvas() function is given an
|
|
otherwise optional second Boolean <true> argument that explicitly makes
|
|
it a non-retina canvas.
|
|
|
|
Not the whole canvas API is supported by Morphic's retina utilities.
|
|
Especially if your code uses putImageData() you will want to "downgrade"
|
|
a target high-resolution canvas to a normal-resolution ("non-retina")
|
|
one before using
|
|
|
|
normalizeCanvas(aCanvas [, copyFlag])
|
|
|
|
This will change the target canvas' resolution in place (!). If you
|
|
pass in the optional second Boolean <true> flag the function returns
|
|
a non-retina copy and leaves the target canvas unchanged. An example
|
|
of this normalize mechanism is converting the penTrails layer of Snap's
|
|
stage (high-resolution) into a sprite-costume (normal resolution).
|
|
|
|
|
|
(9) animations
|
|
---------------
|
|
Animations handle gradual transitions between one state and another over a
|
|
period of time. Transition effects can be specified using easing functions.
|
|
An easing function maps a fraction of the transition time to a fraction of
|
|
the state delta. This way accelerating / decelerating and bouncing sliding
|
|
effects can be accomplished.
|
|
|
|
Animations are generic and not limited to motion, i.e. they can also handle
|
|
other transitions such as color changes, transparency fadings, growing,
|
|
shrinking, turning etc.
|
|
|
|
Animations need to be stepped by a scheduler, e. g. an interval function.
|
|
In Morphic the preferred way to run an animation is to register it with
|
|
the World by adding it to the World's animation queue. The World steps each
|
|
registered animation once per display cycle independently of the Morphic
|
|
stepping mechanism.
|
|
|
|
For an example how to use animations look at how the Morph's methods
|
|
|
|
glideTo()
|
|
fadeTo()
|
|
|
|
and
|
|
|
|
slideBackTo()
|
|
|
|
are implemented.
|
|
|
|
|
|
(10) minifying morphic.js
|
|
-------------------------
|
|
Coming from Smalltalk and being a Squeaker at heart I am a huge fan
|
|
of browsing the code itself to make sense of it. Therefore I have
|
|
included this documentation and (too little) inline comments so all
|
|
you need to get going is this very file.
|
|
|
|
Nowadays with live streaming HD video even on mobile phones 250 KB
|
|
shouldn't be a big strain on bandwith, still minifying and even
|
|
compressing morphic.js down do about 100 KB may sometimes improve
|
|
performance in production use.
|
|
|
|
Being an attorney-at-law myself you programmer folk keep harassing
|
|
me with rabulistic nitpickings about free software licenses. I'm
|
|
releasing morphic.js under an AGPL license. Therefore please make
|
|
sure to adhere to that license in any minified or compressed version.
|
|
|
|
|
|
VIII. acknowledgements
|
|
----------------------
|
|
The original Morphic was designed and written by Randy Smith and
|
|
John Maloney for the SELF programming language, and later ported to
|
|
Squeak (Smalltalk) by John Maloney and Dan Ingalls, who has also
|
|
ported it to JavaScript (the Lively Kernel), once again setting
|
|
a "Gold Standard" for self sustaining systems which morphic.js
|
|
cannot and does not aspire to meet.
|
|
|
|
This Morphic implementation for JavaScript is not a direct port of
|
|
Squeak's Morphic, but still many individual functions have been
|
|
ported almost literally from Squeak, sometimes even including their
|
|
comments, e.g. the morph duplication mechanism fullCopy(). Squeak
|
|
has been a treasure trove, and if morphic.js looks, feels and
|
|
smells a lot like Squeak, I'll take it as a compliment.
|
|
|
|
Evelyn Eastmond has inspired and encouraged me with her wonderful
|
|
implementation of DesignBlocksJS. Thanks for sharing code, ideas
|
|
and enthusiasm for programming.
|
|
|
|
John Maloney has been my mentor and my source of inspiration for
|
|
these Morphic experiments. Thanks for the critique, the suggestions
|
|
and explanations for all things Morphic and for being my all time
|
|
programming hero.
|
|
|
|
I have originally written morphic.js in Florian Balmer's Notepad2
|
|
editor for Windows, later switched to Apple's Dashcode and later
|
|
still to Apple's Xcode. I've also come to depend on both Douglas
|
|
Crockford's JSLint and later the JSHint project, as well as on
|
|
Mozilla's Firebug and Google's Chrome to get it right.
|
|
|
|
|
|
IX. contributors
|
|
----------------------
|
|
Joe Otto found and fixed many early bugs and taught me some tricks.
|
|
Nathan Dinsmore contributed mouse wheel scrolling, cached
|
|
background texture handling, countless bug fixes and optimizations.
|
|
Ian Reynolds contributed backspace key handling for Chrome.
|
|
Davide Della Casa contributed performance optimizations for Firefox.
|
|
Jason N (@cyderize) contributed native copy & paste for text editing.
|
|
Bartosz Leper contributed retina display support.
|
|
Zhenlei Jia and Dariusz Dorożalski pioneered IME text editing.
|
|
Bernat Romagosa contributed to text editing and to the core design.
|
|
Michael Ball found and fixed a longstanding scrolling bug.
|
|
Brian Harvey contributed to the design and implementation of submenus.
|
|
Ken Kahn contributed to Chinese keboard entry and Android support.
|
|
Brian Broll contributed clickable URLs in text elements.
|
|
|
|
- Jens Mönig
|