Tips and Limits

A few things that are good to know once your programs grow: what keeps them fast, what happens to a loop that never waits, and why a drawing can disappear.

Reads are free

Your code calls the canvas directly, so a value comes back at once: the width of some text, where a mesh is, what a ray hit. There is no await on a read and no frame lost to it, so read whenever it is useful, every frame if you like.

  • ctx.measureText(text).width, mesh.position.x and hits.length are ordinary numbers. Use them in sums and in if.
  • await is only for time and files: frame(), flush(), sleep(seconds), loadText(path) and loadJSON(path). await vsync() is another name for await frame().
  • Reading pixels with ctx.getImageData is the one read that is slow, because it copies every pixel. Keep it for small areas or one-off effects.

Canvas frozen? A loop must wait for the next frame

The browser draws between frames, and a scene runs on the same thread as the rest of Circuitry. A loop that runs forever without waiting never gives the browser the chance, and while it runs nothing answers: the preview, the editor, the buttons.

// This loop never waits. It is stopped after four seconds.
let x = 0
while (true) {
  x += 1
  ctx.fillRect(x % width, 50, 10, 10)
}

Put await frame() at the end of the loop, and each pass becomes one frame:

let x = 0
while (true) {
  ctx.fillStyle = "white"
  ctx.fillRect(0, 0, width, height)
  ctx.fillStyle = "black"
  ctx.fillRect(x % width, 50, 10, 10)
  x += 2
  await frame()
}

Or use update() and draw(), which can never hold things up this way.

If a loop does get stuck, Circuitry stops answering for about four seconds. Then the loop is stopped, and the preview shows an error with the loop's line:

Line 3: A loop ran for 4 seconds without waiting, so it was stopped. Put `await frame()` inside it, or draw in draw()   [Run again]

Add the missing await frame() and the scene runs again. The preview runs your code about half a second after you stop typing, so this can happen while a loop is half written: wait the four seconds and carry on.

One kind of loop is not checked: a loop written without braces, such as while (busy) step(). Give every loop its { }.

Starting and stopping

A .js file that Circuitry takes for a scene starts by itself when its tab comes on screen. There is nothing to press.

  • Run in the footer starts it from the top, with the code as it is now. While it is running, the same button is a red Stop.
  • Restart the preview, beside it, starts the scene over in one press: a fresh preview, the code exactly as it is now, from its first line. Use it to begin a game again. Run after Stop does the same.
  • Typing starts it over. About half a second after you stop typing, the scene runs again from the top with the new code. A half-typed line leaves the running picture alone; if the code is still not right after four seconds, the mistake is shown. After you press Stop, typing does not start it: press Run.
  • Only the tab you are looking at runs. Switch to another tab and the scene there waits: it draws no frames and keeps its last picture, so it does not use your device's power in the background. Come back and it carries on from where it was.
  • After Stop, the picture stays as it was, and a small pause symbol shows in the top right corner of the preview. It goes when you press Run or Restart. If the frame rate is showing, its last reading stays too, dimmed, so you can still read it.

Making it wait for Run

For a program that does a lot of work, or one you do not want to start until you are ready, put this line near the top of the file:

// pictures: manual
function draw() {
  background("navy")
}

The preview then shows "Run to draw here" and waits for Run and typing no longer starts the scene over. Run, Stop and Restart work as usual. The line must be a comment on a line of its own, within the first 30 lines; capitals and spaces do not matter. // pictures.js: manual works too. Take the line out and the file starts by itself again.

When the preview changes size

The canvas follows the size of the preview. It changes size when you drag the split, rotate a tablet or turn a phone, and when you switch views.

  • A drawing made once, at the top level, is lost: a canvas is wiped when it changes size. Press Run to draw it at the new size.
  • A program with draw() redraws every frame using the new width and height.
  • A resize also resets the colours, font, line width and every other setting of ctx. Set them in draw(), next to what they style.
  • 3D scenes are not refitted for you. Put these three lines at the top of draw() and the picture follows the preview:
camera.aspect = width / height
camera.updateProjectionMatrix()
renderer.setSize(width, height, false)

Paint your own background

The canvas starts see-through, and what is behind it shows: black in the preview, and whatever the page has on a web page of your own. Start draw() by painting your own background (ctx.fillRect(0, 0, width, height), or scene.background in 3D), so your picture looks the way you meant it everywhere.

Fit any screen

The preview can be a tall, narrow phone or a wide desktop pane. Work out positions from width and height (width / 2 for the middle, height - 50 for near the bottom) rather than fixed numbers, and read them inside draw() and update() so they follow a resize.

2D or 3D, not both

A canvas is either 2D or 3D. A scene is 3D when its code uses THREE. or addons., and then it has no ctx. Two things follow:

  • A 2D scene must not use THREE. in its code. A comment or a string that says THREE. does not count. If ctx is suddenly undefined, look for a line that uses it.
  • To put 2D drawing into a 3D scene, such as a label or a score, draw it on a small canvas of your own and use that as a texture (3D functions: textures).

Speed

  • Most devices draw 60 frames a second, and some 120. To see yours, turn on Frame rate in the footer (Finding mistakes).
  • Move things by speed * dt in update(dt), so they move at the same speed whatever the frame rate, and in a recorded video.
  • Thousands of shapes a frame are fine. For tens of thousands, draw fewer, bigger things, or join lines into one path and stroke it once. In 3D, one Points object with thousands of positions is far quicker than thousands of separate meshes.
  • Do heavy work once, at the start, and keep the results.
  • dt is never more than 0.1, so one slow frame does not make things jump.

Keyboard focus

Key presses go to the scene only after you click or tap the preview. Until then they go to your code. If the arrow keys do nothing, click the preview first.

