Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ syncs back to Python.
| AnnotationWidget | `wigglystuff.annotation.AnnotationWidget` | `action`, `action_timestamp`, `note`, `listening`, `actions`, `keyboard_mapping`, `gamepad_mapping`, `debounce_ms`, `width` | Annotation input surface with buttons, keyboard, gamepad, and speech-to-text |
| ApiDoc | `wigglystuff.api_doc.ApiDoc` | `doc`, `width`, `show_private` | Renders API docs for Python classes/functions |
| Slider2D | `wigglystuff.slider2d.Slider2D` | `x`, `y`, `x_bounds`, `y_bounds`, `width`, `height` | 2D pointer for coupled parameters |
| Knob | `wigglystuff.knob.Knob` | `value`, `min_value`, `max_value`, `step`, `start_angle`, `end_angle`, `ticks`, `steps`, `size`, `label`, `show_value`, `color`, `midi`, `midi_cc`, `midi_channel`, `midi_device`, `midi_key`, `midi_scope` | Audio-panel rotary knob with configurable sweep, detents, and Web MIDI learn |
| Fader | `wigglystuff.fader.Fader` | `value`, `min_value`, `max_value`, `step`, `ticks`, `steps`, `orientation`, `length`, `label`, `show_value`, `color`, `midi`, `midi_cc`, `midi_channel`, `midi_device`, `midi_key`, `midi_scope` | Mixing-console fader with a configurable tick scale, detents, and Web MIDI learn |
| BezierCurve | `wigglystuff.bezier_curve.BezierCurve` | `points`, `samples`, `x`, `y`, `t`, `closed`, `playing`, `loop`, `interval_ms`, `duration_ms`, `sync_throttle_ms`, `show_axes`, `n_samples`, `x_bounds`, `y_bounds`, `width`, `height` | Arbitrary-degree Bezier curve editor with draggable control points, playback, and optional axis ticks |
| CurveEditor | `wigglystuff.curve_editor.CurveEditor` | `points`, `samples`, `x`, `y`, `t`, `curve`, `closed`, `playing`, `loop`, `tension`, `alpha`, `selected_index`, `show_axes`, `n_samples`, `x_bounds`, `y_bounds`, `width`, `height` | Chart-space curve editor with D3 line interpolators, path progress, and optional axis ticks |
| ChartPuck | `wigglystuff.chart_puck.ChartPuck` | `x`, `y`, `x_bounds`, `y_bounds`, `axes_pixel_bounds`, `width`, `height`, `chart_base64`, `puck_radius`, `puck_color`, `throttle` | Draggable puck overlay for matplotlib charts |
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

All notable changes to this project will be documented in this file.

## [0.5.25] - 2026-08-12

### Added

- New `Knob` widget: an audio-panel rotary knob. Partial-arc by default (configurable `start_angle`/`end_angle`, up to a gapless full 360° circle), with a pointer, a configurable tick scale (`ticks`), and discrete detents (`steps`) for a rotary selector.
- New `Fader` widget: a mixing-console fader (vertical or horizontal) with a configurable tick scale and the same `steps` detents.
- Both support optional Web MIDI "learn" (`midi=True`): click the button, move a hardware control, and the next control-change binds to the widget and drives its value. Bindings can be set upfront (`midi_cc`/`midi_channel`) and persist across restarts in browser localStorage, namespaced by the notebook URL path (override with `midi_key`/`midi_scope`). Web MIDI is Chromium-only and needs a secure context.
- Demos at `demos/knob.py` and `demos/fader.py`.

## [0.5.24] - 2026-08-10

