# animot-presenter

Embed animated presentations anywhere. A single `<animot-presenter>` tag that plays [Animot](https://animot.io) animation JSON files with morphing transitions, code syntax highlighting, charts, particles, and more.

**Works with any framework:** vanilla HTML/JS, React, Vue, Angular, Svelte, or any frontend stack.

## Install

### npm (recommended for bundled projects)

```bash
npm install animot-presenter
```

### CDN (for vanilla HTML or quick prototyping)

```html
<link rel="stylesheet" href="https://unpkg.com/animot-presenter/dist/cdn/animot-presenter.css">
<script src="https://unpkg.com/animot-presenter/dist/cdn/animot-presenter.min.js"></script>
```

Or use the ES module version:

```html
<link rel="stylesheet" href="https://unpkg.com/animot-presenter/dist/cdn/animot-presenter.css">
<script type="module" src="https://unpkg.com/animot-presenter/dist/cdn/animot-presenter.esm.js"></script>
```

## Quick Start

```html
<animot-presenter src="/my-animation.json" autoplay loop controls></animot-presenter>
```

That's it. The component loads the JSON, renders the animation canvas, and handles everything — morphing, transitions, code highlighting, keyboard navigation.

## Usage by Framework

### Vanilla HTML / JavaScript

```html
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/animot-presenter/dist/cdn/animot-presenter.min.js"></script>
  <style>
    /* Size the presenter however you want */
    .hero-animation {
      width: 100%;
      height: 500px;
    }
  </style>
</head>
<body>
  <animot-presenter
    class="hero-animation"
    src="/animations/hero.json"
    autoplay
    loop
    controls
  ></animot-presenter>

  <script>
    const presenter = document.querySelector('animot-presenter');

    // Listen for events
    presenter.addEventListener('slidechange', (e) => {
      console.log(`Slide ${e.detail.index + 1} of ${e.detail.total}`);
    });

    presenter.addEventListener('complete', () => {
      console.log('Animation finished');
    });

    // Load data programmatically
    fetch('/animations/demo.json')
      .then(res => res.json())
      .then(data => {
        presenter.data = data;
      });

    // Control programmatically
    presenter.next();
    presenter.prev();
  </script>
</body>
</html>
```

### Svelte 5

```bash
npm install animot-presenter
```

```svelte
<script>
  import { browser } from '$app/environment';
  import { AnimotPresenter } from 'animot-presenter';
</script>

<!-- Wrap with {#if browser} in SvelteKit to skip SSR -->
<div style="width: 100%; height: 500px;">
  {#if browser}
    <AnimotPresenter
      src="/animations/hero.json"
      autoplay
      loop
      controls
      onslidechange={(index, total) => console.log(`${index + 1}/${total}`)}
    />
  {/if}
</div>
```

> **SvelteKit SSR:** The component uses browser APIs (canvas, ResizeObserver) and cannot be server-rendered. Always wrap with `{#if browser}` in SvelteKit, or use `ssr: false` in your `+page.ts`.

### React

```bash
npm install animot-presenter
```

```jsx
// Import once to register the custom element
import 'animot-presenter/element';
import { useEffect, useRef } from 'react';

function HeroAnimation() {
  const ref = useRef(null);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;

    const handleSlideChange = (e) => {
      console.log(`Slide ${e.detail.index + 1} of ${e.detail.total}`);
    };

    el.addEventListener('slidechange', handleSlideChange);
    return () => el.removeEventListener('slidechange', handleSlideChange);
  }, []);

  return (
    <animot-presenter
      ref={ref}
      src="/animations/hero.json"
      autoplay
      loop
      controls
      style={{ width: '100%', height: '500px', display: 'block' }}
    />
  );
}

// With programmatic data
function ProgrammaticDemo({ data }) {
  const ref = useRef(null);

  useEffect(() => {
    if (ref.current && data) {
      ref.current.data = data;
    }
  }, [data]);

  return (
    <animot-presenter
      ref={ref}
      controls
      arrows
      style={{ width: '100%', height: '400px', display: 'block' }}
    />
  );
}
```

**TypeScript:** Add this to your `react-app-env.d.ts` or a global `.d.ts` file:

```ts
declare namespace JSX {
  interface IntrinsicElements {
    'animot-presenter': React.DetailedHTMLProps<
      React.HTMLAttributes<HTMLElement> & {
        src?: string;
        autoplay?: boolean;
        loop?: boolean;
        controls?: boolean;
        arrows?: boolean;
        progress?: boolean;
        keyboard?: boolean;
        duration?: number;
        'start-slide'?: number;
      },
      HTMLElement
    >;
  }
}
```

### Vue 3

```bash
npm install animot-presenter
```

```vue
<script setup>
import 'animot-presenter/element';
import { ref, onMounted } from 'vue';

const presenterRef = ref(null);
const animationData = ref(null);

function onSlideChange(e) {
  console.log(`Slide ${e.detail.index + 1} of ${e.detail.total}`);
}

// Load data programmatically
onMounted(async () => {
  const res = await fetch('/animations/demo.json');
  animationData.value = await res.json();
  if (presenterRef.value) {
    presenterRef.value.data = animationData.value;
  }
});
</script>

<template>
  <!-- Option A: Load from URL -->
  <animot-presenter
    src="/animations/hero.json"
    autoplay
    loop
    controls
    style="width: 100%; height: 500px; display: block"
    @slidechange="onSlideChange"
  />

  <!-- Option B: Programmatic data -->
  <animot-presenter
    ref="presenterRef"
    controls
    arrows
    style="width: 800px; height: 450px; display: block"
  />
</template>
```

**Important:** Tell Vue to treat `animot-presenter` as a custom element. In `vite.config.ts`:

```ts
export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: {
          isCustomElement: (tag) => tag === 'animot-presenter'
        }
      }
    })
  ]
});
```

### Angular

```bash
npm install animot-presenter
```

**1. Register the custom element** in `main.ts`:

```ts
import 'animot-presenter/element';
```

**2. Allow custom elements** in your module or standalone component:

```ts
// app.module.ts
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';

@NgModule({
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  // ...
})
export class AppModule {}
```

Or in a standalone component:

```ts
@Component({
  selector: 'app-hero',
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <animot-presenter
      src="/animations/hero.json"
      autoplay
      loop
      controls
      style="width: 100%; height: 500px; display: block"
      (slidechange)="onSlideChange($event)"
      (complete)="onComplete()"
    ></animot-presenter>
  `
})
export class HeroComponent {
  onSlideChange(event: CustomEvent) {
    console.log(`Slide ${event.detail.index + 1} of ${event.detail.total}`);
  }

