Skip to content

5 · Heat

So far every colour in the blaster has been a fixed swatch. This part replaces them with one number and one expression, and the prop stops looking scripted.

Live Tap TRIGGER repeatedly. The barrel reddens and brightens as it heats; about eight shots in a row cooks it and it vents blue. Stop, and it cools off on its own.

Open DEFINES ▸ Variables. Type heat into + new var, then ambient. Leave both at 0.

The Variables section of the Studio tree showing two cards, heat and ambient, each with a value box containing 0 and a notes fold, next to a '+ new var' box.
A var is a name and a starting value. That is the whole feature.

Effects read them, actions write them. Three more are built in and never declared: time (seconds since start), audioLevel (0–1, live loudness) and unitId (this board’s own number).

2 · Anywhere a number goes, an expression goes

Section titled “2 · Anywhere a number goes, an expression goes”

This is the part that is easy to miss in a graphical editor: every numeric and colour box on every card also accepts an expression. Type one instead of a value and it is re-evaluated every frame.

Change the idle script’s blink to a pattern (use the verb dropdown on the row — it keeps the step’s flow, loop and notes, and swaps only the verb’s own parameters), and put this in its colour box:

hsv(0.09 - heat*0.09, 1, 0.16 + 0.55*heat + 0.10*sin(time*3 + i*0.7))

One line, re-evaluated for every pixel, every frame:

  • hsv(h, s, v) takes 0–1 arguments and returns a colour. Hue 0.09 is orange; 0.09 - heat*0.09 slides it to 0.00, pure red, as heat reaches 1.
  • The value channel climbs with heat, so a hot barrel is a bright barrel.
  • sin(time*3 + i*0.7) adds a shimmer offset per pixel — pattern hands the expression i (the pixel index) and count (how many there are).

Four verbs animate a var, all in the palette’s Actions group. Picking the right one matters more than it looks:

Verb What it does Ends?
set assign it, now instantly
ramp move toward to at rate units/sec when it arrives
tween move to to over sec, along an ease when the time is up
follow glide continuously toward another var at rate never

Firing uses set, because a shot is an event. On the Firing state’s on enter, add set and target heat — then type an expression into its value box rather than a number:

min(heat + 0.17, 1)

Cooling uses follow, because cooling is a standing fact about the prop rather than an event. Add a follow step to the top of the idle script’s list: follow heat, target ambient, rate 0.22.

Every when row has an if box at its right-hand end, next to then go →. Put heat < 1 in the one on trigger.tapped.

The Ready state card in the Studio showing a when trigger.tapped handler with a do list, a 'then go → Firing' dropdown and an 'if' box containing 'heat < 1'.
then go → Firing, if heat < 1. The guard is one box on the row.

if is an expression evaluated at the moment the event arrives. If it is false the handler does not fire at all — and because the Studio works out which buttons are live from which handlers can fire, the trigger greys itself out when the barrel is cooked. You configured no enable/disable logic.

5 · Rules: a guard that watches every state

Section titled “5 · Rules: a guard that watches every state”

Open BEHAVIORS ▸ Rules ▸ + new rule.

A rule card in the Studio titled 'heat >= 1', with a 'when' box containing heat >= 1 and the hint 'fires on the RISING edge, in any state', an empty DO list with an add bar, and a 'then go → Overheated' dropdown.
when · do · then go. A rule is a handler with no event — just a condition.

Put heat >= 1 in the when box, leave do empty, and set then go → to Overheated.

A rule is a condition watched in every state, firing once on the rising edge. This is the overheat cut-out, and it is one card instead of an identical handler copied into Ready, Firing and every state you add later.

Add the Overheated state, and a vent script for it to run:

Step Verb Set flow
1 play powerdown.wav, vol 0.6 detached
2 ramp heat → 0 @ 0.5 detached
3 blink @barrel, #4d8cff, sec 0.3, off #02060f, stop after 2.2 serial

The blue blink is the only serial child, which makes it the thing that decides how long venting takes — change its stop after and the state comes back sooner, with nothing else to adjust.

