Beat Grid Geometry

beat-grid-geometryFoundations

Pure maths for the bars-across, dB-down plot, sharing the piano roll's x axis.

$ pnpm dlx shadcn@latest add @pluginui/core-beat-grid-geometry
Loading interactive preview…

Install

Copies the source into your project. Anything it depends on comes with it. Run this in your React project with shadcn initialized; no registry configuration is needed.

Pulls in

piano-roll-geometry
Install using the registry URL
$ pnpm dlx shadcn@latest add https://pluginui.com/r/core-beat-grid-geometry.json

Usage

Connect the documented props to your application or host.

example.tsx
import { createBeatGridLayout } from "maruaudio/ui/shared/beat-grid-geometry"

const layout = createBeatGridLayout({ bars: 2, beatsPerBar: 4, axisDb: 24 })

layout.xForBeat(3)          // where beat 3 sits - the piano roll's own mapping
layout.xForSeconds(1.5, 128) // where 1.5s sits, at 128 BPM
layout.yForDb(0)            // the top of the plot: 0 dB
layout.yForDb(14)           // 14 dB of reduction, drawn downward
layout.ceilingY(-14)        // the budget line, clamped into the plot
layout.dbTicks(4)           // [{ db, y, label }] - round steps, honest floor
layout.dbTicks({ floorLabel: "-∞" }) // only when the floor really IS silence
layout.ticks                // [{ beat, x, kind: "bar" | "beat" | "sub" }]
About Beat Grid Geometry

The layout behind beat-grid-scope is also useful to any component that places content on a tempo-synced plot. Its horizontal helpers provide xForBeat, its inverse, xForSeconds at a tempo (for measured buffers timed in seconds rather than beats), and bar, beat and subdivision ticks classified in one pass so their arrays cannot disagree. Its vertical helpers provide a one-sided dB axis with 0 at the top and reduction growing downward, gutter labels in round decibels (1 / 2 / 3 / 6 / 12) and a final label for the axis's actual depth. A caller scaling the axis to live data might end up only three decibels deep, so an infinity symbol at the bottom would be false; “-infinity” remains available by name when the plot floor really is silence. A ceiling helper clamps a budget into the plot instead of drawing it outside the surface. Tests pin the deliberate reuse: the x axis is piano-roll-geometry's, built on the same createLaneLayout, and every beat placement is checked against a lane layout with the same numbers. A beat grid, drum lane and piano roll therefore place beat 3 at the same fraction of their plots. The dB axis cannot use response-geometry's dbToY, which is symmetric around zero for filter curves; gain reduction is one-sided and belongs at the top with the plot below it. This implementation keeps the shared convention that mapping does not clamp, leaving the caller to decide whether an off-axis curve is clipped or allowed to overflow.

Props

PropTypeDefaultDescription
createBeatGridLayout(options)(BeatGridLayoutOptions) => BeatGridLayout—Resolve a plot from { bars, beatsPerBar, subdivisions, left, right, top, bottom, width, height, axisDb, minAxisDb }. Nonsense inputs resolve to defaults rather than throwing, so a face mounting before its schema resolves renders an empty plot.
xForBeat / beatForX(number) => number—Forward and inverse time-axis mappings. They match piano-roll-geometry's and are pinned against createLaneLayout in the tests. The inverse is unclamped so a drag can read beyond the window's end.
xForSeconds(seconds, bpm)(number, number) => number—A moment in seconds at a tempo, for a measured buffer. A non-positive or non-finite bpm returns the left edge rather than NaN.
yForDb / dbForY(number) => number—The dB axis puts 0 dB at the top and reduction below it. It does not clamp, so a curve may run past the axis; the caller decides whether to clip it.
ceilingY(db)(number) => number—The y of a hard budget. Sign-agnostic, and unlike yForDb it IS clamped into the plot, so a budget deeper than the axis draws on the floor.
dbTicks(count | options)(number | BeatGridDbTickOptions) => BeatGridDbTick[]—Returns { db, y, label } from 0 down to the floor. By default, steps snap to round decibels and the final label shows the axis's actual depth. On a three-decibel plot, the floor is -3; printing -infinity would claim silence the plot never shows. Options are { count, floorLabel, niceSteps }. Set floorLabel to “-infinity” when the floor really is silence, or null to keep the rule and omit the text. Set niceSteps to false to restore even axisDb / count divisions. A bare number still means count.
niceDbStep(axisDb, count)(number, number) => number | null—Round step for gutter labels: 1 / 2 / 3 / 6 / 12 or a multiple of 12, or null if none fits. The step must divide the axis exactly so the final labelled tick does not float above the floor, implying the plot continues. It must also land near the requested tick count; round labels are preferable to a gutter of fifteen.
barX / beatX / subdivisionX / ticksnumber[] / BeatGridTick[]—The grid lines, classified in one pass so a bar line is always also a beat line and the arrays cannot drift apart. Triplet grids classify on the slot index, not a float remainder.
axisDb (resolved)number—The dB span after the minAxisDb floor. Callers scaling the axis to live data should read this value rather than assume it.

Interaction

  • Drag with pointer capture, so the gesture keeps tracking past the edge of the control.
  • Shift while dragging for a fine pass; release it mid-gesture and coarse tracking resumes.
  • Double-click resets to defaultValue.
  • Arrow keys step normally, Shift+arrow steps finely, PageUp/PageDown jump by ten, Home/End go to the extremes.
  • Proper slider and switch roles with live aria-valuetext, so a screen reader announces the formatted value rather than the raw number.
  • The scroll wheel adjusts the value while the pointer is over the control; stepped ranges move at least one declared step per notch by default.