Last time we spent the whole episode on client work -- catching a foggy brief, pricing the days, bounding the revisions, getting paid without giving your hours away. And right at the end I told you there's a completely diffrent kind of project waiting for you, one where you're not the lone coder delivering to a client at all. It's the one where you sit down next to a dancer, or a musician, or an architect, or a scientist, and together you make a thing none of you could have made alone. That's today: collaboration across disciplines. The most human, least documented, and honestly most rewarding corner of this whole craft.
I'll tell you upfront, this is the stuff I love most and got wrong most. My first real collaboration was with a cellist for a little audiovisual night in Antwerp, and I walked in treating her like a sensor -- as if she was just a microphone that made the visuals move. It went badly, and it went badly for a reason I only understood later. So this episode is less about clever code and more about the shape you give the code so two heads can actually meet inside it. Allez, let's learn to build with other people.
Here's the thing nobody tells you: when you code alongside another discipline, your real job isn't the graphics. It's translation. A choreographer thinks in energy, weight, and phrase. A composer thinks in dynamics, tempo, and texture. A scientist thinks in variables and units. None of them think in ctx.fillRect, and you can't expect them to. So the very first move, exactly like everywhere in this arc, is to catch their language as data before it evaporates:
// don't make them learn YOUR words. record theirs, then translate.
const collaborator = {
who: "cellist",
speaksIn: ["dynamics", "bowing", "phrase", "silence"],
wants: "the light should breathe WITH me, not chase me",
offGraphics: "she doesn't care about framerate -- she cares about feel",
};
Look at that wants field. "Breathe with me, not chase me" is not a spec, it's a feeling -- and my first mistake was ignoring it and building something that reacted to every note like a startled cat. Writing their words down verbatim, in their vocabulary, is what keeps you honest later when you're tempted to build the thing you think is cool instead of the thing the piece actually needs. The translator serves the shared work, not their own showreel.
Two people can't build one system by both reaching into the same pile of variables -- you'll trip over each other constantly. What works instead is to agree on a small, explicit contract: a named set of parameters that is the only place your two worlds touch. Everything the musician controls flows through it, and everything your visuals read comes out of it. Nothing else crosses the line.
// the ONLY place two disciplines meet. keep it small and named.
function makeContract() {
return {
energy: 0, // 0..1 how hard she's playing right now
pitch: 0, // 0..1 low string to high
density: 0, // 0..1 sparse notes vs a flurry
silence: false, // is she resting?
};
}
That's it -- four fields. The beauty of naming the seam like this is that either side can be rebuilt without the other noticing. She can change her whole performance, I can throw away my visuals and start again, and as long as we both still speak energy, pitch, density, silence, the piece holds together. It's the same lesson as the client brief last episode: the interface between two parties is the most valuable thing you write, because it's what stops a misunderstanding turning into a rebuild.
Now the fun part, and the part that is genuinly an art in itself: deciding how their numbers become your numbers. This is where a collaboration lives or dies. A lazy mapping wires energy straight to brightness and calls it done. A thoughtful one asks what the gesture actually means and maps to that. First, the humble tool that makes all of it possible -- remapping a value from one range to another:
// the workhorse of every collaboration: move a value from one world's range to yours.
function mapRange(v, inMin, inMax, outMin, outMax) {
const t = (v - inMin) / (inMax - inMin);
return outMin + t * (outMax - outMin);
}
We first met this back in the data episodes, and here it's earning its keep again -- her bow pressure might arrive as 0..127 from a sensor, and your particle count wants 0..2000, and mapRange is the little bridge between them. But the real skill is choosing what maps to what, and that's a conversation, not a code decision:
// translate her four fields into MY visual language. this mapping IS the collaboration.
function visualsFrom(contract) {
return {
particleCount: Math.round(mapRange(contract.density, 0, 1, 20, 1500)),
hue: mapRange(contract.pitch, 0, 1, 210, 40), // low = cool blue, high = warm amber
calm: contract.silence, // let it settle when she rests
push: contract.energy, // how forcefully things move
};
}
Notice I mapped silence to calm rather than to blackness. That was her note -- when she stops playing, she doesn't want the screen to die, she wants it to settle, like a held breath. That one decision, made together over coffee, is the difference between visuals that feel like a partner and visuals that feel like a light switch. Makes sense, right? The mapping is where you either listen to your collaborator or quietly overrule them.
Remember the cellist's actual words -- breathe with me, not chase me. The technical translation of that is smoothing. Raw input twitches on every tiny gesture, and twitchy visuals feel nervous and cheap. So I never feed the raw contract to the render; I let it ease toward the target, the same lerp we've leaned on since the very start of the animation phase:
// raw input twitches. ease toward it so the visuals breathe instead of flinch.
function follow(current, target, ease = 0.08) {
return current + (target - current) * ease; // small ease = calm, slow follow
}
That ease value is a dial I tuned with her in the room, not at my desk. Too high and the light chased every note like an anxious dog; too low and it lagged so far behind it felt disconnected. We found 0.08 together by playing and watching, and that shared tuning session was the moment the piece actually became ours. You cannot find that number alone -- it only exists in the space between two people performing.
Here's where collaborations quietly fall apart: two systems that don't agree on when. If your visuals run on their own clock and her music on hers, they drift, and drift is death for anything that's meant to feel synchronised. The fix is one shared clock that both sides read from -- a single source of truth for time, never two:
// two systems MUST share one clock, or they drift apart within seconds.
function makeClock(bpm = 90) {
return {
bpm,
start: performance.now(),
beat(now = performance.now()) {
const beatsPerMs = this.bpm / 60 / 1000;
return (now - this.start) * beatsPerMs; // fractional beats since we began
},
};
}
Driving your motion off clock.beat() instead of a raw frame counter means your visuals are pinned to musical time, so a pulse lands on the beat whether the machine is running at 60fps or struggling at 30. This connects straight back to the sequencing episodes, where we learned that rhythm is just counting time in a shared unit. In a collaboration, that shared unit has to be shared with a human, which raises the stakes but doesn't change the maths.
When the other discipline runs on their own machine -- a musician on a laptop full of synths, you on yours full of pixels -- you can't just call each other's functions. You send messages. And the lovely thing is the creative-coding world settled on protocols for exactly this years ago: MIDI and OSC, which we met back in episode 126. The idea is simple: agree on an address and a value, and fling little tagged messages across:
// cross-machine collaboration talks in messages, not function calls. OSC-style:
function oscMessage(address, value) {
return { address, value, at: performance.now() };
}
// she sends these; I route them into my contract by their address.
function applyMessage(contract, msg) {
const key = msg.address.replace("/cello/", ""); // "/cello/energy" -> "energy"
if (key in contract) return { ...contract, [key]: msg.value };
return contract;
}
The reason to route by address rather than by position is that it's forgiving. If she adds a new control tomorrow -- /cello/vibrato -- my code simply ignores an address it doesn't know instead of crashing. Loose coupling like this is what lets two collaborators evolve their halves independently between rehearsals without breaking the show. It's the messaging-system version of the small named contract, and the two ideas reinforce each other beautifully.
Now the unglamorous truth that separates a rehearsal toy from a real performance: at some point, live, her feed will cut out. A cable wiggles, a laptop hiccups, WiFi sulks. If your visuals freeze or explode the instant the messages stop, the whole audience sees the seam. So I always make the visual side survive a silent partner -- if no fresh message has landed for a moment, I ease everything gently back to a resting state instead of holding a dead frame:
// live feeds DROP. when hers goes quiet, glide to rest -- don't freeze on a dead value.
function withFallback(contract, lastMsgAt, now = performance.now()) {
const stale = now - lastMsgAt > 400; // no word for 400ms? assume she dropped
if (!stale) return contract;
return {
energy: contract.energy * 0.95, // bleed the energy away, don't slam to zero
pitch: contract.pitch,
density: contract.density * 0.95,
silence: true,
};
}
That slow bleed to rest means a dropped connection reads as a natural lull, not a crash -- most of the audience never even notices. This is the exact same instinct as the self-healing watchdog we wrote for client installations last episode: a collaborative piece has to survive the other person's half failing, not just your own. Design for the drop and the show always goes on.
You do not get a cross-discipline piece right by thinking. You get it right by playing it together, badly, over and over, and adjusting. But here's the trap -- in the heat of a rehearsal you'll try ten tweaks and forget which nine you already ruled out. So I treat rehearsal like debugging: I keep a running log of what we changed and how it felt, as data, in the room:
// rehearsal is iteration. log every change + how it FELT, or you'll re-argue it tomorrow.
function logTweak(notes, change, feeling) {
return [...notes, { n: notes.length + 1, change, feeling, keep: null }];
}
let notes = [];
notes = logTweak(notes, "ease 0.15 -> 0.08", "much calmer, she liked it");
notes = logTweak(notes, "hue follows pitch", "obvious, maybe too literal?");
That keep field starts as null and becomes a shared decision -- yes we keep it, no we don't. Reviewing this list at the end of a session, together, is how two people build a common memory of the piece instead of two private and slowly diverging ones. Over a few rehearsals the log becomes the piece's design document, written by both of you, one honest reaction at a time.
A collaboration has more places to trip than a solo job, because now there are feelings and there's shared credit. Early on I assumed things -- who'd handle the audio routing, whether I could post the video, whose name went first -- and every single assumption became an awkward conversation later. So now I write the roles down at the start, plainly, the same way I scoped a client brief:
// unspoken roles breed resentment. name who owns what, out loud, on day one.
const roles = {
cellist: ["all sound", "performance", "final say on 'feel'"],
me: ["all visuals", "the sync system", "the tech on the night"],
shared: ["structure of the piece", "when it starts and ends", "credit"],
};
That shared list is the one that matters most and the one beginners skip. The structure -- how the piece is shaped, when it begins and ends -- belongs to both of you, and pretending otherwise is how one collaborator quietly starts feeling like hired help. Naming what's shared is how you keep it a collaboration instead of one person's project with a guest. Spell it out while everyone's still excited and friendly, long before there's anything to argue about.
And the last one, which I learned the hard, slightly embarassing way: split the credit deliberately, in writing, before the piece ever goes public. Who's named first? How is it described? Where does each person's link go? If you leave it vague, someone gets erased -- usually the person who wasn't loud about it -- and that's a friendship-ender:
// credit vague = someone gets erased. settle it in words, before it goes public.
function creditLine(roles, title) {
return `"${title}" -- a live audiovisual work. ` +
`Cello and composition by the cellist, generative visuals by femdev. ` +
`Made together, shown together.`;
}
"Made together, shown together" isn't decoration -- it's the whole ethic of the thing in four words. A good collaboration is generous with credit by default, because the alternative costs you the one thing that actually makes cross-discipline work possible: people who trust you enough to make the next piece with you. Guard the relationship harder than you guard the byline, and the byline sorts itself out.
So that's the real shape of collaboration, and you'll notice how little of it was about clever rendering. The code was the easy half again -- a contract, a mapping, a shared clock, a graceful fallback. The hard, beautiful half is human: listening well enough to translate someone else's art into yours without flattening it, and being generous enough with time and credit that they'd want to build with you again. Get that right and you unlock the kind of work you simply cannot make alone, the pieces where a coder and a cellist together become something bigger than either of them.
Here's the thread forward. Everything in this arc -- the portfolio, the submissions, the clients, and now collaborators -- has quietly been about sharing your work outward, to eyes and to partners. But there's one more way the knowledge in your head wants to travel, and it's maybe the most lasting of all: passing it on to someone who's exactly where you were when you started. Handing the craft to the next person is its own strange skill, and next time we get into it. 't Was plezant to make some noise together today :-).
Sallukes! Thanks for reading.
X