mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
1 line
13 KiB
JSON
1 line
13 KiB
JSON
{"name":"GlassSurface","title":"GlassSurface","description":"Advanced Apple-style glass surface with real-time distortion + lighting.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<template>\n <div ref=\"containerRef\" :class=\"[glassSurfaceClasses, focusVisibleClasses, className]\" :style=\"containerStyles\">\n <svg class=\"w-full h-full pointer-events-none absolute inset-0 opacity-0 -z-10\" xmlns=\"http://www.w3.org/2000/svg\">\n <defs>\n <filter :id=\"filterId\" color-interpolation-filters=\"sRGB\" x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\">\n <feImage ref=\"feImageRef\" x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" preserveAspectRatio=\"none\" result=\"map\" />\n\n <feDisplacementMap ref=\"redChannelRef\" in=\"SourceGraphic\" in2=\"map\" id=\"redchannel\" result=\"dispRed\" />\n <feColorMatrix\n in=\"dispRed\"\n type=\"matrix\"\n values=\"1 0 0 0 0\n 0 0 0 0 0\n 0 0 0 0 0\n 0 0 0 1 0\"\n result=\"red\"\n />\n\n <feDisplacementMap ref=\"greenChannelRef\" in=\"SourceGraphic\" in2=\"map\" id=\"greenchannel\" result=\"dispGreen\" />\n <feColorMatrix\n in=\"dispGreen\"\n type=\"matrix\"\n values=\"0 0 0 0 0\n 0 1 0 0 0\n 0 0 0 0 0\n 0 0 0 1 0\"\n result=\"green\"\n />\n\n <feDisplacementMap ref=\"blueChannelRef\" in=\"SourceGraphic\" in2=\"map\" id=\"bluechannel\" result=\"dispBlue\" />\n <feColorMatrix\n in=\"dispBlue\"\n type=\"matrix\"\n values=\"0 0 0 0 0\n 0 0 0 0 0\n 0 0 1 0 0\n 0 0 0 1 0\"\n result=\"blue\"\n />\n\n <feBlend in=\"red\" in2=\"green\" mode=\"screen\" result=\"rg\" />\n <feBlend in=\"rg\" in2=\"blue\" mode=\"screen\" result=\"output\" />\n <feGaussianBlur ref=\"gaussianBlurRef\" in=\"output\" stdDeviation=\"0.7\" />\n </filter>\n </defs>\n </svg>\n\n <div class=\"w-full h-full flex items-center justify-center p-2 rounded-[inherit] relative z-10\">\n <slot />\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, type CSSProperties, useTemplateRef, onMounted, computed, watch, nextTick, onUnmounted } from 'vue';\n\ninterface GlassSurfaceProps {\n width?: string | number;\n height?: string | number;\n borderRadius?: number;\n borderWidth?: number;\n brightness?: number;\n opacity?: number;\n blur?: number;\n displace?: number;\n backgroundOpacity?: number;\n saturation?: number;\n distortionScale?: number;\n redOffset?: number;\n greenOffset?: number;\n blueOffset?: number;\n xChannel?: 'R' | 'G' | 'B';\n yChannel?: 'R' | 'G' | 'B';\n mixBlendMode?:\n | 'normal'\n | 'multiply'\n | 'screen'\n | 'overlay'\n | 'darken'\n | 'lighten'\n | 'color-dodge'\n | 'color-burn'\n | 'hard-light'\n | 'soft-light'\n | 'difference'\n | 'exclusion'\n | 'hue'\n | 'saturation'\n | 'color'\n | 'luminosity'\n | 'plus-darker'\n | 'plus-lighter';\n className?: string;\n style?: CSSProperties;\n}\n\nconst props = withDefaults(defineProps<GlassSurfaceProps>(), {\n width: '200px',\n height: '200px',\n borderRadius: 20,\n borderWidth: 0.07,\n brightness: 70,\n opacity: 0.93,\n blur: 11,\n displace: 0.5,\n backgroundOpacity: 0,\n saturation: 1,\n distortionScale: -180,\n redOffset: 0,\n greenOffset: 10,\n blueOffset: 20,\n xChannel: 'R',\n yChannel: 'G',\n mixBlendMode: 'difference',\n className: '',\n style: () => ({})\n});\n\nconst isDarkMode = ref(false);\n\nconst updateDarkMode = () => {\n if (typeof window === 'undefined') return;\n\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n isDarkMode.value = mediaQuery.matches;\n\n const handler = (e: MediaQueryListEvent) => {\n isDarkMode.value = e.matches;\n };\n\n mediaQuery.addEventListener('change', handler);\n\n return () => mediaQuery.removeEventListener('change', handler);\n};\n\n// Generate unique IDs for SVG elements\nconst generateUniqueId = () => {\n return Math.random().toString(36).substring(2, 15);\n};\n\nconst uniqueId = generateUniqueId();\nconst filterId = `glass-filter-${uniqueId}`;\nconst redGradId = `red-grad-${uniqueId}`;\nconst blueGradId = `blue-grad-${uniqueId}`;\n\nconst containerRef = useTemplateRef<HTMLDivElement>('containerRef');\nconst feImageRef = useTemplateRef<SVGSVGElement>('feImageRef');\nconst redChannelRef = useTemplateRef<SVGSVGElement>('redChannelRef');\nconst greenChannelRef = useTemplateRef<SVGSVGElement>('greenChannelRef');\nconst blueChannelRef = useTemplateRef<SVGSVGElement>('blueChannelRef');\nconst gaussianBlurRef = useTemplateRef<SVGSVGElement>('gaussianBlurRef');\n\nlet resizeObserver: ResizeObserver | null = null;\n\nconst generateDisplacementMap = () => {\n const rect = containerRef.value?.getBoundingClientRect();\n const actualWidth = rect?.width || 400;\n const actualHeight = rect?.height || 200;\n const edgeSize = Math.min(actualWidth, actualHeight) * (props.borderWidth * 0.5);\n\n const svgContent = `\n <svg viewBox=\"0 0 ${actualWidth} ${actualHeight}\" xmlns=\"http://www.w3.org/2000/svg\">\n <defs>\n <linearGradient id=\"${redGradId}\" x1=\"100%\" y1=\"0%\" x2=\"0%\" y2=\"0%\">\n <stop offset=\"0%\" stop-color=\"#0000\"/>\n <stop offset=\"100%\" stop-color=\"red\"/>\n </linearGradient>\n <linearGradient id=\"${blueGradId}\" x1=\"0%\" y1=\"0%\" x2=\"0%\" y2=\"100%\">\n <stop offset=\"0%\" stop-color=\"#0000\"/>\n <stop offset=\"100%\" stop-color=\"blue\"/>\n </linearGradient>\n </defs>\n <rect x=\"0\" y=\"0\" width=\"${actualWidth}\" height=\"${actualHeight}\" fill=\"black\"></rect>\n <rect x=\"0\" y=\"0\" width=\"${actualWidth}\" height=\"${actualHeight}\" rx=\"${props.borderRadius}\" fill=\"url(#${redGradId})\" />\n <rect x=\"0\" y=\"0\" width=\"${actualWidth}\" height=\"${actualHeight}\" rx=\"${props.borderRadius}\" fill=\"url(#${blueGradId})\" style=\"mix-blend-mode: ${props.mixBlendMode}\" />\n <rect x=\"${edgeSize}\" y=\"${edgeSize}\" width=\"${actualWidth - edgeSize * 2}\" height=\"${actualHeight - edgeSize * 2}\" rx=\"${props.borderRadius}\" fill=\"hsl(0 0% ${props.brightness}% / ${props.opacity})\" style=\"filter:blur(${props.blur}px)\" />\n </svg>\n `;\n\n return `data:image/svg+xml,${encodeURIComponent(svgContent)}`;\n};\n\nconst updateDisplacementMap = () => {\n if (feImageRef.value) {\n feImageRef.value.setAttribute('href', generateDisplacementMap());\n }\n};\n\nconst supportsSVGFilters = () => {\n if (typeof window === 'undefined' || typeof navigator === 'undefined') return false;\n\n const isWebkit = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);\n const isFirefox = /Firefox/.test(navigator.userAgent);\n\n if (isWebkit || isFirefox) {\n return false;\n }\n\n const div = document.createElement('div');\n div.style.backdropFilter = `url(#${filterId})`;\n return div.style.backdropFilter !== '';\n};\n\nconst supportsBackdropFilter = () => {\n if (typeof window === 'undefined') return false;\n return CSS.supports('backdrop-filter', 'blur(10px)');\n};\n\nconst containerStyles = computed(() => {\n const baseStyles: Record<string, string | number> = {\n ...props.style,\n width: typeof props.width === 'number' ? `${props.width}px` : props.width,\n height: typeof props.height === 'number' ? `${props.height}px` : props.height,\n borderRadius: `${props.borderRadius}px`,\n '--glass-frost': props.backgroundOpacity,\n '--glass-saturation': props.saturation\n };\n\n const svgSupported = supportsSVGFilters();\n const backdropFilterSupported = supportsBackdropFilter();\n\n if (svgSupported) {\n return {\n ...baseStyles,\n background: isDarkMode.value\n ? `hsl(0 0% 0% / ${props.backgroundOpacity})`\n : `hsl(0 0% 100% / ${props.backgroundOpacity})`,\n backdropFilter: `url(#${filterId}) saturate(${props.saturation})`,\n boxShadow: isDarkMode.value\n ? `0 0 2px 1px color-mix(in oklch, white, transparent 65%) inset,\n 0 0 10px 4px color-mix(in oklch, white, transparent 85%) inset,\n 0px 4px 16px rgba(17, 17, 26, 0.05),\n 0px 8px 24px rgba(17, 17, 26, 0.05),\n 0px 16px 56px rgba(17, 17, 26, 0.05),\n 0px 4px 16px rgba(17, 17, 26, 0.05) inset,\n 0px 8px 24px rgba(17, 17, 26, 0.05) inset,\n 0px 16px 56px rgba(17, 17, 26, 0.05) inset`\n : `0 0 2px 1px color-mix(in oklch, black, transparent 85%) inset,\n 0 0 10px 4px color-mix(in oklch, black, transparent 90%) inset,\n 0px 4px 16px rgba(17, 17, 26, 0.05),\n 0px 8px 24px rgba(17, 17, 26, 0.05),\n 0px 16px 56px rgba(17, 17, 26, 0.05),\n 0px 4px 16px rgba(17, 17, 26, 0.05) inset,\n 0px 8px 24px rgba(17, 17, 26, 0.05) inset,\n 0px 16px 56px rgba(17, 17, 26, 0.05) inset`\n };\n } else {\n if (isDarkMode.value) {\n if (!backdropFilterSupported) {\n return {\n ...baseStyles,\n background: 'rgba(0, 0, 0, 0.4)',\n border: '1px solid rgba(255, 255, 255, 0.2)',\n boxShadow: `inset 0 1px 0 0 rgba(255, 255, 255, 0.2),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.1)`\n };\n } else {\n return {\n ...baseStyles,\n background: 'rgba(255, 255, 255, 0.1)',\n backdropFilter: 'blur(12px) saturate(1.8) brightness(1.2)',\n WebkitBackdropFilter: 'blur(12px) saturate(1.8) brightness(1.2)',\n border: '1px solid rgba(255, 255, 255, 0.2)',\n boxShadow: `inset 0 1px 0 0 rgba(255, 255, 255, 0.2),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.1)`\n };\n }\n } else {\n if (!backdropFilterSupported) {\n return {\n ...baseStyles,\n background: 'rgba(255, 255, 255, 0.4)',\n border: '1px solid rgba(255, 255, 255, 0.3)',\n boxShadow: `inset 0 1px 0 0 rgba(255, 255, 255, 0.5),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.3)`\n };\n } else {\n return {\n ...baseStyles,\n background: 'rgba(255, 255, 255, 0.25)',\n backdropFilter: 'blur(12px) saturate(1.8) brightness(1.1)',\n WebkitBackdropFilter: 'blur(12px) saturate(1.8) brightness(1.1)',\n border: '1px solid rgba(255, 255, 255, 0.3)',\n boxShadow: `0 8px 32px 0 rgba(31, 38, 135, 0.2),\n 0 2px 16px 0 rgba(31, 38, 135, 0.1),\n inset 0 1px 0 0 rgba(255, 255, 255, 0.4),\n inset 0 -1px 0 0 rgba(255, 255, 255, 0.2)`\n };\n }\n }\n }\n});\n\nconst glassSurfaceClasses =\n 'relative flex items-center justify-center overflow-hidden transition-opacity duration-[260ms] ease-out';\n\nconst focusVisibleClasses = computed(() => {\n return isDarkMode.value\n ? 'focus-visible:outline-2 focus-visible:outline-[#0A84FF] focus-visible:outline-offset-2'\n : 'focus-visible:outline-2 focus-visible:outline-[#007AFF] focus-visible:outline-offset-2';\n});\n\nconst updateFilterElements = () => {\n const elements = [\n { ref: redChannelRef, offset: props.redOffset },\n { ref: greenChannelRef, offset: props.greenOffset },\n { ref: blueChannelRef, offset: props.blueOffset }\n ];\n\n elements.forEach(({ ref, offset }) => {\n if (ref.value) {\n ref.value.setAttribute('scale', (props.distortionScale + offset).toString());\n ref.value.setAttribute('xChannelSelector', props.xChannel);\n ref.value.setAttribute('yChannelSelector', props.yChannel);\n }\n });\n\n if (gaussianBlurRef.value) {\n gaussianBlurRef.value.setAttribute('stdDeviation', props.displace.toString());\n }\n};\n\nconst setupResizeObserver = () => {\n if (!containerRef.value || typeof ResizeObserver === 'undefined') return;\n\n resizeObserver = new ResizeObserver(() => {\n setTimeout(updateDisplacementMap, 0);\n });\n\n resizeObserver.observe(containerRef.value);\n};\n\nwatch(\n [\n () => props.width,\n () => props.height,\n () => props.borderRadius,\n () => props.borderWidth,\n () => props.brightness,\n () => props.opacity,\n () => props.blur,\n () => props.displace,\n () => props.distortionScale,\n () => props.redOffset,\n () => props.greenOffset,\n () => props.blueOffset,\n () => props.xChannel,\n () => props.yChannel,\n () => props.mixBlendMode\n ],\n () => {\n updateDisplacementMap();\n updateFilterElements();\n }\n);\n\nwatch([() => props.width, () => props.height], () => {\n setTimeout(updateDisplacementMap, 0);\n});\n\nonMounted(() => {\n const cleanup = updateDarkMode();\n\n nextTick(() => {\n updateDisplacementMap();\n updateFilterElements();\n setupResizeObserver();\n });\n\n onUnmounted(() => {\n if (cleanup) cleanup();\n if (resizeObserver) {\n resizeObserver.disconnect();\n }\n });\n});\n</script>\n","path":"GlassSurface/GlassSurface.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[],"devDependencies":[],"categories":["Components"]} |