Control Jitter (jitter.video) for exporting animations, replacing assets, modifying text, and rendering frames at specific times programmatically via Playwriter.
Control Jitter (jitter.video) for exporting animations, replacing assets, modifying text, and rendering frames at specific times programmatically via Playwriter.
Jitter Programmatic Control Skill
Control Jitter (jitter.video) for exporting animations, replacing assets, and modifying text.
Setup
Load utils before interacting with Jitter:
// Load once per page (before navigation or via addInitScript)await page.addInitScript({ path: './skills/jitter/dist/jitter-utils.js' })
// Navigate to projectawait page.goto('https://jitter.video/file/?id=YOUR_FILE_ID')
// Wait for app to be readyawait page.evaluate(() => jitterUtils.waitForApp())
API Reference
Traversal
Function
Description
findNodeById(id)
Find node by ID
findAllMediaNodes()
Get all images/SVGs/videos/GIFs
findAllTextNodes()
Get all text nodes
getArtboards()
Get all artboards with dimensions
findNodesByType(type)
Find nodes by layer type
findNodesByName(name)
Find nodes by name (partial match)
flattenTree()
Get all nodes as flat array
Actions
Function
Description
replaceAssetUrl(nodeId, url)
Replace image/SVG/video URL
replaceText(nodeId, text)
Replace text content
updateNode(nodeId, props)
Update any node properties
batchReplace(replacements)
Batch update multiple nodes
selectNodes(nodeIds)
Select nodes by ID
removeNodes(nodeIds)
Remove nodes
undo() / redo()
Undo/redo actions
Export
Function
Description
generateExportUrl(opts)
Generate export URL with options
generateExportUrlFromCurrentProject(opts)
Export URL for current project
parseJitterUrl(url)
Parse file/node IDs from URL
getFileMeta()
Get current file metadata
Snapshot & Restore
Function
Description
createSnapshot(nodeIds)
Save node states
restoreFromSnapshot(snapshot)
Restore saved states
duplicateProject()
Clone current project
withTemporaryChanges(nodeIds, changes, callback)
Apply temp changes, run callback, restore
Waiting
Function
Description
waitForApp(timeout?)
Wait for app to load
waitForSync(delay?)
Wait for server sync
waitForNode(nodeId, timeout?)
Wait for node to exist
isAppReady()
Check if app is ready
Examples
Replace Assets and Export
// Get all media nodesconst media = await page.evaluate(() => jitterUtils.findAllMediaNodes())
// Replace specific assetsawait page.evaluate(() => {
jitterUtils.batchReplace([
{ nodeId: 'abc123', data: { url: 'https://example.com/new-image.svg' } },
{ nodeId: 'def456', data: { url: 'https://example.com/new-photo.jpg' } },
])
})
// Wait for sync then exportawait page.evaluate(() => jitterUtils.waitForSync())
const exportUrl = await page.evaluate(() =>
jitterUtils.generateExportUrlFromCurrentProject({ profile: 'lottie' }),
)
await page.goto(exportUrl)
NodeIds are NOT preserved in exported Lottie - cannot map back to Jitter nodes
Text becomes shapes - not editable Lottie text layers
Images are embedded as base64, no external URLs
Videos export as first frame only
Workaround: Always modify assets in Jitter before export using replaceAssetUrl().
Inspecting exported Lottie files
Lottie JSON files from Jitter contain giant base64-encoded image strings that make
them unreadable. Use jq with walk() to truncate long strings before inspecting:
# Pretty-print with long strings truncated and huge arrays summarized
jq 'walk(
if type == "string" and length > 80 then .[:80] + "..."
elif type == "array" and length >= 20 then .[:3] + ["\(length - 3) more items..."]
else . end
)' exported.json
# Pipe to less for paging
jq 'walk(
if type == "string" and length > 80 then .[:80] + "..."
elif type == "array" and length >= 20 then .[:3] + ["\(length - 3) more items..."]
else . end
)' exported.json | less
This replaces embedded base64 image data with truncated previews and collapses
large arrays (20+ items, common in Bezier path data) to just the first 3 elements
plus a count, so you can read the animation structure without scrolling through
walls of encoded pixels or coordinate lists.
Rendering Frames (Seek + Screenshot)
Jitter has a hidden /api/renderer/ page with a headless rendering engine. It exposes
window.jitter.renderFrame(timeMs) which renders a full-quality frame at any point
in the animation and returns a raw PNG. This is the same engine Jitter's own export
backend uses (server-side Puppeteer calling renderFrame() in a loop).
Getting file and artboard IDs
From the editor page URL https://jitter.video/file/?id=FILE_ID&nodeId=ARTBOARD_ID,
or programmatically:
renderFrame(timeMs) returns a raw binary PNG string. Convert to base64 inside
the browser, then decode to a Buffer in the sandbox:
// Render frame at 4000ms (4 seconds into the animation)const base64Png = await state.rendererPage.evaluate(async () => {
const result = awaitwindow.jitter.renderFrame(4000)
returnbtoa(result.pngString)
})
const fs = require('node:fs')
const buf = Buffer.from(base64Png, 'base64')
fs.writeFileSync('/tmp/frame-4000.png', buf)
The output is a full-resolution PNG matching the width/height params, with no
editor UI, selection handles, or chrome. Just the clean artboard content at that
exact animation frame.
The artboard may be letterboxed inside the PNG. With width=1920&height=1080
and superSampling=2 the artboard rendered at half size (960x540) centered with
transparent padding. Before comparing against other renders, find the content
bbox once (magick frame.png -alpha extract -threshold 50% -format "%@" info:)
and crop + flatten onto the artboard fill color.
If you just need to visually scrub the editor timeline without the full renderer,
click on the timeline ruler element. The ruler spans from x=272 (0ms) to the
duration end. Clicking sets the time cursor:
// On the editor page (not the renderer page)const rulerY = 1120// vertical center of the ruler barconst rulerStartX = 272const rulerEndX = 1962// corresponds to total durationconst fraction = 4000 / 8450// target time / total durationconst clickX = rulerStartX + (rulerEndX - rulerStartX) * fraction
await state.page.mouse.click(clickX, rulerY)
This updates the editor canvas preview but the canvas only shows the viewport-visible
portion of the artboard. For full-resolution artboard screenshots, use the
/api/renderer/ approach above.
Porting to Remotion / egaki
See PORTING-TO-REMOTION.md for the full guide:
scene graph extraction, concept mapping table, easing usage, additive animation
model, and frame-by-frame comparison workflow.
All Jitter easings are exported from egaki/video as continuous-intensity
preset functions (e.g. impulseOvershoot(96)), generated by a port of
Jitter's own curve engine. See the easing mapping table in the porting guide.
Quick reference for smooth:standard:v1:
cubic-bezier(lerp(0.3, 0.9, intensity/100), 0, 0, 1). At intensity 50:
Easing.bezier(0.5, 0, 0, 1).
Tips
Wait for sync after modifications before exporting (1-2 seconds)
Asset URLs must be publicly accessible - Jitter fetches server-side
All *WithUndo actions can be undone with Ctrl+Z
Node IDs are stable and bookmarkable via ?nodeId=xxx
Export URLs require being logged in with project access
superSampling must be 2 or higher when using /api/renderer/; value 1 fails Zod validation
Use a separate page for the renderer so you don't lose editor state