Script FX

Script FX Guide

Program custom overlays that render straight into your video. This guide covers the scripting model, the fx helpers, the rules that keep exports deterministic, and fifteen complete examples you can paste in.

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:

NameTypeMeaning
ctxCanvasRenderingContext2DThe 2D drawing context, sized to the project.
tnumberClip-local time in seconds (starts at 0).
wnumberCanvas width in pixels (project width).
hnumberCanvas height in pixels (project height).
fxobjectSmall helper library (see below).

The fx helpers

A small set of math helpers you will reach for constantly:

fx-helpers.js
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

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.

progress.fx.js
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.

countdown.fx.js
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.

line.fx.js
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.

polygon.fx.js
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.

gradient.fx.js
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.

typewriter.fx.js
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.

ring.fx.js
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.

eq.fx.js
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.

starfield.fx.js
// 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.

ball.fx.js
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.

lower.fx.js
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.

pulse.fx.js
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.

clock.fx.js
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.

grid.fx.js
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.

vignette.fx.js
// 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

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.