Rendering, Brightness and LEDs
Companion does the drawing. It renders each control according to the user's configuration and pushes the result to your surface instance for the matching control. Your job is to put that onto the hardware — you don't draw button graphics yourself.
draw
draw(signal, drawProps) is called whenever a control changes. What drawProps contains depends
on what you asked for in that control's style preset:
controlId— which control this is (the id from your layout).image— aUint8Arrayof pixels, in the size and format you requested viabitmap.color— a hex background colour, if you requestedcolors.text— button text, if you requestedtext.leds— aUint8Arrayof packed RGB colours (3 bytes per segment, sosegments * 3bytes long) for the control's LED strip or ring, if you requestedleds. See Indicator LEDs below.pageNumber— the Companion page the surface is on, where applicable.
async draw(signal: AbortSignal, drawProps) {
if (signal.aborted) return
const control = this.layoutLookup.get(drawProps.controlId)
if (!control) return
if (drawProps.image) {
await this.device.fillKeyBuffer(control.keyIndex, drawProps.image)
} else if (drawProps.color) {
await this.device.fillKeyColor(control.keyIndex, drawProps.color)
}
}
The signal is an AbortSignal that fires if the draw is superseded before you finish — check it
and bail out early on slow hardware so you don't draw stale frames.
Because Companion pushes finished output, you generally don't need to track button state yourself — render what you're given, when you're given it.
Brightness and clearing
Two related methods round out output:
setBrightness(percent)—0–100. Only called if you setbrightness: trueinregisterProps.blank()— set all pixels to black/off, e.g. on shutdown.
Indicator LEDs
An addressable strip or ring of LEDs belonging to a control — an encoder's ring, a T-bar's LEDs —
is declared with leds on that control's style preset, and arrives as the leds
draw prop. A gauge on the button drives it, and Companion samples the gauge down to one colour per
segment for you:
if (drawProps.leds) {
for (let i = 0; i < segments; i++) {
const color = readLedColor(drawProps.leds, i)
await this.device.setEncoderColor(control.encoderIndex, i, color)
}
}
readLedColor(buffer, index) is exported from @companion-surface/base. For monochrome LEDs,
colorToIntensity(color) flattens a colour to a single 0–255 value.
LEDs require @companion-surface/base v1.4 — see the changelog, and
check supportsLeds on the host capabilities before declaring them.
Gamma correction
Don't send the colours to the hardware unchanged. Companion's colours are perceptually encoded (sRGB-ish), the same as any button graphic, while most LEDs are driven linearly — so a raw passthrough makes the top of the range look flat, with 70% and 100% nearly indistinguishable, and wastes the bottom of the range.
Correct for this with a gamma curve. Build a lookup table once, rather than calling Math.pow() per
segment on every draw:
const LED_GAMMA = 2.5 // customise this
const LED_GAMMA_LUT = new Uint8Array(256)
for (let i = 0; i < 256; i++) {
LED_GAMMA_LUT[i] = Math.round(255 * Math.pow(i / 255, LED_GAMMA))
}
// then, per segment:
const color = readLedColor(drawProps.leds, i)
await this.device.setEncoderColor(control.encoderIndex, i, {
r: LED_GAMMA_LUT[color.r],
g: LED_GAMMA_LUT[color.g],
b: LED_GAMMA_LUT[color.b],
})
The right exponent is a property of your hardware, not of Companion: it depends on whether the
driver is PWM or current-controlled, whether it applies a curve of its own, and how the LEDs
themselves behave. Start around 2.2 (the sRGB-ish value) and tune by eye — sweep a gauge slowly
across the whole range and check that the steps look evenly spaced.
If your LED driver has more than 8 bits of resolution, apply the curve into that wider range instead of back into 8 bits. An 8-bit result loses a lot of precision at the dark end, where many input values collapse onto the same output.
LEDs that aren't attached to a control at all — standalone status lights — are instead modelled as output transfer variables rather than draws; see Input & variables.
See the generated reference for the exact
SurfaceDrawProps and pixel formats, and the
Elgato Stream Deck module for
real image handling.