mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
Merge pull request #51 from snepsnepy/feat/glass-surface
Migrated 'Glass Surface' component.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// Highlighted sidebar items
|
||||
export const NEW = ['Target Cursor', 'Ripple Grid', 'Magic Bento', 'Galaxy', 'Text Type'];
|
||||
export const NEW = ['Target Cursor', 'Ripple Grid', 'Magic Bento', 'Galaxy', 'Text Type', 'Glass Surface'];
|
||||
export const UPDATED = [];
|
||||
|
||||
// Used for main sidebar navigation
|
||||
@@ -60,6 +60,7 @@ export const CATEGORIES = [
|
||||
subcategories: [
|
||||
'Animated List',
|
||||
'Masonry',
|
||||
'Glass Surface',
|
||||
'Magic Bento',
|
||||
'Profile Card',
|
||||
'Dock',
|
||||
|
||||
@@ -48,6 +48,7 @@ const textAnimations = {
|
||||
const components = {
|
||||
'animated-list': () => import('../demo/Components/AnimatedListDemo.vue'),
|
||||
'masonry': () => import('../demo/Components/MasonryDemo.vue'),
|
||||
'glass-surface': () => import('../demo/Components/GlassSurfaceDemo.vue'),
|
||||
'magic-bento': () => import('../demo/Components/MagicBentoDemo.vue'),
|
||||
'profile-card': () => import('../demo/Components/ProfileCardDemo.vue'),
|
||||
'dock': () => import('../demo/Components/DockDemo.vue'),
|
||||
|
||||
17
src/constants/code/Components/glassSurfaceCode.ts
Normal file
17
src/constants/code/Components/glassSurfaceCode.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import code from '@content/Components/GlassSurface/GlassSurface.vue?raw';
|
||||
import { createCodeObject } from '../../../types/code';
|
||||
|
||||
export const glassSurface = createCodeObject(code, 'Components/GlassSurface', {
|
||||
usage: `<template>
|
||||
<GlassSurface
|
||||
:width="300"
|
||||
:height="200"
|
||||
:border-radius="24"
|
||||
style="custom-style"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import GlassSurface from "./GlassSurface.vue";
|
||||
</script>`
|
||||
});
|
||||
377
src/content/Components/GlassSurface/GlassSurface.vue
Normal file
377
src/content/Components/GlassSurface/GlassSurface.vue
Normal file
@@ -0,0 +1,377 @@
|
||||
<template>
|
||||
<div ref="containerRef" :class="[glassSurfaceClasses, focusVisibleClasses, className]" :style="containerStyles">
|
||||
<svg class="w-full h-full pointer-events-none absolute inset-0 opacity-0 -z-10" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<filter :id="filterId" color-interpolation-filters="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 class="w-full h-full flex items-center justify-center p-2 rounded-[inherit] relative z-10">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, type CSSProperties, useTemplateRef, onMounted, computed, watch, nextTick, onUnmounted } 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: -180,
|
||||
redOffset: 0,
|
||||
greenOffset: 10,
|
||||
blueOffset: 20,
|
||||
xChannel: 'R',
|
||||
yChannel: 'G',
|
||||
mixBlendMode: 'difference',
|
||||
className: '',
|
||||
style: () => ({})
|
||||
});
|
||||
|
||||
const isDarkMode = ref(false);
|
||||
|
||||
const updateDarkMode = () => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
isDarkMode.value = mediaQuery.matches;
|
||||
|
||||
const handler = (e: MediaQueryListEvent) => {
|
||||
isDarkMode.value = e.matches;
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handler);
|
||||
|
||||
return () => mediaQuery.removeEventListener('change', handler);
|
||||
};
|
||||
|
||||
// Generate unique IDs for SVG elements
|
||||
const generateUniqueId = () => {
|
||||
return Math.random().toString(36).substring(2, 15);
|
||||
};
|
||||
|
||||
const uniqueId = generateUniqueId();
|
||||
const filterId = `glass-filter-${uniqueId}`;
|
||||
const redGradId = `red-grad-${uniqueId}`;
|
||||
const blueGradId = `blue-grad-${uniqueId}`;
|
||||
|
||||
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');
|
||||
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
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}" x1="100%" y1="0%" x2="0%" y2="0%">
|
||||
<stop offset="0%" stop-color="#0000"/>
|
||||
<stop offset="100%" stop-color="red"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="${blueGradId}" 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})" />
|
||||
<rect x="0" y="0" width="${actualWidth}" height="${actualHeight}" rx="${props.borderRadius}" fill="url(#${blueGradId})" 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 = () => {
|
||||
if (feImageRef.value) {
|
||||
feImageRef.value.setAttribute('href', generateDisplacementMap());
|
||||
}
|
||||
};
|
||||
|
||||
const supportsSVGFilters = () => {
|
||||
if (typeof window === 'undefined' || typeof navigator === 'undefined') return false;
|
||||
|
||||
const isWebkit = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
|
||||
const isFirefox = /Firefox/.test(navigator.userAgent);
|
||||
|
||||
if (isWebkit || isFirefox) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.style.backdropFilter = `url(#${filterId})`;
|
||||
return div.style.backdropFilter !== '';
|
||||
};
|
||||
|
||||
const supportsBackdropFilter = () => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return CSS.supports('backdrop-filter', 'blur(10px)');
|
||||
};
|
||||
|
||||
const containerStyles = computed(() => {
|
||||
const baseStyles: Record<string, string | number> = {
|
||||
...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
|
||||
};
|
||||
|
||||
const svgSupported = supportsSVGFilters();
|
||||
const backdropFilterSupported = supportsBackdropFilter();
|
||||
|
||||
if (svgSupported) {
|
||||
return {
|
||||
...baseStyles,
|
||||
background: isDarkMode.value
|
||||
? `hsl(0 0% 0% / ${props.backgroundOpacity})`
|
||||
: `hsl(0 0% 100% / ${props.backgroundOpacity})`,
|
||||
backdropFilter: `url(#${filterId}) saturate(${props.saturation})`,
|
||||
boxShadow: isDarkMode.value
|
||||
? `0 0 2px 1px color-mix(in oklch, white, transparent 65%) inset,
|
||||
0 0 10px 4px 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`
|
||||
: `0 0 2px 1px color-mix(in oklch, black, transparent 85%) inset,
|
||||
0 0 10px 4px color-mix(in oklch, black, transparent 90%) 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`
|
||||
};
|
||||
} else {
|
||||
if (isDarkMode.value) {
|
||||
if (!backdropFilterSupported) {
|
||||
return {
|
||||
...baseStyles,
|
||||
background: 'rgba(0, 0, 0, 0.4)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
boxShadow: `inset 0 1px 0 0 rgba(255, 255, 255, 0.2),
|
||||
inset 0 -1px 0 0 rgba(255, 255, 255, 0.1)`
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...baseStyles,
|
||||
background: 'rgba(255, 255, 255, 0.1)',
|
||||
backdropFilter: 'blur(12px) saturate(1.8) brightness(1.2)',
|
||||
WebkitBackdropFilter: 'blur(12px) saturate(1.8) brightness(1.2)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
boxShadow: `inset 0 1px 0 0 rgba(255, 255, 255, 0.2),
|
||||
inset 0 -1px 0 0 rgba(255, 255, 255, 0.1)`
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (!backdropFilterSupported) {
|
||||
return {
|
||||
...baseStyles,
|
||||
background: 'rgba(255, 255, 255, 0.4)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.3)',
|
||||
boxShadow: `inset 0 1px 0 0 rgba(255, 255, 255, 0.5),
|
||||
inset 0 -1px 0 0 rgba(255, 255, 255, 0.3)`
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...baseStyles,
|
||||
background: 'rgba(255, 255, 255, 0.25)',
|
||||
backdropFilter: 'blur(12px) saturate(1.8) brightness(1.1)',
|
||||
WebkitBackdropFilter: 'blur(12px) saturate(1.8) brightness(1.1)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.3)',
|
||||
boxShadow: `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)`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const glassSurfaceClasses =
|
||||
'relative flex items-center justify-center overflow-hidden transition-opacity duration-[260ms] ease-out';
|
||||
|
||||
const focusVisibleClasses = computed(() => {
|
||||
return isDarkMode.value
|
||||
? 'focus-visible:outline-2 focus-visible:outline-[#0A84FF] focus-visible:outline-offset-2'
|
||||
: 'focus-visible:outline-2 focus-visible:outline-[#007AFF] focus-visible:outline-offset-2';
|
||||
});
|
||||
|
||||
const updateFilterElements = () => {
|
||||
const elements = [
|
||||
{ ref: redChannelRef, offset: props.redOffset },
|
||||
{ ref: greenChannelRef, offset: props.greenOffset },
|
||||
{ ref: blueChannelRef, offset: props.blueOffset }
|
||||
];
|
||||
|
||||
elements.forEach(({ ref, offset }) => {
|
||||
if (ref.value) {
|
||||
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 setupResizeObserver = () => {
|
||||
if (!containerRef.value || typeof ResizeObserver === 'undefined') return;
|
||||
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
setTimeout(updateDisplacementMap, 0);
|
||||
});
|
||||
|
||||
resizeObserver.observe(containerRef.value);
|
||||
};
|
||||
|
||||
watch(
|
||||
[
|
||||
() => props.width,
|
||||
() => props.height,
|
||||
() => props.borderRadius,
|
||||
() => props.borderWidth,
|
||||
() => props.brightness,
|
||||
() => props.opacity,
|
||||
() => props.blur,
|
||||
() => props.displace,
|
||||
() => props.distortionScale,
|
||||
() => props.redOffset,
|
||||
() => props.greenOffset,
|
||||
() => props.blueOffset,
|
||||
() => props.xChannel,
|
||||
() => props.yChannel,
|
||||
() => props.mixBlendMode
|
||||
],
|
||||
() => {
|
||||
updateDisplacementMap();
|
||||
updateFilterElements();
|
||||
}
|
||||
);
|
||||
|
||||
watch([() => props.width, () => props.height], () => {
|
||||
setTimeout(updateDisplacementMap, 0);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
const cleanup = updateDarkMode();
|
||||
|
||||
nextTick(() => {
|
||||
updateDisplacementMap();
|
||||
updateFilterElements();
|
||||
setupResizeObserver();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (cleanup) cleanup();
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
261
src/demo/Components/GlassSurfaceDemo.vue
Normal file
261
src/demo/Components/GlassSurfaceDemo.vue
Normal file
@@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<TabbedLayout>
|
||||
<template #preview>
|
||||
<div class="relative overflow-y-auto no-scrollbar demo-container">
|
||||
<GlassSurface
|
||||
:key="key"
|
||||
:width="360"
|
||||
:height="100"
|
||||
:border-radius="borderRadius"
|
||||
:background-opacity="backgroundOpacity"
|
||||
:saturation="saturation"
|
||||
:border-width="borderWidth"
|
||||
:brightness="brightness"
|
||||
:opacity="opacity"
|
||||
:blur="blur"
|
||||
:displace="displace"
|
||||
:distortion-scale="distortionScale"
|
||||
:red-offset="redOffset"
|
||||
:green-offset="greenOffset"
|
||||
:blue-offset="blueOffset"
|
||||
style="position: sticky; top: 50%; transform: translateY(-50%); z-index: 10"
|
||||
/>
|
||||
|
||||
<div class="absolute flex flex-col items-center gap-6 top-0 left-0 right-0">
|
||||
<div
|
||||
class="absolute translate-y-1/2 top-12 text-4xl font-bold text-[#27FF64] z-0 whitespace-nowrap text-center"
|
||||
>
|
||||
Try scrolling.
|
||||
</div>
|
||||
|
||||
<!-- Top Spacer -->
|
||||
<div class="h-60 w-full" />
|
||||
|
||||
<!-- Image Blocks -->
|
||||
<div v-for="(item, index) in imageBlocks" :key="index" class="relative">
|
||||
<img :src="item.src" class="w-128 rounded-2xl object-cover grayscale-100" />
|
||||
<div
|
||||
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 font-extrabold text-center leading-[100%] text-[3rem] min-w-72"
|
||||
>
|
||||
{{ item.text }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Spacer -->
|
||||
<div class="h-60 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Customize>
|
||||
<PreviewSlider title="Border Radius" v-model="borderRadius" :min="0" :max="50" :step="1" />
|
||||
<PreviewSlider title="Background Opacity" v-model="backgroundOpacity" :min="0" :max="1" :step="0.01" />
|
||||
<PreviewSlider title="Saturation" v-model="saturation" :min="0" :max="3" :step="1" />
|
||||
<PreviewSlider title="Border Width" v-model="borderWidth" :min="0" :max="0.2" :step="0.01" />
|
||||
<PreviewSlider title="Brightness" v-model="brightness" :min="0" :max="100" :step="1" />
|
||||
<PreviewSlider title="Opacity" v-model="opacity" :min="0" :max="1" :step="0.01" />
|
||||
<PreviewSlider title="Blur" v-model="blur" :min="0" :max="30" :step="1" />
|
||||
<PreviewSlider title="Displace" v-model="displace" :min="0" :max="5" :step="0.1" />
|
||||
<PreviewSlider title="Distortion Scale" v-model="distortionScale" :min="-300" :max="300" :step="1" />
|
||||
<PreviewSlider title="Red Offset" v-model="redOffset" :min="-50" :max="50" :step="1" />
|
||||
<PreviewSlider title="Green Offset" v-model="greenOffset" :min="-50" :max="50" :step="1" />
|
||||
<PreviewSlider title="Blue Offset" v-model="blueOffset" :min="-50" :max="50" :step="1" />
|
||||
</Customize>
|
||||
|
||||
<PropTable :data="propData" />
|
||||
</template>
|
||||
|
||||
<template #code>
|
||||
<CodeExample :code-object="glassSurface" />
|
||||
</template>
|
||||
|
||||
<template #cli>
|
||||
<CliInstallation :command="glassSurface.cli" />
|
||||
</template>
|
||||
</TabbedLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
||||
import PropTable from '../../components/common/PropTable.vue';
|
||||
import CliInstallation from '../../components/code/CliInstallation.vue';
|
||||
import CodeExample from '../../components/code/CodeExample.vue';
|
||||
import Customize from '../../components/common/Customize.vue';
|
||||
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
||||
import GlassSurface from '../../content/Components/GlassSurface/GlassSurface.vue';
|
||||
import { glassSurface } from '@/constants/code/Components/glassSurfaceCode';
|
||||
import { useForceRerender } from '@/composables/useForceRerender';
|
||||
|
||||
const { rerenderKey: key, forceRerender } = useForceRerender();
|
||||
|
||||
const borderRadius = ref(50);
|
||||
const backgroundOpacity = ref(0.1);
|
||||
const saturation = ref(1);
|
||||
const borderWidth = ref(0.07);
|
||||
const brightness = ref(50);
|
||||
const opacity = ref(0.93);
|
||||
const blur = ref(11);
|
||||
const displace = ref(0.5);
|
||||
const distortionScale = ref(-180);
|
||||
const redOffset = ref(0);
|
||||
const greenOffset = ref(10);
|
||||
const blueOffset = ref(20);
|
||||
|
||||
watch(
|
||||
() => [
|
||||
borderWidth.value,
|
||||
brightness.value,
|
||||
opacity.value,
|
||||
blur.value,
|
||||
displace.value,
|
||||
distortionScale.value,
|
||||
redOffset.value,
|
||||
greenOffset.value,
|
||||
blueOffset.value
|
||||
],
|
||||
() => {
|
||||
forceRerender();
|
||||
}
|
||||
);
|
||||
|
||||
const imageBlocks = [
|
||||
{
|
||||
src: 'https://images.unsplash.com/photo-1500673587002-1d2548cfba1b?q=80&w=1740&auto=format&fit=crop',
|
||||
text: 'The Summer Of Glass'
|
||||
},
|
||||
{
|
||||
src: 'https://images.unsplash.com/photo-1594576547505-1be67997401e?q=80&w=1932&auto=format&fit=crop',
|
||||
text: 'Can Hold Any Content'
|
||||
},
|
||||
{
|
||||
src: 'https://images.unsplash.com/photo-1543127172-4b33cb699e35?q=80&w=1674&auto=format&fit=crop',
|
||||
text: 'Has Built-In Fallback'
|
||||
}
|
||||
];
|
||||
|
||||
const propData = [
|
||||
{
|
||||
name: 'width',
|
||||
type: 'number | string',
|
||||
default: '200',
|
||||
description: "Width of the glass surface (pixels or CSS value like '100%')"
|
||||
},
|
||||
{
|
||||
name: 'height',
|
||||
type: 'number | string',
|
||||
default: '80',
|
||||
description: "Height of the glass surface (pixels or CSS value like '100vh')"
|
||||
},
|
||||
{
|
||||
name: 'borderRadius',
|
||||
type: 'number',
|
||||
default: '20',
|
||||
description: 'Border radius in pixels'
|
||||
},
|
||||
{
|
||||
name: 'borderWidth',
|
||||
type: 'number',
|
||||
default: '0.07',
|
||||
description: 'Border width factor for displacement map'
|
||||
},
|
||||
{
|
||||
name: 'brightness',
|
||||
type: 'number',
|
||||
default: '50',
|
||||
description: 'Brightness percentage for displacement map'
|
||||
},
|
||||
{
|
||||
name: 'opacity',
|
||||
type: 'number',
|
||||
default: '0.93',
|
||||
description: 'Opacity of displacement map elements'
|
||||
},
|
||||
{
|
||||
name: 'blur',
|
||||
type: 'number',
|
||||
default: '11',
|
||||
description: 'Input blur amount in pixels'
|
||||
},
|
||||
{
|
||||
name: 'displace',
|
||||
type: 'number',
|
||||
default: '0',
|
||||
description: 'Output blur (stdDeviation)'
|
||||
},
|
||||
{
|
||||
name: 'backgroundOpacity',
|
||||
type: 'number',
|
||||
default: '0',
|
||||
description: 'Background frost opacity (0-1)'
|
||||
},
|
||||
{
|
||||
name: 'saturation',
|
||||
type: 'number',
|
||||
default: '1',
|
||||
description: 'Backdrop filter saturation factor'
|
||||
},
|
||||
{
|
||||
name: 'distortionScale',
|
||||
type: 'number',
|
||||
default: '-180',
|
||||
description: 'Main displacement scale'
|
||||
},
|
||||
{
|
||||
name: 'redOffset',
|
||||
type: 'number',
|
||||
default: '0',
|
||||
description: 'Red channel extra displacement offset'
|
||||
},
|
||||
{
|
||||
name: 'greenOffset',
|
||||
type: 'number',
|
||||
default: '10',
|
||||
description: 'Green channel extra displacement offset'
|
||||
},
|
||||
{
|
||||
name: 'blueOffset',
|
||||
type: 'number',
|
||||
default: '20',
|
||||
description: 'Blue channel extra displacement offset'
|
||||
},
|
||||
{
|
||||
name: 'xChannel',
|
||||
type: "'R' | 'G' | 'B'",
|
||||
default: "'R'",
|
||||
description: 'X displacement channel selector'
|
||||
},
|
||||
{
|
||||
name: 'yChannel',
|
||||
type: "'R' | 'G' | 'B'",
|
||||
default: "'G'",
|
||||
description: 'Y displacement channel selector'
|
||||
},
|
||||
{
|
||||
name: 'mixBlendMode',
|
||||
type: 'BlendMode',
|
||||
default: "'difference'",
|
||||
description: 'Mix blend mode for displacement map'
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
default: "''",
|
||||
description: 'Additional CSS class names'
|
||||
},
|
||||
{
|
||||
name: 'style',
|
||||
type: 'CSSProperties',
|
||||
default: '{}',
|
||||
description: 'Inline styles object'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
padding: 0;
|
||||
}
|
||||
.demo-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user