Skip to content
design
Instruments

Roll

The roll (heel) of the vessel in degrees on a bottom arc, with an average band and caution zones.

On this page
Both arcs, the yacht enlarged and the ten-second band
A 60 degree arc, zoomed to fill the box
14.0
Heel
DEG
The same heel as a number
Close-hauled in gusts on a live feed: the yacht heels past the caution threshold, on both arcs, zoomed onto a narrow arc and as a number.
Code
import { ObcRollType, Priority, Roll, VesselImage } from "@bosunkit/instruments";
import { useEffect, useState, type CSSProperties, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "Close-hauled in gusts on a live feed: the yacht heels to starboard and rolls in the swell, the band of the last " +
  "ten seconds follows, and the 20 degree caution zones trigger in the gusts, on both arcs, zoomed onto a narrow arc " +
  "and as a number.";

const SAMPLE_SECONDS: number = 0.5;
const TREND_SECONDS: number = 10;
const CAUTION_DEGREES: number = 20;

const PANEL: CSSProperties = {
  display: "flex",
  flexWrap: "wrap",
  alignItems: "flex-end",
  justifyContent: "center",
  gap: 24,
  width: "100%",
  maxWidth: 660,
};

const FIGURE: CSSProperties = { display: "flex", flexDirection: "column", alignItems: "center", gap: 6, margin: 0 };
const CAPTION: CSSProperties = { fontSize: 13, color: "var(--text-dim)", textAlign: "center", maxWidth: 200 };

/** Heel in degrees at a time in seconds, positive to starboard: a steady heel, gusts and the roll of the swell. */
function rollAt(time: number): number {
  const gust: number = Math.max(0, Math.sin(time / 6)) ** 3;
  return 14 + 8 * gust + 3 * Math.sin((2 * Math.PI * time) / 6.5);
}

/** The lowest and highest heel over the trend window ending at `time`. */
function bandAt(time: number): { low: number; high: number } {
  let low: number = Number.POSITIVE_INFINITY;
  let high: number = Number.NEGATIVE_INFINITY;
  for (let past: number = 0; past <= TREND_SECONDS; past += SAMPLE_SECONDS) {
    const value: number = rollAt(time - past);
    low = Math.min(low, value);
    high = Math.max(high, value);
  }
  return { low, high };
}

export default function Showcase(): ReactElement {
  const [sample, setSample]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);

  useEffect((): (() => void) => {
    const interval: number = window.setInterval((): void => {
      setSample((previous: number): number => previous + 1);
    }, SAMPLE_SECONDS * 1000);
    return (): void => {
      window.clearInterval(interval);
    };
  }, []);

  const time: number = sample * SAMPLE_SECONDS;
  const roll: number = rollAt(time);
  const { low, high }: { low: number; high: number } = bandAt(time);
  const beyondCaution: boolean = high > CAUTION_DEGREES;

  return (
    <div style={PANEL}>
      <figure style={FIGURE}>
        <div style={{ width: 260, height: 260 }}>
          <Roll
            roll={roll}
            minAvgRoll={low}
            maxAvgRoll={high}
            vesselImageFore={VesselImage.sailingYachtFore}
            scaleForeImage={1.4}
            maxRollAdvice={CAUTION_DEGREES}
            triggerRollAdvice={beyondCaution}
            type={ObcRollType.dualScale}
            priority={Priority.enhanced}
          />
        </div>
        <figcaption style={CAPTION}>Both arcs, the yacht enlarged and the ten-second band</figcaption>
      </figure>

      <figure style={FIGURE}>
        <div style={{ width: 170, height: 170 }}>
          <Roll
            roll={roll}
            minAvgRoll={low}
            maxAvgRoll={high}
            vesselImageFore={VesselImage.sailingYachtFore}
            maxRollAdvice={CAUTION_DEGREES}
            triggerRollAdvice={beyondCaution}
            arcAngle={30}
            zoomToFitArc
          />
        </div>
        <figcaption style={CAPTION}>A 60 degree arc, zoomed to fill the box</figcaption>
      </figure>

      <figure style={FIGURE}>
        <div style={{ width: 150, height: 150 }}>
          <Roll
            roll={roll}
            minAvgRoll={low}
            maxAvgRoll={high}
            maxRollAdvice={CAUTION_DEGREES}
            triggerRollAdvice={beyondCaution}
            hasReadout
            label="Heel"
            unit="DEG"
            fractionDigits={1}
            priority={Priority.enhanced}
          />
        </div>
        <figcaption style={CAPTION}>The same heel as a number</figcaption>
      </figure>
    </div>
  );
}

The roll instrument shows the vessel's heel in degrees on an arc at the bottom, positive when the vessel rolls to starboard. A fore-view silhouette and an indicator line turn with the value, a band marks the recent average, and caution zones start at the threshold the crew sets. It ports obc-roll, which shares the single-axis inclinometer with Pitch; the overview lists the other instruments.

Examples

Roll

A heel of 8 degrees to starboard, with the indicator turned to match.
Code
import { Roll } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string = "A heel of 8 degrees to starboard, with the indicator turned to match.";

export default function RollExample(): ReactElement {
  return (
    <div style={{ width: 200, height: 200 }}>
      <Roll roll={8} />
    </div>
  );
}

Starboard convention and limits

Roll is positive to starboard. Keep the sensor reference, numeric readout and vessel silhouette consistent, and identify the window represented by the recent-average band. Do not infer a safe operating envelope from the size of the dial.

Caution zones depict configured limits; alert generation and acknowledgment live outside the instrument. Test both signs and missing measurements with the actual mounting convention. A centered silhouette alone cannot distinguish an upright vessel from a lost sensor.

Props

PropTypeDefaultDescription
rollnumber | undefined0Roll (heel) angle in degrees; positive rolls to starboard.
minAvgRollnumber | undefined0Lower edge of the average-roll band, degrees.
maxAvgRollnumber | undefined0Upper edge of the average-roll band, degrees.
vesselImageForeVesselImage | undefinedVesselImage.psvForeFore-view vessel silhouette rolling with the value.
scaleForeImagenumber | undefined1Extra vessel scaling (clamped to [0, 2]).
maxRollAdvicenumber | undefined–Inner caution threshold in degrees; undefined hides the caution bands.
triggerRollAdviceboolean | undefinedfalseWhether the caution bands are in the triggered state.
labelstring | undefined"Roll"Readout label. Default Roll.
unitstring | undefined"DEG"Readout unit. Default DEG.
fractionDigitsnumber | undefined0Number of fraction digits shown in the readout. Default 0.
typeObcRollType | undefinedObcRollType.singleScalesingle-scale shows one arc at the bottom (default); dual-scale also shows the scale on the top arc (the indicator's opposite end).
zoomToFitArcboolean | undefinedfalseEnlarges the visible arc to fill the container; the vessel and the indicator keep their natural size. Default false.
hasReadoutboolean | undefinedfalseCenter readout instead of the horizon/indicator/vessel.
priorityPriority | undefinedPriority.regularColor priority: regular (gray palette, default) or enhanced (blue palette) for the scale fill, the indicator and the readout value.
arcAnglenumber | undefined45Half-extent of the watch arc in degrees (default 45).
classNamestring | undefined–Class names added to the instrument's root element.