Writing a sketch · 7 of 7

Writing a sketch with AI

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

AI writes the code. Sialk Stage runs it.

Sialk Stage has no prompt box. It does not need one: an assistant you already pay for writes GLSL, p5 and Three.js well, and a sketch it writes is a folder you drag onto Sialk Stage 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 Stage 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 Stage 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 Stage. Anything hosting web content inherits it. What Sialk Stage 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 Stage, a macOS application that runs
web sketches as live, audio-reactive layers on stage.

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

  - A shader  -> a single sketch.glsl
  - A 3D scene, or anything that wants Three.js
              -> a single scene.js, an ES module (the ONLY file that may
                 import, and only from 'three' and 'three/addons/...')
  - Anything else -> a single sketch.js, p5.js in GLOBAL mode
                     (top-level `function setup()` / `function draw()`),
                     no imports

Sialk Stage supplies the page, the render loop, p5 itself and three 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.

=== A THREE.JS SCENE (scene.js) ===

One ES module. Import three the way its own docs do, and export ONE
function that builds the scene and returns what to draw:

  import * as THREE from 'three';
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // addons work too

  export default function (stage) {
    // stage: { THREE, renderer, canvas, width, height, sialk }
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(55, stage.width / stage.height, 0.1, 100);
    camera.position.z = 9;
    const mesh = new THREE.Mesh(new THREE.IcosahedronGeometry(2, 3), new THREE.MeshStandardMaterial());
    scene.add(mesh, new THREE.PointLight(0xffffff, 60, 0, 1).translateY(6));
    return {
      scene,
      camera,
      update({ audio, transport, parameters, width, height }) {
        // called every frame, before the render; the same fields as p5 above
        mesh.scale.setScalar(1 + audio.bass * 0.6);
        mesh.rotation.y = transport.elapsed * 0.3 * Number(parameters.speed ?? 1);
      },
    };
  }

Sialk Stage owns the canvas, the WebGLRenderer, the loop and resizing. So:
NO `new WebGLRenderer`, NO `setAnimationLoop` or `requestAnimationFrame`,
NO `document.body.appendChild`, NO CDN import - `three` resolves to the
copy Sialk Stage ships, offline. The layer is transparent where the scene does
not paint; set `scene.background` if you want it opaque. A perspective
camera's aspect is kept for you; return `resize(width, height)` to do it
yourself, or `render(frame)` to take over drawing (post-processing with an
EffectComposer built on `stage.renderer`). The function may be `async` if
a model has to load first; the layer stays transparent until it resolves.

Controls for a scene are declared in a visual.json beside it (below), any
type, and arrive on `parameters` in update() under their own names.

=== 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 OR A SCENE (visual.json beside the file) ===

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). A Three.js
scene declares its controls the same way, and may use every type:

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

In a shader 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. In a
scene they arrive as `parameters.height` and `parameters.warm` ('#c2ab94')
in update().

=== RULES ===

1. Use createCanvas(windowWidth, windowHeight) in p5. Sialk Stage 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 and Three.js
   care about COUNT, not resolution - a quarter of a million points is slow
   at every size; prefer one InstancedMesh to a thousand meshes. 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 Stage 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 Stage plays, three 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 Stage 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 Stage 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 Stage.

Worked example - p5, with controls

Using the Sialk Stage 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 Stage 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 Stage does not do

Sialk Stage 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 Stage runs what it writes.

A prompt box inside Sialk Stage is now on the pricing page as Sialk Stage 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.