UNPKG

@lobehub/ui

Version:

Lobe UI is an open-source UI component library for building AIGC web apps

1 lines 13.5 kB
{"version":3,"file":"TypewriterEffect.mjs","names":[],"sources":["../../../src/awesome/TypewriterEffect/TypewriterEffect.tsx"],"sourcesContent":["'use client';\n\nimport { cx } from 'antd-style';\nimport { createElement, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport { useMotionComponent } from '@/MotionProvider';\n\nimport { styles } from './style';\nimport type { TypewriterEffectProps } from './type';\n\nconst TypewriterEffect = memo<TypewriterEffectProps>(\n ({\n sentences,\n as: Component = 'div',\n typingSpeed = 100,\n initialDelay = 0,\n pauseDuration = 2000,\n deletingSpeed = 50,\n deletePauseDuration = 0,\n loop = true,\n className = '',\n color,\n showCursor = true,\n hideCursorWhileTyping = false,\n cursorCharacter,\n cursorClassName = '',\n cursorColor,\n cursorBlinkDuration = 0.8,\n cursorFade = true,\n cursorStyle = 'pipe',\n textColors = [],\n variableSpeed,\n onSentenceComplete,\n startOnVisible = false,\n reverseMode = false,\n segmentMode = 'grapheme',\n ...props\n }: TypewriterEffectProps) => {\n const Motion = useMotionComponent();\n const cxStyles = cx;\n const [displayedText, setDisplayedText] = useState('');\n const [currentCharIndex, setCurrentCharIndex] = useState(0);\n const [isDeleting, setIsDeleting] = useState(false);\n const [currentTextIndex, setCurrentTextIndex] = useState(0);\n const [isVisible, setIsVisible] = useState(!startOnVisible);\n const [isDeletePausing, setIsDeletePausing] = useState(false);\n const containerRef = useRef<HTMLElement>(null);\n\n const textArray = useMemo(\n () => (Array.isArray(sentences) ? sentences : [sentences]),\n [sentences],\n );\n\n // Helper function to split text based on segment mode\n const splitText = useCallback(\n (text: string): string[] => {\n // Use Intl.Segmenter if available\n if (typeof Intl !== 'undefined' && 'Segmenter' in Intl) {\n const segmenter = new Intl.Segmenter(undefined, { granularity: segmentMode });\n return Array.from(segmenter.segment(text), (segment) => segment.segment);\n }\n\n // Fallback when Intl.Segmenter is not available\n if (segmentMode === 'word') {\n // Simple word splitting fallback\n return text.split(/(\\s+)/).filter(Boolean);\n }\n\n // Grapheme fallback\n return Array.from(text);\n },\n [segmentMode],\n );\n\n const getRandomSpeed = useCallback(() => {\n if (!variableSpeed) return typingSpeed;\n const { min, max } = variableSpeed;\n return Math.random() * (max - min) + min;\n }, [variableSpeed, typingSpeed]);\n\n const getCurrentTextColor = () => {\n if (textColors.length > 0) {\n return textColors[currentTextIndex % textColors.length];\n }\n return color;\n };\n\n const getCurrentCursorColor = () => {\n return cursorColor || color;\n };\n\n useEffect(() => {\n if (!startOnVisible || !containerRef.current) return;\n\n const observer = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n setIsVisible(true);\n }\n });\n },\n { threshold: 0.1 },\n );\n\n observer.observe(containerRef.current);\n\n return () => observer.disconnect();\n }, [startOnVisible]);\n\n useEffect(() => {\n if (!isVisible) return;\n\n let timeout: ReturnType<typeof setTimeout>;\n\n const currentText = textArray[currentTextIndex];\n // Split text based on segment mode\n const textSegments = splitText(currentText);\n const processedText = reverseMode ? textSegments.reverse().join('') : currentText;\n\n // Handle delete pause state\n if (isDeletePausing) {\n timeout = setTimeout(() => {\n setIsDeletePausing(false);\n }, deletePauseDuration);\n return () => clearTimeout(timeout);\n }\n\n const executeTypingAnimation = () => {\n if (isDeleting) {\n if (displayedText === '') {\n setIsDeleting(false);\n if (currentTextIndex === textArray.length - 1 && !loop) {\n return;\n }\n if (onSentenceComplete) {\n onSentenceComplete(textArray[currentTextIndex], currentTextIndex);\n }\n setCurrentTextIndex((prev) => (prev + 1) % textArray.length);\n setCurrentCharIndex(0);\n\n if (deletePauseDuration > 0) {\n setIsDeletePausing(true);\n return;\n }\n } else {\n timeout = setTimeout(() => {\n setDisplayedText((prev) => {\n const segments = splitText(prev);\n return segments.slice(0, -1).join('');\n });\n }, deletingSpeed);\n }\n } else {\n const processedSegments = splitText(processedText);\n if (currentCharIndex < processedSegments.length) {\n timeout = setTimeout(\n () => {\n setDisplayedText((prev) => prev + processedSegments[currentCharIndex]);\n setCurrentCharIndex((prev) => prev + 1);\n },\n variableSpeed ? getRandomSpeed() : typingSpeed,\n );\n } else if (textArray.length >= 1) {\n if (!loop && currentTextIndex === textArray.length - 1) return;\n\n timeout = setTimeout(() => {\n setIsDeleting(true);\n }, pauseDuration);\n }\n }\n };\n\n if (currentCharIndex === 0 && !isDeleting && displayedText === '') {\n timeout = setTimeout(executeTypingAnimation, initialDelay);\n } else {\n executeTypingAnimation();\n }\n\n return () => clearTimeout(timeout);\n }, [\n currentCharIndex,\n displayedText,\n isDeleting,\n isDeletePausing,\n typingSpeed,\n deletingSpeed,\n deletePauseDuration,\n pauseDuration,\n textArray,\n currentTextIndex,\n loop,\n initialDelay,\n isVisible,\n reverseMode,\n variableSpeed,\n onSentenceComplete,\n getRandomSpeed,\n splitText,\n ]);\n\n const getCursorStyle = () => {\n if (cursorCharacter) return styles.cursorCustom;\n\n switch (cursorStyle) {\n case 'block': {\n return styles.cursorBlock;\n }\n case 'dot': {\n return styles.cursorDot;\n }\n case 'underscore': {\n return styles.cursorUnderscore;\n }\n case 'pipe': {\n return styles.cursor;\n }\n }\n };\n\n const currentTextLength = splitText(textArray[currentTextIndex]).length;\n const isTyping = currentCharIndex < currentTextLength && !isDeleting;\n const isAfterTyping = currentCharIndex === currentTextLength && !isDeleting;\n\n const shouldHideCursor = (() => {\n if (hideCursorWhileTyping === true) return true; // 完全隐藏\n if (hideCursorWhileTyping === 'typing') return isTyping || isDeleting; // 打字和删除时隐藏\n if (hideCursorWhileTyping === 'afterTyping') return isAfterTyping; // 打字完成后隐藏\n return false;\n })();\n\n const textColor = getCurrentTextColor();\n const finalCursorColor = getCurrentCursorColor();\n\n // Split displayed text for animation\n const characters = splitText(displayedText);\n\n return createElement(\n Component,\n {\n className: cxStyles(styles.container, className),\n ref: containerRef,\n ...props,\n },\n <>\n <span className={styles.text} style={textColor ? { color: textColor } : undefined}>\n {characters.map((char, index) => (\n <Motion.span\n animate={{ opacity: 1 }}\n initial={{ opacity: 0 }}\n key={`${currentTextIndex}-${index}`}\n style={{ display: 'inline-block' }}\n transition={{\n duration: typingSpeed / 500,\n ease: 'easeInOut',\n }}\n >\n {char === ' ' ? '\\u00A0' : char}\n </Motion.span>\n ))}\n </span>\n {showCursor &&\n (cursorFade ? (\n <Motion.span\n animate={{ opacity: shouldHideCursor ? 0 : 1 }}\n className={cxStyles(getCursorStyle(), cursorClassName)}\n initial={{ opacity: 0 }}\n style={finalCursorColor ? { backgroundColor: finalCursorColor } : undefined}\n transition={{\n duration: shouldHideCursor ? 0.2 : cursorBlinkDuration,\n ease: 'easeInOut',\n repeat: shouldHideCursor ? 0 : Number.POSITIVE_INFINITY,\n repeatType: 'reverse',\n }}\n >\n {cursorCharacter}\n </Motion.span>\n ) : (\n <span\n className={cxStyles(getCursorStyle(), cursorClassName)}\n style={{\n backgroundColor: finalCursorColor,\n opacity: shouldHideCursor ? 0 : 1,\n }}\n >\n {cursorCharacter}\n </span>\n ))}\n </>,\n );\n },\n);\n\nTypewriterEffect.displayName = 'TypewriterEffect';\n\nexport default TypewriterEffect;\n"],"mappings":"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"}