FilmStripDiscreteControl

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

FilmStripDiscreteControl is a discrete 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 mapping the current value to a frame index based on the number of options.

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
valuestring | number—RequiredCurrent value of the control
onChange(event: AudioControlEvent<string | number>) => void—OptionalHandler for value changes
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
adaptiveSizebooleanfalseOptionalWhether the component stretches to fill its container
size"xsmall" | "small" | "normal" | "large" | "xlarge"normalOptionalSize of the component
parameterDiscreteParameter—OptionalAudio Parameter definition (Model). If provided, overrides options/label from ad-hoc props
optionsArray<{ value: string | number; label?: string; midiValue?: number }>—OptionalArray of option definitions (strict mode)
childrenReact.ReactNode—OptionalOptionView children for ad-hoc or hybrid mode

Understanding Options vs Children

FilmStripDiscreteControl distinguishes between two concepts:

  • options prop: Defines the parameter model (value, label, midiValue). Used for parameter structure.
  • children (OptionView components): In this component, discrete visuals are handled by the filmstrip image (each frame), not by OptionView content. OptionView here is mainly syntactic sugar: it populates the option set in ad-hoc mode (values and count); the component uses the number of OptionView children to derive frame index. The visual content inside OptionView (e.g. text or icons) is never displayed.

These serve different purposes and can be used together: use options or a parameter when you have data-driven option definitions or need MIDI mapping; use OptionView children for simple, ad-hoc setups when you want the option set inferred from the children.

Note:

Basic use: For basic use of FilmStripDiscreteControl, and especially when using ad-hoc props (no parameter prop), prefer OptionView children. In that case the option set is inferred from the OptionViews and the frame count should match the number of options. Use the options prop or a full parameter when you need explicit parameter structure (e.g. data-driven options, MIDI mapping). For a full disambiguation of options vs OptionView, see Options vs OptionView.

Basic Usage

FilmStripDiscreteControl requires a filmstrip sprite sheet image and frame dimensions. Use OptionView children to define the discrete options:

