mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 22:49:31 -07:00
1 line
8.2 KiB
JSON
1 line
8.2 KiB
JSON
{"name":"FuzzyText","title":"FuzzyText","description":"Vibrating fuzzy text with controllable hover intensity.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { onMounted, onUnmounted, watch, nextTick, useTemplateRef } from 'vue';\n\ninterface FuzzyTextProps {\n text: string;\n fontSize?: number | string;\n fontWeight?: string | number;\n fontFamily?: string;\n color?: string;\n enableHover?: boolean;\n baseIntensity?: number;\n hoverIntensity?: number;\n}\n\nconst props = withDefaults(defineProps<FuzzyTextProps>(), {\n text: '',\n fontSize: 'clamp(2rem, 8vw, 8rem)',\n fontWeight: 900,\n fontFamily: 'inherit',\n color: '#fff',\n enableHover: true,\n baseIntensity: 0.18,\n hoverIntensity: 0.5\n});\n\nconst canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef');\nlet animationFrameId: number;\nlet isCancelled = false;\nlet cleanup: (() => void) | null = null;\n\nconst waitForFont = async (fontFamily: string, fontWeight: string | number, fontSize: string): Promise<boolean> => {\n if (document.fonts?.check) {\n const fontString = `${fontWeight} ${fontSize} ${fontFamily}`;\n\n if (document.fonts.check(fontString)) {\n return true;\n }\n\n try {\n await document.fonts.load(fontString);\n return document.fonts.check(fontString);\n } catch (error) {\n console.warn('Font loading failed:', error);\n return false;\n }\n }\n\n return new Promise(resolve => {\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d');\n if (!ctx) {\n resolve(false);\n return;\n }\n\n ctx.font = `${fontWeight} ${fontSize} ${fontFamily}`;\n const testWidth = ctx.measureText('M').width;\n\n let attempts = 0;\n const checkFont = () => {\n ctx.font = `${fontWeight} ${fontSize} ${fontFamily}`;\n const newWidth = ctx.measureText('M').width;\n\n if (newWidth !== testWidth && newWidth > 0) {\n resolve(true);\n } else if (attempts < 20) {\n attempts++;\n setTimeout(checkFont, 50);\n } else {\n resolve(false);\n }\n };\n\n setTimeout(checkFont, 10);\n });\n};\n\nconst initCanvas = async () => {\n if (document.fonts?.ready) {\n await document.fonts.ready;\n }\n\n if (isCancelled) return;\n\n const canvas = canvasRef.value;\n if (!canvas) return;\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n\n const computedFontFamily =\n props.fontFamily === 'inherit' ? window.getComputedStyle(canvas).fontFamily || 'sans-serif' : props.fontFamily;\n\n const fontSizeStr = typeof props.fontSize === 'number' ? `${props.fontSize}px` : props.fontSize;\n let numericFontSize: number;\n\n if (typeof props.fontSize === 'number') {\n numericFontSize = props.fontSize;\n } else {\n const temp = document.createElement('span');\n temp.style.fontSize = props.fontSize;\n temp.style.fontFamily = computedFontFamily;\n document.body.appendChild(temp);\n const computedSize = window.getComputedStyle(temp).fontSize;\n numericFontSize = parseFloat(computedSize);\n document.body.removeChild(temp);\n }\n\n const fontLoaded = await waitForFont(computedFontFamily, props.fontWeight, fontSizeStr);\n if (!fontLoaded) {\n console.warn(`Font not loaded: ${computedFontFamily}`);\n }\n\n const text = props.text;\n\n const offscreen = document.createElement('canvas');\n const offCtx = offscreen.getContext('2d');\n if (!offCtx) return;\n\n const fontString = `${props.fontWeight} ${fontSizeStr} ${computedFontFamily}`;\n offCtx.font = fontString;\n\n const testMetrics = offCtx.measureText('M');\n if (testMetrics.width === 0) {\n setTimeout(() => {\n if (!isCancelled) {\n initCanvas();\n }\n }, 100);\n return;\n }\n\n offCtx.textBaseline = 'alphabetic';\n const metrics = offCtx.measureText(text);\n\n const actualLeft = metrics.actualBoundingBoxLeft ?? 0;\n const actualRight = metrics.actualBoundingBoxRight ?? metrics.width;\n const actualAscent = metrics.actualBoundingBoxAscent ?? numericFontSize;\n const actualDescent = metrics.actualBoundingBoxDescent ?? numericFontSize * 0.2;\n\n const textBoundingWidth = Math.ceil(actualLeft + actualRight);\n const tightHeight = Math.ceil(actualAscent + actualDescent);\n\n const extraWidthBuffer = 10;\n const offscreenWidth = textBoundingWidth + extraWidthBuffer;\n\n offscreen.width = offscreenWidth;\n offscreen.height = tightHeight;\n\n const xOffset = extraWidthBuffer / 2;\n offCtx.font = `${props.fontWeight} ${fontSizeStr} ${computedFontFamily}`;\n offCtx.textBaseline = 'alphabetic';\n offCtx.fillStyle = props.color;\n offCtx.fillText(text, xOffset - actualLeft, actualAscent);\n\n const horizontalMargin = 50;\n const verticalMargin = 0;\n canvas.width = offscreenWidth + horizontalMargin * 2;\n canvas.height = tightHeight + verticalMargin * 2;\n ctx.translate(horizontalMargin, verticalMargin);\n\n const interactiveLeft = horizontalMargin + xOffset;\n const interactiveTop = verticalMargin;\n const interactiveRight = interactiveLeft + textBoundingWidth;\n const interactiveBottom = interactiveTop + tightHeight;\n\n let isHovering = false;\n const fuzzRange = 30;\n\n const run = () => {\n if (isCancelled) return;\n ctx.clearRect(-fuzzRange, -fuzzRange, offscreenWidth + 2 * fuzzRange, tightHeight + 2 * fuzzRange);\n const intensity = isHovering ? props.hoverIntensity : props.baseIntensity;\n for (let j = 0; j < tightHeight; j++) {\n const dx = Math.floor(intensity * (Math.random() - 0.5) * fuzzRange);\n ctx.drawImage(offscreen, 0, j, offscreenWidth, 1, dx, j, offscreenWidth, 1);\n }\n animationFrameId = window.requestAnimationFrame(run);\n };\n\n run();\n\n const isInsideTextArea = (x: number, y: number) =>\n x >= interactiveLeft && x <= interactiveRight && y >= interactiveTop && y <= interactiveBottom;\n\n const handleMouseMove = (e: MouseEvent) => {\n if (!props.enableHover) return;\n const rect = canvas.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n isHovering = isInsideTextArea(x, y);\n };\n\n const handleMouseLeave = () => {\n isHovering = false;\n };\n\n const handleTouchMove = (e: TouchEvent) => {\n if (!props.enableHover) return;\n e.preventDefault();\n const rect = canvas.getBoundingClientRect();\n const touch = e.touches[0];\n const x = touch.clientX - rect.left;\n const y = touch.clientY - rect.top;\n isHovering = isInsideTextArea(x, y);\n };\n\n const handleTouchEnd = () => {\n isHovering = false;\n };\n\n if (props.enableHover) {\n canvas.addEventListener('mousemove', handleMouseMove);\n canvas.addEventListener('mouseleave', handleMouseLeave);\n canvas.addEventListener('touchmove', handleTouchMove, { passive: false });\n canvas.addEventListener('touchend', handleTouchEnd);\n }\n\n cleanup = () => {\n window.cancelAnimationFrame(animationFrameId);\n if (props.enableHover) {\n canvas.removeEventListener('mousemove', handleMouseMove);\n canvas.removeEventListener('mouseleave', handleMouseLeave);\n canvas.removeEventListener('touchmove', handleTouchMove);\n canvas.removeEventListener('touchend', handleTouchEnd);\n }\n };\n};\n\nonMounted(() => {\n nextTick(() => {\n initCanvas();\n });\n});\n\nonUnmounted(() => {\n isCancelled = true;\n if (animationFrameId) {\n window.cancelAnimationFrame(animationFrameId);\n }\n if (cleanup) {\n cleanup();\n }\n});\n\nwatch(\n [\n () => props.text,\n () => props.fontSize,\n () => props.fontWeight,\n () => props.fontFamily,\n () => props.color,\n () => props.enableHover,\n () => props.baseIntensity,\n () => props.hoverIntensity\n ],\n () => {\n isCancelled = true;\n if (animationFrameId) {\n window.cancelAnimationFrame(animationFrameId);\n }\n if (cleanup) {\n cleanup();\n }\n isCancelled = false;\n nextTick(() => {\n initCanvas();\n });\n }\n);\n</script>\n\n<template>\n <canvas ref=\"canvasRef\" />\n</template>\n","path":"FuzzyText/FuzzyText.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[],"devDependencies":[],"categories":["TextAnimations"]} |