mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
Migrated 'Glass Surface' component.
This commit is contained in:
313
src/content/Components/GlassSurface/GlassSurface.vue
Normal file
313
src/content/Components/GlassSurface/GlassSurface.vue
Normal file
@@ -0,0 +1,313 @@
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="glass-surface"
|
||||
:class="{
|
||||
'glass-surface--svg': supportsSVGFilters,
|
||||
'glass-surface--fallback': !supportsSVGFilters
|
||||
}"
|
||||
:style="containerStyle"
|
||||
>
|
||||
<svg class="glass-surface__filter" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<filter :id="filterId" colorInterpolationFilters="sRGB" x="0%" y="0%" width="100%" height="100%">
|
||||
<feImage ref="feImageRef" x="0" y="0" width="100%" height="100%" preserveAspectRatio="none" result="map" />
|
||||
|
||||
<feDisplacementMap ref="redChannelRef" in="SourceGraphic" in2="map" id="redchannel" result="dispRed" />
|
||||
<feColorMatrix
|
||||
in="dispRed"
|
||||
type="matrix"
|
||||
values="1 0 0 0 0
|
||||
0 0 0 0 0
|
||||
0 0 0 0 0
|
||||
0 0 0 1 0"
|
||||
result="red"
|
||||
/>
|
||||
|
||||
<feDisplacementMap ref="greenChannelRef" in="SourceGraphic" in2="map" id="greenchannel" result="dispGreen" />
|
||||
<feColorMatrix
|
||||
in="dispGreen"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0
|
||||
0 1 0 0 0
|
||||
0 0 0 0 0
|
||||
0 0 0 1 0"
|
||||
result="green"
|
||||
/>
|
||||
|
||||
<feDisplacementMap ref="blueChannelRef" in="SourceGraphic" in2="map" id="bluechannel" result="dispBlue" />
|
||||
<feColorMatrix
|
||||
in="dispBlue"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0
|
||||
0 0 0 0 0
|
||||
0 0 1 0 0
|
||||
0 0 0 1 0"
|
||||
result="blue"
|
||||
/>
|
||||
|
||||
<feBlend in="red" in2="green" mode="screen" result="rg" />
|
||||
<feBlend in="rg" in2="blue" mode="screen" result="output" />
|
||||
<feGaussianBlur ref="gaussianBlurRef" in="output" stdDeviation="0.7" />
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, type CSSProperties, useTemplateRef, onMounted, computed } from 'vue';
|
||||
|
||||
interface GlassSurfaceProps {
|
||||
width?: string | number;
|
||||
height?: string | number;
|
||||
borderRadius?: number;
|
||||
borderWidth?: number;
|
||||
brightness?: number;
|
||||
opacity?: number;
|
||||
blur?: number;
|
||||
displace?: number;
|
||||
backgroundOpacity?: number;
|
||||
saturation?: number;
|
||||
distortionScale?: number;
|
||||
redOffset?: number;
|
||||
greenOffset?: number;
|
||||
blueOffset?: number;
|
||||
xChannel?: 'R' | 'G' | 'B';
|
||||
yChannel?: 'R' | 'G' | 'B';
|
||||
mixBlendMode?:
|
||||
| 'normal'
|
||||
| 'multiply'
|
||||
| 'screen'
|
||||
| 'overlay'
|
||||
| 'darken'
|
||||
| 'lighten'
|
||||
| 'color-dodge'
|
||||
| 'color-burn'
|
||||
| 'hard-light'
|
||||
| 'soft-light'
|
||||
| 'difference'
|
||||
| 'exclusion'
|
||||
| 'hue'
|
||||
| 'saturation'
|
||||
| 'color'
|
||||
| 'luminosity'
|
||||
| 'plus-darker'
|
||||
| 'plus-lighter';
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<GlassSurfaceProps>(), {
|
||||
width: '200px',
|
||||
height: '200px',
|
||||
borderRadius: 20,
|
||||
borderWidth: 0.07,
|
||||
brightness: 70,
|
||||
opacity: 0.93,
|
||||
blur: 11,
|
||||
displace: 0.5,
|
||||
backgroundOpacity: 0,
|
||||
saturation: 1,
|
||||
distortionScale: -80,
|
||||
redOffset: 0,
|
||||
greenOffset: 10,
|
||||
blueOffset: 20,
|
||||
xChannel: 'R',
|
||||
yChannel: 'G',
|
||||
mixBlendMode: 'difference',
|
||||
className: '',
|
||||
style: () => ({})
|
||||
});
|
||||
|
||||
// Generate unique IDs for SVG elements
|
||||
const generateUniqueId = (prefix: string): string => {
|
||||
return `${prefix}-${Math.random().toString(36).substring(2, 15)}`;
|
||||
};
|
||||
|
||||
const filterId = ref(generateUniqueId('glass-surface-filter'));
|
||||
const redGradId = ref(generateUniqueId('red-grad'));
|
||||
const blueGradId = ref(generateUniqueId('blue-grad'));
|
||||
|
||||
const containerRef = useTemplateRef<HTMLDivElement>('containerRef');
|
||||
const feImageRef = useTemplateRef<SVGSVGElement>('feImageRef');
|
||||
const redChannelRef = useTemplateRef<SVGSVGElement>('redChannelRef');
|
||||
const greenChannelRef = useTemplateRef<SVGSVGElement>('greenChannelRef');
|
||||
const blueChannelRef = useTemplateRef<SVGSVGElement>('blueChannelRef');
|
||||
const gaussianBlurRef = useTemplateRef<SVGSVGElement>('gaussianBlurRef');
|
||||
|
||||
const generateDisplacementMap = () => {
|
||||
const rect = containerRef.value?.getBoundingClientRect();
|
||||
const actualWidth = rect?.width || 400;
|
||||
const actualHeight = rect?.height || 200;
|
||||
const edgeSize = Math.min(actualWidth, actualHeight) * (props.borderWidth * 0.5);
|
||||
|
||||
const svgContent = `
|
||||
<svg viewBox="0 0 ${actualWidth} ${actualHeight}" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="${redGradId.value}" x1="100%" y1="0%" x2="0%" y2="0%">
|
||||
<stop offset="0%" stop-color="#0000"/>
|
||||
<stop offset="100%" stop-color="red"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="${blueGradId.value}" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0000"/>
|
||||
<stop offset="100%" stop-color="blue"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="${actualWidth}" height="${actualHeight}" fill="black"></rect>
|
||||
<rect x="0" y="0" width="${actualWidth}" height="${actualHeight}" rx="${props.borderRadius}" fill="url(#${redGradId.value})" />
|
||||
<rect x="0" y="0" width="${actualWidth}" height="${actualHeight}" rx="${props.borderRadius}" fill="url(#${blueGradId.value})" style="mix-blend-mode: ${props.mixBlendMode}" />
|
||||
<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)" />
|
||||
</svg>
|
||||
`;
|
||||
|
||||
return `data:image/svg+xml,${encodeURIComponent(svgContent)}`;
|
||||
};
|
||||
|
||||
const updateDisplacementMap = () => {
|
||||
feImageRef.value?.setAttribute('href', generateDisplacementMap());
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
updateDisplacementMap();
|
||||
|
||||
if (containerRef.value) {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
setTimeout(updateDisplacementMap, 0);
|
||||
});
|
||||
resizeObserver.observe(containerRef.value);
|
||||
}
|
||||
|
||||
[
|
||||
{ ref: redChannelRef, offset: props.redOffset },
|
||||
{ ref: greenChannelRef, offset: props.greenOffset },
|
||||
{ ref: blueChannelRef, offset: props.blueOffset }
|
||||
].forEach(({ ref, offset }) => {
|
||||
ref.value?.setAttribute('scale', (props.distortionScale + offset).toString());
|
||||
ref.value?.setAttribute('xChannelSelector', props.xChannel);
|
||||
ref.value?.setAttribute('yChannelSelector', props.yChannel);
|
||||
});
|
||||
|
||||
if (gaussianBlurRef.value) {
|
||||
gaussianBlurRef.value.setAttribute('stdDeviation', props.displace.toString());
|
||||
}
|
||||
});
|
||||
|
||||
const supportsSVGFilters = () => {
|
||||
const ua = navigator.userAgent;
|
||||
const isWebkit = /Safari/.test(ua) && !/Chrome/.test(ua);
|
||||
const isFirefox = /Firefox/.test(ua);
|
||||
|
||||
if (isWebkit || isFirefox) return false;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.style.backdropFilter = `url(#${filterId.value})`;
|
||||
return div.style.backdropFilter !== '';
|
||||
};
|
||||
|
||||
const containerStyle = computed(() => ({
|
||||
...props.style,
|
||||
width: typeof props.width === 'number' ? `${props.width}px` : props.width,
|
||||
height: typeof props.height === 'number' ? `${props.height}px` : props.height,
|
||||
borderRadius: `${props.borderRadius}px`,
|
||||
'--glass-frost': props.backgroundOpacity,
|
||||
'--glass-saturation': props.saturation,
|
||||
'--filter-id': `url(#${filterId.value})`
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.glass-surface {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
transition: opacity 0.26s ease-out;
|
||||
}
|
||||
|
||||
.glass-surface__filter {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.glass-surface--svg {
|
||||
background: light-dark(hsl(0 0% 100% / var(--glass-frost, 0)), hsl(0 0% 0% / var(--glass-frost, 0)));
|
||||
backdrop-filter: var(--filter-id, url(#glass-filter)) saturate(var(--glass-saturation, 1));
|
||||
box-shadow:
|
||||
0 0 2px 1px light-dark(color-mix(in oklch, black, transparent 85%), color-mix(in oklch, white, transparent 65%))
|
||||
inset,
|
||||
0 0 10px 4px light-dark(color-mix(in oklch, black, transparent 90%), color-mix(in oklch, white, transparent 85%))
|
||||
inset,
|
||||
0px 4px 16px rgba(17, 17, 26, 0.05),
|
||||
0px 8px 24px rgba(17, 17, 26, 0.05),
|
||||
0px 16px 56px rgba(17, 17, 26, 0.05),
|
||||
0px 4px 16px rgba(17, 17, 26, 0.05) inset,
|
||||
0px 8px 24px rgba(17, 17, 26, 0.05) inset,
|
||||
0px 16px 56px rgba(17, 17, 26, 0.05) inset;
|
||||
}
|
||||
|
||||
.glass-surface--fallback {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
backdrop-filter: blur(12px) saturate(1.8) brightness(1.1);
|
||||
-webkit-backdrop-filter: blur(12px) saturate(1.8) brightness(1.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow:
|
||||
0 8px 32px 0 rgba(31, 38, 135, 0.2),
|
||||
0 2px 16px 0 rgba(31, 38, 135, 0.1),
|
||||
inset 0 1px 0 0 rgba(255, 255, 255, 0.4),
|
||||
inset 0 -1px 0 0 rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.glass-surface--fallback {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(12px) saturate(1.8) brightness(1.2);
|
||||
-webkit-backdrop-filter: blur(12px) saturate(1.8) brightness(1.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow:
|
||||
inset 0 1px 0 0 rgba(255, 255, 255, 0.2),
|
||||
inset 0 -1px 0 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@supports not (backdrop-filter: blur(10px)) {
|
||||
.glass-surface--fallback {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
box-shadow:
|
||||
inset 0 1px 0 0 rgba(255, 255, 255, 0.5),
|
||||
inset 0 -1px 0 0 rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.glass-surface--fallback::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: inherit;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
@supports not (backdrop-filter: blur(10px)) {
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.glass-surface--fallback {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.glass-surface--fallback::before {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.glass-surface:focus-visible {
|
||||
outline: 2px solid light-dark(#007aff, #0a84ff);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user