  onComplete() {
    console.log('Animation finished');
  }
}
```

**3. Programmatic control:**

```ts
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';

@Component({
  selector: 'app-demo',
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <animot-presenter #presenter controls arrows
      style="width: 100%; height: 400px; display: block">
    </animot-presenter>
    <button (click)="loadAnimation()">Load</button>
  `
})
export class DemoComponent implements AfterViewInit {
  @ViewChild('presenter') presenterRef!: ElementRef;

  async loadAnimation() {
    const res = await fetch('/animations/demo.json');
    const data = await res.json();
    this.presenterRef.nativeElement.data = data;
  }
}
```

## Props / Attributes

| Attribute      | Type      | Default | Description                              |
|----------------|-----------|---------|------------------------------------------|
| `src`          | `string`  | —       | URL to an Animot JSON file               |
| `data`         | `object`  | —       | Inline JSON object (JS property only)    |
| `autoplay`     | `boolean` | `false` | Auto-advance slides                      |
| `loop`         | `boolean` | `false` | Loop back to first slide after last      |
| `controls`     | `boolean` | `true`  | Show prev/next/play controls             |
| `arrows`       | `boolean` | `false` | Show left/right carousel arrows          |
| `progress`     | `boolean` | `true`  | Show progress bar at bottom              |
| `keyboard`     | `boolean` | `true`  | Enable arrow key navigation              |
| `duration`     | `number`  | —       | Override all transition durations (ms)   |
| `start-slide`  | `number`  | `0`     | Initial slide index                      |
| `mute-narration` | `boolean` | `false` | Force-disable per-slide voice narration regardless of the project's `settings.narrationEnabled`. Use on previews / gallery cards where audio would be intrusive. |

**Note:** `data` can only be set via JavaScript property, not as an HTML attribute.

### Per-slide voice narration

Projects with `settings.narrationEnabled: true` and per-slide `narration` clips
play voice-over alongside slide changes. Narration is bound to the deck's play
state — pressing the play button starts narration, pause stops it. To disable
narration on a specific embed regardless of the project's setting, set
`mute-narration`.

Recommended pattern for share-link / hero usage with narration: omit `autoplay`
(the deck loads paused) so the viewer's first click on Play doubles as the
gesture browsers require to unlock audio. Embedded gallery / preview surfaces
where audio would be intrusive should pass `mute-narration` and keep
`autoplay`.

## Events

| Event          | Detail                        | Description                          |
|----------------|-------------------------------|--------------------------------------|
| `slidechange`  | `{ index: number, total: number }` | Fired when slide changes       |
| `complete`     | —                             | Fired when last slide is reached     |

```js
// Vanilla JS
presenter.addEventListener('slidechange', (e) => {
  console.log(e.detail.index, e.detail.total);
});

// Svelte
<AnimotPresenter onslidechange={(i, t) => ...} oncomplete={() => ...} />

// Vue
<animot-presenter @slidechange="handler" @complete="handler" />

// Angular
<animot-presenter (slidechange)="handler($event)" (complete)="handler()" />
```

## Styling

The presenter fills its parent container. Control the size by styling the parent:

```css
/* Full page */
animot-presenter {
  display: block;
  width: 100vw;
  height: 100vh;
}

/* Hero section */
animot-presenter {
  display: block;
  width: 100%;
  height: 500px;
}

/* Fixed size card */
animot-presenter {
  display: block;
  width: 400px;
  height: 225px;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
}
```

### Customizing Controls

Override the built-in styles using CSS specificity:

```css
/* Custom control bar background */
animot-presenter .animot-controls {
  background: rgba(30, 30, 30, 0.9);
  border-radius: 20px;
  padding: 6px 12px;
}

/* Custom button style */
animot-presenter .animot-controls button {
  background: transparent;
  border-radius: 50%;
}

animot-presenter .animot-controls button:hover {
  background: rgba(255, 255, 255, 0.15);
}

/* Custom progress bar */
animot-presenter .animot-progress-fill {
  background: #3b82f6;
}

/* Custom arrow buttons */
animot-presenter .animot-arrow {
  background: rgba(0, 0, 0, 0.6);
  width: 48px;
  height: 48px;
}

/* Always show controls (no hover needed) */
animot-presenter .animot-controls,
animot-presenter .animot-arrow,
animot-presenter .animot-progress-bar {
  opacity: 1 !important;
}

/* Hide controls entirely via CSS */
animot-presenter .animot-controls { display: none; }
```

### Tailwind CSS

```html
<animot-presenter
  class="w-full h-[500px] rounded-xl overflow-hidden shadow-2xl"
  src="/animation.json"
  autoplay
  loop
></animot-presenter>
```

## Features

- **Morphing animations** — Elements with the same ID across slides smoothly morph position, size, rotation, color, opacity, border radius, and CSS filters (blur, brightness, contrast, saturate, grayscale)
- **Code highlighting** — Syntax highlighting via Shiki with typewriter, highlight-changes, and instant animation modes
- **Shape morphing** — Rectangles, circles, triangles, stars, hexagons with smooth transitions
- **Charts** — Animated bar, line, area, pie, and donut charts
- **Counters** — Animated number counting with formatting
- **Particles** — Canvas-based particle backgrounds with configurable shapes and connections
- **Confetti** — Burst, continuous, fireworks, and snow confetti effects
- **Floating animations** — Gentle floating motion for elements (vertical, horizontal, or both)
- **Transitions** — Fade, slide, zoom, flip, and morphing (none) transition types
- **Responsive** — Automatically scales to fit any container size
- **Keyboard navigation** — Arrow keys, spacebar, Home/End
- **CSS filters** — Blur, brightness, contrast, saturate, and grayscale on any element, animated across slides
- **Property sequencing** — Fine-grained control over which properties animate first (including filters via the `blur` property sequence)

## JSON Schema

Animot JSON files follow this structure:

```json
{
  "schemaVersion": 1,
  "id": "unique-id",
  "name": "My Animation",
  "slides": [
    {
      "id": "slide-1",
      "name": "Intro",
      "canvas": {
        "width": 1920,
        "height": 1080,
        "background": {
          "type": "gradient",
          "gradient": { "type": "linear", "angle": 135, "colors": ["#1a1a2e", "#16213e"] }
        },
        "elements": [
          {
            "id": "title",
            "type": "text",
            "content": "Hello World",
            "position": { "x": 100, "y": 200 },
            "size": { "width": 600, "height": 80 },
            "fontSize": 48,
            "fontWeight": 700,
            "color": "#ffffff",
            "rotation": 0,
            "visible": true,
            "zIndex": 1,
            "blur": 0,
            "brightness": 100,
            "contrast": 100,
            "saturate": 100,
            "grayscale": 0
          }
        ]
      },
      "transition": { "type": "fade", "duration": 500, "easing": "ease-in-out" },
      "duration": 3000
    }
  ],
  "settings": {
    "defaultCanvasWidth": 1920,
    "defaultCanvasHeight": 1080,
    "defaultTransition": { "type": "fade", "duration": 500, "easing": "ease-in-out" },
    "defaultSlideDuration": 3000
  }
}
```

Create animations visually at [animot.io](https://animot.io) and export as JSON.

### Editing the JSON

You can edit the exported JSON file directly. For example, image elements use a `src` field that accepts any valid image source:

```json
{
  "id": "logo",
  "type": "image",
  "src": "data:image/png;base64,iVBORw0KGgo..."
}
```

You can replace the base64 data URL with a remote or local URL:

```json
// Remote URL
"src": "https://example.com/images/logo.png"

// Relative path (served from your project)
"src": "/images/logo.png"

// Another relative path
"src": "./assets/hero-bg.jpg"
```

This lets you keep your JSON files lightweight by hosting images separately instead of embedding them as base64. The same applies to text elements with `backgroundImage` and background `image` fields.

### CSS Filters

All element types support CSS filter properties that animate smoothly between slides:

```json
{
  "id": "hero-image",
  "type": "image",
  "blur": 5,
  "brightness": 120,
  "contrast": 110,
  "saturate": 80,
  "grayscale": 0
}
```

| Property     | Range   | Default | Description                     |
|--------------|---------|---------|----------------------------------|
| `blur`       | 0–20    | 0       | Gaussian blur in pixels          |
| `brightness` | 0–200   | 100     | Brightness percentage (100 = normal) |
| `contrast`   | 0–200   | 100     | Contrast percentage (100 = normal)   |
| `saturate`   | 0–200   | 100     | Saturation percentage (100 = normal) |
| `grayscale`  | 0–100   | 0       | Grayscale percentage (0 = none)      |

Set different filter values on the same element across slides to create smooth animated transitions (e.g., blur 10 on slide 1 to blur 0 on slide 2 for a reveal effect). Use the `blur` property sequence in `animationConfig.propertySequences` to control filter animation timing independently.

> **Note:** Remote URLs must allow cross-origin requests (CORS) if served from a different domain.

## Bundle Size

| Build | Raw | Gzipped |
|-------|-----|---------|
| CDN (IIFE) | ~5.0 MB | ~850 KB |
| CDN (ESM) | ~5.2 MB | ~870 KB |
| Svelte (tree-shakeable) | ~56 KB | — |

The CDN bundle includes [Shiki](https://shiki.matsu.io/) for code syntax highlighting with 70 web-focused languages (see list below). The npm Svelte package is much smaller since Shiki is loaded lazily at runtime with all 500+ languages available.

### CDN supported languages

The CDN bundle includes these languages for code highlighting:

`angular-html`, `angular-ts`, `astro`, `bash`, `blade`, `c`, `c++`, `coffee`, `cpp`, `css`, `glsl`, `gql`, `graphql`, `haml`, `handlebars`, `html`, `http`, `imba`, `java`, `javascript`, `jinja`, `jison`, `json`, `json5`, `jsonc`, `jsonl`, `js`, `jsx`, `julia`, `less`, `lit`, `markdown`, `marko`, `mdc`, `mdx`, `php`, `postcss`, `pug`, `python`, `r`, `regex`, `sass`, `scss`, `sh`, `shell`, `sql`, `stylus`, `svelte`, `ts`, `tsx`, `typescript`, `vue`, `wasm`, `wgsl`, `xml`, `yaml`, `yml`, `zsh`

> **npm install** gives you all 500+ Shiki languages. The CDN bundle uses the lighter `shiki/bundle/web` with 70 common web/programming languages. If your code blocks use an unsupported language, it falls back to JavaScript highlighting.

## Browser Support

Works in all modern browsers that support Custom Elements v1:
- Chrome 67+
- Firefox 63+
- Safari 10.1+
- Edge 79+

## License

Business Source License 1.1 (BUSL-1.1)

- **Free** for non-commercial and personal use
- **Commercial use** requires a paid license — contact support@beeblock.com.br
- After the change date (4 years from each release), the code converts to Apache-2.0
