import { Meta } from '@storybook/blocks';

import * as ScrollyVideoStories from './ScrollyVideo.stories.svelte';

<Meta of={ScrollyVideoStories} />

# ScrollyVideo

The `ScrollyVideo` component creates interactive video experiences that respond to user scrolling. It is built on top of [ScrollyVideo.js](https://scrollyvideo.js.org/) and is designed to work seamlessly with Svelte.

## Basic demo

To use the `ScrollyVideo` component, import it and provide the video source. Set the `height` prop to any valid CSS height value such as `1200px`, `50vh`, or `500svh` to set the scroll height.

[Demo](?path=/story/components-graphics-scrollyvideo--demo)

```svelte
<script lang="ts">
  import { ScrollyVideo } from '@reuters-graphics/graphics-components';
</script>

<ScrollyVideo src="my-video.mp4" height="500svh" />
```

## Optimising videos

When using the `ScrollyVideo` component, minimise the video file size and ensure that the video is encoded in a format that is widely supported across browsers. Videos encoded at higher frames per second (FPS) are bound to crash on phone devices, so 24 FPS is recommended.

> 💡**TIP:** Set the `showDebugInfo` prop to `true` to see video encoding information

To optimise your video for the web, you can use `ffmpeg` to convert the video to a suitable format. Here is an example terminal command that converts a video to H.264 format with a resolution of 720p and a frame rate of 24 FPS:

```bash
npx ffmpeg -y -i <input_video_src>.mp4 -c:v libx264 -movflags faststart -crf 20 -r 24 -vf scale=720:-1 -profile:v baseline -preset veryslow -level:v 4.1 -color_primaries 1 -color_trc 1 -colorspace 1 -an <output_video>.mp4
```

Adjust the `-crf` value to control the quality. Lower `-crf` value means higher quality, with 20 being a generally good balance. See [Testing Media Capabilities](https://cconcolato.github.io/media-mime-support/mediacapabilities.html) for more.

## Responsive videos

To show different videos based on the screen width, use the `ScrollyVideo` component with conditional logic that uses a different video source depending on the [window width](https://svelte.dev/docs/svelte/svelte-window).

[Demo](?path=/story/components-graphics-scrollyvideo--responsive-videos)

```svelte
<script lang="ts">
  import { ScrollyVideo } from '@reuters-graphics/graphics-components';

  let width = $state(0);
</script>

<svelte:window bind:innerWidth={width} />

{#if width < 600}
  <!-- Video with aspect ratio 9:16 for window width smaller than 600px -->
  <ScrollyVideo src="my-video-sm.mp4" height="500svh" />
{:else if width < 1200}
  <!-- Video with aspect ratio 1:1 for window width between 600px and 1200px -->
  <ScrollyVideo src="my-video-md.mp4" height="500svh" />
{:else}
  <!-- Video with aspect ratio 16:9 for window width above 1200px -->
  <ScrollyVideo src="my-video-lg.mp4" height="500svh" />
{/if}
```

## Embeds

Scrollytelling does not work in iframes. If the prop `embedded` is set to `true`, the video will be rendered as a regular video instead. By default, the video element has the properties `loop`, `muted`, and `playsinline` and `controls`. To customise the video properties, use the `embeddedProps` prop to render the embed video.

To use a different video for the embedded version, pass its source to the `embeddedSrc` prop. If `embeddedSrc` is not provided, the component will use the `src` prop.

> 💡**TIP:** One way to recreate the ScrollyVideo experience for embeds is to record the desktop screen with [Scroll Capture](https://chromewebstore.google.com/detail/scroll-capture/egmhoeaacclmanaimofoooiamhpkimkk?hl=en) while scrollying through the video and use that video instead.

[Demo](?path=/story/components-graphics-scrollyvideo--embed)

```svelte
<script lang="ts">
  import { ScrollyVideo } from '@reuters-graphics/graphics-components';
</script>

<!-- Optionally pass `embeddedSrc` and `embeddedProps` -->
<ScrollyVideo
  embedded={true}
  src="my-video.mp4"
  embeddedSrc="my-video-embedded.mp4"
  embeddedProps={{ autoplay: true }}
/>
```

## Autoplay

The `autoplay` option combines the autoplay and scrollytelling experience. If set to `true`, the video will start playing automatically when the component is mounted, but switch to scrollytelling when the user starts scrolling. The scroll height is calculated based on how much of the video remains, which means that if the user lets the video autoplay to near the end, the user would only have to scroll through a small height to get to the end. If the user lets the video autoplay to the end, there will be no scrolling effect.

[Demo](?path=/story/components-graphics-scrollyvideo--autoplay)

```svelte
<script lang="ts">
  import { ScrollyVideo } from '@reuters-graphics/graphics-components';
</script>

<ScrollyVideo src="my-video.mp4" autoplay={true} />
```

## Time-based text foregrounds with ArchieML

The `ScrollyVideo` component can also be used to display text as foregrounds at specific times in the video. To do so, use the `text` prop in `ScrollyVideoForeground` component.

[Demo](?path=/story/components-graphics-scrollyvideo--archie-ml-foregrounds)

With the graphics kit, you'll likely get your text and prop values from an ArchieML doc...

```yaml
# ArchieML doc

[blocks]
  type: scrolly-video
  id: alps-scrolly
  src: videos/alps.mp4
  height: 800svh

  # Array of foregrounds
  [.foregrounds]
    startTime: 3 # When in the video to start showing the foreground
    endTime: 7 # When to stop showing the foreground
    width: normal # text container width
    position: bottom center # Position of the text. Optional; defaults to 'center center'. Must be a combination of `top/bottom/center center/left/right`
    backgroundColour: rgba(0, 0, 0, 0.8) # Optional; defaults to white
    text: #### The Alps
    The Alps stretch across eight countries: France, Switzerland, Italy, Monaco, Liechtenstein, Austria, Germany, and Slovenia, covering about 1,200 kilometers (750 miles).
    :end

    startTime: 9
    endTime: 12
    width: normal
    position: bottom center
    backgroundColour: rgba(0, 0, 0, 0.8)
    text: Mont Blanc, standing at 4,809 meters (15,777 feet), is the highest peak in the Alps and Western Europe, though there's ongoing debate between France and Italy about exactly where the summit lies.
    :end

    startTime: 16
    endTime: 20
    width: normal
    position: bottom center
    backgroundColour: rgba(0, 0, 0, 0.8)
    text: #### History
    The Alps were formed around **65 million years** ago when the African and Eurasian tectonic plates collided, pushing the land upward.Over 14 million people live in the Alpine region, with tourism supporting approximately 120 million visitors annually.
    :end
  []
[]
```

... which you'll parse out of a ArchieML block object before passing to the `ScrollyVideo` and `ScrollyVideoForeground` components.

```svelte
<script lang="ts">
  import {
    ScrollyVideo,
    ScrollyVideoForeground,
  } from '@reuters-graphics/graphics-components';
  import { assets } from '$app/paths'; // 👈 If using in the graphics kit...
</script>

{#each content.blocks as block}
  <!-- Inside the content.blocks for loop... -->
  {#if block.type == 'scrolly-video'}
    <ScrollyVideo
      id={block.id}
      src={`${assets}/${block.src}}`}
      height={block.height}
    >
      <!-- Loop through foregrounds to add text blurbs that appear/disappear at specific times -->
      {#each block.foregrounds as foreground}
        <ScrollyVideoForeground
          startTime={parseFloat(foreground.startTime)}
          endTime={parseFloat(foreground.endTime)}
          width={foreground.width}
          position={foreground.position}
          backgroundColour={foreground.backgroundColour}
          text={foreground.text}
        />
      {/each}
    </ScrollyVideo>
  {/if}
{/each}
```

## Time-based component foregrounds with ArchieML

The `ScrollyVideo` component can also be used to display components, such as `Headline` or ai2svelte files, as foregrounds at specific times in the video. To do so, use the `Foreground` prop in `ScrollyVideoForeground` component.

> **IMPORTANT❗**: When layering ai2svelte files over a video, the aspect ratio of the ai2svelte graphics should match that of the video. If the ai2svelte graphic is responsive and has, for example, small, medium and large verisons — which is generally the case — make sure to also render small, medium and large versions of the video at the appropriate screen sizes. See [Responsive videos](#responsive-videos) for more details.

[Demo](?path=/story/components-graphics-scrollyvideo--component-archie-ml-foregrounds)

With the graphics kit, you'll likely get your text and prop values from an ArchieML doc...

```yaml
# ArchieML doc

# Headline
hed: Wind and waves
[authors]
 * Jane Doe
[]
publishTime: 2020-01-01T00:00:00Z
startTime: 0 # When in the video to start showing the headline
endTime: 0.3 # When to stop showing the headline

[blocks]
  type: scrolly-video
  id: my-scrolly-video
  height: 800svh

  # Adjust prop names as needed
  srcSm: videos/my-video-sm.mp4
  srcMd: videos/my-video-md.mp4
  srcLg: videos/my-video-lg.mp4

  # Array of foregrounds
  [.foregrounds]
    startTime: 0.3 # When in the video to start showing the foreground
    endTime: 2.2 # When to stop showing the foreground
    width: fluid # foreground container width
    Foreground: Foreground1 # Name of the ai2svelte component to render

    startTime: 2.2
    endTime: 3.2
    width: fluid
    Foreground: Foreground2

    startTime: 3.2
    endTime: 4.5
    width: fluid
    Foreground: Foreground3

    startTime: 6.5
    endTime: 8
    width: fluid
    Foreground: Foreground4
  []
[]
```

... which you'll parse out of a ArchieML block object before passing to the `ScrollyVideo` and `ScrollyVideoForeground` components.

```svelte
<script lang="ts">
  import { assets } from '$app/paths'; // 👈 If using in the graphics kit...
=
  import {
    Headline,
    GraphicBlock,
    ScrollyVideo,
    Foreground,
  } from '@reuters-graphics/graphics-components';

  // Foreground ai2svelte components
  import Foreground1 from './ai2svelte/foreground1.svelte';
  import Foreground2 from './ai2svelte/foreground2.svelte';
  import Foreground3 from './ai2svelte/foreground3.svelte';
  import Foreground4 from './ai2svelte/foreground4.svelte';

  // Add your imported foreground ai2svelte charts to this object
  const aiChartsForeground = {
    Foreground1,
    Foreground2,
    Foreground3,
    Foreground4,
  };

  // Window width for responsive videos
  let width = $state(1);
</script>

<svelte:window bind:innerWidth={width} />

<!-- Loop through content blocks... -->
{#each content.blocks as block}

  {#if block.type == 'scrolly-video'}

  <!-- ScrollVideo snippet to render responsive videos -->
  {#snippet ScrollVideo(height: string, src: string)}
    <ScrollyVideo
      id={block.id}
      {height}
      {src}
    >
      <!-- Headline component as foreground -->
      <ScrollyVideoForeground
        startTime={parseFloat(content.startTime)}
        endTime={parseFloat(content.endTime)}
      >
        <Headline
          hed={content.hed}
          authors={content.authors}
          publishTime={new Date(content.publishTime).toISOString()}
        />
      </ScrollyVideoForeground>

      <!-- Loop through block.foregrounds to render each foreground component -->
      {#each block.foregrounds as foreground}
          <ScrollyVideoForeground
            startTime={parseFloat(foreground.startTime)}
            endTime={parseFloat(foreground.endTime)}
            width={foreground.width}
            Foreground={aiChartsForeground[
              foreground.foreground as keyof typeof aiChartsForeground
            ]}
          />
      {/each}
    </ScrollyVideo>
  {/snippet}

  <!-- Render the ScrollVideo snippet for different screen sizes -->
  {#if width < 600}
    {@render ScrollVideo({block.height}, `${assets}/${block.srcSm}`)}
  {:else if width < 1200}
    {@render ScrollVideo({block.height}, `${assets}/${block.srcMd}`)}
  {:else}
    {@render ScrollVideo({block.height}, `${assets}/${block.srcLg}`)}
  {/if}
{/each}
```

## Using with `ScrollerBase`

The `ScrollyVideo` component can be used inside the [ScrollerBase](?path=/story/components-graphics-scrollerbase--docs) component to add foreground content. This allows for foreground that scrolls up and down over the video, instead of fading in and out at specific times.

> **Note**: To use `ScrollyVideo` with `ScrollerBase`, set `trackScroll` to `false` and pass the bindable prop `progress` from `ScrollerBase` as `videoPercentage` to `ScrollyVideo`.

[Demo](?path=/story/components-graphics-scrollyvideo--scroller-base)

```svelte
<script lang="ts">
  import {
    ScrollyVideo,
    ScrollerBase,
  } from '@reuters-graphics/graphics-components';

  // Pass `progress` as `videoPercentage` to ScrollyVideo
  let progress = $state(0);
</script>

<ScrollerBase bind:progress query="div.step-foreground-container">
  {#snippet backgroundSnippet()}
    <!-- Pass bindable prop `progress` as `videoPercentage` and set `trackScroll` to `false` -->
    <ScrollyVideo
      src="my-video.mp4"
      videoPercentage={progress}
      trackScroll={false}
    />
  {/snippet}
  {#snippet foregroundSnippet()}
    <!-- Add custom foreground HTML or component -->
    <div class="step-foreground-container">
      <h3 class="text-center">Step 1</h3>
    </div>
    <div class="step-foreground-container">
      <h3 class="text-center">Step 2</h3>
    </div>
    <div class="step-foreground-container">
      <h3 class="text-center">Step 3</h3>
    </div>
  {/snippet}
</ScrollerBase>

<style lang="scss">
  .step-foreground-container {
    height: 100vh;
    width: 50%;
    padding: 1em;
    margin: auto;

    h3 {
      display: flex;
      align-items: center;
      justify-content: center;
      height: 100%;
      color: white;
    }
  }
</style>
```

## Advanced usecases

For advanced use cases such as looping a particular section of the video, or jumping to a specific time in the video, you can bind `scrollyVideo` prop and take benefits of methods such as `setVideoPercentage` or bindable methods such as `onReady` and `onChange`. This allows for fine-grained control over the video playback and interaction with the scroll position.

```js
scrollyVideo.setVideoPercentage(0.5, {
  jump: true,
});
```

This will seek the video to 50% progress at a single instant. Setting `jump` to `false` will make smooth transition to the provided progress value.

[Demo](?path=/story/components-graphics-scrollyvideo--advanced)

```svelte
<script lang="ts">
  import {
    ScrollyVideo,
    ScrollerBase,
  } from '@reuters-graphics/graphics-components';
  import { onDestroy } from 'svelte';

  let progress = $state(0);
  let scrollyVideo = $state();
  let now;
  let then = 0;
  let time = 0;
  let currentProgress = 0; // holds progress value for dynamic looping
  let loopCutoff = 0.33; // value between 0-1 to loop the video by
  let totalTime = 9 * 1000; // milliseconds

  let animationId;

  // clamps n value between low and high
  function constrain(n, low, high) {
    return Math.max(Math.min(n, high), low);
  }

  // maps n value between two ranges
  function map(n, start1, stop1, start2, stop2, withinBounds = true) {
    const newval =
      ((n - start1) / (stop1 - start1)) * (stop2 - start2) + start2;
    if (!withinBounds) {
      return newval;
    }
    if (start2 < stop2) {
      return constrain(newval, start2, stop2);
    } else {
      return constrain(newval, stop2, start2);
    }
  }

  // loops the video between 0 and loopCutoff
  function renderVideo() {
    if (progress < loopCutoff) {
      now = Date.now();
      const elapsed = now - then;

      // if (elapsed > fpsInterval) {
      time += elapsed;
      currentProgress = map(time, 0, totalTime, 0, 1);
      scrollyVideo.setVideoPercentage(currentProgress, { jump: true });

      if (currentProgress > loopCutoff) {
        currentProgress = 0;
        time = 0;
        scrollyVideo.setVideoPercentage(0, { jump: true });
      }

      then = now;
      // }
    } else {
      scrollyVideo.setVideoPercentage(progress, { jump: true });
    }

    animationId = requestAnimationFrame(renderVideo);
  }

  // initializes video autoplay
  // when it's ready to play
  function initAutoplay() {
    then = Date.now();
    renderVideo();
  }

  // cancel RAF on destroy
  onDestroy(() => {
    if (animationId) {
      cancelAnimationFrame(animationId);
    }
  });
</script>

<ScrollerBase bind:progress query="div.step-foreground-container" visible>
  {#snippet backgroundSnippet()}
    <ScrollyVideo
      bind:scrollyVideo
      src="my-video.mp4"
      height="100svh"
      trackScroll={false}
      showDebugInfo
      onReady={initAutoplay}
    />
    <!-- Only for debugging -->
    <div id="progress-bar">
      <p>ScrollerBase progress: {progress.toPrecision(2)}</p>
      <progress class="mb-4" value={progress}></progress>
    </div>
  {/snippet}
  {#snippet foregroundSnippet()}
    <!-- Add custom foreground HTML or component -->
    <div class="step-foreground-container">
      <h3 class="text-center">Step 1</h3>
    </div>
    <div class="step-foreground-container">
      <h3 class="text-center">Step 2</h3>
    </div>
    <div class="step-foreground-container">
      <h3 class="text-center">Step 3</h3>
    </div>
  {/snippet}
</ScrollerBase>

<style lang="scss">
  @use '../../../scss/mixins' as mixins;

  // svelte-scroller-background
  #progress-bar {
    background-color: rgba(0, 0, 0, 0.8);
    position: absolute;
    z-index: 4;
    right: 0;
    padding: 1rem;
    top: 0;

    progress {
      height: 6px;
      background-color: #ff000044; /* Background color of the entire bar */
      margin: 0;
    }

    progress::-webkit-progress-value {
      background-color: white;
      border-radius: 10px;
    }

    progress::-webkit-progress-bar {
      background-color: #444444;
      border-radius: 10px;
    }

    p {
      font-family: var(--theme-font-family-sans-serif);
      color: white;
      font-size: var(--theme-font-size-xs);
      padding: 0;
      margin: 0;
    }
  }

  .step-foreground-container {
    height: 100vh;
    width: 50%;
    padding: 1em;
    margin: auto;

    h3 {
      // align center
      display: flex;
      align-items: center;
      justify-content: center;
      height: 100%;
      color: white;
    }
  }
</style>
```
