Writing a sketch

Writing a sketch with AI

Paste the contract into the assistant you already use, and drag the result on.

AI writes the code. Sialk runs it.

Sialk has no prompt box. It does not need one: an assistant you already pay for writes GLSL and p5 well, and a sketch it writes is a folder you drag onto Sialk like any other. Nothing here is a preview or a beta feature, this works today, with the build on the download page.

What this page gives you is the missing half. A model that has never heard of Sialk will write you a beautiful shader that reacts to nothing, because it does not know how the room reaches your sketch. Paste the brief below into your assistant first, and it does.

Why this works better here than elsewhere

Models are materially better at the web stack than at node-graph environments, because the public training data for GLSL, p5 and Three.js dwarfs what exists for the others. That gap is unlikely to close.

The practical form of it: AI-written code is copy-paste in Sialk and a rewrite in TouchDesigner. A shader arrives as a shader. There is no port step, no node graph to rebuild, no equivalent to find.

Honestly, though: that advantage belongs to the web platform, not to Sialk. Anything hosting web content inherits it. What Sialk adds is the half a model cannot write for you, the audio, the frame rate, and two hours without going black.

The brief

Copy everything in this block into your assistant, then ask for what you want.

You are writing a visual sketch for Sialk, a macOS application that runs
web sketches as live, audio-reactive layers on stage.

Produce ONE file, complete and runnable. No build step, no imports, no
package.json, no index.html. Choose the format from what I ask for:

  - A shader  -> a single sketch.glsl
  - Anything else -> a single sketch.js, p5.js in GLOBAL mode
                     (top-level `function setup()` / `function draw()`)

Sialk supplies the page, the render loop, and p5 itself. Do not write any
of them.

=== THE AUDIO, IN p5 (sketch.js) ===

A global `window.sialk` object exists. Every field is mutated in place once
per frame, so read it inside draw() and never cache it.

  sialk.audio.level          0..1   broadband loudness, smoothed
  sialk.audio.bass           0..1   ~20-250 Hz
  sialk.audio.mid            0..1   ~250-2000 Hz
  sialk.audio.high           0..1   ~2000-16000 Hz
  sialk.audio.percussive     0..1   how drum-like the moment is
  sialk.audio.harmonic       0..1   how tonal the moment is
  sialk.audio.stems          8 entries {level,bass,mid,high}; zeros unless a stem input is set up
  sialk.audio.stemCount      how many of those are real, 0 when none
  sialk.audio.spectrum       Float32Array, 64 bins, log-spaced 20Hz-16kHz
  sialk.audio.levelTrail     Float32Array, 128 frames of level, [0] newest
  sialk.audio.hits           number, monotonic onset counter
  sialk.audio.onBeat         1 on the beat, decaying before the next
  sialk.audio.beatPhase      0..1 between beats; 0 when bpm is 0
  sialk.audio.bpm            0 when unknown
  sialk.audio.bpmConfidence  0..1, 0 when unknown
  sialk.audio.silent         boolean, true after 1s with no signal

  sialk.transport.time       seconds, show position - MAY BE SCRUBBED OR RESET
  sialk.transport.elapsed    seconds since this sketch loaded, monotonic
  sialk.transport.delta      seconds since the previous frame
  sialk.transport.frame      frames since load
  sialk.output.width/height/fps

Anything not in that list does not exist. Do not invent fields.

Guard it so the file also runs in a plain browser tab:
  const a = window.sialk?.audio ?? { level: 0, bass: 0, mid: 0, high: 0 };

=== THE AUDIO, IN GLSL (sketch.glsl) ===

