diff --git a/src/components/ScrollerLottie/ScrollerLottie.svelte b/src/components/ScrollerLottie/ScrollerLottie.svelte index ce4cfd0b..c3cdff8f 100644 --- a/src/components/ScrollerLottie/ScrollerLottie.svelte +++ b/src/components/ScrollerLottie/ScrollerLottie.svelte @@ -4,7 +4,14 @@ import { DotLottie } from '@lottiefiles/dotlottie-web'; import { createLottieState } from './ts/lottieState.svelte'; import { isEqual } from 'es-toolkit'; - import { map } from './ts/utils'; + import { + syncLottieState, + getMarkerRange, + calculateTargetFrame, + isReverseMode, + createRenderConfig, + isNullish, + } from './ts/utils'; import { Tween } from 'svelte/motion'; // Components @@ -74,7 +81,7 @@ : []; } - if (marker == '' || marker == null || marker == undefined) { + if (isNullish(marker)) { start = segment ? segment[0] : 0; end = segment ? segment[1] : lottiePlayer.totalFrames - 1; } @@ -92,86 +99,11 @@ } function onRenderEvent() { - const keys = [ - 'currentFrame', - 'totalFrames', - 'duration', - 'loop', - 'speed', - 'loopCount', - 'mode', - 'isPaused', - 'isPlaying', - 'isStopped', - 'isLoaded', - 'isFrozen', - 'segment', - 'autoplay', - 'layout', - 'activeThemeId', - 'marker', - ]; - if (lottiePlayer && lottieState) { - keys.forEach((key) => { - switch (key) { - case 'currentFrame': - lottieState.currentFrame = lottiePlayer!.currentFrame; - break; - case 'totalFrames': - lottieState.totalFrames = lottiePlayer!.totalFrames; - break; - case 'duration': - lottieState.duration = lottiePlayer!.duration; - break; - case 'loop': - lottieState.loop = lottiePlayer!.loop; - break; - case 'speed': - lottieState.speed = lottiePlayer!.speed; - break; - case 'loopCount': - lottieState.loopCount = lottiePlayer!.loopCount; - break; - case 'mode': - lottieState.mode = lottiePlayer!.mode; - break; - case 'isPaused': - lottieState.isPaused = lottiePlayer!.isPaused; - break; - case 'isPlaying': - lottieState.isPlaying = lottiePlayer!.isPlaying; - break; - case 'isStopped': - lottieState.isStopped = lottiePlayer!.isStopped; - break; - case 'isLoaded': - lottieState.isLoaded = lottiePlayer!.isLoaded; - break; - case 'isFrozen': - lottieState.isFrozen = lottiePlayer!.isFrozen; - break; - case 'segment': - lottieState.segment = lottiePlayer!.segment ?? null; - break; - case 'autoplay': - lottieState.autoplay = lottiePlayer!.autoplay ?? false; - break; - case 'layout': - lottieState.layout = lottiePlayer!.layout ?? null; - break; - case 'activeThemeId': - lottieState.activeThemeId = lottiePlayer!.activeThemeId ?? null; - break; - case 'marker': - lottieState.marker = lottiePlayer!.marker ?? undefined; - break; - } - }); - + syncLottieState(lottiePlayer, lottieState); progress = (lottiePlayer.currentFrame + 1) / lottiePlayer.totalFrames; lottieState.progress = progress; - onRender(); // call user-defined onRender function + onRender(); } } @@ -189,12 +121,7 @@ progressTween = new Tween(0, { duration: tweenDuration, easing: easing }); - const _renderConfig = { - autoResize: true, - devicePixelRatio: - window.devicePixelRatio > 1 ? window.devicePixelRatio * 0.75 : 1, - freezeOnOffscreen: true, - }; + const _renderConfig = createRenderConfig(); lottiePlayer = new DotLottie({ canvas, @@ -252,20 +179,11 @@ lottiePlayer?.unfreeze(); lottieState.isFrozen = false; } - const targetFrame = map( - mode == 'reverse' || mode == 'reverse-bounce' ? - 1 - progress - : progress, - 0, - 1, - start, - end - ); + const targetFrame = calculateTargetFrame(progress, mode, start, end); progressTween.target = targetFrame; // lottiePlayer.setFrame(targetFrame); } else if ((progress < 0 || progress > 1) && !lottieState.isFrozen) { - // lottiePlayer.setFrame(progress < 0 ? start : end); - if (mode == 'reverse' || mode == 'reverse-bounce') { + if (isReverseMode(mode)) { progressTween.target = progress < 0 ? end : start; } else { progressTween.target = progress < 0 ? start : end; @@ -297,22 +215,11 @@ // Handles marker change $effect(() => { if (lottieState.isLoaded && lottiePlayer?.marker !== marker) { - if (typeof marker === 'string') { - lottiePlayer?.setMarker(marker); - - start = - lottiePlayer?.markers().find((m) => m.name === marker)?.time ?? 0; - end = - start + - (lottiePlayer?.markers().find((m) => m.name === marker)?.duration ?? - 0); - - // change lottieState marker because - // onRender fires before this - if (lottieState) { - lottieState.marker = marker; - } - } else if (marker === null || marker === undefined) { + if (typeof marker === 'string' && lottiePlayer) { + lottiePlayer.setMarker(marker); + [start, end] = getMarkerRange(lottiePlayer, marker); + lottieState.marker = marker; + } else if (isNullish(marker)) { lottiePlayer?.setMarker(''); } else { console.warn('Invalid marker type:', marker); @@ -488,7 +395,7 @@ {#if children} - {@render children?.()} + {@render children()} {/if} diff --git a/src/components/ScrollerLottie/ts/utils.ts b/src/components/ScrollerLottie/ts/utils.ts index 803d201d..e2ebe277 100644 --- a/src/components/ScrollerLottie/ts/utils.ts +++ b/src/components/ScrollerLottie/ts/utils.ts @@ -1,3 +1,6 @@ +import type { DotLottie } from '@lottiefiles/dotlottie-web'; +import type { LottieState } from './lottieState.svelte'; + function constrain(n: number, low: number, high: number) { return Math.max(Math.min(n, high), low); } @@ -20,3 +23,89 @@ export function map( return constrain(newval, stop2, start2); } } + +/** + * Syncs the lottie player state with the component's lottie state + */ +export function syncLottieState( + lottiePlayer: DotLottie, + lottieState: LottieState +) { + lottieState.currentFrame = lottiePlayer.currentFrame; + lottieState.totalFrames = lottiePlayer.totalFrames; + lottieState.duration = lottiePlayer.duration; + lottieState.loop = lottiePlayer.loop; + lottieState.speed = lottiePlayer.speed; + lottieState.loopCount = lottiePlayer.loopCount; + lottieState.mode = lottiePlayer.mode; + lottieState.isPaused = lottiePlayer.isPaused; + lottieState.isPlaying = lottiePlayer.isPlaying; + lottieState.isStopped = lottiePlayer.isStopped; + lottieState.isLoaded = lottiePlayer.isLoaded; + lottieState.isFrozen = lottiePlayer.isFrozen; + lottieState.segment = lottiePlayer.segment ?? null; + lottieState.autoplay = lottiePlayer.autoplay ?? false; + lottieState.layout = lottiePlayer.layout ?? null; + lottieState.activeThemeId = lottiePlayer.activeThemeId ?? null; + lottieState.marker = lottiePlayer.marker ?? undefined; +} + +/** + * Gets marker info by name + */ +export function getMarkerByName(lottiePlayer: DotLottie, markerName: string) { + return lottiePlayer.markers().find((m) => m.name === markerName); +} + +/** + * Gets the start and end frames for a marker + */ +export function getMarkerRange( + lottiePlayer: DotLottie, + markerName: string +): [number, number] { + const marker = getMarkerByName(lottiePlayer, markerName); + const start = marker?.time ?? 0; + const end = start + (marker?.duration ?? 0); + return [start, end]; +} + +/** + * Calculates target frame based on progress and mode + */ +export function calculateTargetFrame( + progress: number, + mode: string, + start: number, + end: number +): number { + const adjustedProgress = + mode === 'reverse' || mode === 'reverse-bounce' ? 1 - progress : progress; + return map(adjustedProgress, 0, 1, start, end); +} + +/** + * Determines if mode is reverse + */ +export function isReverseMode(mode: string): boolean { + return mode === 'reverse' || mode === 'reverse-bounce'; +} + +/** + * Creates render config with optimized defaults + */ +export function createRenderConfig() { + return { + autoResize: true, + devicePixelRatio: + window.devicePixelRatio > 1 ? window.devicePixelRatio * 0.75 : 1, + freezeOnOffscreen: true, + }; +} + +/** + * Checks if a value is null or undefined (empty marker check) + */ +export function isNullish(value: any): boolean { + return value === null || value === undefined || value === ''; +}