### Fixed
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ uv pip install wigglystuff
<td align="center"><b>Excalidraw</b><br><a href="https://molab.marimo.io/github/koaning/wigglystuff/blob/main/demos/excalidraw.py/wasm?utm_source=wigglystuff"><img src="./docs/assets/gallery/excalidraw.webp" width="260"></a><br><a href="https://molab.marimo.io/github/koaning/wigglystuff/blob/main/demos/excalidraw.py/wasm?utm_source=wigglystuff">molab</a> · <a href="https://koaning.github.io/wigglystuff/reference/excalidraw/">API</a> · <a href="https://koaning.github.io/wigglystuff/reference/excalidraw.md">MD</a></td>
</tr>
<tr>
<td align="center"><b>Knob</b><br><a href="https://molab.marimo.io/github/koaning/wigglystuff/blob/main/demos/knob.py/wasm?utm_source=wigglystuff"><img src="./docs/assets/gallery/knob.webp" width="260"></a><br><a href="https://molab.marimo.io/github/koaning/wigglystuff/blob/main/demos/knob.py/wasm?utm_source=wigglystuff">molab</a> · <a href="https://koaning.github.io/wigglystuff/reference/knob/">API</a> · <a href="https://koaning.github.io/wigglystuff/reference/knob.md">MD</a></td>
<td align="center"><b>Fader</b><br><a href="https://molab.marimo.io/github/koaning/wigglystuff/blob/main/demos/fader.py/wasm?utm_source=wigglystuff"><img src="./docs/assets/gallery/fader.webp" width="260"></a><br><a href="https://molab.marimo.io/github/koaning/wigglystuff/blob/main/demos/fader.py/wasm?utm_source=wigglystuff">molab</a> · <a href="https://koaning.github.io/wigglystuff/reference/fader/">API</a> · <a href="https://koaning.github.io/wigglystuff/reference/fader.md">MD</a></td>
<td></td>
</tr>
<tr>
<td align="center"><b>LiveEdit</b><br><a href="https://molab.marimo.io/github/koaning/wigglystuff/blob/main/demos/liveedit.py/wasm?utm_source=wigglystuff"><img src="./docs/assets/gallery/liveedit.webp" width="260"></a><br><a href="https://molab.marimo.io/github/koaning/wigglystuff/blob/main/demos/liveedit.py/wasm?utm_source=wigglystuff">molab</a> · <a href="https://koaning.github.io/wigglystuff/reference/live-edit/">API</a> · <a href="https://koaning.github.io/wigglystuff/reference/live-edit.md">MD</a></td>
<td align="center"><b>FramePlayer</b><br><a href="https://molab.marimo.io/github/koaning/wigglystuff/blob/main/demos/frame_player.py/wasm?utm_source=wigglystuff"><img src="./docs/assets/gallery/frameplayer.webp" width="260"></a><br><a href="https://molab.marimo.io/github/koaning/wigglystuff/blob/main/demos/frame_player.py/wasm?utm_source=wigglystuff">molab</a> · <a href="https://koaning.github.io/wigglystuff/reference/frame-player/">API</a> · <a href="https://koaning.github.io/wigglystuff/reference/frame-player.md">MD</a></td>
<td align="center"><b>AsyncFlow</b><br><a href="https://molab.marimo.io/github/koaning/wigglystuff/blob/main/demos/asyncflow.py/wasm?utm_source=wigglystuff"><img src="./docs/assets/gallery/asyncflow.webp" width="260"></a><br><a href="https://molab.marimo.io/github/koaning/wigglystuff/blob/main/demos/asyncflow.py/wasm?utm_source=wigglystuff">molab</a> · <a href="https://koaning.github.io/wigglystuff/reference/async-flow/">API</a> · <a href="https://koaning.github.io/wigglystuff/reference/async-flow.md">MD</a></td>
Expand Down
116 changes: 116 additions & 0 deletions demos/fader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "marimo",
# "wigglystuff==0.5.25",
# ]
# ///

import marimo

__generated_with = "0.23.16"
app = marimo.App(width="medium")


@app.cell
def _(mo):
mo.md("""
# Fader
""")
return


@app.cell
def _(mo):
from wigglystuff import Fader

level = mo.ui.anywidget(
Fader(
min_value=-60,
max_value=6,
value=0,
ticks=[(-60, "-60"), (-20, "-20"), (-6, "-6"), (0, "0"), (6, "+6")],
label="Level (dB)",
)
)
return Fader, level


