Scenes: A Program in Several Parts

A program can be made of several scenes: a title screen, the game and a game-over screen, or the parts of a show that play one after another. Each scene has its own setup, update, draw and on functions, and starts empty. This page shows two screens of a game, then scenes on a timer, then the transitions between them, then the scrubber.

A file with no scenes in it is one scene, as it always was. Nothing on this page changes it.

Scenes are set up as Pymations scenes are: a class for each, every one playing in the order written. If you know Pymations says what is the same and what is different.

Scenes cost nothing in a file that does not use them. They are a part of pictures.js of their own, fetched only by a file that has a scene in it. A file with none runs exactly as before and fetches nothing more.

Two screens

A scene is a class that extends Scene. Inside it go the same functions a file has, without the word function.

class Title extends Scene {
  draw() {
    background("midnightblue")
    text("Click to start", width / 2, height / 2, "white", 30, "centre")
  }
  onMouseDown() {
    go("Game")
  }
}

class Game extends Scene {
  setup() {
    this.x = 0
  }
  update(dt) {
    this.x = (this.x + 200 * dt) % width
  }
  draw() {
    background("black")
    circle(this.x, height / 2, 20, "gold")
  }
  onMouseDown() {
    go("Title")
  }
}

You do not create a scene or call anything. pictures.js finds every Scene class in the file. The first one written starts, and go("Game") changes to another by its name. go(Game), with the class itself, does the same.

Three things to know:

  • A scene's own variables go on this. this.x belongs to the scene. Set them in setup().
  • setup() is called every time the scene begins, so each game starts afresh.
  • Only the scene on show is called. While Title is on show, Game's update, draw and onMouseDown are not called.

What is shared and what is a scene's own

Anything made at the top level of the file, outside every class, is shared by all the scenes: the score, a synth, a picture that was loaded.

let score = 0     // shared: Game adds to it and GameOver shows it

Each scene starts empty:

A scene's ownWhat happens when the scene ends
Its variables on thisForgotten. The next time it begins they are as you wrote them
On-screen controls it made in setup() (dial, slider, button…)Taken away. Controls made at the top level of the file stay
The pictureThe next scene starts with a clear canvas and the drawing settings (ctx.fillStyle, ctx.font…) as they start

Sound is not a scene's own. A song or a tune that is playing goes on playing across scenes until something stops it (stopSound(), song.stop()). To stop it with a scene, do so in the scene's onStop(), which is called when the scene ends.

The file's own top-level functions still work. A function onKeyDown(key) outside every class is called for every key, before the scene's own, whichever scene is on show: a good place for a key that mutes the sound. A top-level draw() is drawn before the scene's.

Going between scenes

CallWhat it does
go("Game"), go(Game)Change to that scene. Capital letters in the name do not matter
nextScene()Change to the next scene written. After the last one comes the first
restart()Begin this scene again, from empty
go("Menu", { keep: true })Go back to a scene as it was left: see below

The change happens at the start of the next frame, so the function that asked for it runs to its end.

Keep and resume

go("Menu", { keep: true }) goes back to a scene with its variables as they were when it was left. Its setup() is not called again. If the scene has a resume(), that is called in its place.

Use it for a pause screen: the game goes to "Pause", and the pause screen goes back with go("Game", { keep: true }).

class Game extends Scene {
  setup() {
    this.x = 0
    this.speed = dial("speed", 50, 400, 200)
  }
  resume() {
    this.speed = dial("speed", 50, 400, 200)     // controls are not kept: make them again here
  }
  update(dt) {
    this.x = (this.x + this.speed.value * dt) % width
  }
  draw() {
    background("black")
    circle(this.x, height / 2, 20, "gold")
  }
  onKeyDown(key) {
    if (key === "p") go("Pause")
  }
}

class Pause extends Scene {
  draw() {
    background("midnightblue")
    text("Paused. Click to carry on", width / 2, height / 2, "white", 24, "centre")
  }
  onMouseDown() {
    go("Game", { keep: true })
  }
}

On-screen controls a scene made are not kept. They are taken away when the scene ends, kept or not, so a scene that is come back to makes them again in resume(). Its variables on this are as they were.

Without classes

If you are not ready for classes, scene groups plain functions under a name.

function drawTitle() {
  background("midnightblue")
  text("Click to start", width / 2, height / 2, "white", 30, "centre")
}

function drawGame() {
  background("black")
  circle(mouse.x, mouse.y, 20, "gold")
}

scene("title", { draw: drawTitle, onMouseDown: () => go("game") })
scene("game", { draw: drawGame, onMouseDown: () => go("title") })

The names inside the braces are the ones a class would use: setup, update, draw, onMouseDown, onKeyDown and the rest, and the settings below (length, transition). Scenes made this way keep their variables at the top level of the file, so they are not emptied when a scene begins: set them in the scene's setup.

Scenes on a timer

Give a scene a length in seconds and it ends by itself. The next scene written begins, and after the last one the first comes round again.

