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.xandhits.lengthare ordinary numbers. Use them in sums and inif.awaitis only for time and files:frame(),flush(),sleep(seconds),loadText(path)andloadJSON(path).await vsync()is another name forawait frame().- Reading pixels with
ctx.getImageDatais 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 newwidthandheight. - A resize also resets the colours, font, line width and every other setting of
ctx. Set them indraw(), 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 saysTHREE.does not count. Ifctxis suddenlyundefined, 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 * dtinupdate(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
Pointsobject with thousands of positions is far quicker than thousands of separate meshes. - Do heavy work once, at the start, and keep the results.
dtis 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.
mousefollows the first finger only.toucheshas every finger that is down, and a pressed mouse button too. See Every finger. - Zoom. Multiply by
pinch.changeevery frame for two fingers or a trackpad, and readmouse.wheelfor the mouse wheel. See Zooming. - The wheel is the scene's only when it names it. A scene that reads
mouse.wheelor definesonWheelkeeps 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,pressedandreleasedare set at the start of a frame and are back to nothing at the start of the next. Read them every frame, inupdateor in your loop. - You do not need
addEventListenerfor 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
loadImagetakes adata: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 inctx.font. If the font cannot be loaded, the text is drawn in the next font you name.await use("helpers.js")loads another.jsfile 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.OrbitControlsare 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.
sessionStoragelasts 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,useand three.js's loaders. - A website is asked about first. When the code names a site, in
fetch(...), in a web address given toloadImage,loadText,loadJSONoruse, in aWebSocketand 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.