Expresso
Beta v1.0.0 coming soonStore, organize, copy, and apply After Effects expressions.
freq = 2; amount = 30; wiggle(freq, amount)
freq = 2; amount = 30; x = wiggle(freq, amount)[0]; [x, value[1]]
freq = 1.5; amount = 12; s = wiggle(freq, amount)[0]; [s, s]
min = 0; max = 100; seedRandom(index, true); random(min, max)
freq = 1; amp = 60; loopTime = 3; t = time % loopTime; w1 = wiggle(freq, amp, 1, 0.5, t); w2 = wiggle(freq, amp, 1, 0.5, t - loopTime); linear(t, 0, loopTime, w1, w2)
freq = 3; amp = 25; octaves = 3; wiggle(freq, amp, octaves, 0.6)
freq = 3; maxAmp = 80; rampTime = 2; amp = linear(time, inPoint, inPoint + rampTime, 0, maxAmp); wiggle(freq, amp)
rate = 4; minVal = 0; maxVal = 100; seedRandom(Math.floor(time * rate), true); random(minVal, maxVal)
freq = 1; amp = 60; value + [noise([time * freq, 0]), noise([time * freq, 100])] * amp
width = 0.08; samples = 5; smooth(width, samples)
speed = 60; time * speed
Math.floor(time * thisComp.frameRate)
steps = 6; freq = 8; amount = 40; posterizeTime(steps); wiggle(freq, amount)
timeToCurrentFormat()
start = 60;
function pad(n){ n = Math.floor(n).toString(); while (n.length < 2) n = "0" + n; return n; }
t = Math.max(start - time, 0);
pad(Math.floor(t / 60)) + ":" + pad(t % 60) rate = 8; Math.floor(time * rate) % 2 == 0 ? 100 : 0
freezeTime = 2; time > freezeTime ? valueAtTime(freezeTime) : value
thisLayer.transform.position.speed
loopOut()
loopOut("pingpong") loopOut("continue") loopOut("offset") loopIn()
loopOut("cycle", 1) duration = 2;
loopOutDuration("cycle", duration) loopOut("cycle") period = 2; (time % period) / period
period = 2; lo = 0; hi = 100; half = period / 2; t = time % period; t < half ? linear(t, 0, half, lo, hi) : linear(t, half, period, hi, lo)
amp = 0.08;
freq = 2.5;
decay = 3.0;
n = 0;
if (numKeys > 0){
n = nearestKey(time).index;
if (key(n).time > time) n--;
}
if (n > 0){
t = time - key(n).time;
v = velocityAtTime(key(n).time - 0.001);
value + v * amp * Math.sin(freq * t * 2 * Math.PI) / Math.exp(decay * t);
} else {
value;
} amp = 30; freq = 1; value + amp * Math.sin(time * freq * 2 * Math.PI)
e = 0.7;
g = 5000;
nMax = 9;
n = 0;
if (numKeys > 0){
n = nearestKey(time).index;
if (key(n).time > time) n--;
}
if (n > 0){
t = time - key(n).time;
v = -velocityAtTime(key(n).time - 0.001) * e;
vl = length(v);
if (value instanceof Array){
vu = (vl > 0) ? normalize(v) : [0, 0, 0];
} else {
vu = (v < 0) ? -1 : 1;
}
tCur = 0;
segDur = 2 * vl / g;
tNext = segDur;
nb = 1;
while (tNext < t && nb <= nMax){
vl *= e;
segDur *= e;
tCur = tNext;
tNext += segDur;
nb++;
}
if (nb <= nMax){
delta = t - tCur;
value + vu * delta * (vl - g * delta / 2);
} else {
value;
}
} else {
value;
} amp = 40; freq = 1; decay = 0.8; t = time - inPoint; value + amp * Math.sin(freq * t * 2 * Math.PI) / Math.exp(decay * t)
freq = 18; decay = 6; target = 100; t = Math.max(time - inPoint, 0); s = target * (1 - Math.exp(-decay * t) * Math.cos(freq * t)); [s, s]
radius = 200; speed = 1; a = time * speed * 2 * Math.PI; [thisComp.width / 2 + Math.cos(a) * radius, thisComp.height / 2 + Math.sin(a) * radius]
r = sourceRectAtTime(time, false); [r.left + r.width / 2, r.top + r.height / 2]
delay = 0.1; thisComp.layer(index - 1).transform.position.valueAtTime(time - delay)
target = thisComp.layer("Target").transform.position;
d = target - transform.position;
radiansToDegrees(Math.atan2(d[1], d[0])) L = thisComp.layer("Target");
p = L.toComp(L.transform.anchorPoint);
[p[0], p[1]] sx = thisComp.width / width * 100; sy = thisComp.height / height * 100; f = Math.min(sx, sy); [f, f]
delay = 0.1; valueAtTime(time - (index - 1) * delay)
delay = 0.08;
m = thisComp.layer("Master");
m.transform.position.valueAtTime(time - index * delay) maxStretch = 30; soft = 3000; v = thisLayer.transform.position.velocity[1]; s = clamp(v / soft, -1, 1) * maxStretch; [100 - s, 100 + s]
v = thisLayer.transform.position.velocity; radiansToDegrees(Math.atan2(v[1], v[0]))
radius = 300; speed = 0.4; a = time * speed * 2 * Math.PI; c = [thisComp.width / 2, thisComp.height / 2, 0]; c + [Math.cos(a) * radius, 0, Math.sin(a) * radius]
grid = 50; [Math.round(value[0] / grid) * grid, Math.round(value[1] / grid) * grid]
cols = 5; cellW = 200; cellH = 200; i = index - 1; [(i % cols) * cellW + cellW / 2, Math.floor(i / cols) * cellH + cellH / 2]
amp = 6; freq = 0.4; s = 100 + amp * Math.sin(time * freq * 2 * Math.PI); [s, s]
[thisComp.width / 2, thisComp.height / 2]
thisComp.layer("Controls").effect("Speed")("Slider") a = thisComp.layer("A").transform.position;
b = thisComp.layer("B").transform.position;
length(a, b) thisComp.numLayers
p = [thisComp.width / 2, thisComp.height / 2]; hasParent ? parent.fromComp(p) : p
first = 100; last = thisComp.width - 100; n = thisComp.numLayers; x = n > 1 ? linear(index, 1, n, first, last) : (first + last) / 2; [x, value[1]]
a = thisComp.layer("A").transform.position;
b = thisComp.layer("B").transform.position;
(a + b) / 2 thisComp.layer("Master").text.sourceText Math.floor(time * thisComp.frameRate) + ""
pad = 20;
src = thisComp.layer("My Text");
r = src.sourceRectAtTime(time, false);
[r.width + pad, r.height + pad] thisComp.name
name
function addCommas(n){
var s = Math.round(n).toString();
return s.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
addCommas(effect("Amount")("Slider")) digits = 3;
n = Math.round(effect("Count")("Slider")).toString();
while (n.length < digits) n = "0" + n;
n speed = 12; n = Math.floor((time - inPoint) * speed); value.substring(0, n)
speed = 12;
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
src = value;
n = (time - inPoint) * speed;
out = "";
for (i = 0; i < src.length; i++){
if (i < n){
out += src.charAt(i);
} else if (src.charAt(i) == " "){
out += " ";
} else {
seedRandom(i + Math.floor(time * 30), true);
out += chars.charAt(Math.floor(random(0, chars.length)));
}
}
out Math.round(linear(time, inPoint, outPoint, 0, 100)) + "%"
words = ["Ready", "Set", "Go"]; seedRandom(index, true); words[Math.floor(random(words.length))]
startVal = 0; endVal = 1000; dur = 3; n = ease(time, inPoint, inPoint + dur, startVal, endVal); Math.round(n) + ""
hex = "FF9900"; r = parseInt(hex.substr(0, 2), 16) / 255; g = parseInt(hex.substr(2, 2), 16) / 255; b = parseInt(hex.substr(4, 2), 16) / 255; [r, g, b, 1]
speed = 0.2; h = (time * speed) % 1; hslToRgb([h, 1, 0.5, 1])
speed = 0.1;
base = hexToRgb("E03535");
hsl = rgbToHsl(base);
hsl[0] = (hsl[0] + time * speed) % 1;
hslToRgb(hsl) seedRandom(index, true); [random(), random(), random(), 1]
hexToRgb("FF9900") c1 = hexToRgb("FF3366");
c2 = hexToRgb("33CCFF");
speed = 0.5;
t = (Math.sin(time * speed * 2 * Math.PI) + 1) / 2;
linear(t, 0, 1, c1, c2) seedRandom(index, true);
pal = [hexToRgb("E63946"), hexToRgb("F4A261"), hexToRgb("2A9D8F"), hexToRgb("264653")];
pal[Math.floor(random(pal.length))] step = 0.08; sat = 0.7; light = 0.55; h = (index * step) % 1; hslToRgb([h, sat, light, 1])
min = 0; max = 100; clamp(value, min, max)
inMin = 0; inMax = 100; outMin = 0; outMax = 500; linear(value, inMin, inMax, outMin, outMax)
dist = 150;
target = thisComp.layer("Target").transform.position;
dir = normalize(target - transform.position);
value + dir * dist ang = effect("Angle")("Slider");
dist = effect("Distance")("Slider");
r = degreesToRadians(ang);
value + [Math.cos(r), Math.sin(r)] * dist x = transform.position[0]; linear(x, 0, thisComp.width, 0, 100)
step = 45; Math.round(value / step) * step
lo = 0; hi = 100; freq = 1; lo + (hi - lo) * (Math.sin(time * freq * 2 * Math.PI) + 1) / 2
from = 0; to = 100; ease(time, inPoint, outPoint, from, to)
fadeIn = 0.5; fadeOut = 0.5; a = ease(time, inPoint, inPoint + fadeIn, 0, 100); b = ease(time, outPoint - fadeOut, outPoint, 100, 0); Math.min(a, b)
m1 = marker.key(1).time; m2 = marker.key(2).time; ease(time, m1, m2, 0, 100)
dur = 1;
from = 0;
to = 100;
function bounceOut(x){
var n = 7.5625;
var d = 2.75;
if (x < 1 / d){ return n * x * x; }
else if (x < 2 / d){ x -= 1.5 / d; return n * x * x + 0.75; }
else if (x < 2.5 / d){ x -= 2.25 / d; return n * x * x + 0.9375; }
else { x -= 2.625 / d; return n * x * x + 0.984375; }
}
t = clamp((time - inPoint) / dur, 0, 1);
from + (to - from) * bounceOut(t) dur = 0.8; from = 0; to = 100; s = 1.70158; t = clamp((time - inPoint) / dur, 0, 1) - 1; p = t * t * ((s + 1) * t + s) + 1; from + (to - from) * p
base = 100;
a = thisComp.layer("Audio Amplitude").effect("Both Channels")("Slider");
s = base + a;
[s, s] boost = 4;
a = thisComp.layer("Audio Amplitude").effect("Both Channels")("Slider");
clamp(a * boost, 0, 100) freq = 2;
mult = 1;
a = thisComp.layer("Audio Amplitude").effect("Both Channels")("Slider");
wiggle(freq, a * mult) mult = 2;
a = thisComp.layer("Audio Amplitude").effect("Both Channels")("Slider");
value - [0, a * mult] Imagine mode invents playful, unexpected expressions to spark ideas.
Demo preview — in After Effects this calls Claude with your API key.
- Expresso now notices when a newer version is out and shows an update banner — no more checking the website by hand.
- A What's new changelog is built right in: the v… link at the bottom of the sidebar, or Settings ▸ Updates.
- New Updates section in Settings: turn the launch check on or off, and check on demand.
The real Expresso panel — pick a category, expand a row, or hit ✨ Generate.
Built with
- After Effects
- CEP / Bolt CEP
- React
- TypeScript
A dockable After Effects panel for everything expressions: a 90-expression built-in library, your own saved collection, and one-click copy or apply onto whatever you have selected.
Since v0.2 it also has a built-in AI assistant — describe the motion you want and it writes the expression, or point it at an existing one to explain, improve, or riff on it. Bring your own key (Anthropic, OpenAI, or Gemini); it's stored on your machine and goes nowhere else.
What you get
-
90 expressions, ready to go
A built-in library across 11 categories — wiggle rigs, loops, text counters, physics — each with a plain-English description. One click to copy or apply.
-
Your own library
Save the expressions you actually use, organize them into categories, and carry them between machines with import/export.
-
AI that speaks expressions
Describe what you want and get a working expression — or have one explained, improved, or varied. Works with Anthropic, OpenAI, or Gemini.
-
Your key, your machine
Bring your own AI key. It's stored locally and only ever sent to the provider you chose — nothing passes through anyone else's server.
-
Updates that come to you
The panel checks for new versions and shows what changed, right inside After Effects. Bugs and ideas go straight to the developer from Settings.
Expresso — Changelog
v0.3.1 Jul 13, 2026
Tell me what's broken
Settings ▸ Report a bug / Request a feature now open a feedback form on dexterbrewer.com with the tool and your exact version already filled in — so when something breaks, I get the report with enough detail to reproduce it instead of having to ask you which build you're on.
A broken panel is no longer a blank white rectangle. Expresso's dark theme only ever lived on a div that React draws, so the instant the panel failed to render — for any reason — you were looking at the browser engine's plain white background, with nothing to go on. The theme now sits on the page itself, so a failure looks like a failure and there's something to report.
v0.3.0 Jul 8, 2026
Expresso now keeps itself current from inside After Effects.
In-app update notifications
When a newer version is out, a banner appears at the top of the panel with What's new and Get update — no more checking the website by hand.
Built-in changelog
A "What's new" view is built in — click v… · What's new at the bottom of the sidebar, or open it from Settings.
️ Updates settings
A new Updates section in Settings lets you turn the launch check on/off and check on demand.
v0.2.1 Jul 8, 2026
Expresso v0.2.1 — a fix-up on v0.2.0, verified live in After Effects.
Fixed: the Model picker in Settings
On v0.2.0 the model field wouldn't drop down — you could only type into it. It's now a proper dropdown of the selected provider's models, with a Custom model ID… option if you want to paste an exact/newer id. (The old control relied on an HTML feature that After Effects' built-in browser doesn't render.)
More reliable AI
- Requests now time out after 120s instead of leaving the panel stuck on "Thinking…".
- The output budget is larger and each provider now detects a cut-off response and tells you to try again, instead of failing with a confusing error.
- Refreshed the suggested models (adds Claude Fable 5; OpenAI GPT-5 family; Gemini 2.5).
Under the hood
First unit tests and a CI check on every change — groundwork so future releases stay solid.
v0.2.0 Jul 8, 2026
Expresso v0.2.0 — bring your own AI, tidier sidebar, themed dialogs.
Multiple AI providers
Generate, Explain, Variations, and Improve now run on Anthropic (Claude), OpenAI (GPT), or Google Gemini — pick your provider and paste its key in Settings. Each provider's key is stored separately (switching never wipes one), and the model field is editable, so you can use any model id you like.
Collapsible sidebar sections
The Library, Categories, and Yours sections each collapse and expand on their own now, and the panel remembers how you left them.
Themed delete dialog
Deleting an expression used to pop a plain white system box that ignored the theme. It's now an in-app dialog that matches the rest of the extension (with a proper red Delete button, Enter/Escape, and click-away to cancel). Thanks to the users who reported it.
v0.1.0 Jul 1, 2026
A big feature release — the library is bigger, the panel looks better, and AI now helps with any expression, not just new ones.
v0.0.1 Jul 1, 2026
A dockable After Effects panel that stores, organizes, generates, and applies expressions.