@app.cell
def _(Fader, mo):
send = mo.ui.anywidget(
Fader(min_value=0, max_value=100, value=75, ticks=5, label="Send", color="teal")
)
crossfade = mo.ui.anywidget(
Fader(
min_value=0,
max_value=1,
step=0.01,
value=0.5,
orientation="horizontal",
ticks=[(0, "A"), (1, "B")],
length=180,
label="Crossfade",
)
)
return crossfade, send


@app.cell
def _(crossfade, level, mo, send):
mo.hstack([level, send, crossfade], justify="center", align="center", gap=2)
return


@app.cell(hide_code=True)
def _(crossfade, level, mo, send):
mo.md(f"""
**Level:** `{level.value['value']:.1f} dB` &nbsp;
**Send:** `{send.value['value']:.0f}` &nbsp;
**Crossfade:** `{crossfade.value['value']:.2f}`
""")
return


@app.cell(hide_code=True)
def _(mo):
mo.md("""
## MIDI learn

Click the **MIDI** button, then move a control on your hardware — the next
control-change message binds to the fader (like Ableton). The binding is
remembered in your browser (keyed by the fader's `label`), so it survives a
kernel restart or cell re-run. Needs a Chromium browser and a connected MIDI
device; right-click the button to clear.
""")
return


@app.cell
def _(Fader, mo):
midi_fader = mo.ui.anywidget(
Fader(min_value=0, max_value=127, value=64, midi=True, label="MIDI")
)
midi_fader
return (midi_fader,)


@app.cell(hide_code=True)
def _(midi_fader, mo):
mo.md(f"""
**Value:** `{midi_fader.value['value']:.0f}` &nbsp;
**Bound CC:** `{midi_fader.value['midi_cc']}` &nbsp;
**Device:** `{midi_fader.value['midi_device'] or '—'}`
""")
return


@app.cell
def _():
import marimo as mo

return (mo,)


if __name__ == "__main__":
app.run()
124 changes: 124 additions & 0 deletions demos/knob.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "marimo",
# "wigglystuff==0.5.25",
# ]
# ///

import marimo

__generated_with = "0.23.16"
app = marimo.App()


@app.cell
def _(mo):
mo.md("""
# Knob
""")
return


@app.cell
def _(mo):
from wigglystuff import Knob

gain = mo.ui.anywidget(
Knob(min_value=0, max_value=11, value=5, ticks=12, label="Gain", color="tomato")
)
return Knob, gain


@app.cell
def _(Knob, mo):
# A wider sweep with labelled endpoints, and a smaller pan-style knob.
pan = mo.ui.anywidget(
Knob(
min_value=-1,
max_value=1,
step=0.05,
value=0,
ticks=[(-1, "L"), (0, "C"), (1, "R")],
label="Pan",
)
)
wide = mo.ui.anywidget(
Knob(
min_value=0,
max_value=100,
value=30,
start_angle=-160,
end_angle=160,
ticks=5,
size=110,
label="Wide sweep",
)
)
# A gapless full-circle knob (start_angle=0, end_angle=360) that wraps.
full = mo.ui.anywidget(
Knob(
min_value=0,
max_value=360,
value=90,
start_angle=0,
end_angle=360,
ticks=[(0, "N"), (90, "E"), (180, "S"), (270, "W")],
label="Full circle",
)
)
return full, pan, wide


@app.cell
def _(full, gain, mo, pan, wide):
mo.hstack([gain, pan, wide, full], justify="center", gap=2)
return


@app.cell(hide_code=True)
def _(full, gain, mo, pan, wide):
mo.md(f"""
**Gain:** `{gain.value['value']:.1f}` &nbsp;
**Pan:** `{pan.value['value']:.2f}` &nbsp;
**Wide:** `{wide.value['value']:.0f}` &nbsp;
**Full:** `{full.value['value']:.0f}`
""")
return


@app.cell(hide_code=True)
def _(mo):
mo.md("""
## MIDI learn

Click the **MIDI** button, then move a control on your hardware — the next
control-change message binds to the knob (like Ableton). The binding is
remembered in your browser (keyed by the knob's `label`), so it survives a
kernel restart or cell re-run. Needs a Chromium browser and a connected MIDI
device; right-click the button to clear.
""")
return


