hypnagaga/src/components/ScrollyVideo/ScrollyVideo.svelte
2025-06-03 19:45:39 +05:30

210 lines
7.6 KiB
Svelte

<script lang="ts">
import { onDestroy } from 'svelte';
import { flattenObject } from './js/utils.js';
import ScrollyVideo from './js/ScrollyVideo.js';
import type { Snippet } from 'svelte';
import { setContext } from 'svelte';
/**
* Props for the ScrollyVideo Svelte component.
* @typedef {Object} Props
* @property {string} [class] - CSS class for scrolly container.
* @property {string} [id] - ID of the scrolly container.
* @property {ScrollyVideo} [scrollyVideo] - Bindable instance of ScrollyVideo.
* @property {string} [src] - Video source URL.
* @property {number} [videoPercentage] - Bindable percentage value to control video playback. Ranges from 0 to 1.
* @property {number} [transitionSpeed] - Sets the maximum playbackRate for this video.
* @property {number} [frameThreshold] - When to stop the video animation, in seconds.
* @property {string} [objectFit] - How the video should be resized to fit its container.
* @property {boolean} [sticky] - Whether the video should have position: sticky.
* @property {boolean} [full] - Whether the video should take up the entire viewport.
* @property {boolean} [trackScroll] - Whether this object should automatically respond to scroll. Set this to false while manually controlling `videoPercentage` prop.
* @property {boolean} [lockScroll] - Whether it ignores human scroll while it runs setVideoPercentage with enabled trackScroll.
* @property {boolean} [useWebCodecs] - Whether the library should use the webcodecs method. For more info, visit https://scrollyvideo.js.org/
* @property {() => void} [onReady] - The callback when it's ready to scroll.
* @property {() => void} [onChange] - The callback for video percentage change.
* @property {boolean} [debug] - Whether to log debug information. Internal library logs.
* @property {boolean} [showDebugInfo] - Shows debug information on page.
* @property {string} [height] - Height of the video container. Set it to 100svh when using inside `ScrollerBase`.
* @property {boolean} [autoplay] - Whether the video should autoplay.
* @property {boolean} [embedded] - Variable to control component rendering on embed page.
* @property {string} [embeddedSrc] - Source for the embedded video. If not provided, defaults to `src`.
* @property {Snippet} [children] - Children render function.
*/
interface Props {
/** CSS class for scrolly container */
class?: string;
/** ID of the scrolly container */
id?: string;
/** Bindable instance of ScrollyVideo */
scrollyVideo?: ScrollyVideo;
/** Video source URL */
src?: string;
/** Bindable percentage value to control video playback. **Ranges from 0 to 1** */
videoPercentage?: number;
/** Sets the maximum playbackRate for this video */
transitionSpeed?: number;
/** When to stop the video animation, in seconds */
frameThreshold?: number;
/** How the video should be resized to fit its container */
objectFit?: string;
/** Whether the video should have position: sticky */
sticky?: boolean;
/** Whether the video should take up the entire viewport */
full?: boolean;
/** Whether this object should automatically respond to scroll. Set this to **false** while manually controlling `videoPercentage` prop. */
trackScroll?: boolean;
/** Whether it ignores human scroll while it runs setVideoPercentage with enabled trackScroll */
lockScroll?: boolean;
/** Whether the library should use the webcodecs method. For more info, visit https://scrollyvideo.js.org/ */
useWebCodecs?: boolean;
/** The callback when it's ready to scroll */
onReady?: () => void;
/** The callback for video percentage change */
onChange?: () => void;
/** Whether to log debug information. Internal library logs. */
debug?: boolean;
/** Shows debug information on page */
showDebugInfo?: boolean;
/** Height of the video container. Set it to 100svh when using inside `ScrollerBase` */
height?: string;
/** Whether the video should autoplay */
autoplay?: boolean;
/** Variable to control component rendering on embed page */
embedded?: boolean;
/** Source for the embedded video. If not provided, defaults to `src` */
embeddedSrc?: string;
/** Children render function */
children?: Snippet;
}
/**
* Main logic for ScrollyVideo Svelte component.
* Handles instantiation, prop changes, and cleanup.
*/
let {
scrollyVideo = $bindable(),
videoPercentage,
onReady = $bindable(() => {}),
onChange = $bindable(() => {}),
height = '200svh',
showDebugInfo = false,
class: cls = '',
id = '',
embedded = false,
embeddedSrc = '',
children,
...restProps
}: Props = $props();
// variable to hold the DOM element
/**
* Reference to the scrolly video container DOM element.
* @type {HTMLDivElement | undefined}
*/
let scrollyVideoContainer = $state<HTMLDivElement | undefined>(undefined);
// Store the props so we know when things change
let lastPropsString = '';
$effect(() => {
if (scrollyVideoContainer) {
if (JSON.stringify(restProps) !== lastPropsString) {
// if scrollyvideo already exists and any parameter is updated, destroy and recreate.
if (scrollyVideo && scrollyVideo.destroy) scrollyVideo.destroy();
scrollyVideo = new ScrollyVideo({
scrollyVideoContainer,
onReady,
onChange,
...restProps,
});
// pass on component state to child components
// this controls fade in and out of foregrounds
setContext('scrollyVideoState', scrollyVideo.componentState);
// Save the new props
lastPropsString = JSON.stringify(restProps);
}
// If we need to update the target time percent
if (
scrollyVideo &&
typeof videoPercentage === 'number' &&
videoPercentage >= 0 &&
videoPercentage <= 1
) {
scrollyVideo.setVideoPercentage(videoPercentage);
}
}
});
/**
* Cleanup the component on destroy.
*/
onDestroy(() => {
if (scrollyVideo && scrollyVideo.destroy) scrollyVideo.destroy();
});
/**
* heightChange drives the height of the component when autoplay is set to true.
* @type {string}
*/
let heightChange = $derived.by(() => {
if (scrollyVideo) {
return `calc(${height} * ${1 - scrollyVideo?.componentState.autoplayProgress})`;
} else {
return height;
}
});
</script>
{#if embedded && (embeddedSrc || restProps.src)}
<div class="scrolly-video-container" style="width: 100%;">
<video
class="scrolly-video-embedded"
src={embeddedSrc || restProps.src}
autoplay
loop
muted
playsinline
style="width: 100%;"
></video>
</div>
{:else}
<div
{id}
class="scrolly-video-container {cls}"
style="height: {heightChange}; width: 100%; min-height: 100svh;"
>
<div bind:this={scrollyVideoContainer} data-scrolly-container>
{#if showDebugInfo && scrollyVideo}
<p class="debug-info text-xxs font-sans">
{@html JSON.stringify(flattenObject(scrollyVideo.componentState))
.replace(/[{}"]/g, '')
.split(',')
.join('<br>')}
</p>
<!-- renders foregrounds -->
{#if children}
{@render children()}
{/if}
{/if}
</div>
</div>
{/if}
<style lang="scss">
.debug-info {
position: absolute;
top: 0;
left: 0;
color: white;
background-color: rgba(0, 0, 0, 0.8);
z-index: 3;
padding: 8px;
margin: 0;
min-width: 25vmin;
}
</style>