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
46 changes: 46 additions & 0 deletions src/lottie/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
The MIT License

Copyright David Gamote and other contributors.

This software consists of voluntary contributions made by many
individuals. For exact contribution history, see the revision history
available on GitHub.

The following license applies to all parts of this software except as
documented below:

====

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

====

Copyright and related rights for sample code are waived via CC0. Sample
code is defined as all source code displayed within the prose of the
documentation.

CC0: http://creativecommons.org/publicdomain/zero/1.0/

====

Files located in the node_modules and vendor directories are externally
maintained libraries used by this software which have their own
licenses; we recommend you read them, as their terms may differ from the
terms above.
11 changes: 11 additions & 0 deletions src/lottie/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Lottie-react

This is a minimalistic copy of the [lottie-react](https://github.com/Gamote/lottie-react/) package. The
original one uses `use-lottie` library to render lottie files, and there are several people reporting that
this lib is quiet heavy. In order to avoid this, `use-lottie` provides a "light" version that is way smaller
in size and it seems to have the same results as the original one.

A PR has been created in `lottie-react` to use the light version of `use-lottie`, but it doesn't look like
it's being reviewed (https://github.com/Gamote/lottie-react/pull/86). Therefore, we've copied the library
inside Mistica and updated it to use `lottie-light` instead. In this way, we reduce the space required by this
library in almost 50%.
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