Write an effect
An effect is a small TypeScript file: a function that, thirty times a second, is given the keyboard and the time and paints every key. You write it in Candeo, next to a simulator drawing what it would send, and apply it when it looks right.
Your first effect
- In Effects, click + New effect. The editor opens on a template, with the keyboard simulator on the right.
- Replace the code with the one below, name it Rainbow, and Save: the simulator plays it. Nothing reaches the keyboard yet.
- Apply it on your device when it looks right.
import { bounds, center, defineEffect, hsv } from '@candeo/effects-api'
export default defineEffect({
description: 'A rainbow sweeping across the keyboard',
render({ layout, time, frame }) {
const area = bounds(layout)
for (const key of layout.keys) {
const across = (center(key).x - area.x) / area.w
frame.set(key, hsv(across * 360 - time * 60, 1, 1))
}
},
})
Every key gets a hue from where it sits across the keyboard, and the whole rainbow
slides as time goes by. The file lands in
Documents\candeo\effects\Rainbow.ts: its name is the effect's name. Edit it in
the editor you already use if you prefer, then Refresh in the gallery.
The editor checks the types as you write, with completion for everything below.
What an effect receives
| Name | What it is |
|---|---|
layout |
the device: its keys, each with its place in the matrix
(row, col), its keycap in key units (x,
y, w, h), its name in your keyboard layout
and its scancode
|
time |
seconds since the effect started: animate on it |
frame |
frame.set(key, colour) and frame.fill(colour) |
params |
the settings you declare, as the person set them — see below |
presses |
the keys pressed in the last ten seconds, each with its key and when
(inputs: ['keys'])
|
clock |
the local time: hours, minutes, seconds, the date (inputs: ['clock'])
|
signals |
every value other programs sent, by name (inputs: ['signals'], see
signals)
|
audio |
the sound playing: level, sixteen bands, beat (inputs: ['audio'], see
sound)
|
An input is given only to an effect that declares it, and the gallery marks it: key presses are read, and the sound captured, only while such an effect runs.
Settings people can change
Declared in params, a setting gets its control in the gallery, and arrives in
params typed from its declaration — no casts. Labels and the description
take a string or one per language.
import { defineEffect, mix, BLACK } from '@candeo/effects-api'
export default defineEffect({
description: { en: 'A colour breathing in and out', fr: 'Une couleur qui respire' },
params: {
color: { kind: 'color', label: { en: 'Colour', fr: 'Couleur' }, default: { r: 0, g: 160, b: 255 } },
speed: { kind: 'number', label: { en: 'Breaths a minute', fr: 'Respirations par minute' }, min: 5, max: 60, default: 15 },
},
render({ time, frame, params }) {
const breath = 0.5 - 0.5 * Math.cos((time * params.speed * Math.PI * 2) / 60)
frame.fill(mix(BLACK, params.color, breath))
},
})| Kind | Control | Declares |
|---|---|---|
number |
a slider | min, max, step |
color |
a colour picker | a default { r, g, b } |
boolean |
a switch | |
choice |
a list | options, a value each, with a label if you like |
text |
a one-line field | maxLength, 64 unless said, 256 at most |
Every setting can follow a signal, and a number, a switch or a colour can follow the sound, from the gallery: your effect needs no code for it.
Helpers
From @candeo/effects-api |
What it does |
|---|---|
hsv(h, s, v) |
a colour from a hue in degrees, saturation and value 0 to 1 |
rgb(r, g, b), BLACK |
a colour, each channel clamped to 0–255 |
mix(a, b, t), lerp(a, b, t) |
between two colours, or two numbers, t from 0 to 1 |
center(key) |
the middle of a keycap, in key units: where its LED is |
bounds(layout) |
the keyboard's footprint: x, y, w, h |
banner(text) |
text in a 3×5 font, five lines of # and ., as Clock draws |
Distances are in key units — one is the width of a letter key — so "a ring every six keys"
looks the same on a full keyboard and a small one. Find the middle with
bounds, never from rows and columns: a space bar is one cell of the matrix
but six keys wide.
Recipes
Rings from the middle
import { bounds, center, defineEffect, hsv } from '@candeo/effects-api'
export default defineEffect({
description: 'Rings of colour spreading from the middle of the keyboard',
render({ layout, time, frame }) {
const area = bounds(layout)
const cx = area.x + area.w / 2
const cy = area.y + area.h / 2
for (const key of layout.keys) {
const c = center(key)
// In keys: a ring every six keys, whatever the keyboard's size.
const d = Math.hypot(c.x - cx, c.y - cy)
frame.set(key, hsv(d * 60 - time * 120, 1, 1))
}
},
})Keys that glow when you type
import { center, defineEffect, mix, BLACK } from '@candeo/effects-api'
const GLOW = { r: 255, g: 120, b: 0 }
export default defineEffect({
description: 'Each key you press glows, and its neighbours with it',
inputs: ['keys'],
render({ layout, time, presses, frame }) {
for (const key of layout.keys) {
const c = center(key)
let light = 0
for (const press of presses) {
const p = center(press.key)
const near = Math.max(0, 1 - Math.hypot(c.x - p.x, c.y - p.y) / 2)
const fading = Math.max(0, 1 - (time - press.at) / 0.8)
light = Math.max(light, near * fading)
}
frame.set(key, mix(BLACK, GLOW, light))
}
},
})Presses are key positions and times, never characters, and are not kept beyond ten seconds.
A word on the keys
import { banner, bounds, center, defineEffect } from '@candeo/effects-api'
export default defineEffect({
description: 'A word, still, in the middle of the keyboard',
params: {
word: { kind: 'text', label: 'Word', maxLength: 5, default: 'HI' },
},
render({ layout, frame, params }) {
const lines = banner(params.word)
const area = bounds(layout)
const left = area.x + (area.w - lines[0].length) / 2
for (const key of layout.keys) {
const column = Math.floor(center(key).x - left)
const lit = key.row < 5 && lines[key.row]?.[column] === '#'
frame.set(key, lit ? { r: 255, g: 255, b: 255 } : { r: 0, g: 0, b: 30 })
}
},
})A level meter from the sound
import { bounds, center, defineEffect, hsv } from '@candeo/effects-api'
export default defineEffect({
description: 'The top row as a level meter of the sound playing',
inputs: ['audio'],
render({ layout, audio, frame }) {
const area = bounds(layout)
for (const key of layout.keys) {
const across = (center(key).x - area.x) / area.w
const lit = key.row === 0 && across < audio.level
frame.set(key, lit ? hsv(120 - across * 120, 1, 1) : hsv(0, 0, audio.beat ? 0.3 : 0))
}
},
})A value another program sends
import { defineEffect, mix } from '@candeo/effects-api'
const CALM = { r: 0, g: 200, b: 80 }
const BUSY = { r: 255, g: 30, b: 0 }
export default defineEffect({
description: 'The keyboard from green to red with the CPU load a script sends',
inputs: ['signals'],
render({ signals, frame }) {
const load = Number(signals.cpu ?? 0)
frame.fill(mix(CALM, BUSY, Math.min(1, Math.max(0, load))))
},
})For a single value, a setting following the signal is simpler: this reads every signal at once, for an effect drawing many.
What the engine expects
-
Animate on
time, not on the frame number: the engine aims at thirty frames a second but does not make up the ones a busy computer misses. - Ten milliseconds a frame, 32 MB in all: plenty for a loop over every key. An effect failing thirty frames in a row is stopped, the keyboard turned off, and the gallery says why.
-
No browser, no Node: effects run in an engine of their own, with no
document, nofetch, noconsole, no file access. An effect reaches nothing but its keys. -
State between frames lives outside
render, in the module: a list of rings, a moving average. It starts over when the effect restarts. -
Layouts without geometry:
centerandboundsfail loudly, naming the key, rather than drawing black. Rows and columns always work.
Learn from the shipped ones
The twenty-six effects that ship with Candeo are written against this same API. Open one
in the editor, or Duplicate it to make it yours:
packages/effects. An effect is a
file: share it, drop one in Documents\candeo\effects, and
Refresh.