Programmatic.
Functions, loops, arrays, and trig mean the .lgv is a program — it computes its own keyframe tables instead of you typing them.
The script is a program
Logos already gives you let, fn, for, arrays, and tables; kyfram adds sin(x), cos(x), and pi() so keyframe values can be computed, not typed. Only the declaration calls — scene(), shape(), text(), group(), camera(), audio() — produce video. Everything else is free computation that feeds them. Pair this page with Scripting for the declarations themselves.
let LETTERS = ["K", "Y", "F", "R", "A", "M"]
for i, ch in LETTERS {
shape("l" + toStr(i), "text", table{
"0": table{content: ch, x: 120 + i * 80, y: 180, size: 64, color: "coral"},
"2": table{content: ch, x: 120 + i * 80, y: 180, size: 80, color: "coral"},
})
}
Six letters, one loop. The same idea scales to hundreds of shapes — a letter-by-letter reveal, a row of dots, an entire title sequence — without writing a single table by hand.
Functions — reusable keyframe builders
let name = fn(args) { ... return table{...} } — a function whose body returns a keyframe table is a reusable keyframe builder: pass content, position, size, and times; get a shape()-ready table back. The call lands wherever a table literal would.
let makeKf = fn(content, x, y, size, t0, t1) {
return table{
str(t0): table{content: content, x: x, y: y, size: size, alpha: 0},
str(t1): table{content: content, x: x, y: y, size: size, alpha: 1},
}
}
text("t1", makeKf("hello kyfram", 320, 180, 48, 0, 1))
text("t2", makeKf("reuse your motion", 320, 250, 26, 1, 2))
For loops + arrays
Two loop forms. for i in range(0, n) counts integers (end exclusive); for i, x in arr walks an array with its index. Arrays are [...] — list{...} is not valid. Strings concatenate with +; interpolate with "${x}".
let PALETTE = ["coral", "gold", "mint", "sky"]
for i, c in PALETTE {
shape("chip" + toStr(i), "circle", table{
"0": table{x: 80 + i * 160, y: 180, r: 50, color: c},
"2": table{x: 80 + i * 160, y: 180, r: 70, color: c},
})
}
Computed keyframe tables
A table literal isn't the only way to hand shape() its keyframes. Start empty, then assign with array-style keys — the time is a computed string, so a loop lays down any number of keyframes, and the finished table drops straight into the declaration.
let kf = table{}
for i in range(0, 7) {
let t = i * 0.5
kf[toStr(t)] = table{x: 320, y: 180 + (i - 3) * 40, r: 26, color: "coral"}
}
shape("bob", "circle", kf)
Times are quoted strings in the table — toStr(t) builds them, the engine sorts them. Note the capital S: lowercase tostr fails with identifier not found. Missing props (like alpha here) still hold their defaults per keyframe.
sin(), cos(), pi()
kyfram binds the trig that Logos's std/math lacks: sin(x), cos(x) in radians, and pi() (3.14159…). Combined with a loop they generate whole sine-wave motions inside the script.
let kf = table{}
let t0 = 0.0
let period = 3.0
let phase = 0.0
for i in range(0, 10) {
let t = i * 0.3
let a = 6.28318 * ((t - t0) / period) + phase // the angle — compute it first
kf[toStr(t)] = table{x: 320, y: 240 + 80 * sin(a), r: 26, color: "gold"}
}
shape("wob", "circle", kf)
sin(a) / cos(a) with it. Don't inline the whole 6.28318 * ((t - t0) / period) + phase into the call.Syntax facts
// line comment
/* block comment */
let kf = table{}
kf["0"] = table{color: "#ffffff"} // hex colors are quoted strings
for i in range(0, 3) { /* integer args, end exclusive */ }
for i, ch in ["K", "Y", "F"] { /* array loop */ }
let joined = "frame " + toStr(7) // concat with +
let msg = "at ${x}" // interpolation
| fact | detail |
|---|---|
| comments | // line, /* */ block |
| hex colors | inside keyframe tables they must be quoted strings — color: "#ffffff" |
| while | not supported — only for i in range(0, n) and for i, x in arr |
| range | end is exclusive; arguments must be integers |
| list{...} | not valid — use [...] arrays |
| strings | concat with +, interpolation "${x}" |
Worked: a sine hoop of three circles
A function builds keyframes for one dot on a hoop; the loop spreads three dots a lap's worth of angles apart, each making a full revolution. Angle math happens in variables, cos/sin see only plain values.
scene(3, 30)
// onHoop: one keyframe for a dot riding a hoop of radius R around (cx, cy)
let onHoop = fn(cx, cy, R, a) {
return table{
x: cx + R * cos(a), // a arrives as a plain variable
y: cy + R * sin(a),
r: 22,
color: "coral",
}
}
for i in range(0, 3) {
let a = 6.28318 * (i / 3) // three dots, evenly spread
let keys = table{
"0": onHoop(320, 180, 120, a),
"3": onHoop(320, 180, 120, a + 6.28318), // one full lap by t=3
}
shape("dot" + toStr(i), "circle", keys)
}
See it run
examples/kyfram_ad.lgv is the flagship — data arrays, helper functions, and loop-built sine waves drive the whole ad. examples/patterns.lgv shows stamp() reusing one motion across shapes; examples/groups.lgv shows group transforms. Run any with kyfram render examples/.