UNPKG

react-cursive-handwrite

Version:

React component to animate cursive handwriting text

201 lines (200 loc) 9.38 kB
"use strict"; 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;