The live preview showing the TRIGGER button above a GPIO16 barrel strip whose eight pixels glow a dim orange.
The barrel at rest — dim and orange. Fire and it walks toward bright red.
step-05-heat.json 98 lines
{
  // ===========================================================
  //  BLASTER · STEP 5 — Heat
  //  Stop hard-coding what the prop looks like. Track ONE
  //  number, point the colours at it, and let the number move.
  // ===========================================================
  "schemaVersion": 1,
  "name": "Blaster 05 - Heat",
  "board": "t-display-s3",

  // A var is a named number with a starting value. That is all.
  //   heat    0 = cold, 1 = cooked
  //   ambient the temperature heat always slides back toward.
  //           It is a var and not the literal 0 because `follow`
  //           chases a MOVING target — "room temperature" is
  //           just a target that happens never to move. Make it
  //           move later and the prop gets a cooling fan.
  "vars": { "heat": 0, "ambient": 0 },

  "devices": {
    "barrel": { "kind": "ledstrip", "count": 8, "pin": 16, "order": "GRB", "brightness": 160 }
  },

  "inputs": {
    "trigger": { "label": "TRIGGER", "color": "#ff3b30", "pin": 14, "activeLow": true, "pull": "up" }
  },

  "scripts": {
    "idle": {
      "children": [
        // Always cooling, while we are idle. `follow` glides one
        // number toward another forever at a fixed rate; it never
        // finishes, so it detaches itself and just runs.
        { "follow": "heat", "target": "ambient", "rate": 0.22 },

        // The glow is now ONE EXPRESSION, re-evaluated per pixel,
        // every frame:
        //   hue   0.09 (orange) slides to 0.00 (red) as heat rises
        //   value climbs with heat, plus a slow per-pixel shimmer
        // `pattern` hands the expression `i` (pixel index) and
        // `count`; every expression can also read `time` and any var.
        { "pattern": "@barrel", "loop": true,
          "color": "hsv(0.09 - heat*0.09, 1, 0.16 + 0.55*heat + 0.10*sin(time*3 + i*0.7))" }
      ]
    },

    "shot": {
      "children": [
        { "tone": 720, "wave": "square", "seconds": 0.09, "volume": 0.35, "flow": "detached" },
        { "set": "@barrel", "color": "#ffffff", "sec": 0.04 },
        { "comet": "@barrel", "color": "hsv(0.09 - heat*0.09, 0.9, 1)", "lapSec": 0.2, "tail": 5, "for": 0.2 },
        { "wait": 0.07 }
      ]
    },

    // The cost of firing too much. The blue vent blink runs for
    // 2.2 s and is the only SERIAL child, so it is what decides
    // how long the whole script takes; the sound and the forced
    // cool-down ride alongside it.
    "vent": {
      "children": [
        { "play": "powerdown.wav", "volume": 0.6, "flow": "detached" },
        { "ramp": "heat", "to": 0, "rate": 0.5, "flow": "detached" },
        { "blink": "@barrel", "color": "#4d8cff", "off": "#02060f", "sec": 0.3, "for": 2.2 }
      ]
    }
  },

  // A RULE watches an expression in EVERY state and fires once,
  // on the rising edge. This is the overheat cut-out, and it does
  // not have to be repeated in each state that can reach it.
  "rules": [
    { "when": "heat >= 1", "to": "Overheated" }
  ],

  "states": {
    "initial": "Ready",

    "Ready": {
      "enter": [ { "start": "idle" } ],
      "exit":  [ { "stop": "idle" } ],
      // `when` guards a handler: at full heat the trigger does
      // nothing at all, and the button greys out by itself.
      "on": { "trigger.tapped": { "to": "Firing", "when": "heat < 1" } }
    },

    "Firing": {
      "enter": [ { "set": "heat", "to": "min(heat + 0.17, 1)" }, { "run": "shot" } ],
      "on": { "shot.done": "Ready" }
    },

    "Overheated": {
      "enter": [ { "run": "vent" } ],
      "on": { "vent.done": "Ready" }
    }
  }
}

Next: 6 · The HUD — put a reticle on a screen.