Laying out a panel

There is nothing to do. Controls line up in the order you make them, on a plate, with the keyboard along the bottom. On a narrow screen the line wraps evenly (six dials are three and three, never five and one), on a touch screen everything is a little bigger, and when the picture changes size it is all laid out again.

This page is for when you want to say more: what goes with what, how many to a line, how big, and what happens on a phone.

A label is a heading

A label made just before some controls is the title of their plate, and they are kept together: a plate moves to the next line whole, never leaving its heading behind. Plates with titles fill the width. They stand side by side, as many to a line as fit, and row() says where a line should end when one has to.

This is a whole synth panel, and it needs nothing else:

const lead = makeSynth("lead")

label("oscillator")
select("wave", lead, "wave")
dial("width", lead, "width")

row()
label("unison")
dial("voices", lead, "voices")
dial("detune", lead, "detune")
label("filter")
dial("cutoff", lead, "cutoff")
dial("resonance", lead, "resonance")

scope(lead)
keyboard(lead)

Groups

group says the same thing out loud, and takes options. In JavaScript what the function makes is in the group. In Python it is what is made inside with.

group("filter", () => {
  dial("cutoff", lead, "cutoff")
  dial("resonance", lead, "resonance")
})

Or in a straight line, with no brackets to match and nothing to indent: group("filter") and then its controls, until the next group, row() or page.

The plates of a line share its width and are as tall as each other, and every line is as wide as the others, so the panel looks made and not poured. This is a whole synth in seven groups:

A group takes these options:

OptionWhat it does
colourThe colour of its title and of every control in it
columnscolumns: 3 lays its controls out three to a line, as a grid
gapThe room between its controls
align"start", "center" or "end". Left out, its controls are spread along the line
dividerstrue draws a line between the groups inside it
platefalse leaves the plate out
wrapfalse keeps its controls on one line, whatever the width
at"left", "right", "top" or "bottom": a strip along that edge. See Along an edge

Rows, columns and grids

Inside a group, controls stand in a line. To arrange them another way, put these in it. They can go inside each other.

JavaScriptPythonWhat it does
row(() => { … })with row():In a line. row() with nothing in it starts a new line
column(() => { … })with column():One under another
grid(3, () => { … })with grid(3):Three to a line, in columns that line up; fewer where three will not fit
spacer()spacer()All the room that is left over, so what follows is pushed to the far end. spacer(20) is 20 points

Three options go on any control:

OptionWhat it does
size"small", "large", or so many times the usual size: 1.5. A dial, a slider and a meter also take a size in points (a number over 4)
spanIn a grid, span: 2 covers two columns
shareThe control takes the room its line has left over. Two things with shares of 2 and 1 split the line two to one. A group takes it too
group("filter", { colour: "deepskyblue" }, () => {
  column(() => {
    select("kind", lead, "filter")
    toggle("mute")
  })
  grid(2, () => {
    dial("cutoff", lead, "cutoff", { size: "large", span: 2 })
    dial("resonance", lead, "resonance", { size: "small" })
    dial("chorus", lead, "chorus", { size: "small" })
  })
})

Size and fit

By itself, a panel fits whatever it is on. On a phone held upright its plates are one to a line and are moved up and down with a finger, under a keyboard that stays put. In a tablet's split view they are two to a line, and across a whole tablet three or four, each line reaching both edges. On a touch screen everything is a little bigger, and nothing is ever drawn too small for a finger.

Three lines change that: a scale, fitting the width, and a panel of a set size.

Bigger or smaller

layout({ scale: 1.3 })

This draws everything that much bigger, and it works with everything below. Call layout before the controls it is for.

Fit the width

wrap: false keeps every row exactly as you wrote it (each row() is one row, whatever the screen) and draws the whole panel smaller until the widest row fits the width. There is one scale for the whole panel, set by that widest row, so a knob is the same size on every row.

layout({ wrap: false })
label("oscillator")
select("wave", lead, "wave")
dial("width", lead, "width")
dial("pwm", lead, "pwm")
dial("sub", lead, "sub")
row()
label("unison")
dial("voices", lead, "voices")
dial("detune", lead, "detune")
label("filter")
dial("cutoff", lead, "cutoff")
dial("resonance", lead, "resonance")
row()
label("shape")
slider("attack", lead, "attack")
slider("decay", lead, "decay")
slider("sustain", lead, "sustain")
slider("release", lead, "release")
scope(lead)
keyboard(lead)

layout({ fit: "width" }) is the same, and also draws the panel bigger where there is room to spare, so that the widest row reaches both edges: no bigger than maxScale (1.6 unless you say), and no bigger than still fits the height. In Python: layout(fit="width", max_scale=2).

Under a finger the panel is not drawn smaller than eight tenths of its size, which is as small as is comfortable to touch (with a mouse, half). If the rows are still too wide they are left that size and moved from side to side. You choose the floor: layout({ wrap: false, minScale: 1 }) never shrinks at all, and minScale: 0 shrinks as far as it takes (min_scale in Python).

A panel of a set size

layout({ size: [900, 640] })

This lays the panel out once, as if every screen were 900 by 640, and then makes the whole of it bigger or smaller to fit the screen it is on, keeping its shape and standing in the middle. Nothing moves about from one screen to the next: only the size changes.

A width alone, layout({ size: 900 }), fills the width, and what does not fit is moved up and down.

A bigger knob is your choice, and it is kept

size on any control makes it that much bigger or smaller than its neighbours, on every screen and at every scale. Its label and its value still line up with theirs.

