Getting Started
Your first PlaySkript program will usually start with a set of drawing commands.
These commands can help you render diferent elements into the screen, including lines,
curves, text as well as images from your library. A typical drawing command is the
draw() commad. The details of this command can be found in the
documentation,
but at a high-level, the command takes as input the name of an image, and draws that image at a particular location.
// draw bluedog1 at coordinates (12, 12) with height 5.
draw("bluedog1", 12, 12, 5);
The command returns a handle that you can use to refer to this image and manipulate it with any of the
Object transformation commands.
dog = draw("bluedog1", 12, 12, 5);
//the commands below make the dog jump up and down in 5 timesteps.
move(dog, up, 5);
move(dog, down, 5);
In addition to
draw(), the most common drawing commands you are likely to use are
write to write text,
and
shape to create shapes.
//Draw a square at coordinates (4, 10) with height 10 and width 10.
shape("rect", 4, 10,{h:10, w:10, color: rgb(169,209,142,1), border: rgb(56,87,35,1)});
//Draw a circle
shape("circle", 9, 15, {r:2.5, color:rgb(169,209,142,1), border:rgb(56,87,35,1)});
//Write 'Hello!'
write(`Hello!`, 4.5, 11, {size:2, font:"Arial", color:rgb(56,87,35,1)});
Handles
A handle does two things: it identifies a set of images in the screen and it relates them to a particular
image in your library or to a particular shape descriptor in the case of shapes like lines, circles and curves.
dog1 = draw("bluedog1", 12, 12, 5);
dog2 = draw("bluedog1", 22, 12, 5);
//Two dogs are drawn, each generates its own handle
//The program can then perform transformations using the handle.
move(dog1, up, 5); //moves only dog1
move(dog1, down, 5);
Object transformation commands can also be applied to groups of objects.
//Draw two dogs.
dog1 = draw("bluedog1", 12, 12, 5);
dog2 = draw("bluedog1", 22, 12, 5);
//Make dog1 jump.
move(dog1, up, 5);
move(dog1, down, 5);
//flip both dog1 and dog2
flip([dog1, dog2], reverse);
//Move to the right any object created with "bluedog1", which includes both dog1 and dog2.
move("bluedog1", right, 10);
You can also dynamically change the image associated with a handle.
//Animates a picture by changing the image pointed to by a handle
dogA = draw("bluedog1", 12, 12, 5);
for(i=0; i<5; ++i){
wait(800); // wait for 800 milliseconds
define(dogA, "bluedogTongue");
wait(400);
define(dogA, "bluedog1");
}
For example, the code above draws a dog using the image "bluedog1", but then
replaces it with the image "bluedogTongue". The images are swapped back and forth
after a few milliseconds five times, producing an animation effect.
You can also use the
define command to create aliases for image names.
define("dog", "bluedog1"); //"dog" is an alias for "bluedog1"
dogA = draw("dog", 12, 12, 5);
dogB = draw("bluedog1", 20, 12, 5);
for(i=0; i<5; ++i){
wait(800); // wait for 800 milliseconds
define("dog", "bluedogTongue"); //This redefines the "dog" alias and
wait(400); //transforms any dog that was created with that alias, but not dogB.
define("dog", "bluedog1");
}
Deprecated: In some cases, we may want to treat a uniform collection of objects as a single unit. You can do this by
making them share the same handle. However, you want to be careful when you do this, because once two
objects share the same handle, they can never be disentangled. This functionality is unnecessary, since all routines that
take a handle as input can also take an array of handles, so there is no need to merge multiple handles into a single
handle.
//Draw two dogs that share the same handle.
dog1 = draw("bluedog1", 12, 12, 5);
draw(dog1, 22, 12, 5);
//Any transformation will be applied to both dogs sharing the handle.
move(dog1, up, 5);
move(dog1, down, 5);
flip(dog1, reverse);
We have already seen some examples of transforming objects, whether by moving them,
or by redefining the image to which they point to.
Shapes such as circles and rectangles have a number of attributes that can
be transformed with the
update command.
box = shape("rect",22,12,
{h:5, w:4, color:rgb(169, 209, 142), border:rgb(56, 87, 35)});
wait(500);
update(box, {color:rgb(10, 10, 40)}, 30)
For example, in the code above, a new rectangle is created, and then its color
is changed with the
update command. The last parameter
30
tells the command that the transformation should occur gradually over 30
timesteps, rather than in one step. You can find more information about
transformations
here.
One very powerful transformation is the
transform command, which
allows you to simultaneously scale, rotate and translate collections of objects.
The command takes as input a handle, the coordinate around which you want to rotate or scale, and the
parameters of the transformation. For example, the code below draws a set of shapes and then rotates and scales them around the
point (31.5, 25.75).
px0 = 31.5;
py0 = 25.75;
sh0 = shape("line", px0, py0,{h:-4, w:0, arrowEnd:2,
arrowEndSize:1, color:rgb(56,87,35,1)});
sh1 = shape("line", px0, py0,{h:0, w:4, arrowEnd:2,
arrowEndSize:1, color:rgb(56,87,35,1)});
sh2 = shape("circle", 22.33, 15.82, {r:2.5, color:rgb(169,209,142,1)});
sh3 = curve("curve", 36.65, 21.82,[[0, 0],[1.5, -2.08],[4.25, -1],[7, -0.25],
[8.34, 2.25],[4.42, 5.34],[2.5, 2.25],
[0.42, 2.84],[-1.5, 0.67],],
{open:false, color:rgb(56,87,35,1)});
sh4 = shape("rect", 17.25, 9.99,{h:24.42, w:32.58,
color: rgb(255,255,255,0), border: rgb(0,0,0,1)});
transform([sh0, sh1, sh2, sh3, sh4], px0, py0,
{ang:deg(180), scale:0.2}, 200);
Another transformation worth mentioning is the
morph transformation, which is used to modify a
curve.
A curve is defined by a set of points relative to the reference point of the curve. The
morph
command replaces those points with a different set of points, gradually morphing from one set of points to the other.
p1 = [[0, 0],[7.75, 3.17],[5, 10.08],
[-1.17, 7.17],[0.75, 2.92],[4.08, 5.92],];
c1 = curve("curve", 28.08, 17.08,
p1, {open:true, color:rgb(56, 87, 35)}); //An initial curve containing
//five points that make it look vaguely like a spiral.
p2 = [[0, 0],[6.25,0],[13.66,0],
[20.58,0],[25.24,0],[29.49,0],]
morph(c1, p2, 100); //morph replaces the original set of points with a new one
//the third argument tells it to morph slowly across 10 timesteps.
wait(1000);
morph(c1, p1, 100); //We can then morph back to the original points.
Finally, the
say transformation, which makes an object "say" something through a speech bubble.
dog = draw("bluedog1",12.4,13.66, 5);
say(dog, "Welcome to playskript!", 2);
wait(2000)
say(dog, `Welcome to playskript!
We hope you enjoy it.`, 2,
{
border:rgb(100,0,0),
background:rgb(219,111,112),
color:'white'
font:'Bitter'
lineWidth:5,
offX:-5.34, offY:-3.18,
tipX:1.25,
tipY:-1.34,
cornerRadius:0.5,
shadow:true
}
);
The basic form of the speech bubble is very bare bones, but the addional parameters offer significant sope
for customization.
Coordinates and Attachment Points
Given a handle, you can use it to get the coordinates of an already created object. You can do this
by using the
getX and
getY functions. For example, the code below first draws a line
and then draws a box with coordinates at the same point as the line origin.
line0 = shape("line", 17.41, 14.16,{h:-6, w:11.5, color:rgb(56, 87, 35)});
shape("rect", getX(line0), getY(line0),{h:10.17, w:11.33,
color: rgb(169, 209, 142), border: rgb(56, 87, 35)});
Additionally, shapes and text in PlaySkript have "attachment points", which correspond to points in the
perimeter of the shape whose coordinates you can get. For example, circles have the following attachment points:
0 - center, 1 - noon, 2 - 1:30, 3- 3:00, 4 - 4:30 , 5 - 6:00, 6-7:30, 7 - 9:00, 8 - 10:30. Similarly, for a
rectangle, the attachment points correspond to :
0 - center, 1 - middle of upper edge, 2 - upper right corner, 3- middle of right edge, 4 - lower right corner,
5 - middle of bottom edge, 6 - lower left corner, 7 - middle of left edge, 8 - upper left corner.
The attachment points for text are analogous to those of rectangles, and the attachment points for lines are simply
0 - origin, 1 - end point. Additionally, both lines and circles support fractional attachment points.
These are illustrated by the example below.
sh0 = shape("circle", 46.33, 10.66, {r:7.5,
color:rgb(169, 209, 142), border:rgb(56, 87, 35)});
sh1 = shape("line", getX(sh0, 8), getY(sh0, 8),
{py:getY(sh0, 4), px:getX(sh0, 4), color:rgb(56, 87, 35)});
shape("line", getX(sh1, 0.75), getY(sh1, 0.75),
{py:getY(sh0, 5), px:getX(sh0, 5), color:rgb(56, 87, 35)});
In the code above, sh0 is a circle, sh1 is a line that starts in the middle of the upper left quadrant of the circle,
where a clock would have the 10:30 mark, and ends in the lower right quadrant at 4:30. Finally, the last line
starts three quarters of the way towards the end of the previous line and ends at 6 o'clock in the circle.
Sub-handles
The handles for lines and curves contain sub-handles that allow you to move individual parts of a shape.
A line has sub-handles 'start' and 'end' that allow you to independently move the start or the end of a line.
sh0 = shape("line", 10, 10,{h:0, w:20, color:rgb(56, 87, 35)});
wait(500);
moveTo(sh0.start, 20, 20);
wait(500);
moveTo(sh0.end, 10, 10);
For example, the code above draws a line from (10,10) to (10, 20). The first moveTo command moves only the start of the line to a new
position at coordinates (20,20).
The second moveTo command moves only the end point of the line to the position where the start of the line used to be.
Curves also have sub-handles for every point in the curve which can be referenced with 'pt[index]'. So for example,
the code below draws a curve with 3 points, and then independently moves the second point (pt[1]) down
and then the first point (pt[0]) to the right.
sh0 = curve("curve", 28.91, 21.66,[[0, 0],[11.09, -10],[20.17, 0.42],],
{open:true, segments:true, color:rgb(56, 87, 35)});
move(sh0.pt[1], down, 20)
move(sh0.pt[0], right, 10)
Scenes and Scene navigation
A program in PlaySkript can be divided into scenes. Each scene starts with the marker
>> "scene_id" , where scene_id is an optional name for the scene.
If your program has only one scene, you do not need the marker at the beginning of the scene.
Each scene can have any of the following: code to draw the scene, function definitions and event
definitions. You must explicitly navigate from one scene to the another using any of the commands below:
next() Jump to the next scene.
prev() Jump to the previous scene.
scene(id) Jump to the scene with id id. The id can be either a number, or the string
"scene_id" given as a name to the scene.
A very useful routine is
defaultHandlers(), which automatically creates navigation handlers so that the right
arrow (or the page down button or swiping right) will take you to the next scene, and the left arrow (or the page up button or swiping left) will take you to the previous scene.
For example, the code below creates a program with two scenes, and uses defaultHandlers(). The code
also uses together blocks to draw multiple objects at the same time that it sets the default handlers.
The code will draw the first scene with a dog and a plate of food, and
when you click on the screen or use the right arrow, it will navigate to
the second scene with the dog sticking its tongue out and the plate of food empty.
together{
defaultHandlers();
shape("rect", 15.5, 8.66,{h:22.67, w:34.75,
color: rgb(255,255,255,0), border: rgb(0,0,0,1)});
draw("bluedog1", 36.75, 20.16, 10.5);
draw("plateFood3", 18.08, 25.41, 5);
}
>>
together{
defaultHandlers();
shape("rect", 15.5, 8.66,{h:22.67, w:34.75,
color: rgb(255,255,255,0), border: rgb(0,0,0,1)});
draw("bluedogTongue", 36.75, 20.16, 10.5);
draw("plateFood1", 18.08, 25.41, 5);
}
Deprecated: Each scene will start out empty, but you can populate a scene with all the elements from the previous scene by using the keep command.
keep() Restores the state from the previous scene in the program. This includes the state of the canvas, as well
as all local variables. However, it does not include any handlers or timers.
keep Should always be the first command in a scene. If the previous scene has not executed, for example because
you jumped to the current scene using the scene command, there will be no state for keep to recover.
The use of keep() is not recommended because it introduces dependencies between scenes which can make it harder to reuse scenes
that use keep in other projects.
Events
PlaySkript supports the following events.
@click Run this code when the user clicks anywhere.
@click handle Run this code when the user clicks on the object with the given handle
@hover_in handle Run this code when the user hovers into the object with the given handle
@hover_out handle Run this code when the user hovers into the object with the given handle
@when test Run this code when the test is true.
@timer t Run this code every t milliseconds.
@key keyname Run this code when the keyname key is pressed. Playskript currently only supports the following keys:
left, right, up, down, space.
Event handlers can either occur at the end of a scene, or in a function. Having event handlers inside a function allows you to add them dynamically to dynamically created objects.
For example, the code below draws a dog and then adds a handler to make it grow larger when it is clicked.
dog = draw("bluedog1", 10, 10, 5);
@click dog
resize(dog, 2, 0.5, 0.5 );
However, now suppose we want to add multiple dogs, each with its own event handler. We can do this by creating the dogs in a function, and adding the event handler there.
function freshDog(x,y){
dog = draw("bluedog1", x, y, 5);
@click dog
resize(dog, 2, 0.5, 0.5 ); // This function multiplies the size by 2.
}
for(i=0; i<5; ++i){
freshDog(i*10, 10);
}
A few things to note about the code above. First, if you put event handlers in a function, they always go at the end of the function. They will have access to any variable defined in the function, and will execute
after the function body, even if the function returns somewhere in the middle of its body.
The event handler can access variables in the function scope even after the function returns. This can be used to maintain state specific to that object. For example, consider the code below.
function freshDog(x,y){
BIG= 1;
SMALL = 0;
state = SMALL;
dog = draw("bluedog1", x, y, 5);
@click dog
if(state == SMALL){
resize(dog, 2, 0.5, 0.5 );
state = BIG;
}else{
resize(dog, 0.5, 0.5, 0.5 );
state = SMALL;
}
wait(500);
}
for(i=0; i<5; ++i){
freshDog(i*10, 10);
}
With that code, every dog will have access to the state variable for the function invocation that created it and will be able to use it to track whether it is small and should be grown
or if it is already large and it should shrink. Also note the
wait at the end of the handler. This is to control the speed
with which the click will react if you leave the mouse button pressed.
You can even add a timer inside a function to animate each of the drawn dogs.
function freshDog(x,y){
BIG= 1;
SMALL = 0;
state = SMALL;
dog = draw("bluedog1", x, y, 5);
@click dog
if(state == SMALL){
resize(dog, 2, 0.5, 0.5 );
state = BIG;
}else{
resize(dog, 0.5, 0.5, 0.5 );
state = SMALL;
}
wait(500);
@timer 100 //rotate by 5 degrees every 100 ms
turn(dog, deg(5), n)
}
One important aspect of click events is occlusion: If two objects have event handlers associated with them
and one is occluding the other, the event will be caught by whichever object is in front. However, if the occluding objecty (the one in front)
does not have an event handler associated with it, then the event will be caught by the object behind, even if that object is not visible.
together{
c = shape("circle", 17.5, 12.58,
{r:2.5, color:'darkred', border:rgb(56, 87, 35)});
r = shape("rect", 9.07, 7.24,{h:37.49, w:55.67, color: rgb(169, 209, 142), border: rgb(56, 87, 35)});
}
@click c
moveForward(c);
For example, in the code above, the circle is hidden behind a large rectangle. Since the rectangle does not have an event handler
of its own, however, clicking in the position of the circle will trigger the circle's event handler which will move the circle forward.
Another important aspect of click events is the notion of a bounding box. For efficiency reasons, the click events are actually
caught by a bounding box around the shape. This means that, for example, a circle can catch a click event even if the click fell
outside the circle, as long as the click is inside the bounding box around the circle. Another important aspect of bounding boxes is that
they do not rotate when you change the angle of the shape. This means that, for example, if you draw a rotated rectangle, the bounding
box will stay where it would have been if the rectangle had not been rotated (this is subject to change in future versions of the language).
Finally, it is very common to want to have event handlers that navigate to the previous or next scene when you press the left or right
arrow keys respectively. PlaySkript has a function
defaultHandlers() that automatically adds these handlers.
defaultHandlers();
write(`Scene 1`, 8.5, 12.33, {size:2});
>>
defaultHandlers();
write(`Scene 2`, 8.5, 12.33, {size:2});
>>
defaultHandlers();
write(`Scene 3`, 8.5, 12.33, {size:2});
>>
defaultHandlers();
write(`Scene 4`, 8.5, 12.33, {size:2});
For example, in the code above, clicking the right arrow from any scene will navigate to the next scene, while clicking the
left arrow will navigate to the previous one. The function also adds a default mouse click handler that navigates to the next scene.
Waiting
We have already seen the
wait(n) command, which forces the script to wait for a fixed number of milliseconds before
continuing with the animation. There are two other additional wait commands that wait for particular events:
waitEvent and
waitAndCheckpoint.
Of these,
waitAndCheckpoint is more useful for things like presentations, where you want to be able to go back to the previous step in an animation,
but
waitEvent offers more flexibility, which can be useful for other applications.
waitEvent((key|handle) waits until a particular event has been issued before continuing with
the execution of the program. In its simplest form,
waitEvent() with no arguments simply waits for any event
before continuing with the execution. For example, the code below will print "Step 1: first step", draw a circle and move
it to the right and then stop and wait for an event. When the user clicks on the canvas or presses any arrow key,
then execution will continue, writing the second message, drawing a second circle and moving it.
write(`Step 1: first step`, 5.33, 3.41, {size:2});
sh0 = shape("circle", 20, 3.16, {r:1.5, color:rgb(169, 209, 142), border:rgb(56, 87, 35)});
move(sh0, right, 20);
waitEvent();
write(`Step 2: second step`, 5.33, 8.66, {size:2});
sh1 = shape("circle", 20, 8.66, {r:1.5, color:rgb(169, 209, 142), border:rgb(56, 87, 35)});
move(sh1, right, 20);
An important aspect of
waitEvent() is that it overrides any event handlers already active.
For example, consider the program below.
function circle(x){
sh0 = shape("circle", x, 3.16, {r:1.5, color:rgb(169, 209, 142), border:rgb(56, 87, 35)});
@click sh0
update(sh0, {color:'red'})
}
for(i=0; i<5; ++i){
circle(5+i*10);
waitEvent();
}
Each circle drawn in the loop has an event handler that turns the circle red when you click on it. However,
when waiting on
waitEvent, clicking on a circle will cause the function to stop waiting, but it will not
cause the circle to change color since the click event will be handled by
waitEvent instead of by the
original handler.
The function
waitEvent can also be made to listen selectively to only certain events.
For example, consider the code below.
function circle(x){
sh0 = shape("circle", x, 3.16, {r:1.5, color:rgb(169, 209, 142), border:rgb(56, 87, 35)});
return sh0;
@click sh0
update(sh0, {color:'red'})
}
for(i=0; i<5; ++i){
c = circle(5+i*10);
waitEvent(c);
}
In this case,
waitEvent is given a handle to an object, so the wait will only be released
by clicks to that object. Clicking anywhere else, or pressing any key will not cause execution to
continue, only clicking on the last circle. This means that clicking on the other circles will
cause them to change color, since those events are no longer caught by
waitEvent.
Alternatively, the code below waits for any click that isn't already handled by the existing handlers.
So clicking on any of the circles causes them to change color, but clicking on the background behind them
releases the wait from the loop.
function circle(x){
sh0 = shape("circle", x, 3.16, {r:1.5, color:rgb(169, 209, 142), border:rgb(56, 87, 35)});
return sh0;
@click sh0
update(sh0, {color:'red'})
}
for(i=0; i<5; ++i){
c = circle(5+i*10);
waitEvent(background());
}
One shortcoming of
waitEvent is that it helps you step forward but not back; in the example above, we saw
that clicking back takes you to the previous scene, but what if you only want to go back to the previous step in the loop?
For that, you can use
waitAndCheckpoint. The
waitAndCheckpoint() function is similar to
waitEvent() with
no parameters, but it also creates a checkpoint of the state of the animation at the point it is invoked. If you
press the back or up keys, instead of moving forward, execution jumps back to the previous checkpoint. If there is no previous
checkpoint, then the event is passed to the original event handler.
defaultHandlers();
write(`Scene 1`, 8.5, 12, {size:2});
>>
defaultHandlers();
write(`Scene 2`, 8.5, 12, {size:2});
waitAndCheckpoint();
for(i=0; i<5; ++i){
shape("circle", 5+i*10, 15, {r:1.5, color:rgb(169, 209, 142), border:rgb(56, 87, 35)});
waitAndCheckpoint();
}
>>
defaultHandlers();
write(`Scene 3`, 8.5, 12, {size:2});
In the code above, scene 2 will write the text to the canvas and wait. Clicking on the screen or
clicking the forward or down arrows will continue the execution to draw the first circle. Doing it
again will draw the next circle and so on. Clicking back or up while the execution is waiting
at a
waitAndCheckpoint will jump to the previous one, but doing it while waiting on the
first
waitAndCheckpoint in the scene will just pass the event through, in this case to the
default handler, which takes you back to the previous scene.
An important aspect of both
waitEvent and
waitAndCheckpoint is that they only override
previous event handlers while the execution is waiting on them. That means if you have a long animation
between them, pressing a key during that animation will simply invoke the previous handlers.
defaultHandlers();
write(`Scene 1`, 8.5, 12, {size:2});
>>
defaultHandlers();
write(`Scene 2`, 8.5, 12, {size:2});
waitAndCheckpoint();
for(i=0; i<5; ++i){
sh0 = shape("circle", 5+i*10, 15, {r:1.5, color:rgb(169, 209, 142), border:rgb(56, 87, 35)});
move(sh0, down, 20);
waitAndCheckpoint();
}
>>
defaultHandlers();
write(`Scene 3`, 8.5, 12, {size:2});
For example, in the code above, if you are on scene 2 and you click the left arrow when the code is
waiting at the
waitAndCheckpoint after drawing a circle and moving it, execution will
revert to the previous checkpoint. However, if you click the left arrow while the circle is moving
but before execution reaches the
waitAndCheckpoint, then the left arrow press will be handled
by the default handler and you will jump to the previous scene.
Another important aspect of checkpoints is that you can pass an additional boolean parameter to
prev,
prev(toLastCheckpoint).
If this argument is true, then when you jump back to the previous scene, you jump to the last checkpoint in that scene. This can be useful
if you have a long scene with multiple checkpoints and you want to jump back to the last checkpoint in the scene, rather than the beginning of the scene.
Arrays and dictionaries
If you have programmed with JavaScript in the past, some of the syntax
in playSkript probably looks familiar. For example, the syntax for loops,
conditionals and function definitions is directly borrowed from JavaScript.
In PlaySkript you can also define arrays and dictionaries using familiar
JavaSkript syntax. However, PlaySkript dictionaries also support some additional
operations as illustrated by the example below. In an earlier example, you saw the
use of the
shape command, which takes as its last parameter a dictionary
of arguments. The code below further illustrates how dictionaries can be used.
attributes = {h:5, w:4, color:rgb(169, 209, 142), border:rgb(56, 87, 35)}
//attributes is a dictionary that maps
// h to the value 5, w to the value 4, and color and border
// to two different colors.
box1 = shape("rect",22,12,attributes);
//I can also update an entry of a dictionary
attributes.w = 10;
box2 = shape("rect",22,22,attributes);
//PlaySkript also lets me join together two dictionaries with +
attributes = attributes + {lineWidth:20}
box3 = shape("rect",22,32,attributes);
wait(1000);
//most transformation commands let me pass them an array of
//handles instead of just a single handle
update([box1, box2, box3], {color:rgb(10, 10, 40)}, 30)
Drawing Arrays of pixels
Another interesting feature of PlaySkript is that you can treat a two dimensional array
of colors as an image, and draw it using the
draw() command. In constructing these
arrays, the array arithmetic can be very useful. For example, the code below will
generate a banded pattern of blue and yellow.
blue = [rgb(0,0,200)];
yellow = [rgb(220,220,0)];
row = (blue*10+yellow*10)*10; //10 blue pixels followed by 10 yellow pixels, ten times.
image = [row]*100; //An image is an array with 100 rows.
draw(image,3,15, 15); //draw the image at pos (3,15) with height 15.
for(i=100; i<120; ++i){ image[50][i] = 'red';}
draw(image,40,15, 15); //draw the modified image
Initially, the blue array has a single element with the color blue (you can also use string
color names in place of rgb values). Multiplying times 10 creates an array with 10 copies of the
color blue. Concatenating with an array of 10 yellows and multiplying by 10 creates an array
of length 200 with alternating sequences of blue and yellow.
The array [row] has a single such row, but multiplying by 100 creates an array with 100 rows
of pixels which will be drawn as an image. Note that the multiplication operations
clone the arrays (but not other kinds of objects inside the arrays), so it is possible to change
the color in an individual row to create an image with a small red line in the middle.
These computed images can also be used with the
background command.
Functions and variable scoping
One big difference between PlaySkript and JavaScript is variable scoping.
In particular, variables are never defined in PlaySkript.
All variables are local in scope, so for example in the code below, the x inside foo is a different variable from the x outside.
x = 1;
function foo(){
x = 3;
write("inner x=" + x, 39.33, 30.5, 5);
}
foo();
write("outer x=" + x, 39.33, 10.5, 5);
So the code will print "inner x=3" and "outer x = 1". Each scene also introduces its own fresh scope, so a value stored in x in one scene will not be available
in the next scene.
However, if a variable has not been initialized inside a function, it will have as its initial value the last value that it had outside the
function before the function was called.
function foo(){
write("inner1 x=" + x, 39.33, 20.5, 5);
x = 3;
write("inner2 x=" + x, 39.33, 30.5, 5);
}
x = 1;
foo();
write("outer x=" + x, 39.33, 10.5, 5);
So for example, the code above will print "inner1 x=1" "inner2 x=3",
and "outer x=1", because when the first write executes, the variable
x inside the function has not been initialized inside the function,
so it takes as its initial value the value of x from the outer scope.
This behavior is very useful for functions defined inside other functions, but it is not a good
idea to rely on this behavior for functions defined in a scene, because such functions may be
called by other projects that import this project.
There is, however, a global dictionary called "global", so if there is any data that you want to persist across multiple scopes, you cana store it there.
global.x = 1;
function foo(){
global.x = 3;
write("global.x=" + global.x, 39.33, 30.5, 5);
}
foo();
write("global.x=" + global.x, 39.33, 10.5, 5);
So for example, the code above will print "global.x=3" both times. The global dictionary persists even from one scene to the other.
In general, it is a bad idea to use globals, because they can create dependencies between scenes that then makes it difficult
to reuse scenes across projects. If you are tempted to use globals to store constants that will be used in many places in your project,
it is much better to use
Styles.
Lambdas, Implicit Lambdas and Composition
PlaySkript supports anonymous functions (lambdas) similar to JavaScript. The notation (params)=>expression
defines a function that takes as input the list of parameters given in params and returns the value of the expression.
These anonymous functions are especially useful in combination with array methods such as map and filter.
For example, consider the code below:
function f(x){
shape("circle", x, 10, {r:2.5})
}
lst = [10, 15, 18, 25];
for(i=0; i<lst.length; ++i){
f(lst[i]);
}
The code can be expressed much more succinctly using lambdas and map:
[10, 15, 18, 25].map((x)=>shape("circle", x, 10, {r:2.5}))
In addition to these JavaScript-style lambdas, PlaySkript also supports the creation of
Implicit lambdas.
For any function, you can replace any parameter or set of parameters with '_' (underscore), and that has the effect
of creating a lambda implicitly. So for example, given a function of two parameters
f(x,y), you can write
f(_, 7) and that is equivalent to writing
(x)=>f(x,7). Or if you have a function
g(x,y,z),
you can write
g(3,_,_) and that's equivalent to writing
(x,y)=>g(3,x,y). Using this shorthand, you
can write the example above even more succinctly as:
[10, 15, 18, 25].map(shape("circle", _, 10, {r:2.5}))
PlaySkript also introduces a new composition operator
x -> f such that
x -> f is equivalent to
f(x).
The operator is left associative, so that
x -> f -> g -> h is equivalent to
((x->f)->g)->h which in turn is
equivalent to
h(g(f(x))). Using this operator, it is possible to write code like the one below:
[10, 15, 18, 25].map(shape("circle", _, 10, {r:2.5}))
->
update(_, {r:10}, 20)
->
update(_, {color:'darkred'});
together, sequential and canvas
The together construct. Commands execute sequentially one after another, with a small delay in between them. This means that,
for example, if you draw 10 circles in a row in a loop, you will see them appear one after another. Moreover, some commands such as move,
update and turn can be made to execute over multiple timesteps. So for example, the code below will draw circles one at a time and then move them down
one at a time:
lst = [];
for(i=0; i<10; ++i){
x=shape("circle", 7.58+5*i, 6.58, {r:2.5});
lst.push(x);
}
for(i in lst){
move(lst[i], down, 10);
}
The example above also illustrates the use of arrays to keep track of a set of handles,
as well as the
for( in ) loop to iterate over the indices in an array.
In some cases, however, we may want many things to happen all at once. We can do this by using the
together construct. The together block will
execute sequentially, but all the actions in the block will be executed in tandem, so for example, in the code below,
all the circles will appear together, and then they will all move down together in tandem.
lst = [];
together{
for(i=0; i<10; ++i){
x=shape("circle", 7.58+5*i, 6.58, {r:2.5});
lst.push(x);
}
}
together{
for(i in lst){
move(lst[i], down, 10);
}
}
We can further simplify the code above by taking advantage of the fact that together also returns an array of all the handles
for all the objects created within its scope, so there is no need to populate the lst array ourselves.
lst = together{
for(i=0; i<10; ++i){
shape("circle", 7.58+5*i, 6.58, {r:2.5});
}
}
together{
for(i in lst){
move(lst[i], down, 10);
}
}
Sometimes, we may want to sequentially execute a set of commands within a together block. To do this, there is an analogous construct called
sequential that
executes commands sequentially, and whose main use is inside a together block.
For example, consider the code below; we have a function that sequentially draws a line of circles starting at coordinate x and y.
If we want to draw two lines of circles, one below the other, we can invoke the function twice, but that would first draw the first line,
and then the second line. If we wrap the two calls in a together block, that will draw all the circles in the two lines all at once.
However, if we want to draw the circles in each line sequentially, we can wrap each call to the function in a sequential block.
This causes the two lines to be drawn together, but each line is drawn sequentially.
function circleLine(x,y, color){
for(i=0; i<10; ++i){
shape("circle", x+5*i,y, {r:2.5, color:color});
wait(50);
}
}
together{
sequential{
circleLine(7.58, 6.58, 'darkred')
}
sequential{
circleLine(7.58, 16.58, 'purple')
}
}
It is important to note that together does not actually introduce parallelism. All the commands in a together block are executed sequentially, but they are
rendered in tandem.
For example, in the code below has a sequential dependency because the position of each square depends on the width of the squares before it,
but they will all be rendered together in one step.
c = shape("rect", 10,10, {h:2.5, w:2.5,
color:rgb(105,105,105,1), border:rgb(0,0,0,1)});
x = getX(c, 3);
together{
for(i=1; i<10; ++i){
r = Math.random()*4+1;
c = shape("rect", x,10, {h:r, w:r,
color:rgb(105,105,105,1), border:rgb(0,0,0,1)});
x = getX(c, 3);
}
}
However, you do have to be careful not to have inconsistent actions happen together on the same object.
Inonsistent actions are actions that have mutually inconsistent effects on the same object. This is because
code executes sequentially and then the effects on the screen are displayed in parallel, but this can create problems when
the effects of the sequential execution are inconsistent with applying the effects to the screen in paralle.
c = shape("rect", 10,10, {h:2.5, w:2.5,
color:rgb(105,105,105,1), border:rgb(0,0,0,1)});
together{
moveTo(c, 70, 10, 100);
moveTo(c, 30, 40, 50);
}
x = getX(c);
y = getY(c);
alert(x+", "+y);
If you run the code above, you will get an error message about inconsistent values. This is because
the sequential semantics means that the second moveTo will determine the result of getX and getY, so the
alert will show "30, 40", but when the effect of the two moveTo commands is rendered in parallel, the square will end up at (70,10),
because the second moveTo finishes later than the first one. This should not be a problem as long as you avoid running inconsistent
moveTo commands on the same object in a together block.
canvas(name, width, height){ body } . This construct creates a hidden canvas of a given width and height.
Any commands inside the body will be drawn to this hidden canvas, rather than to the scene. So for example,
the code below will draw a complex curve to a hidden canvas. The name of the canvas can later be used as an image name.
canvas("spiral", 12, 8){
curve("curve",4.16,2.49,
[[1.82,2.74], [2.34,0.71],
[3.94,3.16], [0.39,3.29],
[2.17,-0.85], [5.63,3.03],
[-0.22,4.61]],
{open:true, border:rgb(56, 87, 35)});
}
The code above will have no visible effect on the scene, but now we can draw a "spiral" as if it were any other image, for example, we can draw spirals of different sizes in a loop.
for(i=0; i<5; ++i){
draw("spiral",11.14+8*i,10.49, 3+i);
}
Just like with other images, we can use the handles provided by the draw command to manipulate the spirals.
spiral1= draw("spiral",11.14,10.49, 3);
spiral2= draw("spiral",21.14,10.49, 3);
@click spiral1
move(spiral1, up);
@click spiral2
move(spiral2, down);
Scene Navigation Inside Canvas
You can invoke any of the scene navigation commands inside the canvas. The
effect will be to clear the canvas and replace its content with the content
of the invoked scene. For example, consider the program below.
>> 'Scene1'
background('lightblue');
write('This is scene 1', 5, 5, 3);
>> 'Scene2'
canvas('other', 80, 50){
write('Will you see this?', 5, 8, 3);
prev();
write('This will not execute', 5, 8, 3);
}
draw('other', 10, 10, 20);
The code in the canvas first writes the text 'Will you see this?', but
right after that, it calls the scene navigation function prev().
Just as when prev is called from the main body of the scene, the effect of prev
is to clear the canvas and jump to the previous scene—in this case 'Scene1'—
and never to jump back. So the result is a canvas that has a blue background and
the text 'This is scene 1'.
So at a high-level, the effect of navigating between scenes in the canvas is the same as
outside the canvas, except everything is confined to the canvas. For a more complicated example,
consider the code below.
>> 'Scene1'
background('lightblue');
write('This is scene 1', 5, 5, 3);
next();
>> 'Scene1.5'
keep();
write('Continued in scene 2', 5, 9, 3);
>> 'Scene2'
canvas('other', 80, 50){
scene('Scene1')
}
draw('other', 10, 10, 20);
Now, 'Scene1' automatically jumps to the next scene, which is now 'Scene1.5', which uses
keep() to keep the contents of 'Scene1'. Thus, the canvas in scene 2 will end up with the
two messages from both 'Scene1' and 'Scene1.5'.
Screen Dimensions
The units for the coordinates in the screen are always going to be scaled so that the screen is exactly 50 units height. So if your window has an
aspect ratio of 3:2, then your screen will be 75 units wide. You can use the
screenWidth function to figure
out how wide your screen is.
w = screenWidth();
write("Rightmost ->", w, 10, 5, {centx:1.0});
For example, the code above writes the screen width to variable w, and then writes the text at exactly position w.
Note that the
centx:1.0 parameter means that the position will correspond to the rightmost edge of the text.
This ensures that the text "Rightmost ->" will be flush with the right edge of the screen regardless of how wide
your window is.
Now, for some applications, it is desirable to fix the screen width. For example, if you are creating slides for a presentation,
you want to fix the width of the slides so they display the same way regardless of the window size. You can also use the
screenWidth function to do this. If you pass a width to screenWidth, it will rescale the window to have that new width
(remember, the height will always stay fixed to 50).
screenWidth(60);
//The screen will now be rescaled and possibly padded to have a width of 60.
write("Rightmost ->", 60, 10, 5, {centx:1.0});
If you want the screen to go back to a flexible width that fills the entire screen, you can just call
screenWidth(false).
screenWidth in canvas. A canvas in many ways behaves like a mini-screen whose initial width and height are given by the
parameters passed when creating the canvas. For example, in the code below,
screenWidth() will return 30, because
that is the width of the canvas. When the canvas is drawn, the string will be flush with the right edge of the canvas.
canvas("test", 30, 30){
background('lightblue')
w = screenWidth();
write(new(), "w:"+w+" ->", w, 10, 5, {centx:1.0});
}
draw("test", 10, 10, 15);
It is also possible to rescale the canvas by calling
screenWidth with a numerical parameter. Just like when you rescale the
screen, the height of the canvas will remain unchanged after rescaling.
For example, the code below rescales the canvas to have a screenWidth of 60. Because the original height of the canvas was
30, it will remain 30 after rescaling. Just like when you rescale the screen, the canvas will have two black areas at the top
and at the bottom to make up for the fact that we are now forcing a 2:1 aspect ratio on a square canvas. Notice also the use
of
centx and
centy to align the text at each of the four corners.
canvas("test", 30, 30){
background('lightblue')
w = screenWidth(60);
write(new(), "w:"+w+" ->", w, 10, 5, {centx:1.0});
write(new(), "X", 0, 0, 5, {centx:0, centy:0});
write(new(), "X", 60, 0, 5, {centx:1.0, centy:0});
write(new(), "X", 0, 30, 5, {centx:0, centy:1.0});
write(new(), "X", 60, 30, 5, {centx:1.0, centy:1.0});
}
draw("test", 10, 10, 15);
Importing from Published Projects
You can import from a published project by going to the import window
You can follow the instructions in this window to import projects that you want to
use from within your current project. Once a project is imported, you will assign it a name,
which will be used in the code to access functionality from the project.
For example, suppose you create a project with the following code.
>> 'Scene1'
function fancy(txt, x,y){
return write(txt, x,y, 10, {color:'darkred', font:'Bitter'});
}
x = fancy('This is scene 1', 10, 10);
@click x
update(x, {color:'darkblue'});
@click
next();
>> 'Scene2'
fancy('This is scene 2', 10, 20);
@click
next();
>> 'Scene3'
keep();
fancy('This is scene 3', 10, 30);
@click
scene('Scene1');
Now, if you publish the project, you can reuse this functionality from another project.
For example. Suppose that you import this project with the name 'theother' into a new
project that we will call 'Importer'.
Now, you can use all the functionality from the original project in the Importer project.
For example, you can invoke the 'fancy' function to write text into your new project.
theother.fancy('My fancy text', 1,1);
You can also reuse scenes from a project. For example, if you write the code below,
your new project will have a scene called 'S1' that will have all
the contents from the scene 'Scene1' in the imported project, in addition to
a black circle. The call to 'theother.scene' even adds the event handlers from
the scene in the imported project. So for example, if you click on the text, the
text will change color from red to blue.
>> 'S1'
theother.scene('Scene1');
shape('circle', 20, 15, {r:3, color:'black'})
Recall that in the imported project, Scene1 had an event handler that if you clicked
on the scene, it would transfer you to the next scene. When you invoke a scene from the
imported project, however, all scene navigation commands in the imported
scene—next,prev and scene—will simply stop the execution of the imported
scene, but will not transfer control unless you provide a
scenemap.
The reason for this is that a scene in the Importer project can only transfer control
to another scene in the importer project, so if you want the scene navigation commands in the
theother project to have an effect, you need to map scenes in that project to scenes in the
Importer project.
>> 'S1'
theother.scene('Scene1', {scenemap:{'.next':'S2'}});
shape('circle', 20, 15, {r:3, color:'black'})
>> 'S2'
theother.fancy('The new scene 2!', 1,1);
For example, in the code above, we call 'Scene1' from theother with a scenemap that
tells playskript that if the code tries to navigate to the next scene from 'Scene1',
it should actually navigate to scene 'S2' in the Importer project. Thus, scene 'S1'
will display like 'Scene1' in the imported project, but clicking on the screen
will take us to scene 'S2'.
As you may suspect, scenes that start with keep require some additional care, because
they are design to run after the previous scene and to use the variables and scene elements
defined in that previous scene. Thus, if you want to display such scenes, you need to pass
this context to the scene function as shown below.
>> 'S1'
state = theother.scene('Scene2', {scenemap:{'.next.':'S2'}});
>> 'S2'
keep()
theother.scene('Scene3', {state:state, scenemap:{'Scene1':'S1'}});
For example, consider the code above. In this case, after displaying the scene 'Scene2',
the scene function returns a context which contains all the information that will
be needed by 'Scene3' to execute properly. In the example, we store the context in
a local variable and then use keep() to ensure it can be accessed from scene 'S2' which will
render 'Scene3'. Also, recall that 'Scene3' in the imported project switched to 'Scene1'
when the user clicked on the screen, so we map 'Scene1' to scene 'S1' so that when the user
clicks, control will be transfered to scene 'S1'.
There are situations when we want to import a large set of scenes from a project. PlaySkript
supports bulk import of scenes from another project with the syntax illustrated below.
>> [theother, 0, 2]
The notation above performs a bulk import of scenes 0, 1 and 2 from project 'theother'. PlaySkript will
automatically identify any scenes that use keep() and thread the state to ensure correct behavior.
The first argument should be the name under which the project was imported, and the second and third argument
must be integers representing the range of scenes to be included (the range is inclusive of its endpoints).
Styles
Every project in playskript allows you to define a set of styles that can be passed as
attributes to the different drawing commands. To access the project's styles,
you must first go to the project information window through the toolbar button
,
And then inside the project information, select the Styles interface. Each style has a name
and a list of attributes and their values. For example, suppose I define a style as shown in
the figure below:
The style has three attributes, a color, a border color, and a rounded corner radius.
Because I have marked the check to make it 'default rectangle', then if I use the drag and
drop functionality to draw a rectangle, the code will automatically use this style as
its attributes. So for example, if I drag and drop a rectangle into the canvas at position 10, 10,
I will get the following code.
shape("rect", 10, 10, {h:5, w:4} + Style.rect);
All the styles declared through the style window can be accessed through the global
variable Style. So in this case, the generated code uses Style.rect.
This style can be used anywhere in the project, just like any other dictionary, although
it is an error to attempt to modify it from within the code.
Using styles has a number of advantages over directly defining attribute dictionaries
within the code. First, they give you a centralized place to control the look of your
project. By just changing your style definition in one place, you can globally change
how different elements in your presentation are displayed. In principle, you could achieve
this by storing the styles in the global dictionary, but this becomes problematic once you want
to import scenes from one project into another, because you have to make sure the new project
also initializes the styles in the global dictionary in the same way. With styles, you can
even control which styles can be overriden by the importing project and which styles are
fixed in the project. This is done by setting the 'Final' flag in the style window. If
this flag is not set, a project that imports this project can override this style by simply
defining a style with the same name. However, if the flag is set, the style will be fixed
irrespective of what the importing project has defined.