Files
vue-bits/public/r/TextPressure.json
David Haz e621971723 jsrepo v3
2025-12-15 23:50:24 +02:00

1 line
7.3 KiB
JSON

{"name":"TextPressure","title":"TextPressure","description":"Characters scale / warp interactively based on pointer pressure zone.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<!-- Component ported from https://codepen.io/JuanFuentes/full/rgXKGQ -->\n\n<script setup lang=\"ts\">\nimport { ref, onMounted, onUnmounted, nextTick, computed, watch, useTemplateRef } from 'vue';\n\ninterface TextPressureProps {\n text?: string;\n fontFamily?: string;\n fontUrl?: string;\n width?: boolean;\n weight?: boolean;\n italic?: boolean;\n alpha?: boolean;\n flex?: boolean;\n stroke?: boolean;\n scale?: boolean;\n textColor?: string;\n strokeColor?: string;\n strokeWidth?: number;\n className?: string;\n minFontSize?: number;\n}\n\nconst props = withDefaults(defineProps<TextPressureProps>(), {\n text: 'Compressa',\n fontFamily: 'Compressa VF',\n fontUrl: 'https://res.cloudinary.com/dr6lvwubh/raw/upload/v1529908256/CompressaPRO-GX.woff2',\n width: true,\n weight: true,\n italic: true,\n alpha: false,\n flex: true,\n stroke: false,\n scale: false,\n textColor: '#FFFFFF',\n strokeColor: '#FF0000',\n strokeWidth: 2,\n className: '',\n minFontSize: 24\n});\n\nconst containerRef = useTemplateRef<HTMLDivElement>('containerRef');\nconst titleRef = useTemplateRef<HTMLHeadingElement>('titleRef');\nconst spansRef = ref<(HTMLSpanElement | null)[]>([]);\n\nconst mouseRef = ref({ x: 0, y: 0 });\nconst cursorRef = ref({ x: 0, y: 0 });\n\nconst fontSize = ref(props.minFontSize);\nconst scaleY = ref(1);\nconst lineHeight = ref(1);\n\nconst chars = computed(() => props.text.split(''));\n\nconst dist = (a: { x: number; y: number }, b: { x: number; y: number }) => {\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n return Math.sqrt(dx * dx + dy * dy);\n};\n\nconst handleMouseMove = (e: MouseEvent) => {\n cursorRef.value.x = e.clientX;\n cursorRef.value.y = e.clientY;\n};\n\nconst handleTouchMove = (e: TouchEvent) => {\n const t = e.touches[0];\n cursorRef.value.x = t.clientX;\n cursorRef.value.y = t.clientY;\n};\n\nconst setSize = () => {\n if (!containerRef.value || !titleRef.value) return;\n\n const { width: containerW, height: containerH } = containerRef.value.getBoundingClientRect();\n\n let newFontSize = containerW / (chars.value.length / 2);\n newFontSize = Math.max(newFontSize, props.minFontSize);\n\n fontSize.value = newFontSize;\n scaleY.value = 1;\n lineHeight.value = 1;\n\n nextTick(() => {\n if (!titleRef.value) return;\n const textRect = titleRef.value.getBoundingClientRect();\n\n if (props.scale && textRect.height > 0) {\n const yRatio = containerH / textRect.height;\n scaleY.value = yRatio;\n lineHeight.value = yRatio;\n }\n });\n};\n\nlet rafId: number;\n\nconst animate = () => {\n mouseRef.value.x += (cursorRef.value.x - mouseRef.value.x) / 15;\n mouseRef.value.y += (cursorRef.value.y - mouseRef.value.y) / 15;\n\n if (titleRef.value) {\n const titleRect = titleRef.value.getBoundingClientRect();\n const maxDist = titleRect.width / 2;\n\n spansRef.value.forEach(span => {\n if (!span) return;\n\n const rect = span.getBoundingClientRect();\n const charCenter = {\n x: rect.x + rect.width / 2,\n y: rect.y + rect.height / 2\n };\n\n const d = dist(mouseRef.value, charCenter);\n\n const getAttr = (distance: number, minVal: number, maxVal: number) => {\n const val = maxVal - Math.abs((maxVal * distance) / maxDist);\n return Math.max(minVal, val + minVal);\n };\n\n const wdth = props.width ? Math.floor(getAttr(d, 5, 200)) : 100;\n const wght = props.weight ? Math.floor(getAttr(d, 100, 900)) : 400;\n const italVal = props.italic ? getAttr(d, 0, 1).toFixed(2) : '0';\n const alphaVal = props.alpha ? getAttr(d, 0, 1).toFixed(2) : '1';\n\n span.style.opacity = alphaVal;\n span.style.fontVariationSettings = `'wght' ${wght}, 'wdth' ${wdth}, 'ital' ${italVal}`;\n });\n }\n\n rafId = requestAnimationFrame(animate);\n};\n\nonMounted(() => {\n const styleElement = document.createElement('style');\n styleElement.textContent = dynamicStyles.value;\n document.head.appendChild(styleElement);\n styleElement.setAttribute('data-text-pressure', 'true');\n\n setSize();\n\n window.addEventListener('mousemove', handleMouseMove);\n window.addEventListener('touchmove', handleTouchMove, { passive: false });\n window.addEventListener('resize', setSize);\n\n if (containerRef.value) {\n const { left, top, width, height } = containerRef.value.getBoundingClientRect();\n mouseRef.value.x = left + width / 2;\n mouseRef.value.y = top + height / 2;\n cursorRef.value.x = mouseRef.value.x;\n cursorRef.value.y = mouseRef.value.y;\n }\n\n animate();\n});\n\nonUnmounted(() => {\n const styleElements = document.querySelectorAll('style[data-text-pressure=\"true\"]');\n styleElements.forEach(el => el.remove());\n\n window.removeEventListener('mousemove', handleMouseMove);\n window.removeEventListener('touchmove', handleTouchMove);\n window.removeEventListener('resize', setSize);\n if (rafId) {\n cancelAnimationFrame(rafId);\n }\n});\n\nwatch([() => props.scale, () => props.text], () => {\n setSize();\n});\n\nwatch([() => props.width, () => props.weight, () => props.italic, () => props.alpha], () => {});\n\nconst titleStyle = computed(() => ({\n fontFamily: props.fontFamily,\n fontSize: fontSize.value + 'px',\n lineHeight: lineHeight.value,\n transform: `scale(1, ${scaleY.value})`,\n transformOrigin: 'center top',\n margin: 0,\n fontWeight: 100,\n color: props.stroke ? undefined : props.textColor\n}));\n\nconst dynamicStyles = computed(\n () => `\n @font-face {\n font-family: '${props.fontFamily}';\n src: url('${props.fontUrl}');\n font-style: normal;\n }\n .stroke span {\n position: relative;\n color: ${props.textColor};\n }\n .stroke span::after {\n content: attr(data-char);\n position: absolute;\n left: 0;\n top: 0;\n color: transparent;\n z-index: -1;\n -webkit-text-stroke-width: ${props.strokeWidth}px;\n -webkit-text-stroke-color: ${props.strokeColor};\n }\n`\n);\n\nonMounted(() => {\n const styleElement = document.createElement('style');\n styleElement.textContent = dynamicStyles.value;\n document.head.appendChild(styleElement);\n\n styleElement.setAttribute('data-text-pressure', 'true');\n});\n\nonUnmounted(() => {\n const styleElements = document.querySelectorAll('style[data-text-pressure=\"true\"]');\n styleElements.forEach(el => el.remove());\n});\n</script>\n\n<template>\n <div ref=\"containerRef\" class=\"relative w-full h-full overflow-hidden bg-transparent\">\n <h1\n ref=\"titleRef\"\n :class=\"`text-pressure-title ${className} ${flex ? 'flex justify-between' : ''} ${stroke ? 'stroke' : ''} uppercase text-center`\"\n :style=\"titleStyle\"\n >\n <span\n v-for=\"(char, i) in chars\"\n :key=\"i\"\n :ref=\"el => (spansRef[i] = el as HTMLSpanElement)\"\n :data-char=\"char\"\n class=\"inline-block\"\n >\n {{ char }}\n </span>\n </h1>\n </div>\n</template>\n","path":"TextPressure/TextPressure.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[],"devDependencies":[],"categories":["TextAnimations"]}