mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-04-22 09:54:39 -06:00
1 line
11 KiB
JSON
1 line
11 KiB
JSON
{"name":"BorderGlow","title":"BorderGlow","description":"Glowing mesh-gradient border that follows cursor direction and intensifies near edges.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { computed, ref, useTemplateRef, watch } from 'vue';\n\ninterface BorderGlowProps {\n className?: string;\n edgeSensitivity?: number;\n glowColor?: string;\n backgroundColor?: string;\n borderRadius?: number;\n glowRadius?: number;\n glowIntensity?: number;\n coneSpread?: number;\n animated?: boolean;\n colors?: string[];\n fillOpacity?: number;\n}\n\nfunction parseHSL(hslStr: string): { h: number; s: number; l: number } {\n const match = hslStr.match(/([\\d.]+)\\s*([\\d.]+)%?\\s*([\\d.]+)%?/);\n if (!match) return { h: 40, s: 80, l: 80 };\n return { h: parseFloat(match[1]), s: parseFloat(match[2]), l: parseFloat(match[3]) };\n}\n\nfunction buildBoxShadow(glowColor: string, intensity: number): string {\n const { h, s, l } = parseHSL(glowColor);\n const base = `${h}deg ${s}% ${l}%`;\n const layers: [number, number, number, number, number, boolean][] = [\n [0, 0, 0, 1, 100, true],\n [0, 0, 1, 0, 60, true],\n [0, 0, 3, 0, 50, true],\n [0, 0, 6, 0, 40, true],\n [0, 0, 15, 0, 30, true],\n [0, 0, 25, 2, 20, true],\n [0, 0, 50, 2, 10, true],\n [0, 0, 1, 0, 60, false],\n [0, 0, 3, 0, 50, false],\n [0, 0, 6, 0, 40, false],\n [0, 0, 15, 0, 30, false],\n [0, 0, 25, 2, 20, false],\n [0, 0, 50, 2, 10, false]\n ];\n return layers\n .map(([x, y, blur, spread, alpha, inset]) => {\n const a = Math.min(alpha * intensity, 100);\n return `${inset ? 'inset ' : ''}${x}px ${y}px ${blur}px ${spread}px hsl(${base} / ${a}%)`;\n })\n .join(', ');\n}\n\nfunction easeOutCubic(x: number) {\n return 1 - Math.pow(1 - x, 3);\n}\nfunction easeInCubic(x: number) {\n return x * x * x;\n}\n\ninterface AnimateOpts {\n start?: number;\n end?: number;\n duration?: number;\n delay?: number;\n ease?: (t: number) => number;\n onUpdate: (v: number) => void;\n onEnd?: () => void;\n}\n\nfunction animateValue({\n start = 0,\n end = 100,\n duration = 1000,\n delay = 0,\n ease = easeOutCubic,\n onUpdate,\n onEnd\n}: AnimateOpts) {\n const t0 = performance.now() + delay;\n function tick() {\n const elapsed = performance.now() - t0;\n const t = Math.min(elapsed / duration, 1);\n onUpdate(start + (end - start) * ease(t));\n if (t < 1) requestAnimationFrame(tick);\n else if (onEnd) onEnd();\n }\n setTimeout(() => requestAnimationFrame(tick), delay);\n}\n\nconst GRADIENT_POSITIONS = ['80% 55%', '69% 34%', '8% 6%', '41% 38%', '86% 85%', '82% 18%', '51% 4%'];\nconst COLOR_MAP = [0, 1, 2, 0, 1, 2, 1];\n\nfunction buildMeshGradients(colors: string[]): string[] {\n const gradients: string[] = [];\n for (let i = 0; i < 7; i++) {\n const c = colors[Math.min(COLOR_MAP[i], colors.length - 1)];\n gradients.push(`radial-gradient(at ${GRADIENT_POSITIONS[i]}, ${c} 0px, transparent 50%)`);\n }\n gradients.push(`linear-gradient(${colors[0]} 0 100%)`);\n return gradients;\n}\n\nconst props = withDefaults(defineProps<BorderGlowProps>(), {\n className: '',\n edgeSensitivity: 30,\n glowColor: '40 80 80',\n backgroundColor: '#060010',\n borderRadius: 28,\n glowRadius: 40,\n glowIntensity: 1.0,\n coneSpread: 25,\n animated: false,\n colors: () => ['#c084fc', '#f472b6', '#38bdf8'],\n fillOpacity: 0.5\n});\n\nconst cardRef = useTemplateRef<HTMLDivElement>('cardRef');\nconst isHovered = ref(false);\nconst cursorAngle = ref(45);\nconst edgeProximity = ref(0);\nconst sweepActive = ref(false);\n\nconst getCenterOfElement = (el: HTMLElement) => {\n const { width, height } = el.getBoundingClientRect();\n return [width / 2, height / 2];\n};\n\nconst getEdgeProximity = (el: HTMLElement, x: number, y: number) => {\n const [cx, cy] = getCenterOfElement(el);\n const dx = x - cx;\n const dy = y - cy;\n let kx = Infinity;\n let ky = Infinity;\n if (dx !== 0) kx = cx / Math.abs(dx);\n if (dy !== 0) ky = cy / Math.abs(dy);\n return Math.min(Math.max(1 / Math.min(kx, ky), 0), 1);\n};\n\nconst getCursorAngle = (el: HTMLElement, x: number, y: number) => {\n const [cx, cy] = getCenterOfElement(el);\n const dx = x - cx;\n const dy = y - cy;\n if (dx === 0 && dy === 0) return 0;\n const radians = Math.atan2(dy, dx);\n let degrees = radians * (180 / Math.PI) + 90;\n if (degrees < 0) degrees += 360;\n return degrees;\n};\n\nconst handlePointerMove = (e: PointerEvent) => {\n const card = cardRef.value;\n if (!card) return;\n const rect = card.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n edgeProximity.value = getEdgeProximity(card, x, y);\n cursorAngle.value = getCursorAngle(card, x, y);\n};\n\nwatch(\n () => [props.animated],\n () => {\n if (!props.animated) return;\n const angleStart = 110;\n const angleEnd = 465;\n sweepActive.value = true;\n cursorAngle.value = angleStart;\n\n animateValue({ duration: 500, onUpdate: v => (edgeProximity.value = v / 100) });\n animateValue({\n ease: easeInCubic,\n duration: 1500,\n end: 50,\n onUpdate: v => {\n cursorAngle.value = (angleEnd - angleStart) * (v / 100) + angleStart;\n }\n });\n animateValue({\n ease: easeOutCubic,\n delay: 1500,\n duration: 2250,\n start: 50,\n end: 100,\n onUpdate: v => {\n cursorAngle.value = (angleEnd - angleStart) * (v / 100) + angleStart;\n }\n });\n animateValue({\n ease: easeInCubic,\n delay: 2500,\n duration: 1500,\n start: 100,\n end: 0,\n onUpdate: v => (edgeProximity.value = v / 100),\n onEnd: () => (sweepActive.value = false)\n });\n },\n {\n deep: true,\n immediate: true\n }\n);\n\nconst colorSensitivity = computed(() => props.edgeSensitivity + 20);\nconst isVisible = computed(() => isHovered.value || sweepActive.value);\nconst borderOpacity = computed(() =>\n isVisible.value\n ? Math.max(0, (edgeProximity.value * 100 - colorSensitivity.value) / (100 - colorSensitivity.value))\n : 0\n);\nconst glowOpacity = computed(() =>\n isVisible.value ? Math.max(0, (edgeProximity.value * 100 - props.edgeSensitivity) / (100 - props.edgeSensitivity)) : 0\n);\n\nconst meshGradients = computed(() => buildMeshGradients(props.colors));\nconst borderBg = computed(() => meshGradients.value.map(g => `${g} border-box`));\nconst fillBg = computed(() => meshGradients.value.map(g => `${g} padding-box`));\nconst angleDeg = computed(() => `${cursorAngle.value.toFixed(3)}deg`);\n</script>\n\n<template>\n <div\n ref=\"cardRef\"\n @pointermove=\"handlePointerMove\"\n @pointerenter=\"isHovered = true\"\n @pointerleave=\"isHovered = false\"\n :class=\"`relative grid isolate border border-white/15 ${props.className}`\"\n :style=\"{\n background: props.backgroundColor,\n borderRadius: props.borderRadius + 'px',\n transform: 'translate3d(0, 0, 0.01px)',\n boxShadow:\n 'rgba(0,0,0,0.1) 0 1px 2px, rgba(0,0,0,0.1) 0 2px 4px, rgba(0,0,0,0.1) 0 4px 8px, rgba(0,0,0,0.1) 0 8px 16px, rgba(0,0,0,0.1) 0 16px 32px, rgba(0,0,0,0.1) 0 32px 64px'\n }\"\n >\n <!-- mesh gradient border -->\n <div\n class=\"-z-[1] absolute inset-0 rounded-[inherit]\"\n :style=\"{\n border: '1px solid transparent',\n background: [\n `linear-gradient(${props.backgroundColor} 0 100%) padding-box`,\n 'linear-gradient(rgb(255 255 255 / 0%) 0% 100%) border-box',\n ...borderBg\n ].join(', '),\n opacity: borderOpacity,\n maskImage: `conic-gradient(from ${angleDeg} at center, black ${props.coneSpread}%, transparent ${\n props.coneSpread + 15\n }%, transparent ${100 - props.coneSpread - 15}%, black ${100 - props.coneSpread}%)`,\n WebkitMaskImage: `conic-gradient(from ${angleDeg} at center, black ${props.coneSpread}%, transparent ${\n props.coneSpread + 15\n }%, transparent ${100 - props.coneSpread - 15}%, black ${100 - props.coneSpread}%)`,\n transition: isVisible ? 'opacity 0.25s ease-out' : 'opacity 0.75s ease-in-out'\n }\"\n />\n\n <!-- mesh gradient fill -->\n <div\n class=\"-z-[1] absolute inset-0 rounded-[inherit]\"\n :style=\"{\n border: '1px solid transparent',\n background: fillBg.join(', '),\n maskImage: [\n 'linear-gradient(to bottom, black, black)',\n 'radial-gradient(ellipse at 50% 50%, black 40%, transparent 65%)',\n 'radial-gradient(ellipse at 66% 66%, black 5%, transparent 40%)',\n 'radial-gradient(ellipse at 33% 33%, black 5%, transparent 40%)',\n 'radial-gradient(ellipse at 66% 33%, black 5%, transparent 40%)',\n 'radial-gradient(ellipse at 33% 66%, black 5%, transparent 40%)',\n `conic-gradient(from ${angleDeg} at center, transparent 5%, black 15%, black 85%, transparent 95%)`\n ].join(', '),\n WebkitMaskImage: [\n 'linear-gradient(to bottom, black, black)',\n 'radial-gradient(ellipse at 50% 50%, black 40%, transparent 65%)',\n 'radial-gradient(ellipse at 66% 66%, black 5%, transparent 40%)',\n 'radial-gradient(ellipse at 33% 33%, black 5%, transparent 40%)',\n 'radial-gradient(ellipse at 66% 33%, black 5%, transparent 40%)',\n 'radial-gradient(ellipse at 33% 66%, black 5%, transparent 40%)',\n `conic-gradient(from ${angleDeg} at center, transparent 5%, black 15%, black 85%, transparent 95%)`\n ].join(', '),\n maskComposite: 'subtract, add, add, add, add, add',\n WebkitMaskComposite: 'source-out, source-over, source-over, source-over, source-over, source-over',\n opacity: borderOpacity * props.fillOpacity,\n mixBlendMode: 'soft-light',\n transition: isVisible ? 'opacity 0.25s ease-out' : 'opacity 0.75s ease-in-out'\n }\"\n />\n\n <!-- outer glow -->\n <span\n class=\"z-[1] absolute rounded-[inherit] pointer-events-none\"\n :style=\"{\n inset: `-${props.glowRadius}px`,\n maskImage: `conic-gradient(from ${angleDeg} at center, black 2.5%, transparent 10%, transparent 90%, black 97.5%)`,\n WebkitMaskImage: `conic-gradient(from ${angleDeg} at center, black 2.5%, transparent 10%, transparent 90%, black 97.5%)`,\n opacity: glowOpacity,\n mixBlendMode: 'plus-lighter',\n transition: isVisible ? 'opacity 0.25s ease-out' : 'opacity 0.75s ease-in-out'\n }\"\n >\n <span\n class=\"absolute rounded-[inherit]\"\n :style=\"{\n inset: `${props.glowRadius}px`,\n boxShadow: buildBoxShadow(props.glowColor, props.glowIntensity)\n }\"\n />\n </span>\n\n <!-- content -->\n <div class=\"z-[1] relative flex flex-col overflow-auto\">\n <slot />\n </div>\n </div>\n</template>\n","path":"BorderGlow/BorderGlow.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[],"devDependencies":[],"categories":["Components"]} |