Skip to content

4 · Flow

Everything that happens over time in RFX is a script, or a child of one. The whole control model is one dropdown on each child.

Live Tap TRIGGER for one round. Hold it past 400 ms for a burst of three. Click the preview first for sound.

Every step you add has a flow box on its row. It answers two questions that have nothing to do with each other — does my next sibling wait for me, and does my parent wait for me — and it packs both into one word:

flow Gates the next sibling? Parent waits for it? Use it for
serial (default) ✅ ✅ a step — one thing after another
parallel ❌ ✅ a layer — run together, all awaited
detached ❌ ❌ fire and forget — a background sound or loop

That is the entire vocabulary. There is no container “mode”: a container is always serial-by-default, and its children decide.

Open the idle script from part 2 and add a second child: + effect ▸ spark, targeting @barrel, colour #ffc08a, sec 0.6, loop ticked.

Both children now loop, so neither gates the other: the blink and the random sparks run at once on the same eight pixels, the spark painting over the blink. Add a third and you have three layers. You configured no scheduler.

Make a new script called shot. Add four children — this is the card the whole part is about:

The shot script card in the Studio with four step rows. The first is tone at 720 Hz, wave square, seconds 0.09, volume 0.35, with its flow dropdown set to detached. The second is set on @barrel, colour #ffffff, sec 0.04, flow serial. The third is comet on @barrel, colour #ff7a00, lap 0.2, tail 5, flow serial, with an open timing/conditions fold showing 'stop after 0.2 s — then this block ends'. The fourth is wait 0.07 s, flow serial.
Four children, four flow dropdowns. Read the flow column downward and you have read the timing.
# Add with Verb Set flow
1 + effect tone 720, wave square, seconds 0.09, volume 0.35 detached
2 + action set @barrel, color #ffffff, sec 0.04 serial
3 + effect comet @barrel, #ff7a00, lap 0.2, tail 5 serial
4 + action wait 0.07 serial

Then on the comet, open + timing / condition and add stop after — 0.2 s.

Read the card downward:

  1. The tone is detached, so the muzzle flash on the next row starts immediately instead of waiting 90 ms for a sound to finish. This is the most common use of detached there is.
  2. The flash and the comet are serial steps — the comet does not start until the white flash has held for its 0.04 s.
  3. “stop after” gives the comet a lifetime. A comet with no lifetime and no loop would run one lap and stop; with one, it is a step with a definite end, which is what lets the next child know when to start. (In the file this is the uniform for modifier.)
  4. The wait is recovery time — it gates nothing but the end of the script, which is exactly what stops you machine-gunning the trigger.

When the last serial child finishes, the script is done and shot.done fires.

Make another script, burst. Drag a group block from the palette’s Flow section into its list — a group is a step that contains steps.

Put a copy of the four children inside it, then tick the group’s loop box and type 3 in the count field beside it.

On the Ready state, change the when event dropdown from trigger.pressed to trigger.tapped → Firing, then add a second handler: + when event ▸ trigger.held → Bursting.

Add the Bursting state with run ▸ burst on enter, and burst.done → Ready.

Both events come from the same physical trigger, and the states pick which one they want. Nothing timed the press; the input did that.

Time, speed and intensity flow downhill. Put 0.5 in the shot script’s speed box and every child in it — including the tone’s pitch — halves with it. Drag the Live tab’s master speed slider and the whole show slows, audio and all.

That one property is why the engine has scripts instead of a list of timers, and it is the best debugging tool in the Studio: a 40 ms muzzle flash you cannot see at 1.0 is perfectly legible at 0.2.

step-04-flow.json 81 lines
{
  // ===========================================================
  //  BLASTER · STEP 4 — Flow
  //  Every child answers two questions: does the next sibling
  //  wait for me, and does my parent wait for me? `flow` is the
  //  one word that answers both.
  //
  //    serial   (default)  next waits · parent waits   -> a STEP
  //    parallel            next runs  · parent waits   -> a LAYER
  //    detached            next runs  · parent doesn't -> FORGET IT
  // ===========================================================
  "schemaVersion": 1,
  "name": "Blaster 04 - Flow",
  "board": "t-display-s3",

  "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": {
    // TWO LAYERS. Both children loop, so both run at once and
    // neither gates the other — a looping child is detached
    // automatically, because nothing can wait for forever.
    "idle": {
      "children": [
        { "blink": "@barrel", "color": "#ff6a00", "off": "#140400", "sec": 1.4, "loop": true },
        { "spark": "@barrel", "color": "#ffc08a", "sec": 0.6, "loop": true }
      ]
    },

    // ONE ROUND. Four children, three kinds of flow:
    //   the tone is DETACHED, so the muzzle flash does not wait
    //     for the sound to finish;
    //   the flash and the ejecting comet are SERIAL steps;
    //   `for` gives the comet a lifetime, so it is a step with
    //     an end rather than a look that never stops.
    "shot": {
      "children": [
        { "tone": 720, "wave": "square", "seconds": 0.09, "volume": 0.35, "flow": "detached" },
        { "set": "@barrel", "color": "#ffffff", "sec": 0.04 },
        { "comet": "@barrel", "color": "#ff7a00", "lapSec": 0.2, "tail": 5, "for": 0.2 },
        { "wait": 0.07 }
      ]
    },

    // THREE ROUNDS. A nested container with `loop: 3` runs the
    // shot three times; the parent waits for the whole burst,
    // then `burst.done` fires.
    "burst": {
      "children": [
        { "children": [
            { "tone": 720, "wave": "square", "seconds": 0.09, "volume": 0.35, "flow": "detached" },
            { "set": "@barrel", "color": "#ffffff", "sec": 0.04 },
            { "comet": "@barrel", "color": "#ff7a00", "lapSec": 0.2, "tail": 5, "for": 0.2 },
            { "wait": 0.07 }
          ], "loop": 3 }
      ]
    }
  },

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

    "Ready": {
      "enter": [ { "start": "idle" } ],
      "exit":  [ { "stop": "idle" } ],
      "on": {
        "trigger.tapped": "Firing",    // a quick press -> one round
        "trigger.held":   "Bursting"   // hold it past 400 ms -> three
      }
    },

    "Firing":   { "enter": [ { "run": "shot"  } ], "on": { "shot.done":  "Ready" } },
    "Bursting": { "enter": [ { "run": "burst" } ], "on": { "burst.done": "Ready" } }
  }
}

The file is a fair mirror of the cards: "flow": "detached" is the dropdown, "loop": 3 is the checkbox and its count, and "for": 0.2 is the stop after row. serial never appears, because it is the default — the editor writes the smallest file that means what you clicked.


Next: 5 · Heat — stop hard-coding what the prop looks like.