Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(BrandLoadingScreen): improve lottie animations #1235

Merged
merged 11 commits into from
Sep 13, 2024
Binary file not shown.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@
"@vanilla-extract/dynamic": "^2.1.1",
"@vanilla-extract/sprinkles": "^1.6.2",
"classnames": "^2.3.1",
"lottie-react": "^2.4.0",
"lottie-web": "^5.12.2",
"moment": "^2.29.1",
"react-autosuggest": "^10.1.0",
"react-datetime": "^3.1.1",
Expand Down
231 changes: 231 additions & 0 deletions src/lottie/hooks/use-lottie-interactivity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
'use client';
import * as React from 'react';

import type {AnimationSegment} from 'lottie-web/build/player/lottie_light';
import type {InteractivityProps} from '../types';

// helpers
export function getContainerVisibility(container: Element): number {
const {top, height} = container.getBoundingClientRect();

const current = window.innerHeight - top;
const max = window.innerHeight + height;
return current / max;
}

export function getContainerCursorPosition(
container: Element,
cursorX: number,
cursorY: number
): {x: number; y: number} {
const {top, left, width, height} = container.getBoundingClientRect();

const x = (cursorX - left) / width;
const y = (cursorY - top) / height;

return {x, y};
}

export type InitInteractivity = {
wrapperRef: React.RefObject<HTMLDivElement>;
animationItem: InteractivityProps['lottieObj']['animationItem'];
actions: InteractivityProps['actions'];
mode: InteractivityProps['mode'];
};

export const useInitInteractivity = ({wrapperRef, animationItem, mode, actions}: InitInteractivity): void => {
React.useEffect(() => {
const wrapper = wrapperRef.current;

if (!wrapper || !animationItem || !actions.length) {
return;
}

animationItem.stop();

const scrollModeHandler = () => {
let assignedSegment: Array<number> | null = null;

const scrollHandler = () => {
const currentPercent = getContainerVisibility(wrapper);
// Find the first action that satisfies the current position conditions
const action = actions.find(
({visibility}) =>
visibility && currentPercent >= visibility[0] && currentPercent <= visibility[1]
);

// Skip if no matching action was found!
if (!action) {
return;
}

if (action.type === 'seek' && action.visibility && action.frames.length === 2) {
// Seek: Go to a frame based on player scroll position action
const frameToGo =
action.frames[0] +
Math.ceil(
((currentPercent - action.visibility[0]) /
(action.visibility[1] - action.visibility[0])) *
action.frames[1]
);

// ! goToAndStop must be relative to the start of the current segment
animationItem.goToAndStop(frameToGo - animationItem.firstFrame - 1, true);
}

if (action.type === 'loop') {
// Loop: Loop a given frames
if (assignedSegment === null) {
// if not playing any segments currently. play those segments and save to state
animationItem.playSegments(action.frames as AnimationSegment, true);
assignedSegment = action.frames;
} // if playing any segments currently.
// check if segments in state are equal to the frames selected by action
else if (assignedSegment !== action.frames) {
// if they are not equal. new segments are to be loaded
animationItem.playSegments(action.frames as AnimationSegment, true);
assignedSegment = action.frames;
} else if (animationItem.isPaused) {
// if they are equal the play method must be called only if lottie is paused
animationItem.playSegments(action.frames as AnimationSegment, true);
assignedSegment = action.frames;
}
}

if (action.type === 'play' && animationItem.isPaused) {
// Play: Reset segments and continue playing full animation from current position
animationItem.resetSegments(true);
animationItem.play();
}

if (action.type === 'stop') {
// Stop: Stop playback
animationItem.goToAndStop(action.frames[0] - animationItem.firstFrame - 1, true);
}
};

document.addEventListener('scroll', scrollHandler);

return () => {
document.removeEventListener('scroll', scrollHandler);
};
};

const cursorModeHandler = () => {
const handleCursor = (_x: number, _y: number) => {
let x = _x;
let y = _y;

// Resolve cursor position if cursor is inside container
if (x !== -1 && y !== -1) {
// Get container cursor position
const pos = getContainerCursorPosition(wrapper, x, y);

// Use the resolved position
x = pos.x;
y = pos.y;
}

// Find the first action that satisfies the current position conditions
const action = actions.find(({position}) => {
if (position && Array.isArray(position.x) && Array.isArray(position.y)) {
return (
x >= position.x[0] &&
x <= position.x[1] &&
y >= position.y[0] &&
y <= position.y[1]
);
}

if (position && !Number.isNaN(position.x) && !Number.isNaN(position.y)) {
return x === position.x && y === position.y;
}

return false;
});

// Skip if no matching action was found!
if (!action) {
return;
}

// Process action types:
if (
action.type === 'seek' &&
action.position &&
Array.isArray(action.position.x) &&
Array.isArray(action.position.y) &&
action.frames.length === 2
) {
// Seek: Go to a frame based on player scroll position action
const xPercent =
(x - action.position.x[0]) / (action.position.x[1] - action.position.x[0]);
const yPercent =
(y - action.position.y[0]) / (action.position.y[1] - action.position.y[0]);

animationItem.playSegments(action.frames as AnimationSegment, true);
animationItem.goToAndStop(
Math.ceil(((xPercent + yPercent) / 2) * (action.frames[1] - action.frames[0])),
true
);
}

if (action.type === 'loop') {
animationItem.playSegments(action.frames as AnimationSegment, true);
}

if (action.type === 'play') {
// Play: Reset segments and continue playing full animation from current position
if (animationItem.isPaused) {
animationItem.resetSegments(false);
}
animationItem.playSegments(action.frames as AnimationSegment);
}

if (action.type === 'stop') {
animationItem.goToAndStop(action.frames[0], true);
}
};

const mouseMoveHandler = (ev: MouseEvent) => {
handleCursor(ev.clientX, ev.clientY);
};

const mouseOutHandler = () => {
handleCursor(-1, -1);
};

wrapper.addEventListener('mousemove', mouseMoveHandler);
wrapper.addEventListener('mouseout', mouseOutHandler);

return () => {
wrapper.removeEventListener('mousemove', mouseMoveHandler);
wrapper.removeEventListener('mouseout', mouseOutHandler);
};
};

switch (mode) {
case 'scroll':
return scrollModeHandler();
case 'cursor':
default:
return cursorModeHandler();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mode, animationItem]);
};

const useLottieInteractivity = ({actions, mode, lottieObj}: InteractivityProps): React.ReactElement => {
const {animationItem, View, animationContainerRef} = lottieObj;

useInitInteractivity({
actions,
animationItem,
mode,
wrapperRef: animationContainerRef,
});

return View;
};

export default useLottieInteractivity;
Loading
Loading