Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 18 additions & 19 deletions packages/calligraph/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ export function Calligraph(props: CalligraphProps) {
initial: animateInitial = false,
onComplete,
autoSize = true,
className,
style,
...rest
} = props;
Expand All @@ -115,36 +114,36 @@ export function Calligraph(props: CalligraphProps) {

const rendererProps = {
text: String(children ?? ""),
Component,
transition,
stagger,
animateInitial,
onComplete,
className,
style,
rest,
};

let content: React.ReactNode;

if (variant === "number") {
content = <NumberRenderer {...rendererProps} />;
} else if (variant === "slots") {
content = <SlotsRenderer {...rendererProps} />;
} else {
content = (
const content: React.ReactNode =
variant === "number" ? (
<NumberRenderer {...rendererProps} />
) : variant === "slots" ? (
<SlotsRenderer {...rendererProps} />
) : (
<TextRenderer
{...rendererProps}
driftX={driftX}
driftY={driftY}
trend={trend}
/>
);
}

if (autoSize) {
return <AutoSizeWrapper transition={transition}>{content}</AutoSizeWrapper>;
}

return content;
return (
<Component
{...rest}
style={{ display: "inline-flex", position: "relative", ...style }}
>
{autoSize ? (
<AutoSizeWrapper transition={transition}>{content}</AutoSizeWrapper>
) : (
content
)}
</Component>
);
}
179 changes: 79 additions & 100 deletions packages/calligraph/src/number.tsx
Original file line number Diff line number Diff line change
@@ -1,142 +1,121 @@
import type { Transition } from "motion/react";
import { AnimatePresence, MotionConfig, motion } from "motion/react";
import { useRef, useState } from "react";
import { useState } from "react";
import { reconcileDigitKeys } from "./reconcile";
import { DIGIT_DISTANCE, isDigit, splitGraphemes } from "./shared";

export function NumberRenderer({
text,
Component,
transition,
stagger,
animateInitial,
onComplete,
className,
style,
rest,
}: {
text: string;
Component: React.ElementType;
transition: Transition;
stagger: number;
animateInitial: boolean;
onComplete?: () => void;
className?: string;
style?: React.CSSProperties;
rest: Record<string, unknown>;
}) {
const chars = splitGraphemes(text);

const nextIdRef = useRef(chars.length);
const [nextId, setNextId] = useState(chars.length);
const [prevText, setPrevText] = useState(text);
const [digitKeys, setDigitKeys] = useState<number[]>(() =>
chars.map((_, i) => i),
);
const dirRef = useRef(1);
const [direction, setDirection] = useState(1);

if (text !== prevText) {
const result = reconcileDigitKeys(
prevText,
text,
digitKeys,
nextIdRef.current,
);
nextIdRef.current = result.nextId;
dirRef.current = result.direction;
const result = reconcileDigitKeys(prevText, text, digitKeys, nextId);
setNextId(result.nextId);
setDirection(result.direction);
setDigitKeys(result.keys);
setPrevText(text);
}

const dir = dirRef.current;
const prefixLen = (() => {
const idx = chars.findIndex((c) => isDigit(c));
return idx === -1 ? chars.length : idx;
})();

return (
<MotionConfig transition={transition}>
<Component
aria-label={text}
style={{ display: "inline-flex", position: "relative", ...style }}
className={className}
{...rest}
>
<AnimatePresence mode="popLayout" initial={animateInitial}>
{chars.map((char, i) => {
const isPrefix = i < prefixLen;
const outerKey = isPrefix
? `pre-${i}`
: `col-${chars.length - 1 - i}`;
const delay = i * stagger;
const isLast = i === chars.length - 1;
<AnimatePresence mode="popLayout" initial={animateInitial}>
{chars.map((char, i) => {
const isPrefix = i < prefixLen;
const outerKey = isPrefix
? `pre-${i}`
: `col-${chars.length - 1 - i}`;
const delay = i * stagger;
const isLast = i === chars.length - 1;

return (
<motion.span
key={outerKey}
layout="position"
initial={isPrefix ? false : { opacity: 0 }}
animate={isPrefix ? undefined : { opacity: 1 }}
exit={isPrefix ? undefined : { opacity: 0 }}
style={{ display: "inline-block", position: "relative" }}
>
{isPrefix ? (
<span style={{ display: "inline-block", whiteSpace: "pre" }}>
{char}
</span>
) : (
<AnimatePresence
mode="popLayout"
initial={animateInitial}
propagate
return (
<motion.span
key={outerKey}
layout="position"
initial={isPrefix ? false : { opacity: 0 }}
animate={isPrefix ? undefined : { opacity: 1 }}
exit={isPrefix ? undefined : { opacity: 0 }}
style={{ display: "inline-block", position: "relative" }}
>
{isPrefix ? (
<span style={{ display: "inline-block", whiteSpace: "pre" }}>
{char}
</span>
) : (
<AnimatePresence
mode="popLayout"
initial={animateInitial}
propagate
>
<motion.span
key={digitKeys[i]}
aria-hidden="true"
initial={{
y: isDigit(char)
? direction > 0
? DIGIT_DISTANCE
: -DIGIT_DISTANCE
: 0,
filter: "blur(2px)",
scale: 0.5,
opacity: 0,
}}
animate={{
y: 0,
opacity: 1,
filter: "blur(0px)",
scale: 1,
transition: { delay },
}}
exit={{
y: isDigit(char)
? direction > 0
? -DIGIT_DISTANCE
: DIGIT_DISTANCE
: 0,
opacity: 0,
filter: "blur(2px)",
scale: 0.5,
transition: { delay },
}}
onAnimationComplete={
isLast && onComplete ? onComplete : undefined
}
style={{
display: "inline-block",
whiteSpace: "pre",
}}
>
<motion.span
key={digitKeys[i]}
aria-hidden="true"
initial={{
y: isDigit(char)
? dir > 0
? DIGIT_DISTANCE
: -DIGIT_DISTANCE
: 0,
filter: "blur(2px)",
scale: 0.5,
opacity: 0,
}}
animate={{
y: 0,
opacity: 1,
filter: "blur(0px)",
scale: 1,
transition: { delay },
}}
exit={{
y: isDigit(char)
? dir > 0
? -DIGIT_DISTANCE
: DIGIT_DISTANCE
: 0,
opacity: 0,
filter: "blur(2px)",
scale: 0.5,
transition: { delay },
}}
onAnimationComplete={
isLast && onComplete ? onComplete : undefined
}
style={{
display: "inline-block",
whiteSpace: "pre",
}}
>
{char}
</motion.span>
</AnimatePresence>
)}
</motion.span>
);
})}
</AnimatePresence>
</Component>
{char}
</motion.span>
</AnimatePresence>
)}
</motion.span>
);
})}
</AnimatePresence>
</MotionConfig>
);
}
Loading