dials is designed to bring the joy back into programming, for any age and any level: you write one line and a dial appears, and you turn it and something changes. Dials, sliders, buttons, switches, knobs that click, a piano keyboard, a scope and a level meter go on a scene with nothing to position, nothing to draw and nothing to wire up, and they work with a finger, the mouse and the computer's keys. It is dials.js for pictures.js and pydials for Pyctures, and a control is tied to a setting of a synth in one line.
Free and open source, MIT licence. Made for projects in Circuitry Studio; runs anywhere a browser does.

Six lines: a synth, two dials and a slider that are its settings, the wave it is making, and a piano keyboard. Play the keys with the mouse, with fingers, or with A S D F G H J, and turn a dial while a note sounds.
Open CircuitryA browser keeps a page silent until you click or tap it, so the first touch on the scene turns the sound on. Change the code and press Run (or Cmd/Ctrl+Enter).
You say what you want and it appears. A dial or a slider is a number: speed.value is the number it is on now, and in a sum it stands for its number. A button is asked in update: fire.pressed is true once for each press. A toggle is a switch, and a select is one of a list. The controls line themselves up in a row in the order you make them.
const speed = dial("speed", 0, 10, 3) // from 0 to 10, starting at 3
const size = slider("size", 10, 150, 60)
const fire = button("fire")
const trail = toggle("trail")
const level = select("level", ["easy", "hard"])speed = dial("speed", 0, 10, 3) # from 0 to 10, starting at 3
size = slider("size", 10, 150, 60)
fire = button("fire")
trail = toggle("trail")
level = select("level", ["easy", "hard"])Every control, with its options.
A control can be tied to a setting of a synth. The dial then is that setting: it takes its range, its unit and where it starts from the synth, and turning it sets it. A scope draws the wave the synth is making, a meter shows how loud it is, and a keyboard plays it: with fingers (several at once), with the mouse, and with the keys A S D F G H J.
const lead = makeSynth("lead")
dial("cutoff", lead, "cutoff") // the dial takes its range from the synth, and turning it sets it
select("wave", lead, "wave") // the choices come from the synth too
scope(lead) // the wave it is making
meter(lead) // how loud it is
keyboard(lead) // piano keys that play it, and light up as it playslead = make_synth("lead")
dial("cutoff", lead, "cutoff") # the dial takes its range from the synth, and turning it sets it
select("wave", lead, "wave") # the choices come from the synth too
scope(lead) # the wave it is making
meter(lead) # how loud it is
keyboard(lead) # piano keys that play it, and light up as it playsA key sounds the moment it is touched and a dial is heard as it turns. Tying a control to a synth. Every setting is tied by its name: Every setting lists them with their ranges and the control that suits each.
When none of the controls is the one you want, custom makes one from three parts: what it looks like, what a finger does to it, and what it listens to. It sits in the layout like any other, so a synth or an instrument of your own gets its own ribbon, pad, meter, scope or sequencer, and what it draws can follow any sound: give it a synth as its source and it is handed how loud the sound is and the wave it is making, every frame.
const lead = makeSynth("lead")
const ribbon = custom("ribbon", {
width: 320, height: 100, value: -1,
draw(c) { // what it looks like, in its own points
c.rect(0, 0, c.width, c.height, c.theme.well, 10)
for (let i = 0; i < 8; i++) c.rect(i * c.width / 8 + 3, 4, c.width / 8 - 6, c.height - 8, i === c.value ? c.accent : c.theme.knob, 6)
},
onDown(p, c) { // what a finger does to it
ribbon.set(Math.floor(p.x / c.width * 8))
lead.play(57 + ribbon.value, 0.4)
},
onUp() { ribbon.value = -1 },
})lead = make_synth("lead")
def paint(c): # what it looks like, in its own points
c.rect(0, 0, c.width, c.height, c.theme["well"], 10)
for i in range(8):
c.rect(i * c.width / 8 + 3, 4, c.width / 8 - 6, c.height - 8, c.accent if i == c.value else c.theme["knob"], 6)
def down(p, c): # what a finger does to it
ribbon.set(int(p.x / c.width * 8))
lead.play(57 + ribbon.value, 0.4)
def up(p, c):
ribbon.value = -1
ribbon = custom("ribbon", width=320, height=100, value=-1, draw=paint, on_down=down, on_up=up)Your own controls has a ribbon, a ring that follows the sound and a row of steps that plays itself. A control of your own is drawn at a size you choose, scaled to fit its box and cut at its edge, so it never draws over its neighbours.
spectrum shows the frequencies in a sound as bars, the low notes at the left, with ticks that hold the peaks, beside a scope for the wave and a meter for how loud it is. A control of your own is given the same bands as c.spectrum, to draw an analyser your own way.
const lead = makeSynth("lead")
spectrum(lead, { bands: 48 }) // the frequencies in the sound, as bars: low notes at the left
scope(lead) // its wave
meter(lead) // how loud it is
keyboard(lead, { octave: 3, octaves: 2 })lead = make_synth("lead")
spectrum(lead, bands=48) # the frequencies in the sound, as bars: low notes at the left
scope(lead) # its wave
meter(lead) # how loud it is
keyboard(lead, octave=3, octaves=2)Every control has its options, and the Synth Workshop demo builds an analyser of its own with a level in dB, a spectrum and the wave.
The microphone is a synth to the controls too: its voice is a choice, its pitch is a dial, hearing yourself is a switch, and a scope draws your voice.
const mic = useMicrophone()
select("voice", mic, "voice") // robot, chipmunk, monster and the rest
dial("pitch", mic, "pitch")
toggle("listen", mic, "listening") // wear headphones first
scope(mic) // your voice, as a wavemic = use_microphone()
select("voice", mic, "voice") # robot, chipmunk, monster and the rest
dial("pitch", mic, "pitch")
toggle("listen", mic, "listening") # wear headphones first
scope(mic) # your voice, as a waveA keyboard can have a MIDI button: tap it to connect, then choose which keyboard to listen to and which channel.
const lead = makeSynth("lead")
scope(lead)
keyboard(lead, { octave: 4, midi: true }) // a MIDI button on the keyboard: tap it to choose the device and channellead = make_synth("lead")
scope(lead)
keyboard(lead, octave=4, midi=True) # a MIDI button on the keyboard: tap it to choose the device and channelWith a MIDI controller plugged in, learn ties the next knob you turn to a setting, and the dial on the screen turns with it.
const lead = makeSynth("lead")
const cutoff = dial("cutoff", lead, "cutoff")
lead.learn("cutoff", (value) => { cutoff.value = value }) // the next knob you turn on your controllerlead = make_synth("lead")
dial("cutoff", lead, "cutoff")
lead.learn("cutoff") # the next knob you turn on your controllerThe microphone and MIDI work on a web page of your own and in a scene run with pictures run or pyctures run; in Circuitry's own preview they are not connected yet, and there the controls work by hand. Tying a control to a synth has both.
There is nothing to do: controls line up in the order you make them, wrap evenly on a narrow screen, and are a little bigger under a finger. When you want more, a label before some controls is their heading, a group is a plate with a title, and row, column, grid and page arrange what is inside. One line of layout keeps your rows and fits them to the width, or lays the panel out at a set size and scales the whole of it. A rotary is a knob that clicks between a few choices.

