Reshaping with Patterns
When you’re working with bundles, you often want to transform them—scale all the channels, swap them around, combine them into something new. You could name each intermediate step, creating a new bundle for every little operation, but that gets tedious fast. Patterns let you describe transformations inline, as a pipeline.
The -> operator
The arrow takes a bundle and pipes it into a pattern block. Inside the braces, you describe what the output should look like by referencing strands from the input. The strands are available by name (.r, .g, .b) or by index (.0, .1, .2), and you can put them in any order you want:
img[r,g,b] -> {.g, .r, .b}
This swaps the red and green channels. The pattern receives img, pulls out its strands, and assembles them into a new bundle with green first. Nothing about the original img changes—you’re describing a new bundle derived from it.
Transforming strands
Patterns aren’t just for reordering. Each position in the output can be any expression involving the input strands. If you want to darken an image, you can scale each channel:
img[r,g,b] -> {.0 * 0.5, .1 * 0.5, .2 * 0.5}
Each expression becomes a strand in the output. You’re not limited to simple math—anything you could write as a strand expression works here.
The output bundle doesn’t even need the same number of strands as the input. This is useful when you want to collapse or expand a bundle. For example, converting an RGB image to grayscale means going from three strands to one. You average the channels:
img[r,g,b] -> {(.0 + .1 + .2) / 3}
Going the other direction—expanding a single-strand grayscale back to RGB—you repeat the strand:
gray[val] -> {.0, .0, .0}
The pattern describes the shape of the output. The input’s shape doesn’t constrain it.
Range expansion
When you’re doing the same thing to every strand, writing it out gets repetitive. The range syntax helps. Instead of listing each strand individually:
img[r,g,b] -> {.0 * 0.5, .1 * 0.5, .2 * 0.5}
You can write:
img[r,g,b] -> {0..3 * 0.5}
The range 0..3 expands to .0, .1, .2—the same indices you would have written by hand. When you have multiple ranges in the same expression, they expand together. This lets you do per-strand variation without writing it all out. For example, scaling each channel by a different amount:
sensors[a,b,c,d,e,f] -> {0..6 * [2, 3, 4, 1, 2, 9].(0..6)}
\\ equivalent to
sensors[a,b,c,d,e,f] -> {.0 * 2, .1 * 3, .2 * 4, .3 * 1, .4 * 2, .5 * 9}
Both ranges expand in lockstep: the first iteration gets .0 and index 0, the second gets .1 and index 1, and so on.
Chaining
Because patterns produce bundles, you can chain them. The output of one pattern becomes the input to the next:
img[r,g,b]
-> {0..3 * 0.5}
-> {.2, .1, .0}
-> {(.0 + .1 + .2) / 3}
This reads as a pipeline: darken, reverse channel order, convert to grayscale. Each arrow passes the result along.
One thing to note: the output of a pattern is always an anonymous bundle. Even though img has named strands .r, .g, .b, the next pattern in the chain only sees .0, .1, .2. Names don’t carry through—each pattern works with indices.
Names in patterns
When a bundle goes through a pattern, the output is always anonymous. Even if your input had named strands, the next step in the chain only sees indices.
img[r,g,b] -> {.r, .g, .b} // fine: accessing input by name
-> {.r, .g, .b} // error: output of previous pattern has no .r
The first pattern can use .r because it’s receiving img directly. But its output is just a width-3 bundle with no names attached. The second pattern has to use .0, .1, .2:
img[r,g,b] -> {.r, .g, .b}
-> {.0, .1, .2} // works
If you need names back, assign the result to a new bundle:
swapped[r,g,b] = img[r,g,b] -> {.b, .g, .r}
swapped.r // now you have names again
This is consistent with how patterns work: they describe a new bundle’s shape from scratch. The input’s structure—including its names—is just source material.
External references
Patterns can reach outside the input bundle. If you want to multiply by a value defined elsewhere:
img[r,g,b] -> {0..3 * brightness.val}
The pattern has access to everything in scope, not just the strands being piped in. This is how you combine multiple signals in a pattern—one bundle flows through the pipeline, but it can interact with others along the way.
Limitations
Patterns are intentionally constrained. You can’t name them, store them in a variable, or pass them to a function—they exist only at the point where they’re written. You also can’t define intermediate values inside the braces; each output strand has to be a single expression. When you need more structure than that, spindles are the tool to reach for.