FilmStripContinuousControl

A continuous control that displays frames from a filmstrip sprite sheet, supporting the industry-standard bitmap-based control representation.

FilmStripContinuousControl is a continuous control that displays frames from a filmstrip sprite sheet. It supports the widely-used industry standard for control representation: bitmap sprite sheets (filmstrips).

While bitmap-based visualization is more constrained than SVG (no dynamic theming, fixed visual appearance), this component shares the same layout, parameter model, interaction, and accessibility as other controls (vector, raster, and control primitives); see the Raster introduction.

The frame displayed is determined by the normalized value (0-1 maps to frame 0 to frameCount-1).

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 a single frame in the filmstrip (pixels)
frameHeightnumber—RequiredHeight of a single frame in the filmstrip (pixels)
frameCountnumber—RequiredTotal number of frames in the filmstrip sprite sheet
imageHrefstring—RequiredURL to the sprite sheet/filmstrip image
orientation"vertical" | "horizontal"verticalOptionalOrientation of the filmstrip (frames stacked vertically or horizontally)
frameRotationnumber0OptionalOptional frame rotation in degrees
invertValuebooleanfalseOptionalIf true, inverts the normalized value (0.0 -> 1.0 and 1.0 -> 0.0). Useful for filmstrips where frame 0 represents the maximum value
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

FilmStripContinuousControl requires a filmstrip sprite sheet image and frame dimensions:

import { useState } from "react";
import { FilmStripContinuousControl } from "@cutoff/audio-ui-react";
 
export default function BasicFilmStripExample() {
  const [value, setValue] = useState(50);
 
  return (
    <FilmStripContinuousControl
      value={value}
      min={0}
      max={100}
      label="VU Meter"
      size="large"
      frameWidth={120}
      frameHeight={80}
      frameCount={48}
      imageHref="/images/demo/vu3.png"
      onChange={(e) => setValue(e.value)}
    />
  );
}

Filmstrip Configuration

Filmstrips are bitmap sprite sheets containing multiple frames stacked vertically (default) or horizontally. Each frame represents a different value state.

Frame Dimensions

Specify the dimensions of a single frame:

import { useState } from "react";
import { FilmStripContinuousControl } from "@cutoff/audio-ui-react";
 
export default function FrameDimensionsExample() {
  const [value, setValue] = useState(50);
 
  return (
    <FilmStripContinuousControl
      value={value}
      min={0}
      max={100}
      label="VU Meter"
      size="large"
      frameWidth={120}
      frameHeight={80}
      frameCount={48}
      imageHref="/images/demo/vu3.png"
      onChange={(e) => setValue(e.value)}
    />
  );
}

Orientation

Control whether frames are stacked vertically or horizontally:

import { useState } from "react";
import { FilmStripContinuousControl } from "@cutoff/audio-ui-react";
 
export default function OrientationExample() {
  const [verticalVal, setVerticalVal] = useState(50);
  const [horizontalVal, setHorizontalVal] = useState(50);
 
  return (
    <div className="flex gap-4 items-center flex-wrap">
      <FilmStripContinuousControl
        value={verticalVal}
        min={0}
        max={100}
        label="Vertical"
        size="large"
        frameWidth={120}
        frameHeight={80}
        frameCount={48}
        imageHref="/images/demo/vu3.png"
        orientation="vertical"
        onChange={(e) => setVerticalVal(e.value)}
      />
      <FilmStripContinuousControl
        value={horizontalVal}
        min={0}
        max={100}
        label="Horizontal"
        size="large"
        frameWidth={120}
        frameHeight={80}
        frameCount={48}
        imageHref="/images/demo/vu3.png"
        orientation="horizontal"
        onChange={(e) => setHorizontalVal(e.value)}
      />
    </div>
  );
}

Frame Rotation

Rotate the entire filmstrip container:

import { useState } from "react";
import { FilmStripContinuousControl } from "@cutoff/audio-ui-react";
 
export default function FrameRotationExample() {
  const [value, setValue] = useState(50);
 
  return (
    <FilmStripContinuousControl
      value={value}
      min={0}
      max={100}
      label="Rotated"
      size="large"
      frameWidth={120}
      frameHeight={80}
      frameCount={48}
      imageHref="/images/demo/vu3.png"
      frameRotation={45}
      onChange={(e) => setValue(e.value)}
    />
  );
}

Value Inversion

Invert the value mapping for filmstrips where frame 0 represents the maximum value:

import { useState } from "react";
import { FilmStripContinuousControl } from "@cutoff/audio-ui-react";
 
export default function ValueInversionExample() {
  const [value, setValue] = useState(50);
 
  return (
    <FilmStripContinuousControl
      value={value}
      min={0}
      max={100}
      label="Inverted"
      size="large"
      frameWidth={120}
      frameHeight={80}
      frameCount={48}
      imageHref="/images/demo/vu3.png"
      invertValue={true}
      onChange={(e) => setValue(e.value)}
    />
  );
}

Value Mapping