import { useState } from "react";
import { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";
 
export default function BasicFilmStripDiscreteExample() {
  const [value, setValue] = useState(4);
 
  return (
    <FilmStripDiscreteControl
      value={value}
      label="Traffic Light"
      size="large"
      frameWidth={103}
      frameHeight={260}
      frameCount={8}
      imageHref="/images/demo/traffic-light-filmstrip.png"
      onChange={(e) => setValue(e.value)}
    >
      {[1, 2, 3, 4, 5, 6, 7, 8].map((n) => (
        <OptionView key={n} value={n}>State {n}</OptionView>
      ))}
    </FilmStripDiscreteControl>
  );
}

Modes of Operation

FilmStripDiscreteControl supports three modes of operation:

Ad-Hoc Mode (Children Only)

Model inferred from OptionView children:

import { useState } from "react";
import { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";
 
export default function AdHocFilmStripDiscreteExample() {
  const [value, setValue] = useState(4);
  const options = [1, 2, 3, 4, 5, 6, 7, 8];
 
  return (
    <FilmStripDiscreteControl
      value={value}
      label="Traffic Light"
      size="large"
      frameWidth={103}
      frameHeight={260}
      frameCount={8}
      imageHref="/images/demo/traffic-light-filmstrip.png"
      onChange={(e) => setValue(e.value)}
    >
      {options.map((n) => (
        <OptionView key={n} value={n}>State {n}</OptionView>
      ))}
    </FilmStripDiscreteControl>
  );
}

Strict Mode (Parameter Only)

Model provided via parameter prop. The following example uses children (OptionView); with the full library you would use createDiscreteParameter and the parameter prop:

import { useState } from "react";
import { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";
 
export default function StrictModeFilmStripDiscreteExample() {
  const [value, setValue] = useState(4);
  const options = [1, 2, 3, 4, 5, 6, 7, 8];
 
  return (
    <FilmStripDiscreteControl
      value={value}
      label="Traffic Light"
      size="large"
      frameWidth={103}
      frameHeight={260}
      frameCount={8}
      imageHref="/images/demo/traffic-light-filmstrip.png"
      onChange={(e) => setValue(e.value)}
    >
      {options.map((n) => (
        <OptionView key={n} value={n}>State {n}</OptionView>
      ))}
    </FilmStripDiscreteControl>
  );
}

Hybrid Mode (Parameter + Children)

Model from parameter, View from children (matched by value). Use the same pattern as Strict Mode with OptionView children when the parameter is not in scope.

import { useState } from "react";
import { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";
 
export default function HybridModeFilmStripDiscreteExample() {
  const [value, setValue] = useState(4);
  const options = [1, 2, 3, 4, 5, 6, 7, 8];
 
  return (
    <FilmStripDiscreteControl
      value={value}
      label="Traffic Light"
      size="large"
      frameWidth={103}
      frameHeight={260}
      frameCount={8}
      imageHref="/images/demo/traffic-light-filmstrip.png"
      onChange={(e) => setValue(e.value)}
    >
      {options.map((n) => (
        <OptionView key={n} value={n}>State {n}</OptionView>
      ))}
    </FilmStripDiscreteControl>
  );
}

Frame Mapping

The component maps discrete option values to frame indices:

  • Option index 0 → Frame 0
  • Option index 1 → Frame 1
  • Option index N → Frame N

The number of frames in the filmstrip should match the number of options.

Filmstrip Configuration

Frame Dimensions

Specify the dimensions of a single frame:

import { useState } from "react";
import { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";
 
export default function FrameDimensionsExample() {
  const [value, setValue] = useState(4);
  const options = [1, 2, 3, 4, 5, 6, 7, 8];
 
  return (
    <FilmStripDiscreteControl
      value={value}
      label="Traffic Light"
      size="large"
      frameWidth={103}
      frameHeight={260}
      frameCount={8}
      imageHref="/images/demo/traffic-light-filmstrip.png"
      onChange={(e) => setValue(e.value)}
    >
      {options.map((n) => (
        <OptionView key={n} value={n}>State {n}</OptionView>
      ))}
    </FilmStripDiscreteControl>
  );
}

Orientation

Control whether frames are stacked vertically or horizontally:

import { useState } from "react";
import { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";
 
export default function OrientationExample() {
  const [value, setValue] = useState(4);
  const options = [1, 2, 3, 4, 5, 6, 7, 8];
 
  return (
    <FilmStripDiscreteControl
      value={value}
      label="Traffic Light"
      size="large"
      frameWidth={103}
      frameHeight={260}
      frameCount={8}
      imageHref="/images/demo/traffic-light-filmstrip.png"
      orientation="vertical"
      onChange={(e) => setValue(e.value)}
    >
      {options.map((n) => (
        <OptionView key={n} value={n}>State {n}</OptionView>
      ))}
    </FilmStripDiscreteControl>
  );
}

Frame Rotation

Rotate the entire filmstrip container:

import { useState } from "react";
import { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";
 
export default function FrameRotationExample() {
  const [value, setValue] = useState(4);
  const options = [1, 2, 3, 4, 5, 6, 7, 8];
 
  return (
    <FilmStripDiscreteControl
      value={value}
      label="Traffic Light"
      size="large"
      frameWidth={103}
      frameHeight={260}
      frameCount={8}
      imageHref="/images/demo/traffic-light-filmstrip.png"
      frameRotation={90}
      onChange={(e) => setValue(e.value)}
    >
      {options.map((n) => (
        <OptionView key={n} value={n}>State {n}</OptionView>
      ))}
    </FilmStripDiscreteControl>
  );
}

Adaptive Sizing

The FilmStripDiscreteControl 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 { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";
 
export default function FilmStripDiscreteSizingExample() {
  const [v1, setV1] = useState(4);
  const [v2, setV2] = useState(4);
  const [v3, setV3] = useState(4);
  const [v4, setV4] = useState(4);
  const options = [1, 2, 3, 4, 5, 6, 7, 8];
 
  return (
    <div className="flex gap-4 items-center flex-wrap">
      <FilmStripDiscreteControl
        value={v1}
        label="Small"
        size="small"
        frameWidth={103}
        frameHeight={260}
        frameCount={8}
        imageHref="/images/demo/traffic-light-filmstrip.png"
        onChange={(e) => setV1(e.value)}
      >
        {options.map((n) => (
          <OptionView key={n} value={n}>State {n}</OptionView>
        ))}
      </FilmStripDiscreteControl>
      <FilmStripDiscreteControl
        value={v2}
        label="Normal"
        size="normal"
        frameWidth={103}
        frameHeight={260}
        frameCount={8}
        imageHref="/images/demo/traffic-light-filmstrip.png"
        onChange={(e) => setV2(e.value)}
      >
        {options.map((n) => (
          <OptionView key={n} value={n}>State {n}</OptionView>
        ))}
      </FilmStripDiscreteControl>
      <FilmStripDiscreteControl
        value={v3}
        label="Large"
        size="large"
        frameWidth={103}
        frameHeight={260}
        frameCount={8}
        imageHref="/images/demo/traffic-light-filmstrip.png"
        onChange={(e) => setV3(e.value)}
      >
        {options.map((n) => (
          <OptionView key={n} value={n}>State {n}</OptionView>
        ))}
      </FilmStripDiscreteControl>
      <FilmStripDiscreteControl
        value={v4}
        label="XLarge"
        size="xlarge"
        frameWidth={103}
        frameHeight={260}
        frameCount={8}
        imageHref="/images/demo/traffic-light-filmstrip.png"
        onChange={(e) => setV4(e.value)}
      >
        {options.map((n) => (
          <OptionView key={n} value={n}>State {n}</OptionView>
        ))}
      </FilmStripDiscreteControl>
    </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 { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";
 
export default function FilmStripDiscreteAdaptiveSize() {
  const [wideVal, setWideVal] = useState(4);
  const [tallVal, setTallVal] = useState(4);
  const [distortedVal, setDistortedVal] = useState(4);
  const options = [1, 2, 3, 4, 5, 6, 7, 8];
 
  return (
    <div className="flex gap-6 items-center flex-wrap">
      <div className="w-28 h-40 border rounded-lg p-2">
        <FilmStripDiscreteControl
          value={wideVal}
          onChange={(e) => setWideVal(e.value)}
          adaptiveSize={true}
          label="Wide"
          frameWidth={103}
          frameHeight={260}
          frameCount={8}
          imageHref="/images/demo/traffic-light-filmstrip.png"
        >
          {options.map((n) => (
            <OptionView key={n} value={n}>State {n}</OptionView>
          ))}
        </FilmStripDiscreteControl>
      </div>
      <div className="w-20 h-48 border rounded-lg p-2">
        <FilmStripDiscreteControl
          value={tallVal}
          onChange={(e) => setTallVal(e.value)}
          adaptiveSize={true}
          label="Tall"
          frameWidth={103}
          frameHeight={260}
          frameCount={8}
          imageHref="/images/demo/traffic-light-filmstrip.png"
        >
          {options.map((n) => (
            <OptionView key={n} value={n}>State {n}</OptionView>
          ))}
        </FilmStripDiscreteControl>
      </div>
      <div className="w-24 h-32 border rounded-lg p-2">
        <FilmStripDiscreteControl
          value={distortedVal}
          onChange={(e) => setDistortedVal(e.value)}
          adaptiveSize={true}
          displayMode="fill"
          label="Distorted"
          frameWidth={103}
          frameHeight={260}
          frameCount={8}
          imageHref="/images/demo/traffic-light-filmstrip.png"
        >
          {options.map((n) => (
            <OptionView key={n} value={n}>State {n}</OptionView>
          ))}
        </FilmStripDiscreteControl>
      </div>
    </div>
  );
}

Common Use Cases

Traffic Light (Multiple States)

import { useState } from "react";
import { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";
 
export default function TrafficLightExample() {
  const [value, setValue] = useState(4);
  const options = [1, 2, 3, 4, 5, 6, 7, 8];
 
  return (
    <FilmStripDiscreteControl
      value={value}
      label="Traffic Light"
      size="large"
      frameWidth={103}
      frameHeight={260}
      frameCount={8}
      imageHref="/images/demo/traffic-light-filmstrip.png"
      onChange={(e) => setValue(e.value)}
    >
      {options.map((n) => (
        <OptionView key={n} value={n}>State {n}</OptionView>
      ))}
    </FilmStripDiscreteControl>
  );
}

Waveform Selector

import { useState } from "react";
import { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";
 
export default function WaveformSelectorExample() {
  const [value, setValue] = useState(4);
  const options = [1, 2, 3, 4, 5, 6, 7, 8];
 
  return (
    <FilmStripDiscreteControl
      value={value}
      label="Traffic Light"
      size="large"
      frameWidth={103}
      frameHeight={260}
      frameCount={8}
      imageHref="/images/demo/traffic-light-filmstrip.png"
      onChange={(e) => setValue(e.value)}
    >
      {options.map((n) => (
        <OptionView key={n} value={n}>State {n}</OptionView>
      ))}
    </FilmStripDiscreteControl>
  );
}

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: Must match the number of discrete options
  • Compatibility: Works with filmstrips exported from WebKnobMan and other VST development tools

Next Steps