GSAP Animations with Visual Radio Graphics

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:
typescript
if (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:
  1. Add custom JS to your visual
  1. Set layer ID to my-text-layer (bottom of layer properties)
  1. Turn off default transition on that layer

Core Concepts

The visualLink Object

visualLink connects your JavaScript to the visual lifecycle. Always check it exists:
typescript
if (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.
typescript
const pm = visualLink.playStateManager; // Shorthand for convenience
You register handlers for these moments using pm.on().

Lifecycle Events

Visuals have 3 key moments:
typescript
const 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: cueinenterleave

GSAP 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

typescript
if (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

typescript
if (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

typescript
if (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:
typescript
if (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:
  1. Logo scales up
  1. Title fades in from top (slightly before logo finishes)
  1. Subtitle fades in

Example 5: Full In/Out with Reset

typescript
if (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

typescript
pm.on("enter", () => { gsap.from(layer, { opacity: 0, duration: 0.5 }); }); // No leave handler - uses default out

Pattern: Continuous Animation

typescript
pm.on("enter", () => { gsap.to(layer, { rotation: 360, duration: 2, repeat: -1, // Infinite ease: "none" // Linear }); });

Pattern: Sequence Multiple Animations

typescript
const 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

typescript
const 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 exists
Layer 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 playback
Transition Settings mode matches intent

Element not found

Layer ID set in properties (bottom of panel)
Using visualLink.getCanvasElement() not document.querySelector
ID has no # prefix in getCanvasElement("id")

Animation plays multiple times

Solution: Check if active before restarting:
typescript
pm.on("enter", () => { if (!timeline.isActive()) { timeline.restart(); } });

Visual data not updating

Access visual data via visualLink.data:
typescript
console.log(visualLink.data); // See all available properties const artistName = visualLink.data.artist;

Best Practices

  1. Always check visualLink exists before any code
  1. Always use visualLink.getCanvasElement() for DOM queries
  1. Reset animations in cuein for reliable repeated playback
  1. Disable default transitions on GSAP-animated layers
  1. Use timelines for multi-step animations
  1. Check isActive() before restarting animations
  1. Keep durations under 1s for snappy UI
  1. Test with repeated plays (cueinenterleave)

Reference

visualLink API

typescript
visualLink.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

typescript
pm.on("cuein", handler) // Before show (reset) pm.on("enter", handler) // Show visual (in animation) pm.on("leave", handler) // Hide visual (out animation)

GSAP Essentials

typescript
gsap.to(target, vars) gsap.from(target, vars) gsap.fromTo(target, fromVars, toVars) gsap.timeline(vars)