ImageKnob

A continuous control that rotates an image based on a normalized value, perfect for custom knob designs using bitmap graphics.

ImageKnob is a continuous control that rotates an image based on a normalized value. It shares the same layout, parameter model, interaction, and accessibility as other controls (vector, raster, and control primitives); see the Raster introduction.

The image rotation is determined by the normalized value (0-1 maps to rotation based on openness and rotation offset).

Note:

No Theming Support: This component does NOT support themable props (color, roundness, thickness) as visuals are entirely determined by the image content.

Props

NameTypeDefaultRequiredDescription
valuenumber—RequiredCurrent value of the control
onChange(event: AudioControlEvent<number>) => void—OptionalHandler for value changes
minnumber—OptionalMinimum value (ad-hoc mode)
maxnumber—OptionalMaximum value (ad-hoc mode)
stepnumber—OptionalStep size for value adjustments
bipolarbooleanfalseOptionalWhether the component should operate in bipolar mode (centered at zero)
labelstring—OptionalLabel displayed below the component
frameWidthnumber—RequiredWidth of the viewBox (determines viewBox width)
frameHeightnumber—RequiredHeight of the viewBox (determines viewBox height)
imageHrefstring—RequiredURL to the image to rotate
rotationnumber0OptionalOptional rotation angle offset in degrees
opennessnumber90OptionalOpenness of the arc in degrees (value between 0-360º; 0º closed, 90º 3/4 open, 180º half-circle)
valueAsLabel"labelOnly" | "valueOnly" | "interactive"labelOnlyOptionalControls how the label and value are displayed
adaptiveSizebooleanfalseOptionalWhether the component stretches to fill its container
size"xsmall" | "small" | "normal" | "large" | "xlarge"normalOptionalSize of the component
parameterContinuousParameter—OptionalAudio Parameter definition (Model). If provided, overrides min/max/step/label/bipolar from ad-hoc props
valueFormatter(value: number, parameterDef: AudioParameter) => string | undefined—OptionalCustom renderer for the value display
interactionMode"drag" | "wheel" | "both"bothOptionalInteraction mode
interactionDirection"vertical" | "horizontal" | "circular" | "both"—OptionalDirection of interaction
interactionSensitivitynumber—OptionalSensitivity multiplier for interactions (default varies by control type)

Basic Usage

ImageKnob requires an image URL and frame dimensions:

import { useState } from "react";
import { ImageKnob } from "@cutoff/audio-ui-react";
 
export default function BasicImageKnobExample() {
  const [value, setValue] = useState(50);
 
  return (
    <ImageKnob
      value={value}
      min={0}
      max={100}
      label="Volume"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHref="/images/demo/knob-volume.png"
      onChange={(e) => setValue(e.value)}
    />
  );
}

Rotation Configuration

Openness

Control the rotation range using the openness prop:

import { useState } from "react";
import { ImageKnob } from "@cutoff/audio-ui-react";
 
export default function OpennessExample() {
  const [v90, setV90] = useState(50);
  const [v180, setV180] = useState(50);
  const [v270, setV270] = useState(50);
 
  return (
    <div className="flex gap-4 items-center flex-wrap">
      <ImageKnob
        value={v90}
        min={0}
        max={100}
        label="90° Open"
        size="large"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        openness={90}
        onChange={(e) => setV90(e.value)}
      />
      <ImageKnob
        value={v180}
        min={0}
        max={100}
        label="180° Open"
        size="large"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        openness={180}
        onChange={(e) => setV180(e.value)}
      />
      <ImageKnob
        value={v270}
        min={0}
        max={100}
        label="270° Open"
        size="large"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        openness={270}
        onChange={(e) => setV270(e.value)}
      />
    </div>
  );
}

Rotation Offset

Apply a rotation offset to align the image:

import { useState } from "react";
import { ImageKnob } from "@cutoff/audio-ui-react";
 
export default function RotationOffsetExample() {
  const [zeroVal, setZeroVal] = useState(50);
  const [rot45Val, setRot45Val] = useState(50);
 
  return (
    <div className="flex gap-4 items-center flex-wrap">
      <ImageKnob
        value={zeroVal}
        min={0}
        max={100}
        label="No Offset"
        size="large"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        rotation={0}
        onChange={(e) => setZeroVal(e.value)}
      />
      <ImageKnob
        value={rot45Val}
        min={0}
        max={100}
        label="45° Offset"
        size="large"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        rotation={45}
        onChange={(e) => setRot45Val(e.value)}
      />
    </div>
  );
}

Parameter Model

Use a parameter model for integration with audio parameter systems. The following example uses ad-hoc props; with the full library you would use createContinuousParameter and the parameter prop:

import { useState } from "react";
import { ImageKnob } from "@cutoff/audio-ui-react";
 
export default function ImageKnobParameterModelExample() {
  const [value, setValue] = useState(50);
 
  return (
    <ImageKnob
      min={0}
      max={100}
      label="Volume"
      value={value}
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHref="/images/demo/knob-volume.png"
      onChange={(e) => setValue(e.value)}
    />
  );
}

Adaptive Sizing

The ImageKnob component supports both fixed sizes and adaptive sizing. By default, the size of the component is driven by the Size System and their size attribute:

import { useState } from "react";
import { ImageKnob } from "@cutoff/audio-ui-react";
 
