UNPKG

@ar-dacity/ardacity-text-pressure

Version:

Dynamic text pressure effects for React applications

254 lines (213 loc) 7.75 kB
"use client" import { useEffect, useRef, useState } from "react" const TextPressure = ({ text = "ardacity", fontFamily = "Compressa VF", // This font is just an example, you should not use it in commercial projects. fontUrl = "https://res.cloudinary.com/dr6lvwubh/raw/upload/v1529908256/CompressaPRO-GX.woff2", width = true, weight = true, italic = true, alpha = false, flex = true, stroke = false, scale = false, textColor = "auto", strokeColor = "#FF0000", strokeWidth = 2, className = "", minFontSize = 24, }) => { const containerRef = useRef(null) const titleRef = useRef(null) const spansRef = useRef([]) const mouseRef = useRef({ x: 0, y: 0 }) const cursorRef = useRef({ x: 0, y: 0 }) const [fontSize, setFontSize] = useState(minFontSize) const [scaleY, setScaleY] = useState(1) const [lineHeight, setLineHeight] = useState(1) const [detectedTextColor, setDetectedTextColor] = useState(textColor || "#000000") const chars = text.split("") const dist = (a, b) => { const dx = b.x - a.x const dy = b.y - a.y return Math.sqrt(dx * dx + dy * dy) } // Detect theme and set text color accordingly useEffect(() => { if (textColor) { setDetectedTextColor(textColor) return } const detectTheme = () => { // Check if prefers-color-scheme media query is supported if (window.matchMedia) { const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches setDetectedTextColor(isDarkMode ? "#FFFFFF" : "#000000") } else { // Fallback to checking background color brightness if (containerRef.current) { const parent = containerRef.current.parentElement if (parent) { const bgColor = window.getComputedStyle(parent).backgroundColor const rgb = bgColor.match(/\d+/g) if (rgb && rgb.length >= 3) { // Calculate brightness using the formula: (0.299*R + 0.587*G + 0.114*B) const brightness = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255 setDetectedTextColor(brightness > 0.5 ? "#000000" : "#FFFFFF") } else { setDetectedTextColor("#000000") // Default to black if can't detect } } } } } detectTheme() // Listen for theme changes const darkModeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)") const handleThemeChange = (e) => { setDetectedTextColor(e.matches ? "#FFFFFF" : "#000000") } if (darkModeMediaQuery.addEventListener) { darkModeMediaQuery.addEventListener("change", handleThemeChange) } else if (darkModeMediaQuery.addListener) { // For older browsers darkModeMediaQuery.addListener(handleThemeChange) } return () => { if (darkModeMediaQuery.removeEventListener) { darkModeMediaQuery.removeEventListener("change", handleThemeChange) } else if (darkModeMediaQuery.removeListener) { darkModeMediaQuery.removeListener(handleThemeChange) } } }, [textColor, containerRef.current]) useEffect(() => { const handleMouseMove = (e) => { cursorRef.current.x = e.clientX cursorRef.current.y = e.clientY } const handleTouchMove = (e) => { const t = e.touches[0] cursorRef.current.x = t.clientX cursorRef.current.y = t.clientY } window.addEventListener("mousemove", handleMouseMove) window.addEventListener("touchmove", handleTouchMove, { passive: false }) if (containerRef.current) { const { left, top, width, height } = containerRef.current.getBoundingClientRect() mouseRef.current.x = left + width / 2 mouseRef.current.y = top + height / 2 cursorRef.current.x = mouseRef.current.x cursorRef.current.y = mouseRef.current.y } return () => { window.removeEventListener("mousemove", handleMouseMove) window.removeEventListener("touchmove", handleTouchMove) } }, []) const setSize = () => { if (!containerRef.current || !titleRef.current) return const { width: containerW, height: containerH } = containerRef.current.getBoundingClientRect() let newFontSize = containerW / (chars.length / 2) newFontSize = Math.max(newFontSize, minFontSize) setFontSize(newFontSize) setScaleY(1) setLineHeight(1) requestAnimationFrame(() => { if (!titleRef.current) return const textRect = titleRef.current.getBoundingClientRect() if (scale && textRect.height > 0) { const yRatio = containerH / textRect.height setScaleY(yRatio) setLineHeight(yRatio) } }) } useEffect(() => { setSize() window.addEventListener("resize", setSize) return () => window.removeEventListener("resize", setSize) }, [scale, text]) useEffect(() => { let rafId const animate = () => { mouseRef.current.x += (cursorRef.current.x - mouseRef.current.x) / 15 mouseRef.current.y += (cursorRef.current.y - mouseRef.current.y) / 15 if (titleRef.current) { const titleRect = titleRef.current.getBoundingClientRect() const maxDist = titleRect.width / 2 spansRef.current.forEach((span) => { if (!span) return const rect = span.getBoundingClientRect() const charCenter = { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2, } const d = dist(mouseRef.current, charCenter) const getAttr = (distance, minVal, maxVal) => { const val = maxVal - Math.abs((maxVal * distance) / maxDist) return Math.max(minVal, val + minVal) } const wdth = width ? Math.floor(getAttr(d, 5, 200)) : 100 const wght = weight ? Math.floor(getAttr(d, 100, 900)) : 400 const italVal = italic ? getAttr(d, 0, 1).toFixed(2) : 0 const alphaVal = alpha ? getAttr(d, 0, 1).toFixed(2) : 1 span.style.opacity = alphaVal span.style.fontVariationSettings = `'wght' ${wght}, 'wdth' ${wdth}, 'ital' ${italVal}` }) } rafId = requestAnimationFrame(animate) } animate() return () => cancelAnimationFrame(rafId) }, [width, weight, italic, alpha, chars.length]) return ( <div ref={containerRef} className="relative w-full h-full overflow-hidden bg-transparent"> <style>{` @font-face { font-family: '${fontFamily}'; src: url('${fontUrl}'); font-style: normal; } .stroke span { position: relative; color: ${detectedTextColor}; } .stroke span::after { content: attr(data-char); position: absolute; left: 0; top: 0; color: transparent; z-index: -1; -webkit-text-stroke-width: ${strokeWidth}px; -webkit-text-stroke-color: ${strokeColor}; } `}</style> <h1 ref={titleRef} className={`text-pressure-title ${className} ${ flex ? "flex justify-between" : "" } ${stroke ? "stroke" : ""} uppercase text-center`} style={{ fontFamily, fontSize: fontSize, lineHeight, transform: `scale(1, ${scaleY})`, transformOrigin: "center top", margin: 0, fontWeight: 100, color: stroke ? undefined : detectedTextColor, }} > {chars.map((char, i) => ( <span key={i} ref={(el) => (spansRef.current[i] = el)} data-char={char} className="inline-block"> {char} </span> ))} </h1> </div> ) } export default TextPressure