The component maps continuous values (0-1 normalized) to frame indices (0 to frameCount-1):

  • Value 0.0 (min) → Frame 0
  • Value 1.0 (max) → Frame (frameCount - 1)
  • Intermediate values are linearly interpolated

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 { FilmStripContinuousControl } from "@cutoff/audio-ui-react";
 
export default function FilmStripParameterModelExample() {
  const [value, setValue] = useState(50);
 
  return (
    <FilmStripContinuousControl
      min={0}
      max={100}
      label="Volume"
      value={value}
      size="large"
      frameWidth={120}
      frameHeight={80}
      frameCount={48}
      imageHref="/images/demo/vu3.png"
      onChange={(e) => setValue(e.value)}
    />
  );
}

Adaptive Sizing

The FilmStripContinuousControl 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 { FilmStripContinuousControl } from "@cutoff/audio-ui-react";
 
export default function FilmStripSizingExample() {
  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">
      <FilmStripContinuousControl
        value={v1}
        min={0}
        max={100}
        label="Small"
        size="small"
        frameWidth={120}
        frameHeight={80}
        frameCount={48}
        imageHref="/images/demo/vu3.png"
        onChange={(e) => setV1(e.value)}
      />
      <FilmStripContinuousControl
        value={v2}
        min={0}
        max={100}
        label="Normal"
        size="normal"
        frameWidth={120}
        frameHeight={80}
        frameCount={48}
        imageHref="/images/demo/vu3.png"
        onChange={(e) => setV2(e.value)}
      />
      <FilmStripContinuousControl
        value={v3}
        min={0}
        max={100}
        label="Large"
        size="large"
        frameWidth={120}
        frameHeight={80}
        frameCount={48}
        imageHref="/images/demo/vu3.png"
        onChange={(e) => setV3(e.value)}
      />
      <FilmStripContinuousControl
        value={v4}
        min={0}
        max={100}
        label="XLarge"
        size="xlarge"
        frameWidth={120}
        frameHeight={80}
        frameCount={48}
        imageHref="/images/demo/vu3.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 { FilmStripContinuousControl } from "@cutoff/audio-ui-react";
 
export default function FilmStripContinuousAdaptiveSize() {
  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-12 border rounded-lg p-2">
        <FilmStripContinuousControl
          value={wideVal}
          min={0}
          max={100}
          onChange={(e) => setWideVal(e.value)}
          adaptiveSize={true}
          label="Wide"
          frameWidth={120}
          frameHeight={80}
          frameCount={48}
          imageHref="/images/demo/vu3.png"
        />
      </div>
      <div className="w-20 h-32 border rounded-lg p-2">
        <FilmStripContinuousControl
          value={tallVal}
          min={0}
          max={100}
          onChange={(e) => setTallVal(e.value)}
          adaptiveSize={true}
          label="Tall"
          frameWidth={120}
          frameHeight={80}
          frameCount={48}
          imageHref="/images/demo/vu3.png"
        />
      </div>
      <div className="w-32 h-20 border rounded-lg p-2">
        <FilmStripContinuousControl
          value={distortedVal}
          min={0}
          max={100}
          onChange={(e) => setDistortedVal(e.value)}
          adaptiveSize={true}
          displayMode="fill"
          label="Distorted"
          frameWidth={120}
          frameHeight={80}
          frameCount={48}
          imageHref="/images/demo/vu3.png"
        />
      </div>
    </div>
  );
}

Interaction Modes

Control how users interact with the filmstrip:

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

Common Use Cases

VU Meter

import { useState } from "react";
import { FilmStripContinuousControl } from "@cutoff/audio-ui-react";
 
export default function VUMeterExample() {
  const [value, setValue] = useState(50);
 
  return (
    <FilmStripContinuousControl
      value={value}
      min={0}
      max={100}
      label="VU Meter"
      size="large"
      frameWidth={120}
      frameHeight={80}
      frameCount={48}
      imageHref="/images/demo/vu3.png"
      valueAsLabel="interactive"
      onChange={(e) => setValue(e.value)}
    />
  );
}

Rotary Knob

import { useState } from "react";
import { FilmStripContinuousControl } from "@cutoff/audio-ui-react";
 
export default function RotaryKnobExample() {
  const [value, setValue] = useState(5);
 
  return (
    <FilmStripContinuousControl
      value={value}
      min={1}
      max={10}
      label="Tone"
      size="large"
      frameWidth={120}
      frameHeight={80}
      frameCount={48}
      imageHref="/images/demo/vu3.png"
      interactionDirection="circular"
      valueAsLabel="interactive"
      onChange={(e) => setValue(e.value)}
    />
  );
}

Filmstrip Image Format

Filmstrip images should follow these conventions:

  • Vertical orientation (default): Frames stacked top to bottom
  • Horizontal orientation: Frames stacked left to right
  • Frame dimensions: All frames must have identical dimensions
  • Frame count: Total number of frames in the sprite sheet
  • Compatibility: Works with filmstrips exported from WebKnobMan and other VST development tools

Next Steps