> ## Documentation Index
> Fetch the complete documentation index at: https://docs.visualradioassist.live/llms.txt
> Use this file to discover all available pages before exploring further.

# 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.

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 theme={null}
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
2. Set layer ID to `my-text-layer` (bottom of layer properties)
3. 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 theme={null}
if (typeof visualLink !== "undefined") {
  // Your code here
}
```

<Note>
  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`.
</Note>

### 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 return.

```javascript theme={null}
// NEVER use class names or IDs directly in custom JS
gsap.to("#title-ed8732d"...
gsap.to(".list item"...

document.querySelector("#title-ede8w9")...
```

**Always use** `visualLink.getCanvasElement()`:

```typescript theme={null}
// 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.](/develop-with-vra/develop-visual-graphics/layer-transition-lifecycle-for-css)

```typescript theme={null}
const pm = visualLink.playStateManager;  // Shorthand for convenience
```

You register handlers for these moments using `pm.on()`.

### Lifecycle Events

Visuals have 3 key moments:

```typescript theme={null}
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:** `cuein` → `enter` → `leave`

***

## GSAP Basics

### Three Animation Methods

```typescript theme={null}
// 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 theme={null}
{
  // Position
  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
}
```

[GSAP Cheatsheet](https://gsap.com/cheatsheet)

***

## Complete Examples

### Example 1: Simple Fade In

```typescript theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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
2. Title fades in from top (slightly before logo finishes)
3. Subtitle fades in

### Example 5: Full In/Out with Reset

```typescript theme={null}
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 theme={null}
pm.on("enter", () => {
  gsap.from(layer, { opacity: 0, duration: 0.5 });
});
// No leave handler - uses default out
```

### Pattern: Continuous Animation

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

### Pattern: Sequence Multiple Animations

```typescript theme={null}
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 theme={null}
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 theme={null}
pm.on("enter", () => {
  if (!timeline.isActive()) {
    timeline.restart();
  }
});
```

### Visual data not updating

Access visual data via `visualLink.data`:

```typescript theme={null}
console.log(visualLink.data);  // See all available properties
const artistName = visualLink.data.artist;
```

***

## Best Practices

1. Always check `visualLink` exists before any code
2. Always use `visualLink.getCanvasElement()` for DOM queries
3. Reset animations in `cuein` for reliable repeated playback
4. Disable default transitions on GSAP-animated layers
5. Use timelines for multi-step animations
6. Check `isActive()` before restarting animations
7. Keep durations under 1s for snappy UI
8. Test with repeated plays (`cuein` → `enter` → `leave`)

***

## Reference

### visualLink API

```typescript theme={null}
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 theme={null}
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 theme={null}
gsap.to(target, vars)
gsap.from(target, vars)
gsap.fromTo(target, fromVars, toVars)
gsap.timeline(vars)
```

[Full GSAP Docs](https://gsap.com/docs)[GSAP Cheatsheet](https://gsap.com/cheatsheet)
