Skip to content

Commit

Permalink
Namadillo: timeline animations (#1454)
Browse files Browse the repository at this point in the history
* feat: adding animejs

* feat: first animation version

* feat: first try on animation

* feat: starting to remove framer-motion

* feat: fixing error state

* refactor: removing unused code

* fix: moving animejs deps to the right package.json
  • Loading branch information
pedrorezende authored Dec 30, 2024
1 parent d7a75e3 commit 50f78eb
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 36 deletions.
2 changes: 2 additions & 0 deletions apps/namadillo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@tanstack/query-core": "^5.40.0",
"@tanstack/react-query": "^5.40.0",
"@tanstack/react-query-persist-client": "^5.40.0",
"animejs": "^3.2.2",
"bignumber.js": "^9.1.1",
"clsx": "^2.1.1",
"comlink": "^4.4.1",
Expand Down Expand Up @@ -91,6 +92,7 @@
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/animejs": "^3.1.12",
"@types/invariant": "^2.2.37",
"@types/jest": "^29.5.12",
"@types/lodash.debounce": "^4.0.9",
Expand Down
139 changes: 131 additions & 8 deletions apps/namadillo/src/App/Common/Timeline.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import anime from "animejs";
import clsx from "clsx";
import { useScope } from "hooks/useScope";
import { useRef } from "react";
import { FaCheckCircle } from "react-icons/fa";
import { twMerge } from "tailwind-merge";
export type TransactionStep = {
bullet?: boolean;
Expand Down Expand Up @@ -35,17 +39,17 @@ const StepBullet = ({ disabled }: DisabledProps): JSX.Element => (

const StepContent = ({
children,
isNextStep,
isCurrentStep,
hasError,
disabled,
}: React.PropsWithChildren & {
isNextStep: boolean;
isCurrentStep: boolean;
hasError: boolean;
disabled: boolean;
}): JSX.Element => (
<div
className={clsx("text-center", {
"animate-pulse": isNextStep && !hasError,
"animate-pulse": isCurrentStep && !hasError,
"opacity-20": disabled,
})}
>
Expand All @@ -59,8 +63,109 @@ export const Timeline = ({
hasError,
complete,
}: TransactionTimelineProps): JSX.Element => {
const containerRef = useRef<HTMLDivElement>(null);

useScope(
(query, container) => {
if (!complete) return;

const timeline = anime.timeline({
easing: "easeOutExpo",
duration: 1000,
});

const containerRect = container.getBoundingClientRect();
const items = Array.from(query("i,div"));
const hiding = items.slice(0, -1);
const lastItem = items.slice(-1)[0];
const lastItemText = lastItem.querySelector("p");
const rings = query('[data-animation="ring"]');
const confirmation = query('[data-animation="confirmation"]');

const lastItemTextHeight =
lastItemText ? lastItemText.getBoundingClientRect().height : 0;

const marginTop = 4; // ?

const centerLastItemY =
-containerRect.height / 2 +
lastItem.getBoundingClientRect().height / 2 +
lastItemTextHeight / 2 +
marginTop;

// Hide items, except last one
timeline.add({
targets: hiding,
opacity: 0,
delay: anime.stagger(30),
});

// Move last item to the center of the screen
timeline.add(
{
targets: lastItem,
translateY: centerLastItemY,
},
"-=700"
);

// Try to hide any existing text contained on the last item, as soon
// as the item goes up
timeline.add(
{
targets: lastItem.querySelector("p"),
opacity: 0,
duration: 400,
},
"-=1000"
);

// Display concentric rings, one by one
timeline.add(
{
targets: rings,
opacity: [0, 1],
duration: 800,
scale: [0, 1],
delay: anime.stagger(100),
},
"-=600"
);

// Sucks everything into the screen
timeline.add({
targets: [...rings, lastItem],
scale: 0,
duration: 300,
});

// Displays success box confirmation
timeline.add({
targets: confirmation,
duration: 1000,
scale: [0, 1],
opacity: [0, 1],
});
},
containerRef,
[complete]
);

const renderRing = (className: string): JSX.Element => (
<span
data-animation="ring"
className={clsx(
"absolute aspect-square border-2 border-yellow rounded-full",
className
)}
/>
);

return (
<div>
<div
className={clsx("relative", { "pointer-events-none": complete })}
ref={containerRef}
>
<ul className="flex flex-col items-center gap-3">
{steps.map((step, index: number) => {
return (
Expand All @@ -80,18 +185,36 @@ export const Timeline = ({
<StepBullet disabled={index > currentStepIndex} />
)}
<StepContent
isNextStep={
index === currentStepIndex && !hasError && !complete
}
isCurrentStep={index === currentStepIndex}
disabled={index > currentStepIndex}
hasError={Boolean(hasError)}
hasError={!!hasError}
>
{step.children}
</StepContent>
</li>
);
})}
</ul>
<span
className={clsx(
"absolute w-full h-full circles top-0 left-0",
"flex items-center justify-center pointer-events-none",
{ hidden: !complete }
)}
>
{renderRing("h-30")}
{renderRing("h-60")}
{renderRing("h-96")}
<span
data-animation="confirmation"
className={clsx(
"absolute text-success text-[70px] aspect-square bg-white rounded-full",
{ "opacity-0": complete }
)}
>
<FaCheckCircle />
</span>
</span>
</div>
);
};
53 changes: 30 additions & 23 deletions apps/namadillo/src/App/Sidebars/YourStakingDistribution.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { PieChart, PieChartData } from "@namada/components";
import { shortenAddress } from "@namada/utils";
import BigNumber from "bignumber.js";
import { AnimatePresence, motion } from "framer-motion";
import clsx from "clsx";
import { AnimatePresence } from "framer-motion";
import { useState } from "react";
import { twMerge } from "tailwind-merge";
import { MyValidator } from "types";

type YourStakingDistributionProps = {
Expand Down Expand Up @@ -62,29 +64,34 @@ export const YourStakingDistribution = ({
setDisplayedValidator(myValidators[index]);
}}
>
<div className="max-w-[75%] mx-auto leading-tight">
<div className="relative flex items-center justify-center max-w-[75%] mx-auto leading-tight">
<AnimatePresence>
{displayedValidator === undefined && (
<motion.span
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
Your Stake Distribution
</motion.span>
)}
{displayedValidator && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{displayedValidator.validator.alias}
<span className="block text-neutral-500 text-sm">
{getFormattedPercentage(displayedValidator)}
</span>
</motion.div>
)}
<span
className={twMerge(
clsx("absolute transition-opacity duration-300 opacity-100", {
"opacity-0 pointer-events-none": displayedValidator,
})
)}
>
Your Stake Distribution
</span>
<span
className={twMerge(
clsx(
"flex flex-col text-neutral-500 text-sm opacity-0 pointer-events-none",
"transition-opacity duration-300",
{
"opacity-100 pointer-events-auto": displayedValidator,
}
)
)}
>
{displayedValidator?.validator.alias}
<span className="block">
{displayedValidator &&
getFormattedPercentage(displayedValidator)}
</span>
</span>
</AnimatePresence>
</div>
</PieChart>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ export const TransferTimelineErrorEntry = ({
}: TransferTimelineErrorEntryProps): JSX.Element => {
return (
<>
<i className="flex justify-center text-2xl mb-1 opacity-70 text-fail">
<i className="flex justify-center text-4xl mb-1 text-fail">
<GoXCircle />
</i>
<div className="opacity-70">{children}</div>
<div className="opacity-50 select-none line-through">{children}</div>
<span
className={clsx(
"block text-sm text-fail selection:text-white selection:bg-fail"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ export const TransferTransactionTimeline = ({
transaction,
}: TransactionTransferTimelineProps): JSX.Element => {
const textSteps = [...allTransferStages[transaction.type]];

const hasError = transaction.status === "error";
const isTransparentTransfer = transparentTransferTypes.includes(
transaction.type
Expand All @@ -137,9 +136,13 @@ export const TransferTransactionTimeline = ({
initialImage.length
);

const filteredSteps = textSteps.filter(
(step) => step !== TransferStep.WaitingConfirmation
);

const stepsWithDescription = buildStepEntries(
textSteps.filter((step) => step !== TransferStep.WaitingConfirmation),
currentStepIndex,
filteredSteps,
currentStepIndex + filteredSteps.length - textSteps.length,
hasError,
transaction,
!isTransparentTransfer
Expand Down
22 changes: 22 additions & 0 deletions apps/namadillo/src/hooks/useScope.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { RefObject, useEffect } from "react";

type QueryFnOutput = ReturnType<typeof document.querySelectorAll>;
type CallbackFn = (query: string) => QueryFnOutput;
type CallbackProps = (callback: CallbackFn, container: HTMLElement) => void;

export const useScope = (
callback: CallbackProps,
scope: RefObject<HTMLElement>,
dependencies: unknown[] = []
): void => {
useEffect(() => {
const queryFn = (query: string): QueryFnOutput => {
if (!scope.current)
throw "You must pass a valid scope for useAnimation hook";
return scope.current.querySelectorAll(query);
};

if (!scope.current) return;
callback(queryFn, scope.current);
}, [...dependencies]);
};
16 changes: 16 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3671,6 +3671,7 @@ __metadata:
"@testing-library/jest-dom": "npm:^6.5.0"
"@testing-library/react": "npm:^16.0.0"
"@testing-library/user-event": "npm:^14.5.2"
"@types/animejs": "npm:^3.1.12"
"@types/invariant": "npm:^2.2.37"
"@types/jest": "npm:^29.5.12"
"@types/lodash.debounce": "npm:^4.0.9"
Expand All @@ -3682,6 +3683,7 @@ __metadata:
"@types/styled-components": "npm:^5.1.22"
"@types/traverse": "npm:^0.6.36"
"@vitejs/plugin-react": "npm:^4.3.1"
animejs: "npm:^3.2.2"
autoprefixer: "npm:^10.4.16"
axios: "npm:^1.7.9"
bignumber.js: "npm:^9.1.1"
Expand Down Expand Up @@ -4875,6 +4877,13 @@ __metadata:
languageName: node
linkType: hard

"@types/animejs@npm:^3.1.12":
version: 3.1.12
resolution: "@types/animejs@npm:3.1.12"
checksum: 21d10ed4fbaa99572e252b4d6e666b62a19089f63b13b0e9f8edf5fb0b708afc2b38c3055c52f6a4771bc71793d7a7aec83754c16d0e5606a0ad92b34778ba61
languageName: node
linkType: hard

"@types/aria-query@npm:^5.0.1":
version: 5.0.4
resolution: "@types/aria-query@npm:5.0.4"
Expand Down Expand Up @@ -6360,6 +6369,13 @@ __metadata:
languageName: node
linkType: hard

"animejs@npm:^3.2.2":
version: 3.2.2
resolution: "animejs@npm:3.2.2"
checksum: f43dfcc0c743a2774e76fbfcb16a22350da7104f413d9d1b85c48128b0c078090642809deb631e21dfa0a40651111be39d9d7f694c9c1b70d8637ce8b6d63116
languageName: node
linkType: hard

"ansi-align@npm:^3.0.1":
version: 3.0.1
resolution: "ansi-align@npm:3.0.1"
Expand Down

1 comment on commit 50f78eb

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.