layout({ wrap: false })
group("filter", () => {
  rotary("kind", lead, "filter")
  dial("cutoff", lead, "cutoff", { size: "large" })
  dial("resonance", lead, "resonance")
  dial("sweep", lead, "sweep", { size: "small" })
})
group("mix", () => {
  dial("volume", lead, "volume", { size: 1.5 })
  dial("pan", lead, "pan")
})

Controlling wrapping

Say nothing and the panel decides. Say something and it does as it is told.

JavaScriptPythonWhat it does
layout({ wrap: false })layout(wrap=False)Never wrap. Every row is the row you wrote, drawn smaller to fit the width
layout({ wrap: true })layout(wrap=True)Every row() starts a new line, and a row is split only when it does not fit
layout({ columns: 3 })layout(columns=3)Three plates to a line, always
layout({ columns: { phone: 1, tablet: 2, wide: 3 } })layout(columns={"phone": 1, "tablet": 2, "wide": 3})How many at each width. phone is any width, tablet 600 points and up, wide 900 and up
group("shape", { wrap: false }, …)with group("shape", wrap=False):This group's controls stay on one line, whatever the width
row({ wrap: false }, () => { … })with row(wrap=False):The groups made inside stay side by side
row()row()A new line starts here. Inside a group too
wrap()wrap()If this line has to be split, split it here. Nothing happens while it fits

When a row that may not wrap is too wide, the whole panel is drawn smaller, not that row alone: one scale, as above.

layout({ columns: { phone: 1, tablet: 2, wide: 3 } })

group("shape", () => {
  slider("attack", lead, "attack")
  slider("decay", lead, "decay")
  wrap()                                  // split here, if it must be split: never between sustain and release
  row({ wrap: false }, () => {
    slider("sustain", lead, "sustain")
    slider("release", lead, "release")
  })
})

Pages

When there is too much for one screen, give it pages. Each has a tab. The keyboard, and what is made before the first page and after the last, stay on every page.

page("osc")
group("oscillator")
select("wave", lead, "wave")
dial("width", lead, "width")
page("filter")
group("filter")
dial("cutoff", lead, "cutoff")
dial("resonance", lead, "resonance")
page()                      // the pages end here
scope(lead)
keyboard(lead)

A page can hold its controls in a function too, and show() turns to it from your code:

const fx = page("fx", () => {
  dial("echo", lead, "echo")
  dial("reverb", lead, "reverb")
})
button("effects", () => { fx.show() })

When there is more than fits

Controls are drawn a little smaller before a line has to wrap, but never tiny. When there is more than fits, the plates can be moved: with a finger (they carry on a little when it lets go), the mouse wheel, a trackpad or the thin bar beside them. The keyboard and the scope above it stay where they are.

  • A drag that starts on a dial turns the dial.
  • A drag that starts on a plate's title, on words or between the plates moves them, passing over dials without turning any.
  • On a slider that lies across, a finger going along it moves the slider, and one going up or down moves the plates.

The keyboard is the instrument, and keeps its height. When the plates, the scope and the keyboard do not all fit, the scope is made shorter (never too short to read a wave), and if that is not enough it is the plates that are moved, under a scope and a keyboard that stay put. keyboard(lead, { height: 200 }) is kept to, whatever the room (height=200).

On a phone

A phone held upright has one plate to a line, moved up and down with a finger under the keyboard. On a phone on its side, where there is hardly any height, the keyboard gives up some of its own and the scope goes away.

A panel of a set size made for a tablet is small on a phone. One more line gives the phone its own:

layout(layout().width < 600 ? { wrap: false } : { size: [900, 640] })

layout() with nothing gives back what the layout came to: its width, height and scale. onResize is called with the same three when the picture changes size, so a scene can take everything away and make another arrangement (on_resize in Python). In Python the page tells these with a frame: in a scene's very first lines the scale is still 1.

Along an edge, and the room that is left

layout({ at: "bottom" }) puts the plates at the bottom, and "left" or "right" in a column down that side. One group can be a strip along an edge, and room() is the rectangle no control is on, for the scene to draw in:

group("moves", { at: "left" }, () => {
  dial("speed", 0, 10, 4)
  button("jump")
  spacer()
  toggle("trail")
})
group({ at: "bottom" }, () => {
  button("left")
  button("right")
  spacer()
  button("fire", { size: "large" })
})

function draw() {
  const area = room()                     // { x, y, width, height }
  rect(area.x, area.y, area.width, area.height, "steelblue")
}

In Python the page works the rectangle out and tells Python with a frame, so in the scene's very first lines room() is still the whole picture.

Exactly where you say

x and y on a control or a group place its top left corner, and control.place(x, y) moves it. With a panel of a set size those numbers are in that panel, which is scaled to fit the picture, so a front panel drawn once is right on every screen:

layout({ size: [800, 480] })
dial("cutoff", lead, "cutoff", { x: 360, y: 30, size: "large" })
group("osc", { x: 20, y: 20, width: 300 }, () => {
  select("wave", lead, "wave")
  dial("sub", lead, "sub")
})
keyboard(lead, { x: 20, y: 340, width: 760, height: 120 })

layout({ image: "panel.png" }) puts a picture of your own behind the controls.

The look

layout also takes these, and theme is one call for the colours.

OptionWhat it does
compacttrue: less room around everything
dividerstrue: a line between plates
platesfalse: no plates, for a picture of your own behind
backdroptrue, false or a colour: a panel behind everything
align"center": plates keep their own widths
marginThe space round the plates