Skip to content

<notatio-knob>

Two taps this close together mean "let me type it". */ const DOUBLE_TAP_MS = 400;

/ <notatio-knob name="cookies" value="3"> -- a number you drag inside a sentence**, after Bret Victor's [Tangle](http://worrydream.com/Tangle/). It is not a slider and not a field: it renders exactly what the value looks like when typeset, marked as interactive with a dotted underline, and you change it by dragging across it.

Drag right to raise, left to lower (or set axis="y" and drag up to raise); the value follows the pointer's *position*, so dragging back returns you to where you started. A complex knob adds the second axis -- up raises the imaginary part, down lowers it -- and shows both parts at all times, since an axis that reads 0 is still an axis you can drag. choices scrubs a discrete list instead (an index into a family of objects, say) rather than a numeric range.

Gears. Every gesture is in one of three gears: normal moves by step, coarse by ten steps, fine by a tenth of one (an integer knob's fine gear keeps whole numbers and asks ten times the travel instead). Hold Shift for coarse and Alt for fine -- while dragging or at the keyboard -- or, on a one-axis knob, drag off the axis: up the ladder is coarse, down is fine, and a small ladder beside the value shows which increment you are in. On a touchscreen a second finger anywhere means fine.

Keyboard. The arrows along the drag axis step (left/right, or up/down for axis="y"; a complex knob takes both pairs), and holding one accelerates. PageUp/PageDown step coarse, Home/End go to the ends, and Enter -- or a double tap -- turns the value into a field you can type into.

A knob always has an axis, which is what makes it a knob: choices scrubs its list along that axis rather than offering a menu of it. For a value with no axis to scrub -- a word, an either/or -- reach for <notatio-toggler>, which is exactly that: the same binding, a click to cycle, and a menu on a long press.

Playback. Space sweeps the knob through its range one step at a time, and again to stop; any drag or key stops it too. play adds a small button beside the value for the mouse, autoplay starts the sweep when the knob scrolls into view and pauses it when it leaves, interval sets the milliseconds per step (the default paces a whole sweep at a few seconds) and rate multiplies that.

Loop. loop says what an iteration does at the ends -- playback and the arrow keys alike: cycle wraps, reflect turns round, none stops (and a play-through that has reached its end rewinds when played again). A range has ends, so a numeric knob is none by default; a list goes round, so choices is cycle. A long press on the play button opens a panel for the speed and the loop.

Give it children and they become the grip. A knob with slotted content renders no number of its own — you drag the content instead, and the value it is publishing is whatever that content is showing. That is how a family of objects becomes something you brush through directly rather than through a number sitting next to it:

``html <notatio-knob name="k" value="1" min="1" max="24"> <notatio-figure kind="permutation" value="At(Permutations(Range(1,4)), _k)" /> </notatio-knob> ``

Inside a <notatio-tangle> the knob's name becomes the wildcard _name, and every notatio template in the surrounding prose re-derives as it moves.

``html <notatio-tangle> Eat <notatio-knob name="n" value="3" min="0" max="12" step="1" /> cookies and take on <notatio-dynamic value="_n * 50" /> calories. </notatio-tangle> ``

The name is descriptive, not a Wolfram symbol: Wolfram has no inline draggable value (Manipulator is a slider with chrome), and the repo already calls a bound, swept parameter a knob.

Stories in the playground·packages/notatio-lit/src/notatio-knob.ts· in Vue: <Knob>

Attributes

AttributeTypeDefaultNotes
namestring""The binding this knob drives: name="a" fills the wildcard _a.reflects
valuestring"0"The starting value: 3, -1.5, 3+2i, or an entry of choices.
minnumberNumber.NaNLowest value a drag can reach. Defaults to a symmetric range around the start.
maxnumberNumber.NaNHighest value a drag can reach.
stepnumberNumber.NaNHow much one step of the drag moves the value; also fixes the printed places.
complexbooleanfalseAdd the imaginary axis: dragging up/down moves the imaginary part.reflects
choicesstring""A |-separated list to scrub through instead of a numeric range.
sensitivitynumberDEFAULT_PIXELS_PER_STEPPixels of travel per step — lower drags faster.
axis"x" | "y""x"The drag direction of a one-axis knob: x (default, right raises) or y (up raises).reflects
playbooleanfalseShow a play/pause button beside the value. Space toggles playback regardless.reflects
autoplaybooleanfalseStart sweeping when scrolled into view; pause when scrolled out.
intervalnumberNumber.NaNMilliseconds per playback step. Defaults to a whole sweep in a few seconds.
ratenumberrate), interval: () => Number.isFinite(this.interval) && this.interval > 0 ? this.interval : this.#pace, onState: () => (this._playing = this.#sweep.playing), }, openPlaybackMenu, LONG_PRESS_MS, )Playback speed as a multiplier on interval: 2 is twice as fast.reflects
loopLoop | ""loop), rate: () => (Number.isFinite(this.rate) && this.rate > 0 ? this.rate : 1), setRate: (rate) => (this.rate = rate), interval: () => Number.isFinite(this.interval) && this.interval > 0 ? this.interval : this.#pace, onState: () => (this._playing = this.#sweep.playing), }, openPlaybackMenu, LONG_PRESS_MS, )What an iteration does at the ends: cycle, reflect or none.reflects