label("oscillator") // a label before controls is their heading
rotary("wave", lead, "wave") // a knob that clicks between choices
dial("width", lead, "width")
group("filter", () => { // or say it out loud
dial("cutoff", lead, "cutoff", { size: "large" })
dial("resonance", lead, "resonance")
})
layout({ wrap: false }) // keep my rows, and fit them to the widthlabel("oscillator") # a label before controls is their heading
rotary("wave", lead, "wave") # a knob that clicks between choices
dial("width", lead, "width")
with group("filter"): # or say it out loud
dial("cutoff", lead, "cutoff", size="large")
dial("resonance", lead, "resonance")
layout(wrap=False) # keep my rows, and fit them to the widthWhen there is more than fits, the plates move under a finger while the keyboard and the scope stay put. Laying out a panel has all of it, with pictures.
A control can be tied to any object, not only a synth. With nothing else to go on it reads and writes the property you name.
const ball = { radius: 40 }
dial("size", ball, "radius", { min: 5, max: 120 })
function draw() {
background("midnightblue")
circle(width / 2, height / 2, ball.radius, "gold")
}ball = {"radius": 40}
dial("size", ball, "radius", min=5, max=120)
def draw():
background("midnightblue")
circle(width / 2, height / 2, ball["radius"], "gold")The controls are drawn on a canvas of their own, laid exactly over the scene's, so they sit on whatever the scene draws, in 2D or 3D, and cost the scene nothing. Layout and touch.
In Circuitry Studio there is nothing to install. A JavaScript scene has the names ready, with nothing to import. A Python scene starts with from pyctures import * and from pydials import *. Both run in the preview beside your code, on a computer, an iPad or an iPhone.
Outside Circuitry, dials.js comes with pictures.js and pydials comes with Pyctures: pictures run panel.js or pyctures run panel.py opens your browser on the scene. Both are on npm and PyPI as betas (npm install dials.js, pip install pydials), and on GitHub. Getting started says how to use them on a page of your own.