export default function ImageKnobSizingExample() {
  const [v1, setV1] = useState(50);
  const [v2, setV2] = useState(50);
  const [v3, setV3] = useState(50);
  const [v4, setV4] = useState(50);
 
  return (
    <div className="flex gap-4 items-center flex-wrap">
      <ImageKnob
        value={v1}
        min={0}
        max={100}
        label="Small"
        size="small"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        onChange={(e) => setV1(e.value)}
      />
      <ImageKnob
        value={v2}
        min={0}
        max={100}
        label="Normal"
        size="normal"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        onChange={(e) => setV2(e.value)}
      />
      <ImageKnob
        value={v3}
        min={0}
        max={100}
        label="Large"
        size="large"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        onChange={(e) => setV3(e.value)}
      />
      <ImageKnob
        value={v4}
        min={0}
        max={100}
        label="XLarge"
        size="xlarge"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        onChange={(e) => setV4(e.value)}
      />
    </div>
  );
}

Adaptive sizing allows the component to adapt to the size of its parent container, adjusting its dimensions intelligently without causing distortion (scaleToFit display mode). The fill display mode makes the component fill the entire container, potentially distorting it.

import { useState } from "react";
import { ImageKnob } from "@cutoff/audio-ui-react";
 
export default function ImageKnobAdaptiveSize() {
  const [wideVal, setWideVal] = useState(50);
  const [tallVal, setTallVal] = useState(50);
  const [distortedVal, setDistortedVal] = useState(50);
 
  return (
    <div className="flex gap-6 items-center flex-wrap">
      <div className="w-48 h-16 border rounded-lg p-2">
        <ImageKnob
          value={wideVal}
          min={0}
          max={100}
          onChange={(e) => setWideVal(e.value)}
          adaptiveSize={true}
          label="Wide"
          frameWidth={100}
          frameHeight={100}
          imageHref="/images/demo/knob-volume.png"
        />
      </div>
      <div className="w-24 h-48 border rounded-lg p-2">
        <ImageKnob
          value={tallVal}
          min={0}
          max={100}
          onChange={(e) => setTallVal(e.value)}
          adaptiveSize={true}
          label="Tall"
          frameWidth={100}
          frameHeight={100}
          imageHref="/images/demo/knob-volume.png"
        />
      </div>
      <div className="w-32 h-20 border rounded-lg p-2">
        <ImageKnob
          value={distortedVal}
          min={0}
          max={100}
          onChange={(e) => setDistortedVal(e.value)}
          adaptiveSize={true}
          displayMode="fill"
          label="Distorted"
          frameWidth={100}
          frameHeight={100}
          imageHref="/images/demo/knob-volume.png"
        />
      </div>
    </div>
  );
}

Interaction Modes

Control how users interact with the knob:

import { useState } from "react";
import { ImageKnob } from "@cutoff/audio-ui-react";
 
export default function ImageKnobInteractionExample() {
  const [dragVal, setDragVal] = useState(50);
  const [wheelVal, setWheelVal] = useState(50);
 
  return (
    <div className="flex gap-4 items-center flex-wrap">
      <ImageKnob
        value={dragVal}
        min={0}
        max={100}
        label="Drag Only"
        interactionMode="drag"
        size="large"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        onChange={(e) => setDragVal(e.value)}
      />
      <ImageKnob
        value={wheelVal}
        min={0}
        max={100}
        label="Wheel Only"
        interactionMode="wheel"
        size="large"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        onChange={(e) => setWheelVal(e.value)}
      />
    </div>
  );
}

Common Use Cases

Volume Knob

import { useState } from "react";
import { ImageKnob } from "@cutoff/audio-ui-react";
 
export default function VolumeKnobExample() {
  const [value, setValue] = useState(50);
 
  return (
    <ImageKnob
      value={value}
      min={0}
      max={100}
      label="Volume"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHref="/images/demo/knob-volume.png"
      valueAsLabel="interactive"
      onChange={(e) => setValue(e.value)}
    />
  );
}

Tone Knob

import { useState } from "react";
import { ImageKnob } from "@cutoff/audio-ui-react";
 
export default function ToneKnobExample() {
  const [value, setValue] = useState(5);
 
  return (
    <ImageKnob
      value={value}
      min={1}
      max={10}
      label="Tone"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHref="/images/demo/knob-tone.png"
      valueAsLabel="interactive"
      onChange={(e) => setValue(e.value)}
    />
  );
}

Direction Indicator

import { useState } from "react";
import { ImageKnob } from "@cutoff/audio-ui-react";
 
export default function DirectionIndicatorExample() {
  const [value, setValue] = useState(3);
 
  return (
    <ImageKnob
      value={value}
      min={0}
      max={10}
      label="Direction"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHref="/images/demo/finger-pointing.png"
      onChange={(e) => setValue(e.value)}
    />
  );
}

Image Requirements

  • Format: Any image format supported by browsers (PNG, JPG, SVG, etc.)
  • Dimensions: The frameWidth and frameHeight props determine the viewBox dimensions
  • Aspect Ratio: The image will be scaled to fit the viewBox while preserving aspect ratio
  • Rotation: The image rotates around its center point based on the normalized value

Rotation Calculation

The rotation angle is calculated as:

  • Normalized value (0-1) maps to rotation range based on openness
  • rotation offset is added to align the image
  • For bipolar mode, the rotation is centered around the midpoint

Next Steps