@gfazioli/mantine-text-animate
Version:
The TextAnimate component allows you to animate text with various effects.
329 lines (325 loc) • 10.7 kB
JavaScript
'use client';
;
var React = require('react');
const characterSets = {
alphanumeric: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
alphabetic: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
numeric: "0123456789",
symbols: "!@#$%^&*()_+-=[]{}|;:,.<>?/"
};
const easingFunctions = {
linear: (t) => t,
"ease-in": (t) => t * t,
"ease-out": (t) => 1 - (1 - t) ** 2,
"ease-in-out": (t) => t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2
};
const shuffleArray = (array) => {
const result = [...array];
for (let i = result.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[result[i], result[j]] = [result[j], result[i]];
}
return result;
};
function useTextTicker({
value,
initialText = "random",
animate = true,
characterSet = "alphanumeric",
customCharacters = "",
delay = 0,
speed = 1,
easing = "ease-out",
randomChangeSpeed = 1,
revealDirection = "left-to-right",
scrambleDuration,
staggerDelay = 50,
onCompleted
}) {
const [displayText, setDisplayText] = React.useState("");
const [isAnimating, setIsAnimating] = React.useState(false);
const animationFrameRef = React.useRef(null);
const delayTimeoutRef = React.useRef(null);
const startTimeRef = React.useRef(0);
const charStabilityRef = React.useRef([]);
const randomOrderRef = React.useRef([]);
const isFirstRenderRef = React.useRef(true);
const manualStartRef = React.useRef(false);
const animatePropRef = React.useRef(animate);
const animationCompletedRef = React.useRef(false);
const displayTextRef = React.useRef([]);
const charStartTimesRef = React.useRef([]);
const getCharacterPool = () => {
if (characterSet === "custom" && customCharacters && customCharacters.length > 0) {
return customCharacters;
}
return characterSets[characterSet === "custom" ? "alphanumeric" : characterSet];
};
const generateRandomText = () => {
const pool = getCharacterPool();
let result = "";
for (let i = 0; i < value.length; i++) {
result += pool[Math.floor(Math.random() * pool.length)];
}
return result;
};
const generateInitialText = () => {
if (initialText === "none") {
return "";
}
if (initialText === "target") {
return value;
}
return generateRandomText();
};
const getCharIndexOrder = () => {
const indices = Array.from({ length: value.length }, (_, i) => i);
switch (revealDirection) {
case "right-to-left":
return indices.reverse();
case "center-out": {
const result = [];
const mid = Math.floor(value.length / 2);
result.push(mid);
for (let i = 1; i <= mid; i++) {
if (mid - i >= 0) {
result.push(mid - i);
}
if (mid + i < value.length) {
result.push(mid + i);
}
}
return result;
}
case "random": {
if (randomOrderRef.current.length === value.length) {
return randomOrderRef.current;
}
const randomOrder = shuffleArray(indices);
randomOrderRef.current = randomOrder;
return randomOrder;
}
case "left-to-right":
default:
return indices;
}
};
const getOrderMap = () => {
const order = getCharIndexOrder();
const map = /* @__PURE__ */ new Map();
order.forEach((charIndex, orderPosition) => {
map.set(charIndex, orderPosition);
});
return map;
};
const animateFrameScramble = (timestamp) => {
if (!startTimeRef.current) {
startTimeRef.current = timestamp;
charStabilityRef.current = Array(value.length).fill(false);
const orderMap = getOrderMap();
charStartTimesRef.current = Array.from(
{ length: value.length },
(_, i) => timestamp + (orderMap.get(i) || 0) * staggerDelay
);
}
const pool = getCharacterPool();
const newTextArray = [];
let allSettled = true;
for (let i = 0; i < value.length; i++) {
if (charStabilityRef.current[i]) {
newTextArray.push(value[i]);
continue;
}
if (timestamp < charStartTimesRef.current[i]) {
newTextArray.push(value[i] === " " ? " " : pool[Math.floor(Math.random() * pool.length)]);
allSettled = false;
continue;
}
const charElapsed = timestamp - charStartTimesRef.current[i];
if (charElapsed >= scrambleDuration) {
charStabilityRef.current[i] = true;
newTextArray.push(value[i]);
continue;
}
if (value[i] === " ") {
newTextArray.push(" ");
} else {
newTextArray.push(pool[Math.floor(Math.random() * pool.length)]);
}
allSettled = false;
}
displayTextRef.current = newTextArray;
setDisplayText(newTextArray.join(""));
if (allSettled) {
displayTextRef.current = [...value];
setDisplayText(value);
setIsAnimating(false);
startTimeRef.current = 0;
animationCompletedRef.current = true;
onCompleted?.();
return;
}
animationFrameRef.current = requestAnimationFrame(animateFrameScramble);
};
const animateFrameDefault = (timestamp) => {
if (!startTimeRef.current) {
startTimeRef.current = timestamp;
charStabilityRef.current = Array(value.length).fill(false);
}
const elapsed = timestamp - startTimeRef.current;
const duration = 1e3 / speed;
const progress = Math.min(elapsed / duration, 1);
const easingFunction = easingFunctions[easing] || easingFunctions["ease-out"];
const easedProgress = easingFunction(progress);
let allStable = true;
const pool = getCharacterPool();
const changeThreshold = 1 / randomChangeSpeed;
const charOrder = getCharIndexOrder();
const newTextArray = Array(value.length).fill("");
for (let orderIndex = 0; orderIndex < charOrder.length; orderIndex++) {
const i = charOrder[orderIndex];
const positionFactor = orderIndex / charOrder.length;
const charStability = Math.min(1, easedProgress * 3 - positionFactor);
if (charStabilityRef.current[i] || Math.random() < charStability) {
charStabilityRef.current[i] = true;
newTextArray[i] = value[i];
} else {
if (Math.random() < changeThreshold) {
newTextArray[i] = pool[Math.floor(Math.random() * pool.length)];
} else {
const currentChar = displayTextRef.current[i] || pool[Math.floor(Math.random() * pool.length)];
newTextArray[i] = currentChar;
}
allStable = false;
}
}
displayTextRef.current = newTextArray;
setDisplayText(newTextArray.join(""));
if (progress >= 1 || allStable) {
displayTextRef.current = [...value];
setDisplayText(value);
setIsAnimating(false);
startTimeRef.current = 0;
animationCompletedRef.current = true;
if (onCompleted) {
onCompleted();
}
return;
}
animationFrameRef.current = requestAnimationFrame(animateFrameDefault);
};
const animateFrame = scrambleDuration != null ? animateFrameScramble : animateFrameDefault;
const start = () => {
animationCompletedRef.current = false;
if (!animatePropRef.current) {
manualStartRef.current = true;
} else {
manualStartRef.current = false;
}
if (isAnimating) {
return;
}
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = null;
}
if (delayTimeoutRef.current) {
clearTimeout(delayTimeoutRef.current);
delayTimeoutRef.current = null;
}
startTimeRef.current = 0;
charStabilityRef.current = Array(value.length).fill(false);
setIsAnimating(true);
delayTimeoutRef.current = setTimeout(() => {
animationFrameRef.current = requestAnimationFrame(animateFrame);
}, delay * 1e3);
};
const stop = () => {
manualStartRef.current = false;
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = null;
}
if (delayTimeoutRef.current) {
clearTimeout(delayTimeoutRef.current);
delayTimeoutRef.current = null;
}
setIsAnimating(false);
};
const reset = () => {
stop();
const initial = generateInitialText();
displayTextRef.current = [...initial];
setDisplayText(initial);
startTimeRef.current = 0;
charStabilityRef.current = Array(value.length).fill(false);
animationCompletedRef.current = false;
};
React.useEffect(() => {
if (isFirstRenderRef.current) {
isFirstRenderRef.current = false;
const prefersReducedMotion = typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReducedMotion && animate) {
displayTextRef.current = [...value];
setDisplayText(value);
animationCompletedRef.current = true;
onCompleted?.();
return;
}
const initial = generateInitialText();
displayTextRef.current = [...initial];
setDisplayText(initial);
}
}, []);
React.useEffect(() => {
if (animate !== animatePropRef.current) {
animatePropRef.current = animate;
if (animate && !isAnimating) {
animationCompletedRef.current = false;
start();
} else if (!animate && isAnimating) {
stop();
}
} else if (animate && !isAnimating && !animationCompletedRef.current) {
start();
}
}, [isAnimating, animate]);
React.useEffect(() => {
if (randomOrderRef.current.length !== value.length) {
randomOrderRef.current = [];
}
if (isAnimating) {
stop();
startTimeRef.current = 0;
charStabilityRef.current = Array(value.length).fill(false);
animationCompletedRef.current = false;
const initial = generateInitialText();
displayTextRef.current = [...initial];
setDisplayText(initial);
delayTimeoutRef.current = setTimeout(() => start(), 0);
} else {
const initial = generateInitialText();
displayTextRef.current = [...initial];
setDisplayText(initial);
}
}, [value, initialText, characterSet, customCharacters]);
React.useEffect(() => {
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
if (delayTimeoutRef.current) {
clearTimeout(delayTimeoutRef.current);
}
};
}, []);
return {
text: displayText,
start,
stop,
reset,
isAnimating
};
}
exports.useTextTicker = useTextTicker;
//# sourceMappingURL=use-text-ticker.cjs.map