Learn Creative Coding (#158) - Exhibitions and Submissions

in StemSocial9 hours ago

Learn Creative Coding (#158) - Exhibitions and Submissions

cc-banner

Last time we built your portfolio -- the curated gallery, the hi-res exports, the contact sheets, all of it sitting on your own patch of the internet. And right at the end I told you the truth about it: a portfolio on your own website is a lovely thing, but it's still you, alone, in your room. At some point the work wants to go out. In front of real eyes, into rooms and onto screens where strangers who owe you nothing decide whether to stop and look. So today we start knocking on doors. This is the episode about exhibitions and submissions -- where generative art actually lives in the art world, and how you get your work into those spaces without losing your mind or your dignity.

I'll be honest with you: this was the scariest part for me. Making the art felt safe, because the only judge was me. Submitting it meant handing it to a jury of strangers and waiting weeks to be told no. And I got told no a lot. So this episode is partly a map of the landscape and partly the thing I wish someone had told me sooner -- that submitting is a craft you can get good at, and most of it is boringly practical, not mystical. Allez, let's turn the terror into a checklist.

Where generative art actually lives

First, the landscape, because when I started I genuinly had no idea where code-based art was even welcome. It turns out there's a whole world, and it splits into a few kinds of place. There are the big festivals -- Ars Electronica in Linz, the SIGGRAPH Art Gallery, Mutek and Sonar+D on the audiovisual side. These are prestigious, huge audiences, and brutally competitive. There are the on-chain and online platforms where a lot of generative art now lives natively. There are open prompts like Genuary, which aren't really submissions at all, just a shared month of daily challenges that quietly build your name. And there are small local group shows -- a code-and-art night at a bar in Antwerp -- which nobody's heard of but which put your work on a real wall in front of real people, which matters more than you'd think.

The mistake I made early was treating all of these the same. They aren't. So, same as always in this arc, I start by turning the fog into data. Every opportunity is just a record -- an honest note of what it actually wants, not the vibe it gives off:

// every open call is just a record. write down what it actually WANTS, not the vibe.
const calls = [
  { id: "ars-2027", venue: "Ars Electronica", kind: "festival",
    medium: ["installation", "realtime"], deadline: "2027-03-06",
    fee: 0, theme: "human + machine", notes: "prestige, huge crowd, brutal jury" },
  { id: "genuary-27", venue: "Genuary", kind: "open prompt",
    medium: ["still", "loop"], deadline: "2027-01-31",
    fee: 0, theme: "open", notes: "no jury -- pure practice + exposure" },
  { id: "antwerp-night", venue: "Code + Art night", kind: "group show",
    medium: ["screen", "loop"], deadline: "2026-10-01",
    fee: 10, theme: "open", notes: "small, friendly, a REAL wall" },
];

Look at that medium field especially. A festival that wants physical installations is not going to take your 30-second loop, no matter how pretty. Writing down what each place actually accepts saves you from the most common heartbreak in this whole game: pouring a week into a submission the call was never going to consider.

Don't spray and pray -- score the fit

Here's the beginner instinct, and I had it bad: see an open call, get excited, submit whatever you've got. That's spray-and-pray, and it wastes the two things you have least of -- time and emotional energy. A far kinder approach to yourself is to score how well a given piece fits a given call before you spend a single evening preparing it:

// score how well a piece FITS a call before you spend a night on it.
function fit(piece, call) {
  const mediumMatch = call.medium.includes(piece.medium) ? 2 : 0;
  const themeMatch = call.theme === "open" ? 1
    : piece.tags.some((t) => call.theme.includes(t)) ? 2 : 0;
  const affordable = call.fee <= 15 ? 1 : 0;
  return mediumMatch + themeMatch + affordable;   // 0..5, higher = worth the effort
}

That mediumMatch weight is doing the heavy lifting on purpose -- if the medium doesn't match, the whole thing is close to hopeless and the low score tells you so. I run this over my shortlist against every open call and only prepare the pairings that score high. It feels cold, reducing your art to a number against a call, but it's the same ruthlessness we practised in episode 157 when we cut 180 sketches down to eleven. Being honest early is a kindness to future-you at 2am the night before a deadline.

Deadlines are the real enemy

Nothing has cost me more good opportunities than simply missing the date. Open calls close, and they don't care that your piece was ready and you just forgot. So the very first tool I built for myself was a deadline tracker. Turn each deadline into "days from now", then surface the ones closing soon:

// turn a deadline into "days from now", then surface the ones closing soon.
function daysUntil(deadline, today = new Date()) {
  const ms = new Date(deadline) - today;
  return Math.ceil(ms / (1000 * 60 * 60 * 24));
}

function closingSoon(calls, within = 30, today = new Date()) {
  return calls
    .map((c) => ({ ...c, days: daysUntil(c.deadline, today) }))
    .filter((c) => c.days >= 0 && c.days <= within)
    .sort((a, b) => a.days - b.days);   // most urgent first
}

That c.days >= 0 filter quietly drops the ones already past, so you're never staring at a deadline you've already blown. Wire closingSoon up to run when you open your laptop in the morning and it becomes a gentle nag -- three weeks out, one week out, tomorrow. It sounds almost too simple to bother writing, but it has saved me more submissions than any clever bit of rendering ever did.

Read the spec sheet, then obey it

Here's a thing that took me an embarassing number of rejections to learn: juries reject on technicalities first and art second. If they asked for a 4K PNG at exactly 3840x2160 and you sent them your 800-pixel sketch canvas, you're out before anyone even looks at the image. The spec sheet is not a suggestion. And -- lovely news -- because of the habit we drilled in episode 157, our art doesn't bake its canvas size in, so obeying any spec is trivial. The spec is just more data:

// a call's technical spec is data too. read it, then MAKE your render obey it exactly.
const spec = { width: 3840, height: 2160, format: "png", fps: 60, seconds: 30 };

function renderToSpec(drawFn, seed, spec) {
  const canvas = document.createElement("canvas");
  canvas.width = spec.width;          // exactly what they asked for, no guessing
  canvas.height = spec.height;
  drawFn(canvas.getContext("2d"), spec.width, spec.height, seed);
  return canvas;
}

This is where all that seed discipline from episode 24 pays off yet again. The venue wants 4K? Re-grow the exact piece at 4K from its seed. They want a diffrent aspect ratio? Same seed, new dimensions. You're not remaking the art, you're re-rendering a system to a new spec, and that flexibility is a genuine competitive edge when you're racing a deadline.

But sometimes the venue's screen isn't the shape you built for, and you can't just stretch -- stretching a square piece onto a wide projector makes everything look wrong and amateur. The polite move is to fit inside their screen and letterbox the rest, exactly like a film on a TV:

// venues have fixed screens. fit your piece inside theirs without ever distorting it.
function fitInside(srcW, srcH, dstW, dstH) {
  const scale = Math.min(dstW / srcW, dstH / srcH);   // "contain" -- never squash
  const w = srcW * scale, h = srcH * scale;
  return { w, h, x: (dstW - w) / 2, y: (dstH - h) / 2 };  // centred, letterboxed
}

The Math.min of the two scale factors is the whole trick: pick the smaller so the piece fits in both directions, and it can never spill off an edge. Centre it with that / 2 and you get a clean, professional letterbox instead of a stretched mess. Small maths, big diffrence in how finished your work reads on someone else's hardware.

Loops that don't jump

A lot of exhibition work runs on a screen for hours. If your loop has a visible jump where the last frame snaps back to the first, every viewer sees it eventually, and it screams "unfinished". The fix is to drive all your motion from a phase that returns cleanly to where it started:

// gallery loops run for HOURS. drive motion by a phase that returns to its start,
// so the last frame melts into the first with no visible jump.
function loopPhase(frame, totalFrames) {
  return (frame / totalFrames) * Math.PI * 2;   // sweeps 0 .. 2pi, then wraps cleanly
}

// anything built from sin/cos of this phase is automatically seamless.
function samplePoint(phase, radius) {
  return { x: Math.cos(phase) * radius, y: Math.sin(phase) * radius };
}

The reason this works is just the nature of sin and cos: at phase 2*pi they're exactly back to their value at phase 0. So if every moving thing in your piece is ultimately a function of loopPhase, the loop is seamless by construction -- you never have to fiddle frame-by-frame to hide the seam. This is one of those tricks that feels like cheating the first time it works. Build the wrap into the maths and the seam disappears on its own.

The documentation IS the submission

Now the part that surprises people: most juries never see your live sketch. They see a video and a couple of stills. So your documentation is, quite literally, the thing being judged -- not the code. A shaky screen-grab of a beautiful piece loses to clean documentation of a decent one. So plan the video properly. From the spec's frame rate and duration, work out exactly how many frames to render and what to call them:

// most calls want a video, not a live sketch. work out exactly what to render.
function videoPlan(spec) {
  const frames = spec.fps * spec.seconds;
  return {
    frames,
    filenames: Array.from({ length: frames },
      (_, i) => `frame-${String(i).padStart(5, "0")}.png`),
  };
}

console.log(videoPlan({ fps: 60, seconds: 30 }).frames);   // 1800 frames

That padStart(5, "0") matters more than it looks -- zero-padded names (frame-00042.png) sort correctly, so when you stitch them into a video with a tool like ffmpeg the frames line up in the right order instead of frame-1, frame-10, frame-100. Render each frame with your seamless loopPhase, encode, and you've got a clean loop the jury can actually watch. And do pair the video with a still contact sheet like we built last episode -- showing the family of seeds behind the one frame is often what makes a generative submission stand out from a static one.

The proposal, made survivable

Right, the bit everyone dreads: the written proposal or artist statement. Facing a blank box that says "describe your work" makes my brain empty out completely. So I stopped writing prose and started filling in fields, then reading them back as sentences. A statement is scary as an essay and easy as data:

// a proposal is terrifying as prose but easy as fields. fill the blanks, read it back.
function statement(p) {
  return [
    `"${p.title}" is a generative ${p.medium} built from ${p.system}.`,
    `A single seed grows ${p.what}, so no two viewings are ever alike.`,
    `I made it to ask a question: ${p.question}`,
  ].join(" ");
}

console.log(statement({
  title: "Negotiated Borders", medium: "loop", system: "autonomous agents",
  what: "territories that fight and then settle",
  question: "where does an edge actually come from?",
}));

The output isn't going to win a literature prize, but it's clear, it's true, and it tells the jury three things they genuinly want to know: what the piece is, what makes it generative, and why you made it. That last one -- the question the work asks -- is what separates art from a tech demo in a juror's eyes. You can polish the sentences afterward, but starting from honest fields gets you past the blank-box paralysis, which is the actual hard part. Keep it short and keep it real. Nobody has ever been rejected for being too clear.

Never miss a required file

Back to technicalities, because this is where good work dies quietly. Every call has a list of required deliverables -- a hi-res still, a video, a statement, a short bio, sometimes a list of seeds. Miss one and you're often auto-rejected before a human reads a word. So the last thing I do before hitting send is run a dumb little validator over my package:

// juries reject on missing files before they judge art. never miss a required deliverable.
const required = ["hi-res-still", "video", "statement", "bio", "seed-list"];

function missing(pkg) {
  return required.filter((item) => !pkg[item]);   // empty array = cleared to submit
}

console.log(missing({ "hi-res-still": true, video: true, statement: true }));
// -> ["bio", "seed-list"]  : fix THESE before you even think about sending

An empty array means go. Anything in it means stop and fix. It's about as far from glamorous as code gets, but I have absolutely been the person who submitted at 23:58 and forgot the bio, and this ten-line check is why I don't do that anymore. Automate the boring gate so your tired brain doesn't have to be the gate.

Track everything -- it's a funnel, not a verdict

Here's the mindset shift that actually kept me going: rejection is the job, not a judgment on you. Working artists get rejected constantly -- the ones you admire simply submit far more than you'd guess and let the noes wash past. To live with that, you have to stop treating each submission as a referendum on your worth and start treating the whole thing as a funnel you manage. Which means tracking. Every submission moves through a little lifecycle, and I model it as a plain state machine so nothing ever falls through the cracks:

// track every submission through its life. no more "wait, did I ever hear back?"
const flow = {
  draft: ["submitted"],
  submitted: ["under-review", "withdrawn"],
  "under-review": ["accepted", "rejected"],
};

function advance(sub, to) {
  const allowed = flow[sub.status] || [];
  if (!allowed.includes(to)) throw new Error(`can't go ${sub.status} -> ${to}`);
  return { ...sub, status: to, updated: "today" };
}

We built state machines like this way back for animations, and it's the exact same idea pointed at your career instead of a sprite -- only legal moves allowed, so your records can never end up in a nonsense state. Once every submission is a tracked record, you can finally see the shape of your own effort. And that lets you compute the one number that matters, your acceptance rate:

// measure the rate so you can respect the process instead of fearing each result.
function stats(subs) {
  const done = subs.filter((s) => s.status === "accepted" || s.status === "rejected");
  const yes = done.filter((s) => s.status === "accepted").length;
  return {
    sent: subs.length,
    decided: done.length,
    accepted: yes,
    rate: done.length ? (yes / done.length * 100).toFixed(0) + "%" : "n/a",
  };
}

When I first ran this on my own history I was genuinly relieved. My rate was something like fifteen percent, which felt like failure until I saw it written down as a number -- because fifteen percent means you submit to seven places to get into one, and that's completely normal. The number turned "I keep getting rejected" into "I need to keep seven things in flight". Seeing it made the whole thing feel like weather instead of a wound.

And that reframe gives you your real operating rule: keep the funnel full. A healthy practice always has a few live submissions out at any moment, so no single no can flatten you -- there's always another still pending. So my very last little tool just checks that I haven't let the funnel run dry:

// a healthy practice keeps a few live at all times. nag when the funnel runs empty.
function funnelHealth(subs, floor = 3) {
  const live = subs.filter((s) =>
    s.status === "submitted" || s.status === "under-review").length;
  return live < floor
    ? `only ${live} live -- find ${floor - live} more calls this week`
    : `${live} live -- funnel healthy`;
}

Below three live, it tells me to go find more calls. That one nudge is what turns submitting from a rare terrifying event into a quiet steady habit, and habit is the only thing that survives a run of rejections. Oh -- and don't forget residencies in your funnel. Places like the Ars Electronica residency give you time, space, and often money to make work rather than just show it, and they're one of the best things you can aim for once you've got a portfolio worth backing.

't Komt erop neer...

  • Map the landscape as data. Festivals (Ars Electronica, SIGGRAPH, Mutek, Sonar+D), on-chain platforms, open prompts like Genuary, and small local shows are all diffrent beasts -- write down what each actually accepts before you fall for the vibe
  • Score the fit, don't spray and pray. Weight the medium match heavily; if the medium doesn't fit, the call was never yours. Prepare only the high-scoring pairings and save your energy
  • Deadlines are the real enemy. A tiny "days until" tracker that surfaces what's closing soon has saved me more submissions than any clever render
  • Obey the spec sheet exactly. Juries reject on technicalities first, art second. Because our pieces don't bake in a canvas size (episode 157), re-rendering to any resolution or aspect from the seed is trivial -- and fitInside letterboxes cleanly instead of stretching
  • Make loops that don't jump. Drive all motion from a phase that wraps at 2*pi, and gallery loops are seamless by construction, no frame-by-frame fiddling
  • The documentation IS the submission. Juries watch a video and see stills, not your live sketch. Plan the exact frame count, zero-pad the names so they stitch in order, and pair it with a seed contact sheet
  • Turn the proposal into fields. Beat the blank-box paralysis by filling in title, system, and -- crucially -- the question the work asks, then reading it back as plain honest sentences
  • Track it as a funnel, not a verdict. A state machine keeps records honest, your acceptance rate turns "I keep failing" into "I need seven in flight", and keeping three live at all times makes rejection into weather instead of a wound

So that's the whole machine for getting your work out the door -- from mapping where generative art lives, through obeying the boring specs, to surviving the emotional funnel of submitting. And I want to leave you with the bit that matters more than any of the code, because I really did learn it the hard way: the rejections are not about you. They're about fit, timing, a jury's mood, a theme you didn't quite match. The only failure in this whole game is stopping. Keep the funnel full, keep the work honest, and keep hitting send.

Here's the thread forward. So far everything we've done -- the portfolio, the submissions -- has been you deciding what to make and offering it to the world on your terms. But sooner or later something diffrent happens: someone comes to you and asks you to make a piece for them, on their brief, for money. And that changes the whole game -- the constraints, the conversations, the way you protect both the work and yourself. That's a genuinly diffrent skill, and next time we get into it. 't Was plezant to help you knock on some doors today :-).

Sallukes! Thanks for reading.

X

@femdev