@app.cell
def _(Knob, mo):
midi_knob1 = mo.ui.anywidget(
Knob(min_value=0, max_value=127, value=64, midi=True, label="MIDI")
)
midi_knob2 = mo.ui.anywidget(
Knob(min_value=0, max_value=127, value=64, midi=True, label="MIDI")
)
mo.hstack([midi_knob1, midi_knob2], justify="center")
return


@app.cell
def _():
import marimo as mo

return (mo,)


if __name__ == "__main__":
app.run()
Binary file added docs/assets/gallery/fader.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/gallery/knob.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,16 @@ The documentation for wigglystuff is designed for humans (via hosted marimo note
<div class="gallery-links"><a target="_blank" href="https://molab.marimo.io/github/koaning/wigglystuff/blob/main/demos/frame_player.py/wasm?utm_source=wigglystuff">molab</a><a href="reference/frame-player/">API</a><a href="reference/frame-player.md">MD</a></div>
</div>
<div class="gallery-item">
<div class="gallery-title"><a href="reference/knob/">Knob</a></div>
<a target="_blank" href="https://molab.marimo.io/github/koaning/wigglystuff/blob/main/demos/knob.py/wasm?utm_source=wigglystuff" class="gallery-img"><img src="assets/gallery/knob.webp" alt="Knob"></a>
<div class="gallery-links"><a target="_blank" href="https://molab.marimo.io/github/koaning/wigglystuff/blob/main/demos/knob.py/wasm?utm_source=wigglystuff">molab</a><a href="reference/knob/">API</a><a href="reference/knob.md">MD</a></div>
</div>
<div class="gallery-item">
<div class="gallery-title"><a href="reference/fader/">Fader</a></div>
<a target="_blank" href="https://molab.marimo.io/github/koaning/wigglystuff/blob/main/demos/fader.py/wasm?utm_source=wigglystuff" class="gallery-img"><img src="assets/gallery/fader.webp" alt="Fader"></a>
<div class="gallery-links"><a target="_blank" href="https://molab.marimo.io/github/koaning/wigglystuff/blob/main/demos/fader.py/wasm?utm_source=wigglystuff">molab</a><a href="reference/fader/">API</a><a href="reference/fader.md">MD</a></div>
</div>
<div class="gallery-item">
<div class="gallery-title"><a href="reference/circular-slider/">CircularSlider</a></div>
<a target="_blank" href="https://molab.marimo.io/github/koaning/wigglystuff/blob/main/demos/circular_slider.py/wasm?utm_source=wigglystuff" class="gallery-img"><img src="assets/gallery/circle-slider.webp" alt="CircularSlider"></a>
<div class="gallery-links"><a target="_blank" href="https://molab.marimo.io/github/koaning/wigglystuff/blob/main/demos/circular_slider.py/wasm?utm_source=wigglystuff">molab</a><a href="reference/circular-slider/">API</a><a href="reference/circular-slider.md">MD</a></div>
Expand Down
2 changes: 2 additions & 0 deletions docs/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Install via `pip install wigglystuff` or `uv pip install wigglystuff`.

- [AnnotationWidget](https://koaning.github.io/wigglystuff/reference/annotation.md): Annotation input surface with buttons, keyboard shortcuts, gamepad, and speech-to-text
- [Slider2D](https://koaning.github.io/wigglystuff/reference/slider2d.md): 2D slider for x/y value selection
- [Knob](https://koaning.github.io/wigglystuff/reference/knob.md): Audio-panel rotary knob with a configurable sweep, tick scale, discrete detents, and optional Web MIDI learn (bindings persist in browser localStorage)
- [Fader](https://koaning.github.io/wigglystuff/reference/fader.md): Mixing-console fader (vertical or horizontal) with a configurable tick scale, discrete detents, and optional Web MIDI learn
- [BezierCurve](https://koaning.github.io/wigglystuff/reference/bezier-curve.md): Interactive arbitrary-degree Bezier curve editor with draggable control points
- [CurveEditor](https://koaning.github.io/wigglystuff/reference/curve-editor.md): Chart-space curve editor with draggable knots, D3 line interpolators, closed paths, and progress output
- [ChartPuck](https://koaning.github.io/wigglystuff/reference/chart-puck.md): Draggable puck overlay for matplotlib charts
Expand Down
59 changes: 59 additions & 0 deletions docs/reference/fader.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
title: "Fader: mixing-console fader widget"
description: Fader is a channel-strip fader with a configurable tick scale, discrete detents, vertical or horizontal orientation, and optional Web MIDI learn, in marimo or Jupyter.
image: fader
image_alt: Vertical and horizontal mixing-console faders with dB-style tick scales beside the tracks
---

# Fader API

<!-- no-md -->
<div class="wiggly-demo-wrap">
<button class="wiggly-demo" type="button" data-demo="fader" data-demo-title="Fader live demo">
<img class="wiggly-demo__poster" src="../assets/gallery/fader.webp" alt="Vertical and horizontal mixing-console faders with tick scales" decoding="async">
<span class="wiggly-demo__cta">Run this demo live in your browser <span class="wiggly-demo__play">▶</span></span>
</button>
</div>
<!-- /no-md -->

`Fader` is a linear slider drawn to look like a channel fader on a mixing
console: a cap slides along a slot, with a configurable tick scale (e.g. dB
marks) alongside the track. It is vertical by default with `max_value` at the
top; pass `orientation="horizontal"` for a left-to-right fader.

Ticks are configurable — `ticks=N` for evenly spaced marks, a list of values, or
`(value, label)` pairs. Pass `steps` instead for a **stepped fader** that snaps
to discrete detents. With `midi=True` the fader shows a "MIDI learn" button:
click it, move a control on your hardware, and the next control-change message
binds to the fader (Web MIDI, Chromium browsers). The binding is remembered in
browser localStorage so it survives a restart.

See also: [Knob](knob.md) for the rotary console version, and
[HoverSlider](hover-slider.md) for a linear slider that also reports the value
under the pointer.

::: wigglystuff.fader.Fader

## Synced traitlets

| Traitlet | Type | Notes |
| --- | --- | --- |
| `value` | `float` | Current value, mapped along the track. |
| `min_value` | `float` | Lower bound (bottom / left). |
| `max_value` | `float` | Upper bound (top / right). |
| `step` | `float` | Snap increment in value units (continuous mode). |
| `ticks` | `list[dict]` | Normalized `{"value", "label"}` tick marks. |
| `steps` | `list[float]` | Discrete detents to snap to; empty means continuous. |
| `orientation` | `str` | `"vertical"` (default) or `"horizontal"`. |
| `length` | `int` | Track length in pixels (the long dimension). |
| `label` | `str` | Optional text label shown above the fader. |
| `show_value` | `bool` | Render the current value as text next to the fader. |
| `color` | `str` | CSS color for the filled track and cap. Empty follows the theme. |
| `midi` | `bool` | Show the MIDI-learn button and listen for control-change. |
| `midi_cc` | `int` | Bound control-change number (0-127), or `-1` when unbound. |
| `midi_channel` | `int` | Bound MIDI channel (0-15), or `-1` for any. |
| `midi_device` | `str` | Name of the bound MIDI input device. |
| `midi_supported` | `bool` | Whether the browser exposes Web MIDI (set from JS). |
| `midi_learning` | `bool` | Whether the fader is currently in learn mode. |
| `midi_key` | `str` | localStorage key for the persisted binding (defaults to `label`). |
| `midi_scope` | `str` | Namespace for the binding; empty uses the browser URL path. |
2 changes: 2 additions & 0 deletions docs/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ the link at the top of any page. [`llms.txt`](../llms.txt) lists them all.
## Sliders and scalar inputs

- [Slider2D](slider2d.md) — 2D pointer for two coupled parameters
- [Knob](knob.md) — audio-panel rotary knob with configurable sweep, detents and MIDI learn
- [Fader](fader.md) — mixing-console fader with a configurable tick scale and MIDI learn
- [CircularSlider](circular-slider.md) — circular dial for a single value or a span
- [HoverSlider](hover-slider.md) — reports the committed value and the live hover value
- [PlaySlider](play-slider.md) — slider with a play/pause button that auto-advances
Expand Down
Loading
Loading