To add GSAP animations to your Visuals in Visual Radio Assist, you can make use of the custom JS in the Designer.
Quick Start
Hello World Animation
Fade in a text layer when the visual appears:
typescriptif (typeof visualLink !== "undefined") { const pm = visualLink.playStateManager; const getCanvasElement = visualLink.getCanvasElement; pm.on("enter", () => { gsap.to(getCanvasElement("my-text-layer"), { opacity: 1, duration: 1 }); }); }
Required Steps:
- Add custom JS to your visual
- Set layer ID to
my-text-layer(bottom of layer properties)
- Turn off default transition on that layer
Core Concepts
The visualLink Object
visualLink connects your JavaScript to the visual lifecycle. Always check it exists:typescriptif (typeof visualLink !== "undefined") { // Your code here }
When writing custom JavaScript for visuals, you have access to the
visualLink object that provides methods to interact with your visual canvas. It's the link between the code and the visual lifecycle and data. Every layer is accessible via the visualLink.Getting Elements
Never use
document.querySelector - visuals renderer places elements in different contexts on the page, there is no guarantee that the querySelector will query the element you expect it to .javascript// DONT use class names or IDs directly gsap.to("#title-ed8732d"... gsap.to(".list item"... document.querySelector
Always use
visualLink.getCanvasElement():typescript// By ID const title = visualLink.getCanvasElement("title-layer"); // By class (returns multiple) const items = visualLink.getCanvasElement(".list-item");
Set IDs at bottom of layer properties panel.
The Play State Manager
visualLink.playStateManager controls when your visual appears and disappears. It fires events at key moments in the visual's lifecycle.typescriptconst pm = visualLink.playStateManager; // Shorthand for convenience
You register handlers for these moments using
pm.on().Lifecycle Events
Visuals have 3 key moments:
typescriptconst pm = visualLink.playStateManager; // 1. Before showing (prepare state) pm.on("cuein", () => { // Reset animations to start position }); // 2. Show visual (play in animation) pm.on("enter", () => { // Start your animation }); // 3. Hide visual (play out animation) pm.on("leave", () => { // Exit animation });
Execution order:
cuein → enter → leaveGSAP Basics
Three Animation Methods
typescript// Animate TO a state gsap.to(".element", { x: 100, duration: 1 }); // Animate FROM a state to current gsap.from(".element", { opacity: 0, duration: 1 }); // Define both start and end gsap.fromTo(".element", { opacity: 0, y: 50 }, { opacity: 1, y: 0, duration: 1 } );
Common Properties
typescript{ // Position x: 100, // Move right 100px y: -50, // Move up 50px // Appearance opacity: 0.5, // Fade to 50% scale: 1.2, // Scale to 120% rotation: 45, // Rotate 45deg // Timing duration: 1, // 1 second delay: 0.5, // Wait 0.5s before start ease: "power2.out" // Easing function }
Complete Examples
Example 1: Simple Fade In
typescriptif (typeof visualLink !== "undefined") { const pm = visualLink.playStateManager; const getCanvasElement = visualLink.getCanvasElement; const title = getCanvasElement("title"); pm.on("enter", () => { gsap.from(title, { opacity: 0, duration: 0.5 }); }); }
What this does: Title fades in over 0.5s when visual enters.
Example 2: Slide In + Fade Out
typescriptif (typeof visualLink !== "undefined") { const pm = visualLink.playStateManager; const getCanvasElement = visualLink.getCanvasElement; const subtitle = getCanvasElement("subtitle"); pm.on("enter", () => { gsap.from(subtitle, { x: -100, opacity: 0, duration: 0.6 }); }); pm.on("leave", () => { gsap.to(subtitle, { opacity: 0, duration: 0.3 }); }); }
What this does:
- Enter: Slides in from left with fade
- Leave: Fades out
Example 3: Multiple Layers
typescriptif (typeof visualLink !== "undefined") { const pm = visualLink.playStateManager; const getCanvasElement = visualLink.getCanvasElement; const layers = [ getCanvasElement("layer-1"), getCanvasElement("layer-2"), getCanvasElement("layer-3") ]; pm.on("enter", () => { gsap.from(layers, { opacity: 0, y: 30, duration: 0.5, stagger: 0.2 // 0.2s delay between each }); }); }
What this does: 3 layers appear one after another with 0.2s delay.
Example 4: Using Timelines
For complex multi-step animations:
typescriptif (typeof visualLink !== "undefined") { const pm = visualLink.playStateManager; const getCanvasElement = visualLink.getCanvasElement; const title = getCanvasElement("title"); const subtitle = getCanvasElement("subtitle"); const logo = getCanvasElement("logo"); const timeline = gsap.timeline({ paused: true }); timeline .from(logo, { scale: 0, duration: 0.4 }) .from(title, { opacity: 0, y: -20, duration: 0.3 }, "-=0.2") .from(subtitle, { opacity: 0, duration: 0.3 }); pm.on("enter", () => { timeline.restart(); }); }
What this does:
- Logo scales up
- Title fades in from top (slightly before logo finishes)
- Subtitle fades in
Example 5: Full In/Out with Reset
typescriptif (typeof visualLink !== "undefined") { const pm = visualLink.playStateManager; const getCanvasElement = visualLink.getCanvasElement; const layer = getCanvasElement("animated-layer"); const intro = gsap.timeline({ paused: true }); const outro = gsap.timeline({ paused: true }); intro.fromTo(layer, { opacity: 0, x: -50 }, { opacity: 1, x: 0, duration: 0.6 } ); outro.to(layer, { opacity: 0, x: 50, duration: 0.4 }); pm.on("cuein", () => { intro.progress(0).pause(); outro.progress(0).pause(); }); pm.on("enter", () => { if (!intro.isActive()) intro.restart(); }); pm.on("leave", () => { if (!outro.isActive()) outro.restart(); }); }
What this does:
cuein: Reset both animations
enter: Slide in from left
leave: Slide out to right
Important: Always reset animations in
cuein to prevent state issues.Transition Settings
Found in Visual settings below tag editor. Controls overall visual transition behavior.
Default
Layers transition independently. Each layer uses its own timing.
Use when: Simple visuals with independent layer animations.
Layer Based
Visual waits for ALL layers to finish transitioning.
Duration = sum of all layer transitions
Use when: Coordinated multi-layer animations where all must complete.
Visual Based
Fixed duration for entire visual, layers animate within that time.
Settings:
- In Duration (ms): Time to show visual
- Out Duration (ms): Time to hide visual
Use when: Consistent timing regardless of layer complexity.
Visual Fixed
Single fixed-length visual. No individual layer control.
Settings:
- In Duration (ms): Show time
- Out Duration (ms): Hide time
- Layer transitions disabled
Use when: Simple displays, no complex timing needed.
Common Patterns
Pattern: Layer That Only Animates In
typescriptpm.on("enter", () => { gsap.from(layer, { opacity: 0, duration: 0.5 }); }); // No leave handler - uses default out
Pattern: Continuous Animation
typescriptpm.on("enter", () => { gsap.to(layer, { rotation: 360, duration: 2, repeat: -1, // Infinite ease: "none" // Linear }); });
Pattern: Sequence Multiple Animations
typescriptconst tl = gsap.timeline(); tl.to(layer1, { opacity: 1, duration: 0.3 }) .to(layer2, { x: 100, duration: 0.5 }) .to(layer3, { scale: 1.5, duration: 0.4 });
Pattern: Overlap Animations
typescriptconst tl = gsap.timeline(); tl.to(layer1, { opacity: 1, duration: 0.5 }) .to(layer2, { x: 100, duration: 0.5 }, "-=0.3") // Start 0.3s before layer1 ends .to(layer3, { scale: 1.5, duration: 0.5 }, "+=0.2"); // Start 0.2s after layer2 ends
Troubleshooting
Animation doesn't run
typeof visualLink !== "undefined" wrapper existsLayer ID matches
getCanvasElement("id") Default transition disabled on animated layers
Console for errors
Animation runs but looks wrong
Using lifecycle events correctly (
cuein/enter/leave)Reset animations in
cuein for repeatable playbackTransition Settings mode matches intent
Element not found
Layer ID set in properties (bottom of panel)
Using
visualLink.getCanvasElement() not document.querySelectorID has no
# prefix in getCanvasElement("id")Animation plays multiple times
Solution: Check if active before restarting:
typescriptpm.on("enter", () => { if (!timeline.isActive()) { timeline.restart(); } });
Visual data not updating
Access visual data via
visualLink.data:typescriptconsole.log(visualLink.data); // See all available properties const artistName = visualLink.data.artist;
Best Practices
- Always check
visualLinkexists before any code
- Always use
visualLink.getCanvasElement()for DOM queries
- Reset animations in
cueinfor reliable repeated playback
- Disable default transitions on GSAP-animated layers
- Use timelines for multi-step animations
- Check
isActive()before restarting animations
- Keep durations under 1s for snappy UI
- Test with repeated plays (
cuein→enter→leave)
Reference
visualLink API
typescriptvisualLink.data // Visual content data visualLink.canvas() // Main canvas element visualLink.getCanvasElement(query) // Query elements safely visualLink.playStateManager // Lifecycle events visualLink.abortController // Cleanup controller
Lifecycle Events
typescriptpm.on("cuein", handler) // Before show (reset) pm.on("enter", handler) // Show visual (in animation) pm.on("leave", handler) // Hide visual (out animation)
GSAP Essentials
typescriptgsap.to(target, vars) gsap.from(target, vars) gsap.fromTo(target, fromVars, toVars) gsap.timeline(vars)