react-cursive-handwrite
Version:
React component to animate cursive handwriting text
201 lines (200 loc) • 9.38 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.HandwritingText = void 0;
const jsx_runtime_1 = require("react/jsx-runtime");
// src/components/HandwritingText.tsx
const react_1 = __importStar(require("react"));
const framer_motion_1 = require("framer-motion");
const Loader_1 = require("./Loader");
const QueueManager_1 = require("./QueueManager");
const HandwritingText = ({ children, strokeColor = "#000", strokeWidth = 2, duration = 3, as: Component = "div", fontPath = "google", debug = false, }) => {
const log = (...args) => {
if (debug) {
console.log('[HandwritingText]', ...args);
}
};
const controls = (0, framer_motion_1.useAnimation)();
const [letterPaths, setLetterPaths] = react_1.default.useState({});
const [isLoading, setIsLoading] = react_1.default.useState(true);
const [error, setError] = react_1.default.useState(null);
const [dimensions, setDimensions] = react_1.default.useState({ width: 0, height: 0 });
const [renderedLetters, setRenderedLetters] = (0, react_1.useState)([]);
const [currentLetter, setCurrentLetter] = react_1.default.useState(null);
const queueManagerRef = (0, react_1.useRef)(null);
// Initialize font
(0, react_1.useEffect)(() => {
let mounted = true;
setIsLoading(true);
setError(null);
const loadFont = async () => {
try {
if (!fontPath) {
throw new Error('fontPath is required');
}
log(`Loading font from path: ${fontPath}`);
const paths = await (0, Loader_1.initializeFont)(fontPath);
if (!mounted)
return;
if (!paths || typeof paths !== 'object') {
throw new Error('Invalid font paths returned');
}
if (Object.keys(paths).length === 0) {
log('Warning: No letter paths were loaded');
}
else {
log(`Loaded ${Object.keys(paths).length} letter paths`);
}
setLetterPaths(paths);
queueManagerRef.current = new QueueManager_1.QueueManager(paths);
setIsLoading(false);
}
catch (error) {
log('Error loading font:', error);
if (mounted) {
setError(`Failed to load font: ${error instanceof Error ? error.message : String(error)}`);
setIsLoading(false);
}
}
};
loadFont();
return () => {
mounted = false;
};
}, [fontPath, debug]);
// Process text and update dimensions
(0, react_1.useEffect)(() => {
if (isLoading || !queueManagerRef.current)
return;
const text = typeof children === 'string' ? children : '';
if (!text) {
setError('No text content provided');
return;
}
try {
queueManagerRef.current.reset();
queueManagerRef.current.addText(text);
setRenderedLetters([]); // Clear previous letters
// Add padding to dimensions
const padding = 20;
setDimensions({
width: queueManagerRef.current.getTotalLength() + (padding * 2),
height: queueManagerRef.current.getMaxHeight() + (padding * 2)
});
// Start with first letter
const nextLetter = queueManagerRef.current.getNextLetter();
if (nextLetter) {
setCurrentLetter(nextLetter);
}
}
catch (error) {
log('Error processing text:', error);
setError(`Failed to process text: ${error instanceof Error ? error.message : String(error)}`);
}
}, [children, letterPaths, isLoading]);
// Calculate baseline Y position - use 75% of max height as baseline
const baselineY = Math.floor(dimensions.height * 0.75);
// Animate current letter
(0, react_1.useEffect)(() => {
if (!currentLetter)
return;
// Create a temporary SVG to measure the path length
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", currentLetter.path.path);
svg.appendChild(path);
document.body.appendChild(svg);
const pathLength = path.getTotalLength();
document.body.removeChild(svg);
log(`Animating letter "${currentLetter.letter}" with length ${pathLength}`);
const text = typeof children === 'string' ? children : '';
const letterDuration = (duration / text.length) * 1.25; // Slightly longer duration for overlap
controls.set({
strokeDasharray: pathLength,
strokeDashoffset: pathLength,
opacity: 1
});
controls.start({
strokeDashoffset: 0,
opacity: 1,
transition: {
duration: letterDuration,
ease: [0.33, 1, 0.68, 1], // Custom easing for smoother animation
},
}).then(() => {
if (queueManagerRef.current) {
setRenderedLetters(prev => [...prev, currentLetter]);
queueManagerRef.current.markAsRendered(currentLetter.order);
}
});
// Start next letter when current letter is 60% complete for smoother overlap
const timer = setTimeout(() => {
if (queueManagerRef.current) {
const nextLetter = queueManagerRef.current.getNextLetter();
if (nextLetter) {
setCurrentLetter(nextLetter);
}
}
}, letterDuration * 1000 * 0.6); // Start next letter earlier
return () => clearTimeout(timer);
}, [currentLetter, controls, duration, children]);
const containerStyle = {
position: 'relative',
display: 'inline-block',
width: dimensions.width || 'auto',
height: dimensions.height || 'auto',
minWidth: '100px',
minHeight: '50px',
opacity: isLoading ? 0 : 1, // Hide while loading
transition: 'opacity 0.3s ease-in' // Smooth fade in when ready
};
const svgStyle = {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
overflow: 'visible'
};
if (isLoading) {
return (0, jsx_runtime_1.jsx)(Component, { style: containerStyle }); // Empty container while loading
}
return ((0, jsx_runtime_1.jsx)(Component, { style: containerStyle, children: error ? ((0, jsx_runtime_1.jsx)("div", { style: { fontSize: '14px', color: 'red', padding: '10px' }, children: debug ? error : 'Error loading content' })) : ((0, jsx_runtime_1.jsxs)("svg", { viewBox: `0 0 ${dimensions.width} ${dimensions.height}`, fill: "none", xmlns: "http://www.w3.org/2000/svg", style: svgStyle, preserveAspectRatio: "xMidYMid meet", children: [renderedLetters.map((letter, index) => ((0, jsx_runtime_1.jsx)("path", { d: letter.path.path, stroke: strokeColor, strokeWidth: strokeWidth, fill: "none", strokeLinecap: "round", strokeLinejoin: "round", transform: `translate(${letter.path.xOffset}, ${baselineY - letter.path.height})` }, `rendered-${index}`))), currentLetter && ((0, jsx_runtime_1.jsx)(framer_motion_1.motion.path, { d: currentLetter.path.path, stroke: strokeColor, strokeWidth: strokeWidth, fill: "none", strokeLinecap: "round", strokeLinejoin: "round", transform: `translate(${currentLetter.path.xOffset}, ${baselineY - currentLetter.path.height})`, initial: {
strokeDasharray: 0,
strokeDashoffset: 0,
opacity: 0
}, animate: controls }))] })) }));
};
exports.HandwritingText = HandwritingText;