class Intro extends Scene {
  length = 4
  draw() {
    background("navy")
    text("Part one", width / 2, height / 2, "white", 40, "centre")
  }
}

class Main extends Scene {
  length = 8
  draw() {
    background("black")
    circle(width * sceneProgress, height / 2, 30, "tomato")
  }
}
NameWhat it is
sceneTimeSeconds since this scene began
sceneProgressHow far through its length the scene is, from 0 to 1. It is 0 in a scene with no length
sceneNameThe name of the scene on show

update(dt) works as it always did.

Lengths in beats

A scene that goes with music can be so many beats long, or so many bars. A bar is four beats.

setTempo(120)

class Verse extends Scene {
  bars = 4        // 16 beats: 8 seconds at 120 beats a minute
}

class Break extends Scene {
  beats = 8
}

The beats are the sound library's (setTempo, beat()), so a scene ends on the beat however the frames fall. In a program with no sound a beat is half a second. See Sound and music.

To end a scene when a song reaches a place of its own, ask the song in update and call nextScene():

const song = playSong("tune.mod")

class Opening extends Scene {
  update() {
    if (song.position.order >= 2) nextScene()
  }
}

Settings for every scene

config, at the top of the file, sets things for every scene.

config({ loop: false, transition: "crossfade", transitionTime: 0.8 })
SettingStarts asWhat it is
looptrueAfter the last scene, begin the first again. With false the last scene stays on show
transition"cut"How every scene arrives, unless it says otherwise
transitionTime0.5How long a transition takes, in seconds
transitionColor"black"The colour a "fade" goes through
beatsPerBar4How many beats bars = 1 is

Transitions

A scene can arrive with a transition. Set it on the scene:

class Main extends Scene {
  transition = "crossfade"
  transitionTime = 0.8
}

or say it when you change scene:

go("Game", { transition: "wipe", transitionTime: 0.5 })
TransitionWhat you see
"cut"The new scene at once. This is what happens when nothing is said. "none" is the same
"crossfade"The old scene fades into the new one
"fade"The old scene fades to a colour, and the new one comes out of it. transitionColor = "white" changes the colour
"wipe"An edge crosses from the left with the new scene behind it. "wipe_left", "wipe_up" and "wipe_down" cross the other ways
"slide_left"The old scene slides out to the left and the new one follows it in. Also "slide_right", "slide_up", "slide_down"

During a transition the old scene is a still picture of its last frame. The new scene has already begun: its setup() has been called and its update and draw are running underneath. A transition starts and ends gently.

A recording of the program has its transitions in it.

The first scene of a program arrives with no transition.

A 3D program has the same transitions. A file is 2D or 3D as a whole, so all its scenes are the one or the other.

Sections and seeking: the scrubber

When every scene has a length, the program is a sequence with a length of its own, and it has a scrubber. Move the pointer over the picture to see it.

  • The button pauses and plays.
  • The track has a tick where each scene starts. Point at a tick for the scene's name.
  • Press or drag on the track to go to that time. The scene there is set up, and sceneTime is already at that point in it.

section("name") adds a tick of your own at the moment it is called:

class Main extends Scene {
  length = 20
  update() {
    if (sceneTime > 12) section("The chorus")
  }
}

A program that plays a song can move the song when the scrubber moves, with a top-level onSeek. A song is moved by its orders, so the program says which order that time is:

function onSeek(seconds) {
  song.seek(Math.floor(seconds / 8))    // in this song an order lasts 8 seconds
}

A program with a scene that has no length has no scrubber: its length is not known.

Mistakes

A mistake in a scene says which scene, and its line:

Line 7: In the scene Menu: go("nowhere"): there is no scene called "nowhere". The scenes are: Menu, Game

Things to know:

  • Make things in setup(), not in a constructor. pictures.js makes each scene once before the program starts, to read its length and transition.
  • Every class that extends Scene plays, in the order written, including one that extends another scene.
  • Name scenes at the top level. scene("name", …) inside a function that is called later is too late.

If you know Pymations

Pymations has scenes too, and the words here are the same wherever the thing is the same.

The same
class Intro extends SceneA scene is a class. Every Scene class in the file plays, in the order written, each starting empty
setup()Called when the scene begins
section("name")A tick on the scrubber
transition = "crossfade", transitionTime = 0.5On a scene. Pymations writes transition_time, as Python does; that spelling works here too
"crossfade", "slide_left", "slide_right", "wipe", "none"The same transitions, drawn the same way
config({ transition: "crossfade" })For every scene
The scrubberPlay and pause, a tick where each scene starts, the time
DifferentWhy
setup, update(dt), draw() in place of construct()A Pymations scene describes a timeline once. A scene here runs every frame, for as long as it is on show
go(), nextScene(), restart()A Pymations scene ends when its animations do. Here the program decides, or the scene has a length
length, beats, barsA Pymations scene is as long as what it plays
The last scene is followed by the firstA Pymations timeline stops at its end. config({ loop: false }) does that here
"cut", "fade", the other wipes and slidesAdded here. "none" still works
section() takes a name onlyA transition here belongs to a change of scene