A key's name is what the key types, so with Shift held the A key is "A", not "a". A game that uses letter keys can check both: keys.has("a") || keys.has("A"). Or ask for the key by its place on the keyboard, which Shift and the keyboard's language do not change: keys.codes.has("KeyA").

To ask whether Shift, Control, Alt or Command is held, read keys.shift, keys.ctrl, keys.alt and keys.meta.

For something that should happen once for each press, such as a jump or a shot, use pressed.has(" ") in update, or the function onKeyDown. keys.has(" ") stays true for as long as the key is held. See Once for each press.

Fingers, the wheel and zooming

  • More than one finger. mouse follows the first finger only. touches has every finger that is down, and a pressed mouse button too. See Every finger.
  • Zoom. Multiply by pinch.change every frame for two fingers or a trackpad, and read mouse.wheel for the mouse wheel. See Zooming.
  • The wheel is the scene's only when it names it. A scene that reads mouse.wheel or defines onWheel keeps the wheel while the pointer is over it. This matters on a web page of your own, where the wheel otherwise scrolls the page.
  • Movement since the last frame. mouse.dx, mouse.dy, mouse.wheel, pinch.change, pressed and released are set at the start of a frame and are back to nothing at the start of the next. Read them every frame, in update or in your loop.
  • You do not need addEventListener for these. A listener of your own still works, for anything this list does not cover.

Files and libraries

A scene is a plain script, not a module, so an import line at the top is an error. What a scene has is what the list in How a program is shaped gives it, and everything the browser has.

  • A sprite's picture can be an emoji, which needs no file at all, and loadImage takes a data: address without asking. A picture from a website is a remote call: Circuitry asks first (below).
  • await loadFont("Orbitron", "fonts/Orbitron.woff2") loads a font file beside the scene, ready to use by its name in ctx.font. If the font cannot be loaded, the text is drawn in the next font you name.
  • await use("helpers.js") loads another .js file beside the scene and hands back what it exports, so a long scene can keep its parts apart.
  • A file beside the scene is read by its path from the scene's file: loadImage("cat.png"), await loadText("words.txt"), await loadJSON("level.json"). Save the scene's file first, so that it has a folder. Write each path out in full, in quotes, as these are: in the desktop app and on a Circuit, Circuitry finds the files to hand to the preview by reading the paths in your code, so a path built while the program runs is not found there.
  • A texture in 3D loads the same way: new THREE.TextureLoader().load("textures/brick.png"), with the path written out in quotes.
  • A text or JSON file that is not found stops the scene with an error that names it. A picture that is not found is simply never drawn.
  • A scene cannot save a file into your project. To keep something between runs, such as a high score, use localStorage (below).
  • Of three.js, the core and addons.OrbitControls are there. Its other add-ons are not loaded.
  • npm packages are not available to a scene, and the code editor's npm button is not shown on one. See Node Packages (npm).

The rest of JavaScript

A scene is ordinary JavaScript in a page of its own, so what a web page's JavaScript has is there: Date, Math, Web Audio, localStorage, and document.createElement("canvas") with its own getContext("2d") for drawing off screen. What it can reach outside that page is limited: see What a scene can reach.

This scene keeps a high score in localStorage, so it is still there the next time you open the file:

let score = 0
let best = Number(localStorage.getItem("best") || 0)

function onMouseDown() {
  score += 1
  if (score > best) {
    best = score
    localStorage.setItem("best", String(best))
  }
}

function draw() {
  ctx.fillStyle = "#14213d"
  ctx.fillRect(0, 0, width, height)
  ctx.fillStyle = "white"
  ctx.font = "24px sans-serif"
  ctx.fillText(`Score ${score}   Best ${best}`, 20, 40)
  ctx.fillText("Click or tap to score", 20, 80)
}

localStorage keeps text, so a number is turned back into one with Number(...).

In Circuitry each scene has a store of its own, kept for that file on this device:

  • It is not shared with other scenes or with Circuitry, so "best" in one file is not "best" in another.
  • It holds up to 100 names and 100,000 characters. A write past that fails with an error.
  • Renaming or moving the file starts it empty.
  • sessionStorage lasts only until the preview starts again. Cookies and IndexedDB are not available.

What a scene can reach

A scene's preview is shut off from the rest of Circuitry. It cannot read Circuitry's own storage or cookies, and it cannot call any website until you allow that site.

  • Files beside the scene load without asking: loadImage, loadText, loadJSON, loadFont, use and three.js's loaders.
  • A website is asked about first. When the code names a site, in fetch(...), in a web address given to loadImage, loadText, loadJSON or use, in a WebSocket and so on, a dialog asks Allow remote calls? and lists the sites. Choose Allow or Don't allow.
  • Allowed sites are remembered for that file on this device.
  • With Don't allow, the scene still runs. Requests to those sites fail, and a line under the preview says remote calls are off for the scene, with an Allow button if you change your mind.
  • A server on your own computer can be allowed, one port at a time. The dialog shows it as this computer, port 8000 (with the port your code names), and allowing one port does not allow another.
  • Circuitry itself can never be allowed: not circuitry.dev, not the app, and not the ports a Circuit uses on your computer.

What a JavaScript scene does not have

  • Stop. A scene runs while its preview is open. Turn the preview off to end it.
  • Breakpoints. A scene cannot be paused at a line. See Finding mistakes for what to use.

Pyctures, the Python sibling, has both.

Export as a web page

File → Export as Web Page… writes one .html file that runs the scene in any browser, with no connection, opened by double-clicking it. The pictures.js library, your scene and the files it names by a path are all inside it, and three.js too for a 3D scene. See Use your scenes outside Circuitry.