Overview
Script FX clips let you draw scripted, animated visuals that composite over your video: animated graphs, vector motion, title cards, generative backgrounds, and more. Everything you draw runs in a sandboxed worker and is rasterised into the final export automatically.
To use one, add a Script FX Track from the timeline "+" menu, double-click the track to create a clip (this opens the editor), or double-click any existing Script FX clip to edit it.
The canvas is transparent every frame. Only what you draw is composited over the layers beneath, and Velox clears the canvas for you before each call, so you never need to clear it yourself.
The scripting model
Your script is a function body that runs once per frame. These names are in scope:
| Name | Type | Meaning |
|---|---|---|
| ctx | CanvasRenderingContext2D | The 2D drawing context, sized to the project. |
| t | number | Clip-local time in seconds (starts at 0). |
| w | number | Canvas width in pixels (project width). |
| h | number | Canvas height in pixels (project height). |
| fx | object | Small helper library (see below). |
The fx helpers
A small set of math helpers you will reach for constantly:
fx.TWO_PI // Math.PI * 2
fx.lerp(a, b, k) // linear interpolate: a + (b - a) * k
fx.clamp(v, lo, hi) // constrain v to [lo, hi]
fx.smooth(k) // smoothstep ease of k in [0,1] to eased [0,1]Rules for export
- Be deterministic. The same
tmust always draw the same frame. During export each frame is rendered on its own, so anything time-varying should be a pure function oft. AvoidDate.now(),performance.now(), and unseededMath.random(). Seed randomness from an index instead (see the Starfield example). - t is clip-local. A clip that starts at 0:10 on the timeline still sees
t = 0at its first frame. This keeps effects reusable and drag-independent. - Work in project pixels.
wandhare the export resolution, so a script that looks right in the preview looks identical in the render.
Sandbox limits
Scripts can only draw. There is no network, filesystem, DOM, storage, or access to the editor, by design, so a pasted snippet cannot do anything unsafe. fetch, XMLHttpRequest, WebSocket, indexedDB, and timers to the host are unavailable.
The clip's transform tools still apply on top of your drawing: position, scale, rotation, opacity, keyframes, and transitions all work as they do for any layer.
Examples
Each example is a complete script. Copy the body into the editor and press apply. They all follow the same rules, so you can mix and layer them freely.
1. Progress bar
A rounded fill that eases across the frame. Great as a base pattern for loaders and meters.
const pad = w * 0.1;
const barW = w - pad * 2;
const barH = h * 0.03;
const y = h * 0.9;
const p = fx.clamp(t / 4, 0, 1); // fill over 4 seconds
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.fillRect(pad, y, barW, barH);
ctx.fillStyle = '#3dd6f5';
ctx.fillRect(pad, y, barW * fx.smooth(p), barH);2. Countdown number
A centered number that pops on each whole second, deterministic from t.
const remaining = Math.max(0, Math.ceil(5 - t));
const pop = 1 - (t % 1); // shrink each second
const scale = 1 + pop * 0.4;
ctx.save();
ctx.translate(w / 2, h / 2);
ctx.scale(scale, scale);
ctx.fillStyle = '#ffffff';
ctx.font = '700 ' + Math.round(h * 0.3) + 'px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(String(remaining), 0, 0);
ctx.restore();3. Animated line chart
Reveals a data series left to right with a leading dot on the newest point.
const data = [0.2, 0.5, 0.35, 0.7, 0.6, 0.9, 0.75, 1.0];
const pad = w * 0.08;
const plotW = w - pad * 2;
const plotH = h * 0.5;
const baseY = h * 0.8;
const reveal = fx.clamp(t / 2, 0, 1); // draw left-to-right over 2s
const shown = reveal * (data.length - 1);
ctx.strokeStyle = '#3dd6f5';
ctx.lineWidth = 4;
ctx.lineJoin = 'round';
ctx.beginPath();
for (let i = 0; i < data.length; i++) {
if (i > shown) break;
const x = pad + (plotW * i) / (data.length - 1);
const y = baseY - data[i] * plotH;
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.stroke();
// leading dot
const li = Math.min(Math.floor(shown), data.length - 1);
const lx = pad + (plotW * li) / (data.length - 1);
const ly = baseY - data[li] * plotH;
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(lx, ly, 6, 0, fx.TWO_PI);
ctx.fill();4. Rotating polygon
A stroked hexagon spinning at a steady rate. Change sides for other shapes.
const cx = w / 2, cy = h / 2;
const r = Math.min(w, h) * 0.25;
const sides = 6;
const spin = t * 0.8;
ctx.strokeStyle = '#a678f0';
ctx.lineWidth = 5;
ctx.beginPath();
for (let i = 0; i <= sides; i++) {
const a = spin + (i / sides) * fx.TWO_PI;
const x = cx + Math.cos(a) * r;
const y = cy + Math.sin(a) * r;
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.closePath();
ctx.stroke();5. Gradient sweep
A looping two-color gradient that drifts across the frame as a background.
const shift = (t * 0.15) % 1;
const g = ctx.createLinearGradient(0, 0, w, h);
g.addColorStop((0 + shift) % 1, 'rgba(61,214,245,0.6)');
g.addColorStop((0.5 + shift) % 1, 'rgba(166,120,240,0.6)');
g.addColorStop((0.999), 'rgba(61,214,245,0.6)');
ctx.fillStyle = g;
ctx.fillRect(0, 0, w, h);6. Typewriter title
Types a string at a fixed characters-per-second rate with a blinking caret.
const text = 'VELOX EDITOR';
const cps = 10; // characters per second
const n = fx.clamp(Math.floor(t * cps), 0, text.length);
const shown = text.slice(0, n);
const caret = (t * 2) % 1 < 0.5 ? '|' : '';
ctx.fillStyle = '#ffffff';
ctx.font = '600 ' + Math.round(h * 0.08) + 'px monospace';
ctx.textBaseline = 'middle';
ctx.fillText(shown + caret, w * 0.1, h * 0.5);7. Circular progress ring
A track plus an eased arc with a live percentage in the middle.
const cx = w / 2, cy = h / 2;
const r = Math.min(w, h) * 0.3;
const p = fx.smooth(fx.clamp(t / 3, 0, 1));
ctx.lineWidth = 14;
ctx.lineCap = 'round';
ctx.strokeStyle = 'rgba(255,255,255,0.15)';
ctx.beginPath();
ctx.arc(cx, cy, r, 0, fx.TWO_PI);
ctx.stroke();
ctx.strokeStyle = '#3dd6f5';
ctx.beginPath();
ctx.arc(cx, cy, r, -Math.PI / 2, -Math.PI / 2 + fx.TWO_PI * p);
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = '700 ' + Math.round(h * 0.1) + 'px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(Math.round(p * 100) + '%', cx, cy);8. Equalizer bars
A row of bars driven by a deterministic per-bar wave, so it renders identically every export.
const bars = 24;
const bw = w / bars;
const baseY = h;
for (let i = 0; i < bars; i++) {
// deterministic per-bar wave
const amp = 0.5 + 0.5 * Math.sin(t * 4 + i * 0.6) * Math.sin(t * 1.3 + i);
const bh = Math.abs(amp) * h * 0.5;
ctx.fillStyle = 'hsl(' + (190 + i * 4) + ', 85%, 60%)';
ctx.fillRect(i * bw + 2, baseY - bh, bw - 4, bh);
}9. Starfield (seeded randomness)
Uses a seeded pseudo-random function so the field is reproducible across renders. This is the pattern to reach for whenever you need randomness.
// Pseudo-random from an integer seed. Deterministic across renders.
function rand(seed) {
const x = Math.sin(seed * 127.1) * 43758.5453;
return x - Math.floor(x);
}
const N = 140;
for (let i = 0; i < N; i++) {
const sx = rand(i);
const sy = rand(i + 99);
const speed = 0.05 + sx * 0.15;
const x = ((sx + t * speed) % 1) * w;
const y = sy * h;
const r = 0.5 + sx * 2;
ctx.fillStyle = 'rgba(255,255,255,' + (0.3 + sy * 0.6) + ')';
ctx.beginPath();
ctx.arc(x, y, r, 0, fx.TWO_PI);
ctx.fill();
}10. Bouncing ball with easing
A looping bounce built from fx.smooth, with a subtle squash and stretch.
const period = 1.2;
const phase = (t % period) / period;
// ease-out up, ease-in down for a bounce
const up = phase < 0.5
? fx.smooth(phase * 2)
: fx.smooth((1 - phase) * 2);
const cx = w / 2;
const floor = h * 0.85;
const top = h * 0.25;
const cy = fx.lerp(floor, top, up);
const squash = 1 + (1 - up) * 0.2;
ctx.fillStyle = '#f6ad55';
ctx.beginPath();
ctx.ellipse(cx, cy, 40 / squash, 40 * squash, 0, 0, fx.TWO_PI);
ctx.fill();11. Lower-third title plate
A CSS-style plate that slides in, then fades in its text. Swap the strings for your own titles.
const slide = fx.smooth(fx.clamp(t / 0.5, 0, 1));
const x = w * 0.08, y = h * 0.74;
const plateW = w * 0.44 * slide;
const plateH = h * 0.13;
ctx.fillStyle = 'rgba(10,26,31,0.82)';
ctx.fillRect(x, y, plateW, plateH);
ctx.fillStyle = '#3dd6f5';
ctx.fillRect(x, y, 6, plateH);
if (slide > 0.6) {
ctx.globalAlpha = fx.clamp((slide - 0.6) / 0.4, 0, 1);
ctx.fillStyle = '#fff';
ctx.font = '600 ' + Math.round(h * 0.045) + 'px sans-serif';
ctx.textBaseline = 'middle';
ctx.fillText('YOUR TITLE HERE', x + 22, y + plateH * 0.38);
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = '400 ' + Math.round(h * 0.028) + 'px sans-serif';
ctx.fillText('Subtitle line', x + 22, y + plateH * 0.72);
ctx.globalAlpha = 1;
}12. Radial pulse rings
Expanding rings that fade as they grow. A clean accent for beats or hits.
const cx = w / 2, cy = h / 2;
const maxR = Math.hypot(w, h) / 2;
for (let i = 0; i < 5; i++) {
const phase = (t * 0.4 + i / 5) % 1;
const r = phase * maxR;
ctx.strokeStyle = 'hsla(190,90%,60%,' + (1 - phase) * 0.7 + ')';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, fx.TWO_PI);
ctx.stroke();
}13. Analog clock hand sweep
A dial with ticks and a hand that completes one rotation every four seconds.
const cx = w / 2, cy = h / 2;
const r = Math.min(w, h) * 0.3;
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, fx.TWO_PI);
ctx.stroke();
// ticks
for (let i = 0; i < 12; i++) {
const a = (i / 12) * fx.TWO_PI;
ctx.beginPath();
ctx.moveTo(cx + Math.cos(a) * r * 0.9, cy + Math.sin(a) * r * 0.9);
ctx.lineTo(cx + Math.cos(a) * r, cy + Math.sin(a) * r);
ctx.stroke();
}
// sweeping hand, one rotation every 4s
const a = -Math.PI / 2 + (t / 4) * fx.TWO_PI;
ctx.strokeStyle = '#3dd6f5';
ctx.lineWidth = 5;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + Math.cos(a) * r * 0.8, cy + Math.sin(a) * r * 0.8);
ctx.stroke();14. Grid / matrix background
A drifting grid you can drop behind titles or data for a technical feel.
const cols = 20;
const cell = w / cols;
const rows = Math.ceil(h / cell);
ctx.strokeStyle = 'rgba(61,214,245,0.15)';
ctx.lineWidth = 1;
for (let x = 0; x <= cols; x++) {
const px = x * cell + (t * 20) % cell; // slow horizontal drift
ctx.beginPath(); ctx.moveTo(px, 0); ctx.lineTo(px, h); ctx.stroke();
}
for (let y = 0; y <= rows; y++) {
const py = y * cell;
ctx.beginPath(); ctx.moveTo(0, py); ctx.lineTo(w, py); ctx.stroke();
}15. Vignette / frame border
A soft dark vignette plus a gently pulsing accent frame to finish a shot.
// Soft dark vignette around the edges.
const g = ctx.createRadialGradient(
w / 2, h / 2, Math.min(w, h) * 0.3,
w / 2, h / 2, Math.max(w, h) * 0.7,
);
g.addColorStop(0, 'rgba(0,0,0,0)');
g.addColorStop(1, 'rgba(0,0,0,0.55)');
ctx.fillStyle = g;
ctx.fillRect(0, 0, w, h);
// pulsing accent frame
const a = 0.3 + 0.2 * Math.sin(t * 3);
ctx.strokeStyle = 'rgba(61,214,245,' + a + ')';
ctx.lineWidth = 6;
ctx.strokeRect(20, 20, w - 40, h - 40);Tips & patterns
- Loop an effect over a period
P: uset % P(or(t % P) / Pfor a 0 to 1 phase). Combine withfx.smoothfor eased loops. - Fade in over the clip start:
const a = fx.clamp(t / 0.5, 0, 1), then setctx.globalAlpha = a. - Fade out near the end: pass the clip duration in yourself (for example a constant at the top of your script), or animate the clip's opacity with keyframes.
- Reuse a value: compute expensive constants once at the top. The whole body re-runs every frame, so keep per-frame work lean for long clips.
- Text sizing: base font sizes on
h(for exampleh * 0.08) so they scale with the project resolution. - Colors:
hsl()andhsla()make it easy to animate hue witht.
Reference: what you can call
Everything on the standard Canvas 2D API is available on ctx, including fillRect, strokeRect, clearRect, path building with beginPath, moveTo, lineTo, arc, ellipse, bezierCurveTo, quadraticCurveTo, closePath, plus fill and stroke, the gradient builders createLinearGradient, createRadialGradient, and createConicGradient, text with fillText, strokeText, and measureText, state with save and restore, transforms via translate, rotate, scale, and setTransform, plus clip, globalAlpha, globalCompositeOperation, shadows, and line styling. The full Math object and the fx helpers are available too.