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

  1. In Effects, click + New effect. The editor opens on a template, with the keyboard simulator on the right.
  2. Replace the code with the one below, name it Rainbow, and Save: the simulator plays it. Nothing reaches the keyboard yet.
  3. Apply it on your device when it looks right.
Rainbow.ts
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.

Breathing glow.ts
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

Rings.ts
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

Typing glow.ts
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

Hello.ts
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

Level meter.ts
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

CPU load.ts
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

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.