The same values arrive as uniforms. Do not declare them; they are already
there. Write main() and nothing else - no #version line, no varyings.

  vec2  sialkResolution     the surface, in pixels
  float sialkTime           show transport position, seconds
  float sialkDelta          seconds since the previous frame
  float sialkFrame          frames since load
  float sialkLevel          broadband loudness, 0-1
  float sialkBass  sialkMid  sialkHigh    the three bands, 0-1
  float sialkPercussive  sialkHarmonic   drum-like, tonal, 0-1
  float sialkStemCount   vec4 sialkStems[8]   per-stem level/bass/mid/high, zeros when none
  float sialkHits           onset counter
  float sialkOnBeat         1 on the beat, decaying
  float sialkBeatPhase      ramps 0 -> 1 between beats
  float sialkBpm  sialkBpmConfidence
  float sialkSilent         1 when quiet for a second

  float sialkBand(float t)   t 0..1 across the spectrum
  float sialkTrail(float t)  t 0..1 back through the level trail

GLSL ES 3.00 (write to `fragColor`) and GLSL ES 1.00 (`gl_FragColor`,
`texture2D`) are both accepted. Shadertoy's iTime, iResolution and iFrame
are defined as aliases. iMouse is a constant zero.

=== CONTROLS THE PERFORMER CAN PLAY (p5 only) ===

Declare once at startup, then read live values every frame:

  sialk.parameters.declare({
    speed: { type: 'number', min: 0, max: 4, default: 1, label: 'Speed' },
    mode:  { type: 'enum', options: ['rings','bars'], default: 'rings' },
    tint:  { type: 'colour', default: '#ffbb66' },
    burst: { type: 'trigger' },
  });

Types: number (a fader - ALWAYS give min and max), boolean, enum, colour,
trigger. Read from `sialk.parameters.values`, which is one object mutated
in place - never destructure it at startup. Give every parameter a `label`;
it is read at a dark desk by someone who has never met the sketch.

=== CONTROLS FOR A SHADER (visual.json beside sketch.glsl) ===

A shader has no JavaScript, so its controls are declared in a visual.json
in the same folder, in the same shape, restricted to number, boolean and
colour (a number needs min and max; everything needs a label):

  { "parameters": {
      "height": { "type": "number", "min": 0.2, "max": 1, "default": 0.66, "label": "Bar height" },
      "warm":   { "type": "colour", "default": "#c2ab94", "label": "Low colour" } } }

They arrive as uniforms under their own names: number and boolean as
`uniform float height;`, colour as `uniform vec3 warm;`. Do NOT declare
them in the shader; the host does. Read them like sialkBass.

=== RULES ===

1. Use createCanvas(windowWidth, windowHeight) in p5. Sialk sizes the
   surface.
2. Leave headroom. A value that saturates at 0.9 makes a loud room and a
   very loud room look identical. Map bands into a visible range, do not
   multiply them until they clip.
3. Prefer `beatPhase` over `onBeat` for anything that decays:
   `1.0 - beatPhase` falls in time with the music.
4. Use `transport.elapsed` for anything that should simply keep going, and
   `transport.time` only for something that should follow the show. `time`
   can jump backwards.
5. Cost is asymmetric. GLSL cares about PIXELS - a loop that always runs to
   the end costs its full length 8.3 million times at 4K. p5 cares about
   COUNT, not resolution - a quarter of a million points is slow at every
   size. This runs at 4K/60 in front of a room. Write accordingly.
6. Skipping background() in p5 leaves the layer transparent, so what is
   beneath shows through. That is often what you want.

That block is the whole interface, and it does not move. The contract is frozen: fields are only ever added, never removed, renamed, or given a new meaning. So a brief that works tonight still works after Sialk has moved on, which is the only reason it is worth pasting into anything.

With an agent that reads tools

The same brief, the frozen contract, the kinds Sialk plays, two worked examples and a check for what was written are also packaged as @sialk/agent: an MCP server over stdio (sialk-agent mcp), a command line (sialk-agent check <folder>, which exits 1 on anything Sialk would not play as intended), and a skill an agent that reads one can follow. It is on npm, MIT, beside the contract. Nothing to install: npx @sialk/agent brief prints the brief, npx @sialk/agent check <folder> checks a sketch, and an MCP client is pointed at npx @sialk/agent mcp over stdio. The skill is the skill/sialk-sketch/ folder inside the package; copy it to wherever your agent reads skills from. The library is made with it.

Worked example - a shader

Using the Sialk brief above, write me a raymarched tunnel that pulses on the kick and shifts hue slowly. Warm palette. It has to hold 60 fps at 4K, so keep the march short and let the distance field converge early.

What comes back should look like this, one file, no scaffolding, the audio read straight out of the uniforms:

void main() {
  vec2 uv = (gl_FragCoord.xy - 0.5 * sialkResolution) / sialkResolution.y;

  float pulse = 1.0 - sialkBeatPhase;          // falls in time with the music
  float radius = 0.35 + sialkBass * 0.22;      // headroom left deliberately

  vec3 ro = vec3(0.0, 0.0, sialkTime * 0.6);
  vec3 rd = normalize(vec3(uv, 1.0));

  float t = 0.0;
  for (int i = 0; i < 48; i++) {
    vec3 p = ro + rd * t;
    float d = radius - length(p.xy) + 0.05 * sin(p.z * 3.0 + sialkTime);
    if (d < 0.001) break;                      // converges early, so it is cheap
    t += d * 0.9;
  }

  float glow = 1.0 / (1.0 + t * t * 0.35);
  vec3 warm = vec3(1.0, 0.55, 0.25) + 0.25 * sin(vec3(0.0, 2.1, 4.2) + sialkTime * 0.15);
  fragColor = vec4(warm * glow * (0.7 + 0.6 * pulse), 1.0);
}

Put it in a folder as sketch.glsl and drag the folder onto Sialk.

Worked example - p5, with controls

Using the Sialk brief above, write me a p5 sketch: a ring of bars driven by the spectrum, leaving trails. Give the performer a fader for how long the trails last and a colour swatch.

function setup() {
  createCanvas(windowWidth, windowHeight);
  angleMode(RADIANS);
  sialk.parameters.declare({
    trails: { type: 'number', min: 0, max: 1, default: 0.6, label: 'Trail length' },
    tint:   { type: 'colour', default: '#ffbb66', label: 'Colour' },
  });
}

function draw() {
  const p = sialk.parameters.values;   // read live, never cached
  const a = window.sialk?.audio ?? { spectrum: new Float32Array(64), level: 0 };

  background(0, 0, 0, map(p.trails, 0, 1, 90, 4));

  translate(width / 2, height / 2);
  stroke(p.tint);
  strokeWeight(3);

  const bins = a.spectrum;
  for (let i = 0; i < bins.length; i++) {
    const angle = (i / bins.length) * TWO_PI;
    const inner = 140 + a.level * 60;
    const outer = inner + bins[i] * min(width, height) * 0.28;
    line(cos(angle) * inner, sin(angle) * inner, cos(angle) * outer, sin(angle) * outer);
  }
}

sketch.js in a folder, dragged on. The two controls appear on the layer.

Check it before the room

A model is confident about things it has not run. Three things are worth a minute each:

  • Play music into it. Not a sine sweep, the actual set. A sketch tuned on a hum is tuned on nothing. See the audio.
  • Watch for saturation. If the picture looks the same at loud and very loud, a band was multiplied until it clipped. This is the single commonest thing to come back wrong.
  • Run it at the size you will play it at. A shader that is fine at 720p and collapses at 4K is telling you the march is too long, and that is not something Sialk can fix for you.

The shim is the fastest loop for this: the same window.sialk in an ordinary browser tab, fed from your microphone. Refresh, iterate, then drag the folder on.

What Sialk does not do

Sialk ships no generator. There is no prompt box in the application, no API key to paste, and nothing here talks to a model. You bring the assistant you already use; Sialk runs what it writes.

A prompt box inside Sialk is now on the pricing page as Sialk AI, the one subscription, because it is the one thing with a running cost, and it arrives after v1. It is billed inside that subscription on a monthly allowance rather than asking you for a key, the way Figma and Spline do it.

No date and no version here, on purpose. That is not vagueness; it is the same rule the runtimes are held to. A date printed in a build that has already shipped cannot be corrected and will be quoted back. What can be said honestly is only this: it is committed to, and it is not today.

What is frozen either way is the contract. Whatever writes the sketch, you, a model, or something that does not exist yet, the surface it writes against is the same one, and it does not move.