Merge branch 'main' into feat/scroll-reveal

This commit is contained in:
David
2025-07-12 12:04:10 +03:00
committed by GitHub
212 changed files with 10748 additions and 8736 deletions

View File

@@ -1,27 +1,24 @@
<template>
<div>
<DisplayHeader
v-if="!isCategoryPage"
:activeItem="activeItem"
/>
<DisplayHeader v-if="!isCategoryPage" :activeItem="activeItem" />
<router-view />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import DisplayHeader from '@/components/landing/DisplayHeader/DisplayHeader.vue'
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import DisplayHeader from '@/components/landing/DisplayHeader/DisplayHeader.vue';
const route = useRoute()
const route = useRoute();
const activeItem = computed(() => {
if (route.path === '/') return 'home'
return null
})
if (route.path === '/') return 'home';
return null;
});
const isCategoryPage = computed(() => {
return /^\/[^/]+\/[^/]+$/.test(route.path)
})
return /^\/[^/]+\/[^/]+$/.test(route.path);
});
</script>

View File

@@ -2,12 +2,21 @@
<div class="cli-installation">
<h2 class="demo-title">One-Time Installation</h2>
<VCodeBlock v-if="command" :code="command" :persistent-copy-button="true" highlightjs lang="bash" theme="nord"
:copy-button="true" class="code-block" />
<VCodeBlock
v-if="command"
:code="command"
:persistent-copy-button="true"
highlightjs
lang="bash"
theme="nord"
:copy-button="true"
class="code-block"
/>
<div class="cli-divider"></div>
<h2 class="demo-title">Full CLI Setup</h2>
<p class="jsrepo-info">
Vue Bits uses
<a href="https://jsrepo.dev/" target="_blank" rel="noreferrer">jsrepo</a>
@@ -17,23 +26,57 @@
<Accordion expandIcon="pi pi-chevron-right" collapseIcon="pi pi-chevron-down">
<AccordionPanel value="setup">
<AccordionHeader>Setup Steps</AccordionHeader>
<AccordionContent
:pt="{ transition: { enterFromClass: '', enterActiveClass: '', enterToClass: '', leaveFromClass: '', leaveActiveClass: '', leaveToClass: '' } }">
:pt="{
transition: {
enterFromClass: '',
enterActiveClass: '',
enterToClass: '',
leaveFromClass: '',
leaveActiveClass: '',
leaveToClass: ''
}
}"
>
<div class="setup-content">
<p class="demo-extra-info">1. Initialize a config file for your project</p>
<div class="setup-option">
<VCodeBlock :persistent-copy-button="true" code="npx jsrepo init https://vue-bits.dev/ui" highlightjs
lang="bash" theme="nord" :copy-button="true" class="code-block" />
<VCodeBlock
:persistent-copy-button="true"
code="npx jsrepo init https://vue-bits.dev/ui"
highlightjs
lang="bash"
theme="nord"
:copy-button="true"
class="code-block"
/>
</div>
<p class="demo-extra-info">2. Browse &amp; add components from the list</p>
<VCodeBlock :persistent-copy-button="true" code="npx jsrepo add" highlightjs lang="bash" theme="nord"
:copy-button="true" class="code-block" />
<VCodeBlock
:persistent-copy-button="true"
code="npx jsrepo add"
highlightjs
lang="bash"
theme="nord"
:copy-button="true"
class="code-block"
/>
<p class="demo-extra-info">3. Or just add a specific component</p>
<VCodeBlock :persistent-copy-button="true" code="npx jsrepo add Animations/AnimatedContainer" highlightjs
lang="bash" theme="nord" :copy-button="true" class="code-block" />
<VCodeBlock
:persistent-copy-button="true"
code="npx jsrepo add Animations/AnimatedContainer"
highlightjs
lang="bash"
theme="nord"
:copy-button="true"
class="code-block"
/>
</div>
</AccordionContent>
</AccordionPanel>
@@ -42,15 +85,15 @@
</template>
<script setup lang="ts">
import { VCodeBlock } from '@wdns/vue-code-block'
import Accordion from 'primevue/accordion'
import AccordionPanel from 'primevue/accordionpanel'
import AccordionHeader from 'primevue/accordionheader'
import AccordionContent from 'primevue/accordioncontent'
import { VCodeBlock } from '@wdns/vue-code-block';
import Accordion from 'primevue/accordion';
import AccordionPanel from 'primevue/accordionpanel';
import AccordionHeader from 'primevue/accordionheader';
import AccordionContent from 'primevue/accordioncontent';
const { command } = defineProps<{
command?: string
}>()
command?: string;
}>();
</script>
<style scoped>
@@ -74,7 +117,7 @@ const { command } = defineProps<{
}
.jsrepo-info a {
color: #27FF64;
color: #27ff64;
text-decoration: underline;
}
@@ -166,4 +209,4 @@ const { command } = defineProps<{
:deep(.v-code-block--me-1) {
margin-right: 0 !important;
}
</style>
</style>

View File

@@ -4,14 +4,25 @@
<h2 class="demo-title">{{ getDisplayName(name) }}</h2>
<div v-if="snippet" class="code-container">
<div class="code-wrapper" :class="{ 'collapsed': shouldCollapse(snippet) && !isExpanded(name) }">
<VCodeBlock :code="snippet" highlightjs :lang="getLanguage(name)" theme="nord" :copy-button="true"
:persistent-copy-button="true" class="code-block" />
<div class="code-wrapper" :class="{ collapsed: shouldCollapse(snippet) && !isExpanded(name) }">
<VCodeBlock
:code="snippet"
highlightjs
:lang="getLanguage(name)"
theme="nord"
:copy-button="true"
:persistent-copy-button="true"
class="code-block"
/>
<div v-if="shouldCollapse(snippet) && !isExpanded(name)" class="fade-overlay" />
<button v-if="shouldCollapse(snippet)" class="expand-button" :class="{ 'expanded': isExpanded(name) }"
@click="toggleExpanded(name)">
<button
v-if="shouldCollapse(snippet)"
class="expand-button"
:class="{ expanded: isExpanded(name) }"
@click="toggleExpanded(name)"
>
{{ isExpanded(name) ? 'Collapse Snippet' : 'See Full Snippet' }}
</button>
</div>
@@ -19,6 +30,7 @@
<div v-if="!snippet" class="no-code">
<span>Nothing here yet!</span>
<i class="pi pi-face-sad"></i>
</div>
</div>
@@ -26,58 +38,55 @@
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { VCodeBlock } from '@wdns/vue-code-block'
import type { CodeObject } from '../../types/code'
import { computed, ref } from 'vue';
import { VCodeBlock } from '@wdns/vue-code-block';
import type { CodeObject } from '../../types/code';
const props = defineProps<{
codeObject: CodeObject
}>()
codeObject: CodeObject;
}>();
const skipKeys = [
'cli'
]
const skipKeys = ['cli'];
const expandedSections = ref<Set<string>>(new Set())
const expandedSections = ref<Set<string>>(new Set());
const codeEntries = computed(() => {
return Object.entries(props.codeObject).filter(([name]) => !skipKeys.includes(name))
})
return Object.entries(props.codeObject).filter(([name]) => !skipKeys.includes(name));
});
const shouldCollapse = (snippet: string) => {
const codeLines = snippet?.split('\n').length || 0
return codeLines > 35
}
const codeLines = snippet?.split('\n').length || 0;
return codeLines > 35;
};
const isExpanded = (name: string) => {
return expandedSections.value.has(name)
}
return expandedSections.value.has(name);
};
const toggleExpanded = (name: string) => {
if (expandedSections.value.has(name)) {
expandedSections.value.delete(name)
expandedSections.value.delete(name);
} else {
expandedSections.value.add(name)
expandedSections.value.add(name);
}
}
};
const getDisplayName = (name: string) => {
if (name === 'code') return 'Code'
if (name === 'cli') return 'CLI Command'
if (name === 'utility') return 'Utility'
if (name === 'usage') return 'Usage'
if (name === 'installation') return 'Installation'
return name.charAt(0).toUpperCase() + name.slice(1)
}
if (name === 'code') return 'Code';
if (name === 'cli') return 'CLI Command';
if (name === 'utility') return 'Utility';
if (name === 'usage') return 'Usage';
if (name === 'installation') return 'Installation';
return name.charAt(0).toUpperCase() + name.slice(1);
};
const getLanguage = (name: string) => {
if (name === 'cli') return 'bash'
if (name === 'code') return 'html'
if (name === 'usage') return 'html'
if (name === 'installation') return 'bash'
return 'javascript'
}
if (name === 'cli') return 'bash';
if (name === 'code') return 'html';
if (name === 'usage') return 'html';
if (name === 'installation') return 'bash';
return 'javascript';
};
</script>
<style scoped>

View File

@@ -1,18 +1,17 @@
<template>
<div class="dependencies-container">
<h2 class="demo-title">Dependencies</h2>
<div class="demo-details">
<span v-for="dep in dependencyList" :key="dep" class="dependency-tag">
{{ dep }}
</span>
<span v-for="dep in dependencyList" :key="dep" class="dependency-tag">{{ dep }}</span>
</div>
</div>
</template>
<script setup lang="ts">
interface Props {
dependencyList: string[]
dependencyList: string[];
}
defineProps<Props>()
defineProps<Props>();
</script>

View File

@@ -1,29 +1,33 @@
<template>
<Button :style="{
fontWeight: 500,
borderRadius: '0.75rem',
border: '1px solid #142216',
padding: '1rem',
position: 'fixed',
right: '2.3em',
zIndex: 98,
boxShadow: '10px 0 25px rgba(0, 0, 0, 0.2)',
transition: '0.3s ease',
opacity: visible ? 1 : 0,
bottom: visible ? '2.5em' : '1em',
cursor: visible ? 'pointer' : 'default'
}" class="back-to-top" @click="visible && scrollToTop()">
<i class="pi pi-arrow-up" style="color: #fff; font-size: 1rem;"></i>
<Button
:style="{
fontWeight: 500,
borderRadius: '0.75rem',
border: '1px solid #142216',
padding: '1rem',
position: 'fixed',
right: '2.3em',
zIndex: 98,
boxShadow: '10px 0 25px rgba(0, 0, 0, 0.2)',
transition: '0.3s ease',
opacity: visible ? 1 : 0,
bottom: visible ? '2.5em' : '1em',
cursor: visible ? 'pointer' : 'default'
}"
class="back-to-top"
@click="visible && scrollToTop()"
>
<i class="pi pi-arrow-up" style="color: #fff; font-size: 1rem"></i>
</Button>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useToast } from 'primevue/usetoast'
import Button from 'primevue/button'
import { ref, onMounted, onUnmounted } from 'vue';
import { useToast } from 'primevue/usetoast';
import Button from 'primevue/button';
const toast = useToast()
const visible = ref(false)
const toast = useToast();
const visible = ref(false);
const messages = [
'🐴 Country roads, take me home!',
@@ -33,34 +37,33 @@ const messages = [
'🐉 Fus Ro Dah!',
'🍄 The princess is in another castle!',
'🦸‍♂️ Avengers, assemble!',
'🗡️ It\'s dangerous to go alone! Take this.',
"🗡️ It's dangerous to go alone! Take this.",
'📜 A wizard is never late.',
'💍 Foul Tarnished, in search of the Elden Ring!',
'🐊 See you later, alligator.',
'🔥 Dracarys!'
]
];
const getRandomMessage = (messages: string[]) =>
messages[Math.floor(Math.random() * messages.length)]
const getRandomMessage = (messages: string[]) => messages[Math.floor(Math.random() * messages.length)];
const scrollToTop = () => {
window.scrollTo(0, 0)
window.scrollTo(0, 0);
toast.add({
severity: 'secondary',
summary: getRandomMessage(messages),
life: 3000
})
}
});
};
const onScroll = () => {
visible.value = window.scrollY > 500
}
visible.value = window.scrollY > 500;
};
onMounted(() => {
window.addEventListener('scroll', onScroll)
})
window.addEventListener('scroll', onScroll);
});
onUnmounted(() => {
window.removeEventListener('scroll', onScroll)
})
</script>
window.removeEventListener('scroll', onScroll);
});
</script>

View File

@@ -1,24 +1,19 @@
<template>
<div class="contribute-container">
<h2 class="demo-title-contribute">Help improve this component!</h2>
<div class="contribute-buttons">
<a
:href="bugReportUrl"
target="_blank"
rel="noreferrer"
class="contribute-button"
>
<a :href="bugReportUrl" target="_blank" rel="noreferrer" class="contribute-button">
<i class="pi pi-exclamation-triangle"></i>
<span>Report an issue</span>
</a>
<span class="contribute-separator">or</span>
<a
:href="featureRequestUrl"
target="_blank"
rel="noreferrer"
class="contribute-button"
>
<a :href="featureRequestUrl" target="_blank" rel="noreferrer" class="contribute-button">
<i class="pi pi-lightbulb"></i>
<span>Request a feature</span>
</a>
</div>
@@ -26,22 +21,22 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { computed } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute()
const route = useRoute();
const bugReportUrl = computed(() => {
const category = route.params.category
const subcategory = route.params.subcategory
const title = encodeURIComponent(`[BUG]: ${category}/${subcategory}`)
return `https://github.com/DavidHDev/vue-bits/issues/new?template=1-bug-report.yml&title=${title}&labels=bug`
})
const category = route.params.category;
const subcategory = route.params.subcategory;
const title = encodeURIComponent(`[BUG]: ${category}/${subcategory}`);
return `https://github.com/DavidHDev/vue-bits/issues/new?template=1-bug-report.yml&title=${title}&labels=bug`;
});
const featureRequestUrl = computed(() => {
const category = route.params.category
const subcategory = route.params.subcategory
const title = encodeURIComponent(`[FEAT]: ${category}/${subcategory}`)
return `https://github.com/DavidHDev/vue-bits/issues/new?template=2-feature-request.yml&title=${title}&labels=enhancement`
})
</script>
const category = route.params.category;
const subcategory = route.params.subcategory;
const title = encodeURIComponent(`[FEAT]: ${category}/${subcategory}`);
return `https://github.com/DavidHDev/vue-bits/issues/new?template=2-feature-request.yml&title=${title}&labels=enhancement`;
});
</script>

View File

@@ -1,6 +1,7 @@
<template>
<div>
<h2 class="demo-title">Customize</h2>
<slot />
</div>
</template>
</template>

View File

@@ -1,28 +1,45 @@
<template>
<svg width="141" height="30" viewBox="0 0 193 41" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M66.4663 34.2676L56.3843 7.12372H60.5722L68.7929 30.2348L77.0912 7.12372H81.2015L71.1195 34.2676H66.4663Z"
fill="white" />
<path
d="M66.4663 34.2676L56.3843 7.12372H60.5722L68.7929 30.2348L77.0912 7.12372H81.2015L71.1195 34.2676H66.4663Z"
fill="white"
/>
<path
d="M88.3915 34.7329C86.8662 34.7329 85.5349 34.4227 84.3974 33.8023C83.2858 33.1818 82.4198 32.2512 81.7994 31.0103C81.2048 29.7695 80.9075 28.2055 80.9075 26.3183V14.724H84.7852V25.8918C84.7852 27.7272 85.1859 29.1103 85.9873 30.0409C86.7887 30.9716 87.9391 31.4369 89.4384 31.4369C90.4466 31.4369 91.3514 31.1913 92.1528 30.7001C92.9801 30.2089 93.6264 29.498 94.0917 28.5674C94.557 27.6367 94.7897 26.4993 94.7897 25.155V14.724H98.6674V34.2676H95.2162L94.9448 30.9328C94.3502 32.1219 93.4842 33.0526 92.3467 33.7247C91.2092 34.3969 89.8908 34.7329 88.3915 34.7329Z"
fill="white" />
fill="white"
/>
<path
d="M110.648 34.7329C108.787 34.7329 107.133 34.3064 105.685 33.4533C104.237 32.6002 103.1 31.411 102.273 29.8858C101.471 28.3606 101.071 26.5898 101.071 24.5734C101.071 22.5053 101.471 20.7086 102.273 19.1834C103.1 17.6323 104.237 16.4302 105.685 15.5771C107.133 14.6982 108.813 14.2587 110.726 14.2587C112.639 14.2587 114.281 14.6852 115.651 15.5383C117.021 16.3914 118.081 17.5289 118.83 18.9507C119.58 20.3467 119.955 21.8977 119.955 23.6039C119.955 23.8624 119.942 24.1468 119.916 24.457C119.916 24.7414 119.903 25.0645 119.877 25.4265H103.901V22.6733H116.077C116 21.0447 115.457 19.7779 114.449 18.8731C113.44 17.9425 112.187 17.4772 110.687 17.4772C109.627 17.4772 108.658 17.7228 107.779 18.2139C106.9 18.6793 106.189 19.3772 105.646 20.3079C105.129 21.2127 104.871 22.3631 104.871 23.759V24.8448C104.871 26.2925 105.129 27.5204 105.646 28.5286C106.189 29.511 106.9 30.2606 107.779 30.7777C108.658 31.2689 109.614 31.5144 110.648 31.5144C111.889 31.5144 112.91 31.243 113.712 30.7001C114.513 30.1572 115.108 29.4205 115.496 28.4898H119.373C119.037 29.679 118.468 30.7518 117.667 31.7083C116.866 32.639 115.87 33.3757 114.681 33.9186C113.518 34.4615 112.174 34.7329 110.648 34.7329Z"
fill="white" />
fill="white"
/>
<path
d="M130.615 34.2676V7.12372H140.658C142.545 7.12372 144.122 7.43394 145.389 8.05437C146.656 8.64895 147.599 9.47619 148.22 10.5361C148.866 11.5701 149.189 12.7464 149.189 14.0648C149.189 15.4349 148.892 16.5853 148.297 17.5159C147.703 18.4466 146.914 19.1704 145.932 19.6875C144.975 20.1786 143.941 20.463 142.83 20.5406L143.373 20.1528C144.562 20.1786 145.648 20.5018 146.63 21.1222C147.612 21.7168 148.388 22.5182 148.956 23.5264C149.525 24.5346 149.81 25.6462 149.81 26.8612C149.81 28.2572 149.474 29.5239 148.801 30.6613C148.129 31.773 147.134 32.6519 145.816 33.2982C144.497 33.9445 142.881 34.2676 140.968 34.2676H130.615ZM134.493 31.0491H140.464C142.171 31.0491 143.489 30.6613 144.42 29.8858C145.376 29.0844 145.854 27.9599 145.854 26.5122C145.854 25.0904 145.363 23.9529 144.381 23.0998C143.424 22.2467 142.093 21.8202 140.387 21.8202H134.493V31.0491ZM134.493 18.8344H140.232C141.86 18.8344 143.101 18.4595 143.954 17.7098C144.807 16.9343 145.234 15.8744 145.234 14.5301C145.234 13.2376 144.807 12.2164 143.954 11.4667C143.101 10.6912 141.822 10.3034 140.115 10.3034H134.493V18.8344Z"
fill="white" />
fill="white"
/>
<path
d="M153.736 34.2676V14.724H157.614V34.2676H153.736ZM155.714 11.0402C154.964 11.0402 154.344 10.8075 153.852 10.3422C153.387 9.87689 153.154 9.2823 153.154 8.55847C153.154 7.86048 153.387 7.29175 153.852 6.85228C154.344 6.38696 154.964 6.1543 155.714 6.1543C156.438 6.1543 157.045 6.38696 157.536 6.85228C158.027 7.29175 158.273 7.86048 158.273 8.55847C158.273 9.2823 158.027 9.87689 157.536 10.3422C157.045 10.8075 156.438 11.0402 155.714 11.0402Z"
fill="white" />
fill="white"
/>
<path
d="M170.449 34.2676C169.208 34.2676 168.135 34.0737 167.23 33.6859C166.326 33.2982 165.628 32.6519 165.136 31.7471C164.645 30.8423 164.4 29.6144 164.4 28.0633V18.02H161.026V14.724H164.4L164.865 9.83811H168.277V14.724H173.823V18.02H168.277V28.1021C168.277 29.2137 168.51 29.9763 168.975 30.3899C169.441 30.7777 170.242 30.9716 171.38 30.9716H173.629V34.2676H170.449Z"
fill="white" />
fill="white"
/>
<path
d="M184.885 34.7329C183.23 34.7329 181.782 34.4615 180.542 33.9186C179.301 33.3757 178.318 32.6131 177.595 31.6308C176.871 30.6484 176.431 29.498 176.276 28.1796H180.231C180.361 28.8 180.606 29.3688 180.968 29.8858C181.356 30.4028 181.873 30.8165 182.519 31.1267C183.191 31.4369 183.98 31.592 184.885 31.592C185.738 31.592 186.436 31.4757 186.979 31.243C187.547 30.9845 187.961 30.6484 188.219 30.2348C188.478 29.7953 188.607 29.33 188.607 28.8388C188.607 28.115 188.426 27.5721 188.064 27.2102C187.728 26.8224 187.211 26.5251 186.513 26.3183C185.841 26.0857 185.027 25.8789 184.07 25.6979C183.165 25.5428 182.287 25.336 181.433 25.0775C180.606 24.7931 179.856 24.4441 179.184 24.0305C178.538 23.6169 178.021 23.0998 177.633 22.4794C177.246 21.8331 177.052 21.0447 177.052 20.114C177.052 19.0024 177.349 18.0071 177.943 17.1282C178.538 16.2234 179.378 15.5254 180.464 15.0342C181.576 14.5172 182.881 14.2587 184.38 14.2587C186.552 14.2587 188.297 14.7757 189.615 15.8098C190.934 16.8438 191.709 18.3044 191.942 20.1916H188.181C188.077 19.3126 187.689 18.6405 187.017 18.1752C186.345 17.684 185.453 17.4384 184.342 17.4384C183.23 17.4384 182.377 17.6581 181.782 18.0976C181.188 18.5371 180.891 19.1187 180.891 19.8426C180.891 20.3079 181.059 20.7215 181.395 21.0834C181.731 21.4453 182.222 21.7556 182.868 22.0141C183.54 22.2467 184.355 22.4665 185.311 22.6733C186.681 22.9318 187.909 23.2549 188.995 23.6427C190.081 24.0305 190.947 24.5992 191.593 25.3489C192.239 26.0986 192.562 27.1714 192.562 28.5674C192.588 29.7824 192.278 30.8552 191.632 31.7859C191.011 32.7165 190.119 33.4404 188.956 33.9574C187.819 34.4744 186.462 34.7329 184.885 34.7329Z"
fill="white" />
fill="white"
/>
<path
d="M0.345215 0.450195H15.6363C15.6363 0.450195 22.0583 12.3486 26.6885 19.6614C26.8836 19.9694 26.9797 20.151 27.1894 20.4493C27.3852 20.7278 27.4954 20.8853 27.7214 21.1399C27.9339 21.3795 27.8963 21.356 28.1397 21.6249C28.2726 21.7719 28.3875 21.8829 28.6738 22.068C28.933 22.2355 29.377 22.3481 29.7138 22.2345C29.9746 22.1465 30.0451 22.1136 30.2668 21.9894C31.043 21.5543 31.8402 19.9698 31.8402 19.9698L40.7527 3.71523H32.9372V0.450859L46.3206 0.450195L35.594 19.0675C35.594 19.0675 34.5027 20.8176 33.7209 22.1763C33.5935 22.3977 33.0493 23.1986 32.8374 23.4686C32.4369 23.9791 32.2132 24.2841 31.8402 24.6523C31.4828 25.005 31.1302 25.1777 30.8799 25.2783C30.4928 25.4337 30.0788 25.5121 29.663 25.546C29.3087 25.5749 29.0702 25.5934 28.7179 25.546C28.2786 25.4868 28.2472 25.4717 27.7214 25.2783C27.5003 25.197 27.131 24.9776 26.783 24.7266C26.3964 24.4478 26.1328 24.1992 25.6981 23.7395C25.3155 23.3347 25.1467 23.0947 24.8373 22.6316C24.5125 22.1455 24.2598 21.8766 23.953 21.379C19.7839 14.6168 13.8639 3.71523 13.8639 3.71523H5.99112L23.3635 33.9098L26.6885 28.1777C26.6885 28.1777 27.1444 28.7994 28.7438 28.7475C30.2137 28.6998 30.5672 28.1777 30.5672 28.1777L23.3635 40.437L3.53103 6.05657L2.19899 3.71523L0.345215 0.450195Z"
fill="white" />
fill="white"
/>
<circle cx="29.3282" cy="10.6089" r="3.34281" fill="white" />
</svg>
</template>
@@ -30,5 +47,5 @@
<script setup lang="ts">
defineOptions({
name: 'VueBitsLogo'
})
});
</script>

View File

@@ -1,25 +1,26 @@
<template>
<div class="preview-color">
<span class="color-label">{{ title }}</span>
<input :value="modelValue" @input="handleColorChange" type="color" :disabled="disabled" class="color-input" />
</div>
</template>
<script setup lang="ts">
defineProps<{
title: string
modelValue: string
disabled?: boolean
}>()
title: string;
modelValue: string;
disabled?: boolean;
}>();
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
'update:modelValue': [value: string];
}>();
const handleColorChange = (event: Event) => {
const target = event.target as HTMLInputElement
emit('update:modelValue', target.value)
}
const target = event.target as HTMLInputElement;
emit('update:modelValue', target.value);
};
</script>
<style scoped>

View File

@@ -1,6 +1,7 @@
<template>
<div class="preview-select">
<span class="select-label">{{ title }}</span>
<Select
:model-value="modelValue"
@update:model-value="handleSelectChange"
@@ -14,45 +15,45 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import Select from 'primevue/select'
import { computed } from 'vue';
import Select from 'primevue/select';
interface Option {
label: string
value: string | number
label: string;
value: string | number;
}
const props = defineProps<{
title: string
modelValue: string | number
options: Option[] | string[] | number[]
optionLabel?: string
optionValue?: string
placeholder?: string
disabled?: boolean
}>()
title: string;
modelValue: string | number;
options: Option[] | string[] | number[];
optionLabel?: string;
optionValue?: string;
placeholder?: string;
disabled?: boolean;
}>();
const emit = defineEmits<{
'update:modelValue': [value: string | number]
}>()
'update:modelValue': [value: string | number];
}>();
const handleSelectChange = (value: string | number) => {
emit('update:modelValue', value)
}
emit('update:modelValue', value);
};
const isObjectArray = computed(() => {
return props.options.length > 0 && typeof props.options[0] === 'object'
})
return props.options.length > 0 && typeof props.options[0] === 'object';
});
const selectAttributes = computed(() => {
if (isObjectArray.value) {
return {
optionLabel: props.optionLabel || 'label',
optionValue: props.optionValue || 'value'
}
};
}
return {}
})
return {};
});
</script>
<style scoped>

View File

@@ -1,6 +1,7 @@
<template>
<div class="preview-slider">
<span class="slider-label">{{ title }}</span>
<Slider
:model-value="modelValue"
@update:model-value="handleSliderChange"
@@ -10,31 +11,32 @@
:disabled="disabled"
class="custom-slider"
/>
<span class="slider-value">{{ modelValue }}{{ valueUnit }}</span>
</div>
</template>
<script setup lang="ts">
import Slider from 'primevue/slider'
import Slider from 'primevue/slider';
defineProps<{
title: string
modelValue: number
min?: number
max?: number
step?: number
valueUnit?: string
disabled?: boolean
}>()
title: string;
modelValue: number;
min?: number;
max?: number;
step?: number;
valueUnit?: string;
disabled?: boolean;
}>();
const emit = defineEmits<{
'update:modelValue': [value: number]
}>()
'update:modelValue': [value: number];
}>();
const handleSliderChange = (value: number | number[]) => {
const numValue = Array.isArray(value) ? value[0] : value
emit('update:modelValue', numValue)
}
const numValue = Array.isArray(value) ? value[0] : value;
emit('update:modelValue', numValue);
};
</script>
<style scoped>

View File

@@ -1,6 +1,7 @@
<template>
<div class="preview-switch">
<span class="switch-label">{{ title }}</span>
<ToggleSwitch
:model-value="modelValue"
@update:model-value="$emit('update:modelValue', $event)"
@@ -10,17 +11,17 @@
</template>
<script setup lang="ts">
import ToggleSwitch from 'primevue/toggleswitch'
import ToggleSwitch from 'primevue/toggleswitch';
defineProps<{
title: string
modelValue: boolean
disabled?: boolean
}>()
title: string;
modelValue: boolean;
disabled?: boolean;
}>();
defineEmits<{
'update:modelValue': [value: boolean]
}>()
'update:modelValue': [value: boolean];
}>();
</script>
<style scoped>

View File

@@ -1,6 +1,7 @@
<template>
<div class="prop-table-container">
<h2 class="demo-title">Props</h2>
<div class="table-wrapper">
<DataTable :value="data" class="props-table">
<Column field="name" header="Property">
@@ -8,16 +9,19 @@
<div class="code-cell">{{ data.name }}</div>
</template>
</Column>
<Column field="type" header="Type">
<template #body="{ data }">
<span class="type-text">{{ data.type }}</span>
</template>
</Column>
<Column field="default" header="Default">
<template #body="{ data }">
<div class="code-cell">{{ data.default || '—' }}</div>
</template>
</Column>
<Column field="description" header="Description">
<template #body="{ data }">
<div class="description-text">{{ data.description }}</div>
@@ -29,19 +33,19 @@
</template>
<script setup lang="ts">
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
interface PropData {
name: string
type: string
default: string
description: string
name: string;
type: string;
default: string;
description: string;
}
defineProps<{
data: PropData[]
}>()
data: PropData[];
}>();
</script>
<style scoped>

View File

@@ -5,11 +5,11 @@
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Button from 'primevue/button';
defineEmits<{
refresh: []
}>()
refresh: [];
}>();
</script>
<style scoped>

View File

@@ -5,24 +5,31 @@
<Tab value="0">
<div class="tab-header">
<i class="pi pi-eye"></i>
<span>Preview</span>
</div>
</Tab>
<Tab value="1">
<div class="tab-header">
<i class="pi pi-code"></i>
<span>Code</span>
</div>
</Tab>
<Tab value="2">
<div class="tab-header">
<i class="pi pi-box"></i>
<span>CLI</span>
</div>
</Tab>
<Tab value="3">
<div class="tab-header">
<i class="pi pi-heart"></i>
<span>Contribute</span>
</div>
</Tab>
@@ -32,12 +39,15 @@
<TabPanel value="0">
<slot name="preview" />
</TabPanel>
<TabPanel value="1">
<slot name="code" />
</TabPanel>
<TabPanel value="2">
<slot name="cli" />
</TabPanel>
<TabPanel value="3">
<ContributionSection />
</TabPanel>
@@ -47,12 +57,12 @@
</template>
<script setup lang="ts">
import Tabs from 'primevue/tabs'
import TabList from 'primevue/tablist'
import Tab from 'primevue/tab'
import TabPanels from 'primevue/tabpanels'
import TabPanel from 'primevue/tabpanel'
import ContributionSection from './ContributionSection.vue'
import Tabs from 'primevue/tabs';
import TabList from 'primevue/tablist';
import Tab from 'primevue/tab';
import TabPanels from 'primevue/tabpanels';
import TabPanel from 'primevue/tabpanel';
import ContributionSection from './ContributionSection.vue';
</script>
<style scoped>
@@ -121,8 +131,8 @@ import ContributionSection from './ContributionSection.vue'
:deep(.p-tab-indicator),
:deep(.p-tab::before),
:deep(.p-tab::after),
:deep(.p-tab[aria-selected="true"]::before),
:deep(.p-tab[aria-selected="true"]::after),
:deep(.p-tab[aria-selected='true']::before),
:deep(.p-tab[aria-selected='true']::after),
:deep(.p-tablist::after),
:deep(.p-tablist-tab-list::before),
:deep(.p-tablist-tab-list::after),
@@ -131,14 +141,14 @@ import ContributionSection from './ContributionSection.vue'
display: none !important;
}
:deep(.p-tab[aria-selected="true"]) {
:deep(.p-tab[aria-selected='true']) {
background: transparent !important;
border-bottom: none !important;
}
:deep(.p-tab[aria-selected="true"] .tab-header) {
:deep(.p-tab[aria-selected='true'] .tab-header) {
background: #333333;
color: #A7EF9E;
color: #a7ef9e;
}
:deep(.p-tabpanels) {

View File

@@ -77,7 +77,9 @@
color: inherit;
font-weight: 400;
opacity: 0.6;
transition: opacity 0.3s ease, transform 0.2s ease;
transition:
opacity 0.3s ease,
transform 0.2s ease;
}
.nav-link:hover {
@@ -106,9 +108,7 @@
font-weight: 500;
padding: 0 0 0 1.4rem;
height: calc(60px - 2px);
background: linear-gradient(135deg,
rgb(30, 160, 63),
rgba(24, 47, 255, 0.6));
background: linear-gradient(135deg, rgb(30, 160, 63), rgba(24, 47, 255, 0.6));
background-size: 200% 200%;
backdrop-filter: blur(25px);
-webkit-backdrop-filter: blur(25px);
@@ -121,14 +121,14 @@
align-items: center;
white-space: nowrap;
justify-content: space-between;
transition: .3s ease;
transition: 0.3s ease;
}
.cta-button span {
background-color: #0b0b0b;
margin-left: 1em;
margin-right: calc(1em - 8px);
padding-top: .1em;
padding-top: 0.1em;
height: 45px;
border-radius: 50px;
width: 100px;
@@ -143,16 +143,16 @@
margin-right: 6px;
width: 16px;
height: 16px;
transition: .3s ease;
transition: 0.3s ease;
}
.cta-button:hover {
transition: .3s ease;
transition: 0.3s ease;
}
.cta-button:hover span img {
transform: scale(1.2);
transition: .3s ease;
transition: 0.3s ease;
}
@media (max-width: 900px) {
@@ -307,4 +307,4 @@
.nav-cta-group {
gap: 2rem;
}
}
}

View File

@@ -7,22 +7,12 @@
<div class="nav-cta-group">
<nav class="landing-nav-items" ref="navRef">
<router-link
class="nav-link"
:class="{ 'active-link': activeItem === 'home' }"
to="/"
>
Home
</router-link>
<router-link class="nav-link" to="/text-animations/split-text">
Docs
</router-link>
<router-link class="nav-link" :class="{ 'active-link': activeItem === 'home' }" to="/">Home</router-link>
<router-link class="nav-link" to="/text-animations/split-text">Docs</router-link>
</nav>
<button
class="cta-button"
@click="openGitHub"
>
<button class="cta-button" @click="openGitHub">
Star On GitHub
<span ref="starCountRef" :style="{ opacity: 0 }">
<img :src="starIcon" alt="Star Icon" />
@@ -35,43 +25,48 @@
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { gsap } from 'gsap'
import VueBitsLogo from '@/components/common/Logo.vue'
import { useStars } from '@/composables/useStars'
import starIcon from '@/assets/common/star.svg'
import './DisplayHeader.css'
import { ref, watch } from 'vue';
import { gsap } from 'gsap';
import VueBitsLogo from '@/components/common/Logo.vue';
import { useStars } from '@/composables/useStars';
import starIcon from '@/assets/common/star.svg';
import './DisplayHeader.css';
interface Props {
activeItem?: string | null;
}
defineProps<Props>()
defineProps<Props>();
const navRef = ref<HTMLElement | null>(null)
const starCountRef = ref<HTMLElement | null>(null)
const stars = useStars()
const navRef = ref<HTMLElement | null>(null);
const starCountRef = ref<HTMLElement | null>(null);
const stars = useStars();
const openGitHub = () => {
window.open('https://github.com/DavidHDev/vue-bits', '_blank')
}
window.open('https://github.com/DavidHDev/vue-bits', '_blank');
};
watch(stars, (newStars) => {
if (newStars && starCountRef.value) {
gsap.fromTo(starCountRef.value,
{
scale: 0,
width: 0,
opacity: 0
},
{
scale: 1,
width: "100px",
opacity: 1,
duration: 0.8,
ease: "back.out(1)"
}
)
}
}, { immediate: true })
watch(
stars,
newStars => {
if (newStars && starCountRef.value) {
gsap.fromTo(
starCountRef.value,
{
scale: 0,
width: 0,
opacity: 0
},
{
scale: 1,
width: '100px',
opacity: 1,
duration: 0.8,
ease: 'back.out(1)'
}
);
}
},
{ immediate: true }
);
</script>

View File

@@ -22,7 +22,7 @@
font-weight: 600;
letter-spacing: -2px;
color: #fff;
margin-bottom: .2rem;
margin-bottom: 0.2rem;
background: linear-gradient(135deg, #fff 0%, #60fa89 20%, #55f788 40%, #00ff62 60%, #55f799 80%, #fff 100%);
background-size: 200% 200%;
-webkit-background-clip: text;
@@ -44,7 +44,6 @@
}
@keyframes gradientShift {
0%,
100% {
background-position: 0% 50%;
@@ -120,8 +119,6 @@
grid-column: 2 / 3;
grid-row: 2 / 3;
}
}
@media (min-width: 50rem) {
@@ -145,8 +142,6 @@
grid-column: 1 / 3;
grid-row: 2 / 3;
}
}
@media (min-width: 768px) and (max-width: 49.99rem) {
@@ -169,8 +164,6 @@
grid-column: 1 / 2;
grid-row: 2 / 3;
}
}
.feature-card {
@@ -212,14 +205,20 @@
position: absolute;
inset: 0;
padding: 1px;
background: radial-gradient(200px circle at var(--glow-x) var(--glow-y),
rgba(132, 0, 255, calc(var(--glow-intensity) * 0.8)) 0%,
rgba(132, 0, 255, calc(var(--glow-intensity) * 0.4)) 30%,
transparent 60%);
background: radial-gradient(
200px circle at var(--glow-x) var(--glow-y),
rgba(132, 0, 255, calc(var(--glow-intensity) * 0.8)) 0%,
rgba(132, 0, 255, calc(var(--glow-intensity) * 0.4)) 30%,
transparent 60%
);
border-radius: inherit;
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: subtract;
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
pointer-events: none;
transition: opacity 0.3s ease;
@@ -230,8 +229,6 @@
background: #07160a;
}
.feature-card:hover::before {
opacity: 1;
}
@@ -314,7 +311,9 @@
}
.feature-card.particle-container:hover {
box-shadow: 0 4px 20px rgba(24, 78, 42, 0.4), 0 0 30px rgba(0, 255, 76, 0.2);
box-shadow:
0 4px 20px rgba(24, 78, 42, 0.4),
0 0 30px rgba(0, 255, 76, 0.2);
background: #07160b;
}
@@ -481,8 +480,6 @@
z-index: 2;
}
@media (max-width: 479px) {
.features-section {
padding: 4rem 1rem 2rem;
@@ -510,8 +507,6 @@
font-size: 4rem;
}
.feature-card h3 {
font-size: 1rem;
margin-bottom: 0.5rem;
@@ -553,8 +548,6 @@
font-size: 4rem;
}
.feature-card h3 {
font-size: 0.95rem;
margin-bottom: 0.4rem;
@@ -659,4 +652,4 @@
.feature-card p {
font-size: 0.8rem;
}
}
}

View File

@@ -3,6 +3,7 @@
<div class="features-container">
<div class="features-header">
<h3 class="features-title">Zero cost, all the cool.</h3>
<p class="features-subtitle">Everything you need to add flair to your websites</p>
</div>
@@ -13,11 +14,16 @@
<div className="messages-gif-wrapper">
<img src="/assets/messages.gif" alt="Messages animation" className="messages-gif" />
</div>
<h2>
<template v-if="isMobile">100</template>
<CountUp v-else :to="100" />%
<CountUp v-else :to="100" />
%
</h2>
<h3>Free &amp; Open Source</h3>
<p>Loved by developers around the world</p>
</ParticleCard>
@@ -25,19 +31,24 @@
<div className="components-gif-wrapper">
<img src="/assets/components.gif" alt="Components animation" className="components-gif" />
</div>
<h2>
<template v-if="isMobile">40</template>
<CountUp v-else :to="40" />+
<CountUp v-else :to="40" />
+
</h2>
<h3>Curated Components</h3>
<p>Growing weekly &amp; only getting better</p>
</ParticleCard>
<ParticleCard class="feature-card card4" :disable-animations="isMobile">
<h2>
Modern
</h2>
<h2>Modern</h2>
<h3>Technologies</h3>
<p>TypeScript + Tailwind, ready to ship</p>
</ParticleCard>
</div>
@@ -46,26 +57,26 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, defineComponent, h } from 'vue'
import { gsap } from 'gsap'
import CountUp from '../../../content/Animations/CountUp/CountUp.vue'
import './FeatureCards.css'
import { ref, onMounted, onUnmounted, defineComponent, h } from 'vue';
import { gsap } from 'gsap';
import CountUp from '../../../content/Animations/CountUp/CountUp.vue';
import './FeatureCards.css';
const isMobile = ref(false)
const gridRef = ref<HTMLDivElement | null>(null)
const isMobile = ref(false);
const gridRef = ref<HTMLDivElement | null>(null);
const checkIsMobile = () => {
isMobile.value = window.innerWidth <= 768
}
isMobile.value = window.innerWidth <= 768;
};
onMounted(() => {
checkIsMobile()
window.addEventListener('resize', checkIsMobile)
})
checkIsMobile();
window.addEventListener('resize', checkIsMobile);
});
onUnmounted(() => {
window.removeEventListener('resize', checkIsMobile)
})
window.removeEventListener('resize', checkIsMobile);
});
const ParticleCard = defineComponent({
name: 'ParticleCard',
@@ -76,114 +87,119 @@ const ParticleCard = defineComponent({
}
},
setup(props, { slots }) {
const cardRef = ref<HTMLDivElement | null>(null)
const particlesRef = ref<HTMLDivElement[]>([])
const timeoutsRef = ref<number[]>([])
const isHoveredRef = ref(false)
const memoizedParticles = ref<HTMLDivElement[]>([])
const particlesInit = ref(false)
const cardRef = ref<HTMLDivElement | null>(null);
const particlesRef = ref<HTMLDivElement[]>([]);
const timeoutsRef = ref<number[]>([]);
const isHoveredRef = ref(false);
const memoizedParticles = ref<HTMLDivElement[]>([]);
const particlesInit = ref(false);
const createParticle = (x: number, y: number): HTMLDivElement => {
const el = document.createElement('div')
el.className = 'particle'
const el = document.createElement('div');
el.className = 'particle';
el.style.cssText = `
position:absolute;width:4px;height:4px;border-radius:50%;
background:rgba(132,0,255,1);box-shadow:0 0 6px rgba(132,0,255,.6);
pointer-events:none;z-index:100;left:${x}px;top:${y}px;
`
return el
}
`;
return el;
};
const memoizeParticles = () => {
if (particlesInit.value || !cardRef.value) return
const { width, height } = cardRef.value.getBoundingClientRect()
if (particlesInit.value || !cardRef.value) return;
const { width, height } = cardRef.value.getBoundingClientRect();
Array.from({ length: 12 }).forEach(() => {
memoizedParticles.value.push(createParticle(Math.random() * width, Math.random() * height))
})
particlesInit.value = true
}
memoizedParticles.value.push(createParticle(Math.random() * width, Math.random() * height));
});
particlesInit.value = true;
};
const clearParticles = () => {
timeoutsRef.value.forEach(clearTimeout)
timeoutsRef.value = []
timeoutsRef.value.forEach(clearTimeout);
timeoutsRef.value = [];
particlesRef.value.forEach(p =>
gsap.to(p, {
scale: 0,
opacity: 0,
duration: 0.3,
ease: "back.in(1.7)",
ease: 'back.in(1.7)',
onComplete: () => {
if (p.parentNode) {
p.parentNode.removeChild(p)
p.parentNode.removeChild(p);
}
},
}
})
)
particlesRef.value = []
}
);
particlesRef.value = [];
};
const animateParticles = () => {
if (!cardRef.value || !isHoveredRef.value) return
if (!particlesInit.value) memoizeParticles()
if (!cardRef.value || !isHoveredRef.value) return;
if (!particlesInit.value) memoizeParticles();
memoizedParticles.value.forEach((particle, i) => {
const id = setTimeout(() => {
if (!isHoveredRef.value || !cardRef.value) return
const clone = particle.cloneNode(true) as HTMLDivElement
cardRef.value.appendChild(clone)
particlesRef.value.push(clone)
if (!isHoveredRef.value || !cardRef.value) return;
const clone = particle.cloneNode(true) as HTMLDivElement;
cardRef.value.appendChild(clone);
particlesRef.value.push(clone);
gsap.set(clone, { scale: 0, opacity: 0 })
gsap.to(clone, { scale: 1, opacity: 1, duration: 0.3, ease: "back.out(1.7)" })
gsap.set(clone, { scale: 0, opacity: 0 });
gsap.to(clone, { scale: 1, opacity: 1, duration: 0.3, ease: 'back.out(1.7)' });
gsap.to(clone, {
x: (Math.random() - 0.5) * 100,
y: (Math.random() - 0.5) * 100,
rotation: Math.random() * 360,
duration: 2 + Math.random() * 2,
ease: "none",
ease: 'none',
repeat: -1,
yoyo: true,
})
gsap.to(clone, { opacity: 0.3, duration: 1.5, ease: "power2.inOut", repeat: -1, yoyo: true })
}, i * 100)
timeoutsRef.value.push(id)
})
}
yoyo: true
});
gsap.to(clone, { opacity: 0.3, duration: 1.5, ease: 'power2.inOut', repeat: -1, yoyo: true });
}, i * 100);
timeoutsRef.value.push(id);
});
};
const handleMouseEnter = () => {
isHoveredRef.value = true
animateParticles()
}
isHoveredRef.value = true;
animateParticles();
};
const handleMouseLeave = () => {
isHoveredRef.value = false
clearParticles()
}
isHoveredRef.value = false;
clearParticles();
};
onMounted(() => {
if (props.disableAnimations || !cardRef.value) return
if (props.disableAnimations || !cardRef.value) return;
const node = cardRef.value
node.addEventListener('mouseenter', handleMouseEnter)
node.addEventListener('mouseleave', handleMouseLeave)
})
const node = cardRef.value;
node.addEventListener('mouseenter', handleMouseEnter);
node.addEventListener('mouseleave', handleMouseLeave);
});
onUnmounted(() => {
if (cardRef.value) {
cardRef.value.removeEventListener('mouseenter', handleMouseEnter)
cardRef.value.removeEventListener('mouseleave', handleMouseLeave)
cardRef.value.removeEventListener('mouseenter', handleMouseEnter);
cardRef.value.removeEventListener('mouseleave', handleMouseLeave);
}
isHoveredRef.value = false
clearParticles()
})
isHoveredRef.value = false;
clearParticles();
});
return () => h('div', {
ref: cardRef,
class: 'particle-container',
style: { position: 'relative', overflow: 'hidden' }
}, slots.default?.())
return () =>
h(
'div',
{
ref: cardRef,
class: 'particle-container',
style: { position: 'relative', overflow: 'hidden' }
},
slots.default?.()
);
}
})
});
const GlobalSpotlight = defineComponent({
name: 'GlobalSpotlight',
@@ -198,89 +214,88 @@ const GlobalSpotlight = defineComponent({
}
},
setup(props) {
const spotlightRef = ref<HTMLDivElement | null>(null)
const isInsideSectionRef = ref(false)
const spotlightRef = ref<HTMLDivElement | null>(null);
const isInsideSectionRef = ref(false);
const handleMouseMove = (e: MouseEvent) => {
if (!spotlightRef.value || !props.gridRef.value) return
const section = props.gridRef.value.closest('.features-section')
const rect = section?.getBoundingClientRect()
if (!spotlightRef.value || !props.gridRef.value) return;
const section = props.gridRef.value.closest('.features-section');
const rect = section?.getBoundingClientRect();
const inside =
rect &&
e.clientX >= rect.left && e.clientX <= rect.right &&
e.clientY >= rect.top && e.clientY <= rect.bottom
rect && e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;
isInsideSectionRef.value = inside
const cards = props.gridRef.value.querySelectorAll('.feature-card')
isInsideSectionRef.value = inside;
const cards = props.gridRef.value.querySelectorAll('.feature-card');
if (!inside) {
gsap.to(spotlightRef.value, { opacity: 0, duration: 0.3, ease: "power2.out" })
cards.forEach((card: HTMLElement) => card.style.setProperty('--glow-intensity', '0'))
return
gsap.to(spotlightRef.value, { opacity: 0, duration: 0.3, ease: 'power2.out' });
cards.forEach((card: HTMLElement) => card.style.setProperty('--glow-intensity', '0'));
return;
}
let minDist = Infinity
const prox = 100, fade = 150
let minDist = Infinity;
const prox = 100,
fade = 150;
cards.forEach((card: HTMLElement) => {
const r = card.getBoundingClientRect()
const cx = r.left + r.width / 2
const cy = r.top + r.height / 2
const d = Math.hypot(e.clientX - cx, e.clientY - cy) - Math.max(r.width, r.height) / 2
const ed = Math.max(0, d)
minDist = Math.min(minDist, ed)
const r = card.getBoundingClientRect();
const cx = r.left + r.width / 2;
const cy = r.top + r.height / 2;
const d = Math.hypot(e.clientX - cx, e.clientY - cy) - Math.max(r.width, r.height) / 2;
const ed = Math.max(0, d);
minDist = Math.min(minDist, ed);
const rx = ((e.clientX - r.left) / r.width) * 100
const ry = ((e.clientY - r.top) / r.height) * 100
let glow = 0
if (ed <= prox) glow = 1
else if (ed <= fade) glow = (fade - ed) / (fade - prox)
card.style.setProperty('--glow-x', `${rx}%`)
card.style.setProperty('--glow-y', `${ry}%`)
card.style.setProperty('--glow-intensity', String(glow))
})
const rx = ((e.clientX - r.left) / r.width) * 100;
const ry = ((e.clientY - r.top) / r.height) * 100;
let glow = 0;
if (ed <= prox) glow = 1;
else if (ed <= fade) glow = (fade - ed) / (fade - prox);
card.style.setProperty('--glow-x', `${rx}%`);
card.style.setProperty('--glow-y', `${ry}%`);
card.style.setProperty('--glow-intensity', String(glow));
});
gsap.to(spotlightRef.value, { left: e.clientX, top: e.clientY, duration: 0.1, ease: "power2.out" })
const target = minDist <= prox ? 0.8 : minDist <= fade ? ((fade - minDist) / (fade - prox)) * 0.8 : 0
gsap.to(spotlightRef.value, { opacity: target, duration: target > 0 ? 0.2 : 0.5, ease: "power2.out" })
}
gsap.to(spotlightRef.value, { left: e.clientX, top: e.clientY, duration: 0.1, ease: 'power2.out' });
const target = minDist <= prox ? 0.8 : minDist <= fade ? ((fade - minDist) / (fade - prox)) * 0.8 : 0;
gsap.to(spotlightRef.value, { opacity: target, duration: target > 0 ? 0.2 : 0.5, ease: 'power2.out' });
};
const handleMouseLeave = () => {
isInsideSectionRef.value = false
isInsideSectionRef.value = false;
props.gridRef.value
?.querySelectorAll('.feature-card')
.forEach((card: HTMLElement) => card.style.setProperty('--glow-intensity', '0'))
.forEach((card: HTMLElement) => card.style.setProperty('--glow-intensity', '0'));
if (spotlightRef.value) {
gsap.to(spotlightRef.value, { opacity: 0, duration: 0.3, ease: "power2.out" })
gsap.to(spotlightRef.value, { opacity: 0, duration: 0.3, ease: 'power2.out' });
}
}
};
onMounted(() => {
if (props.disableAnimations || !props.gridRef?.value) return
if (props.disableAnimations || !props.gridRef?.value) return;
const spotlight = document.createElement('div')
spotlight.className = 'global-spotlight'
const spotlight = document.createElement('div');
spotlight.className = 'global-spotlight';
spotlight.style.cssText = `
position:fixed;width:800px;height:800px;border-radius:50%;pointer-events:none;
background:radial-gradient(circle,rgba(132,0,255,.15) 0%,rgba(132,0,255,.08) 15%,
rgba(132,0,255,.04) 25%,rgba(132,0,255,.02) 40%,rgba(132,0,255,.01) 65%,transparent 70%);
z-index:200;opacity:0;transform:translate(-50%,-50%);mix-blend-mode:screen;
`
document.body.appendChild(spotlight)
spotlightRef.value = spotlight
`;
document.body.appendChild(spotlight);
spotlightRef.value = spotlight;
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseleave', handleMouseLeave)
})
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseleave', handleMouseLeave);
});
onUnmounted(() => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseleave', handleMouseLeave)
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseleave', handleMouseLeave);
if (spotlightRef.value?.parentNode) {
spotlightRef.value.parentNode.removeChild(spotlightRef.value)
spotlightRef.value.parentNode.removeChild(spotlightRef.value);
}
})
});
return () => null
return () => null;
}
})
</script>
});
</script>

View File

@@ -48,13 +48,13 @@
}
.footer-heart {
color: #27FF64;
color: #27ff64;
font-size: 1em;
display: inline-block;
}
.footer-creator-link {
color: #27FF64;
color: #27ff64;
text-decoration: none;
transition: color 0.2s ease;
}
@@ -152,4 +152,4 @@
padding: 0.5rem 0.75rem;
text-align: center;
}
}
}

View File

@@ -4,10 +4,14 @@
<div class="footer-content">
<div class="footer-left">
<img :src="vueBitsLogo" alt="Vue Bits" class="footer-logo" />
<p class="footer-description">
A library created with <i class="pi pi-heart-fill footer-heart"></i> by
A library created with
<i class="pi pi-heart-fill footer-heart"></i>
by
<a href="https://davidhaz.com/" target="_blank" class="footer-creator-link">this guy</a>
</p>
<p class="footer-copyright">© {{ currentYear }} Vue Bits</p>
</div>
@@ -15,15 +19,12 @@
<a href="https://github.com/DavidHDev/vue-bits" target="_blank" rel="noopener noreferrer" class="footer-link">
GitHub
</a>
<router-link to="/text-animations/split-text" class="footer-link">
Docs
</router-link>
<a href="https://www.jsrepo.com/" target="_blank" class="footer-link">
CLI
</a>
<a href="https://reactbits.dev/" target="_blank" class="footer-link">
React Bits
</a>
<router-link to="/text-animations/split-text" class="footer-link">Docs</router-link>
<a href="https://www.jsrepo.com/" target="_blank" class="footer-link">CLI</a>
<a href="https://reactbits.dev/" target="_blank" class="footer-link">React Bits</a>
</div>
</div>
</footer>
@@ -31,10 +32,10 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import vueBitsLogo from '../../../assets/logos/vue-bits-logo.svg'
import FadeContent from '@/content/Animations/FadeContent/FadeContent.vue'
import './Footer.css'
import { computed } from 'vue';
import vueBitsLogo from '../../../assets/logos/vue-bits-logo.svg';
import FadeContent from '@/content/Animations/FadeContent/FadeContent.vue';
import './Footer.css';
const currentYear = computed(() => new Date().getFullYear())
</script>
const currentYear = computed(() => new Date().getFullYear());
</script>

View File

@@ -2,18 +2,41 @@
<div class="landing-content">
<div class="hero-main-content">
<h1 class="landing-title">
<ResponsiveSplitText :is-mobile="isMobile" text="Animated Vue components" class-name="hero-split"
split-type="chars" :delay="30" :duration="2" ease="elastic.out(0.5, 0.3)" />
<ResponsiveSplitText
:is-mobile="isMobile"
text="Animated Vue components"
class-name="hero-split"
split-type="chars"
:delay="30"
:duration="2"
ease="elastic.out(0.5, 0.3)"
/>
<br />
<ResponsiveSplitText :is-mobile="isMobile" text="for creative developers" class-name="hero-split"
split-type="chars" :delay="30" :duration="2" ease="elastic.out(0.5, 0.3)" />
<ResponsiveSplitText
:is-mobile="isMobile"
text="for creative developers"
class-name="hero-split"
split-type="chars"
:delay="30"
:duration="2"
ease="elastic.out(0.5, 0.3)"
/>
</h1>
<ResponsiveSplitText :is-mobile="isMobile" class-name="landing-subtitle" split-type="words" :delay="10"
:duration="1" text="Eighty-plus snippets, ready to be dropped into your Vue projects" />
<ResponsiveSplitText
:is-mobile="isMobile"
class-name="landing-subtitle"
split-type="words"
:delay="10"
:duration="1"
text="Eighty-plus snippets, ready to be dropped into your Vue projects"
/>
<router-link to="/text-animations/split-text" class="landing-button">
<span>Browse Components</span>
<div class="button-arrow-circle">
<svg width="16" height="16" viewBox="0 0 16 16" fill="#ffffff" xmlns="http://www.w3.org/2000/svg">
<path d="M6 12L10 8L6 4" stroke="#0b0b0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -25,8 +48,14 @@
<div v-if="!isMobile" class="hero-cards-container">
<div class="hero-card hero-card-1" @click="openUrl('https://vue-bits.dev/backgrounds/dot-grid')">
<div class="w-full h-full relative hero-dot-grid">
<DotGrid base-color="#ffffff" active-color="rgba(138, 43, 226, 0.9)" :dot-size="8" :gap="16"
:proximity="50" />
<DotGrid
base-color="#ffffff"
active-color="rgba(138, 43, 226, 0.9)"
:dot-size="8"
:gap="16"
:proximity="50"
/>
<div class="placeholder-card"></div>
</div>
</div>
@@ -34,11 +63,13 @@
<div class="hero-cards-row">
<div class="hero-card hero-card-2" @click="openUrl('https://vue-bits.dev/backgrounds/letter-glitch')">
<LetterGlitch class-name="hero-glitch" :glitch-colors="['#ffffff', '#999999', '#333333']" />
<div class="placeholder-card"></div>
</div>
<div class="hero-card hero-card-3" @click="openUrl('https://vue-bits.dev/backgrounds/squares')">
<Squares border-color="#fff" :speed="0.2" direction="diagonal" hover-fill-color="#fff" />
<div class="placeholder-card"></div>
</div>
</div>
@@ -47,11 +78,11 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, h, defineComponent } from 'vue'
import DotGrid from '@/content/Backgrounds/DotGrid/DotGrid.vue'
import SplitText from '@/content/TextAnimations/SplitText/SplitText.vue'
import LetterGlitch from '@/content/Backgrounds/LetterGlitch/LetterGlitch.vue'
import Squares from '@/content/Backgrounds/Squares/Squares.vue'
import { ref, onMounted, onUnmounted, h, defineComponent } from 'vue';
import DotGrid from '@/content/Backgrounds/DotGrid/DotGrid.vue';
import SplitText from '@/content/TextAnimations/SplitText/SplitText.vue';
import LetterGlitch from '@/content/Backgrounds/LetterGlitch/LetterGlitch.vue';
import Squares from '@/content/Backgrounds/Squares/Squares.vue';
const ResponsiveSplitText = defineComponent({
props: {
@@ -71,7 +102,7 @@ const ResponsiveSplitText = defineComponent({
},
render() {
if (this.isMobile) {
return h('span', { class: this.className }, this.text)
return h('span', { class: this.className }, this.text);
} else {
return h(SplitText, {
text: this.text,
@@ -86,29 +117,29 @@ const ResponsiveSplitText = defineComponent({
rootMargin: this.rootMargin,
textAlign: this.textAlign,
onLetterAnimationComplete: this.onLetterAnimationComplete as (() => void) | undefined
})
});
}
}
})
});
const openUrl = (url: string) => {
window.open(url)
}
window.open(url);
};
const isMobile = ref(false)
const isMobile = ref(false);
const checkIsMobile = () => {
isMobile.value = window.innerWidth <= 768
}
isMobile.value = window.innerWidth <= 768;
};
onMounted(() => {
checkIsMobile()
window.addEventListener('resize', checkIsMobile)
})
checkIsMobile();
window.addEventListener('resize', checkIsMobile);
});
onUnmounted(() => {
window.removeEventListener('resize', checkIsMobile)
})
window.removeEventListener('resize', checkIsMobile);
});
</script>
<style scoped>
@@ -119,4 +150,4 @@ onUnmounted(() => {
width: 100%;
height: 100%;
}
</style>
</style>

View File

@@ -1,29 +1,32 @@
<template>
<div v-if="!isMobile" ref="containerRef" :style="{
position: 'absolute',
inset: 0,
overflow: 'hidden',
width: '100vw',
height: '100vh'
}">
</div>
<div
v-if="!isMobile"
ref="containerRef"
:style="{
position: 'absolute',
inset: 0,
overflow: 'hidden',
width: '100vw',
height: '100vh'
}"
></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { Renderer, Camera, Transform, Program, Mesh, Geometry } from 'ogl'
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { Renderer, Camera, Transform, Program, Mesh, Geometry } from 'ogl';
interface Props {
xOffset?: number
yOffset?: number
rotationDeg?: number
focalLength?: number
speed1?: number
speed2?: number
dir2?: number
bend1?: number
bend2?: number
fadeInDuration?: number
xOffset?: number;
yOffset?: number;
rotationDeg?: number;
focalLength?: number;
speed1?: number;
speed2?: number;
dir2?: number;
bend1?: number;
bend2?: number;
fadeInDuration?: number;
}
const props = withDefaults(defineProps<Props>(), {
@@ -37,7 +40,7 @@ const props = withDefaults(defineProps<Props>(), {
bend1: 0.9,
bend2: 0.6,
fadeInDuration: 2000
})
});
const vertex = /* glsl */ `
attribute vec2 position;
@@ -46,7 +49,7 @@ void main() {
vUv = position * 0.5 + 0.5;
gl_Position = vec4(position, 0.0, 1.0);
}
`
`;
const fragment = /* glsl */ `
precision mediump float;
@@ -143,40 +146,40 @@ void main() {
mainImage(color, coord);
gl_FragColor = color;
}
`
`;
const isMobile = ref(false)
const isVisible = ref(true)
const containerRef = ref<HTMLDivElement | null>(null)
const uniformOffset = ref(new Float32Array([props.xOffset, props.yOffset]))
const uniformResolution = ref(new Float32Array([1, 1]))
const rendererRef = ref<Renderer | null>(null)
const fadeStartTime = ref<number | null>(null)
const lastTimeRef = ref(0)
const pausedTimeRef = ref(0)
const rafId = ref<number | null>(null)
const resizeObserver = ref<ResizeObserver | null>(null)
const intersectionObserver = ref<IntersectionObserver | null>(null)
const isMobile = ref(false);
const isVisible = ref(true);
const containerRef = ref<HTMLDivElement | null>(null);
const uniformOffset = ref(new Float32Array([props.xOffset, props.yOffset]));
const uniformResolution = ref(new Float32Array([1, 1]));
const rendererRef = ref<Renderer | null>(null);
const fadeStartTime = ref<number | null>(null);
const lastTimeRef = ref(0);
const pausedTimeRef = ref(0);
const rafId = ref<number | null>(null);
const resizeObserver = ref<ResizeObserver | null>(null);
const intersectionObserver = ref<IntersectionObserver | null>(null);
const checkIsMobile = () => {
isMobile.value = window.innerWidth <= 768
}
isMobile.value = window.innerWidth <= 768;
};
const resize = () => {
if (!containerRef.value || !rendererRef.value) return
if (!containerRef.value || !rendererRef.value) return;
const { width, height } = containerRef.value.getBoundingClientRect()
rendererRef.value.setSize(width, height)
uniformResolution.value[0] = width * rendererRef.value.dpr
uniformResolution.value[1] = height * rendererRef.value.dpr
const { width, height } = containerRef.value.getBoundingClientRect();
rendererRef.value.setSize(width, height);
uniformResolution.value[0] = width * rendererRef.value.dpr;
uniformResolution.value[1] = height * rendererRef.value.dpr;
const gl = rendererRef.value.gl
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight)
gl.clear(gl.COLOR_BUFFER_BIT)
}
const gl = rendererRef.value.gl;
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
gl.clear(gl.COLOR_BUFFER_BIT);
};
const initWebGL = () => {
if (isMobile.value || !containerRef.value) return
if (isMobile.value || !containerRef.value) return;
const renderer = new Renderer({
alpha: true,
@@ -184,20 +187,20 @@ const initWebGL = () => {
antialias: false,
depth: false,
stencil: false,
powerPreference: 'high-performance',
})
rendererRef.value = renderer
powerPreference: 'high-performance'
});
rendererRef.value = renderer;
const gl = renderer.gl
gl.clearColor(0, 0, 0, 0)
containerRef.value.appendChild(gl.canvas)
const gl = renderer.gl;
gl.clearColor(0, 0, 0, 0);
containerRef.value.appendChild(gl.canvas);
const camera = new Camera(gl)
const scene = new Transform()
const camera = new Camera(gl);
const scene = new Transform();
const geometry = new Geometry(gl, {
position: { size: 2, data: new Float32Array([-1, -1, 3, -1, -1, 3]) },
})
position: { size: 2, data: new Float32Array([-1, -1, 3, -1, -1, 3]) }
});
const program = new Program(gl, {
vertex,
@@ -215,117 +218,117 @@ const initWebGL = () => {
bend2: { value: props.bend2 },
bendAdj1: { value: 0 },
bendAdj2: { value: 0 },
uOpacity: { value: 0 },
},
})
new Mesh(gl, { geometry, program }).setParent(scene)
uOpacity: { value: 0 }
}
});
new Mesh(gl, { geometry, program }).setParent(scene);
resize()
resize();
resizeObserver.value = new ResizeObserver(resize)
resizeObserver.value.observe(containerRef.value)
resizeObserver.value = new ResizeObserver(resize);
resizeObserver.value.observe(containerRef.value);
const loop = (now: number) => {
if (isVisible.value) {
if (lastTimeRef.value === 0) {
lastTimeRef.value = now - pausedTimeRef.value
lastTimeRef.value = now - pausedTimeRef.value;
}
const t = (now - lastTimeRef.value) * 0.001
const t = (now - lastTimeRef.value) * 0.001;
if (fadeStartTime.value === null && t > 0.1) {
fadeStartTime.value = now
fadeStartTime.value = now;
}
let opacity = 0
let opacity = 0;
if (fadeStartTime.value !== null) {
const fadeElapsed = now - fadeStartTime.value
opacity = Math.min(fadeElapsed / props.fadeInDuration, 1)
opacity = 1 - Math.pow(1 - opacity, 3)
const fadeElapsed = now - fadeStartTime.value;
opacity = Math.min(fadeElapsed / props.fadeInDuration, 1);
opacity = 1 - Math.pow(1 - opacity, 3);
}
uniformOffset.value[0] = props.xOffset
uniformOffset.value[1] = props.yOffset
uniformOffset.value[0] = props.xOffset;
uniformOffset.value[1] = props.yOffset;
program.uniforms.iTime.value = t
program.uniforms.uRotation.value = props.rotationDeg * Math.PI / 180
program.uniforms.focalLength.value = props.focalLength
program.uniforms.uOpacity.value = opacity
program.uniforms.iTime.value = t;
program.uniforms.uRotation.value = (props.rotationDeg * Math.PI) / 180;
program.uniforms.focalLength.value = props.focalLength;
program.uniforms.uOpacity.value = opacity;
renderer.render({ scene, camera })
renderer.render({ scene, camera });
} else {
if (lastTimeRef.value !== 0) {
pausedTimeRef.value = now - lastTimeRef.value
lastTimeRef.value = 0
pausedTimeRef.value = now - lastTimeRef.value;
lastTimeRef.value = 0;
}
}
rafId.value = requestAnimationFrame(loop)
}
rafId.value = requestAnimationFrame(loop);
};
rafId.value = requestAnimationFrame(loop)
}
rafId.value = requestAnimationFrame(loop);
};
const setupIntersectionObserver = () => {
if (!containerRef.value || isMobile.value) return
if (!containerRef.value || isMobile.value) return;
intersectionObserver.value = new IntersectionObserver(
([entry]) => {
isVisible.value = entry.isIntersecting
isVisible.value = entry.isIntersecting;
},
{
rootMargin: '50px',
threshold: 0.1,
threshold: 0.1
}
)
);
intersectionObserver.value.observe(containerRef.value)
}
intersectionObserver.value.observe(containerRef.value);
};
const cleanup = () => {
if (rafId.value) {
cancelAnimationFrame(rafId.value)
rafId.value = null
cancelAnimationFrame(rafId.value);
rafId.value = null;
}
if (resizeObserver.value) {
resizeObserver.value.disconnect()
resizeObserver.value = null
resizeObserver.value.disconnect();
resizeObserver.value = null;
}
if (intersectionObserver.value) {
intersectionObserver.value.disconnect()
intersectionObserver.value = null
intersectionObserver.value.disconnect();
intersectionObserver.value = null;
}
if (rendererRef.value) {
rendererRef.value.gl.canvas.remove()
rendererRef.value = null
rendererRef.value.gl.canvas.remove();
rendererRef.value = null;
}
window.removeEventListener('resize', checkIsMobile)
}
window.removeEventListener('resize', checkIsMobile);
};
onMounted(() => {
checkIsMobile()
window.addEventListener('resize', checkIsMobile)
checkIsMobile();
window.addEventListener('resize', checkIsMobile);
if (!isMobile.value) {
initWebGL()
setupIntersectionObserver()
initWebGL();
setupIntersectionObserver();
}
})
});
onUnmounted(() => {
cleanup()
})
cleanup();
});
watch(isMobile, (newIsMobile) => {
watch(isMobile, newIsMobile => {
if (newIsMobile) {
cleanup()
cleanup();
} else {
initWebGL()
setupIntersectionObserver()
initWebGL();
setupIntersectionObserver();
}
})
</script>
});
</script>

View File

@@ -17,9 +17,7 @@
max-width: 1200px;
user-select: none;
margin: 0 auto;
background: linear-gradient(135deg,
#3aed6d,
rgba(24, 255, 93, 0.6));
background: linear-gradient(135deg, #3aed6d, rgba(24, 255, 93, 0.6));
background-size: 200% 200%;
border-radius: 16px;
padding: 4rem 3rem;
@@ -78,7 +76,7 @@
background: transparent;
color: #0b0b0b;
border: 2px solid #0b0b0b;
padding: .6rem 1.6rem;
padding: 0.6rem 1.6rem;
font-size: 1.1rem;
font-weight: 600;
border-radius: 50px;
@@ -88,7 +86,7 @@
.start-building-button:hover {
background: #0b0b0b;
color: #27FF64;
color: #27ff64;
}
@media (max-width: 1280px) {
@@ -159,4 +157,4 @@
padding: 0.75rem 1.5rem;
font-size: 0.95rem;
}
}
}

View File

@@ -3,16 +3,15 @@
<div class="start-building-container">
<div class="start-building-card">
<h2 class="start-building-title">Start exploring Vue Bits</h2>
<p class="start-building-subtitle">Animations, components, backgrounds - it's all here</p>
<router-link to="/text-animations/split-text" class="start-building-button">
Browse Components
</router-link>
<router-link to="/text-animations/split-text" class="start-building-button">Browse Components</router-link>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import './StartBuilding.css'
</script>
import './StartBuilding.css';
</script>

View File

@@ -1,20 +1,24 @@
<template>
<main class="app-container">
<Header />
<section class="category-wrapper">
<Sidebar />
<div class="category-page">
<router-view />
</div>
</section>
<Toast position="bottom-right"
<Toast
position="bottom-right"
:closeButtonProps="{ style: { right: '0', margin: '0', outline: 'none', border: 'none' } }"
:pt="{
message: {
style: {
borderRadius: '10px',
border: '1px solid #142216',
backgroundColor: '#0b0b0b',
backgroundColor: '#0b0b0b'
}
},
messageContent: {
@@ -27,11 +31,12 @@
display: 'none'
}
}
}" />
}"
/>
</main>
</template>
<script setup lang="ts">
import Header from '../navs/Header.vue';
import Sidebar from '../navs/Sidebar.vue';
</script>
</script>

View File

@@ -28,6 +28,7 @@
<div class="drawer-content" @click.stop>
<div class="drawer-header">
<img :src="Logo" alt="Logo" class="drawer-logo" />
<button class="close-button" aria-label="Close Menu" @click="closeDrawer">
<i class="pi pi-times"></i>
</button>
@@ -35,12 +36,13 @@
<div class="drawer-body">
<!-- Navigation Categories -->
<div class="drawer-navigation">
<div class="categories-container">
<Category
v-for="cat in CATEGORIES"
:key="cat.name"
:category="cat"
<Category
v-for="cat in CATEGORIES"
:key="cat.name"
:category="cat"
:location="route"
:handle-click="onNavClick"
:handle-transition-navigation="handleMobileTransitionNavigation"
@@ -55,9 +57,9 @@
<div class="drawer-section">
<p class="section-title">Useful Links</p>
<router-link to="/text-animations/split-text" @click="closeDrawer" class="drawer-link">
Docs
</router-link>
<router-link to="/text-animations/split-text" @click="closeDrawer" class="drawer-link">Docs</router-link>
<a href="https://github.com/DavidHDev/vue-bits" target="_blank" @click="closeDrawer" class="drawer-link">
GitHub
<i class="pi pi-arrow-up-right arrow-icon"></i>
@@ -68,6 +70,7 @@
<div class="drawer-section">
<p class="section-title">Other</p>
<a href="https://davidhaz.com/" target="_blank" @click="closeDrawer" class="drawer-link">
Who made this?
<i class="pi pi-arrow-up-right arrow-icon"></i>
@@ -80,58 +83,58 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed, defineComponent, h } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useStars } from '../../composables/useStars'
import { CATEGORIES, NEW, UPDATED } from '../../constants/Categories'
import FadeContent from '../../content/Animations/FadeContent/FadeContent.vue'
import Logo from '../../assets/logos/vue-bits-logo.svg'
import Star from '../../assets/common/star.svg'
import { ref, onMounted, onUnmounted, computed, defineComponent, h } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useStars } from '../../composables/useStars';
import { CATEGORIES, NEW, UPDATED } from '../../constants/Categories';
import FadeContent from '../../content/Animations/FadeContent/FadeContent.vue';
import Logo from '../../assets/logos/vue-bits-logo.svg';
import Star from '../../assets/common/star.svg';
const isDrawerOpen = ref(false)
const isTransitioning = ref(false)
const stars = useStars()
const route = useRoute()
const router = useRouter()
const isDrawerOpen = ref(false);
const isTransitioning = ref(false);
const stars = useStars();
const route = useRoute();
const router = useRouter();
const slug = (str: string) => str.replace(/\s+/g, "-").toLowerCase()
const slug = (str: string) => str.replace(/\s+/g, '-').toLowerCase();
const toggleDrawer = () => {
isDrawerOpen.value = !isDrawerOpen.value
}
isDrawerOpen.value = !isDrawerOpen.value;
};
const closeDrawer = () => {
isDrawerOpen.value = false
}
isDrawerOpen.value = false;
};
const openGitHub = () => {
window.open('https://github.com/DavidHDev/vue-bits', '_blank')
}
window.open('https://github.com/DavidHDev/vue-bits', '_blank');
};
const onNavClick = () => {
closeDrawer()
window.scrollTo(0, 0)
}
closeDrawer();
window.scrollTo(0, 0);
};
const handleMobileTransitionNavigation = async (path: string) => {
if (isTransitioning.value || route.path === path) return
if (isTransitioning.value || route.path === path) return;
closeDrawer()
isTransitioning.value = true
closeDrawer();
isTransitioning.value = true;
try {
await router.push(path)
window.scrollTo(0, 0)
await router.push(path);
window.scrollTo(0, 0);
} finally {
isTransitioning.value = false
isTransitioning.value = false;
}
}
};
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isDrawerOpen.value) {
closeDrawer()
closeDrawer();
}
}
};
const Category = defineComponent({
name: 'Category',
@@ -167,63 +170,67 @@ const Category = defineComponent({
},
setup(props) {
interface ItemType {
sub: string
path: string
isActive: boolean
isNew: boolean
isUpdated: boolean
sub: string;
path: string;
isActive: boolean;
isNew: boolean;
isUpdated: boolean;
}
const items = computed(() =>
props.category.subcategories.map((sub: string): ItemType => {
const path = `/${slug(props.category.name)}/${slug(sub)}`
const activePath = props.location.path
const path = `/${slug(props.category.name)}/${slug(sub)}`;
const activePath = props.location.path;
return {
sub,
path,
isActive: activePath === path,
isNew: (NEW as string[]).includes(sub),
isUpdated: (UPDATED as string[]).includes(sub),
}
isUpdated: (UPDATED as string[]).includes(sub)
};
})
)
);
return () => h('div', { class: 'category' }, [
h('p', { class: 'category-name' }, props.category.name),
h('div', { class: 'category-items' },
items.value.map(({ sub, path, isActive, isNew, isUpdated }: ItemType) => {
return h('router-link', {
key: path,
to: path,
class: [
'sidebar-item',
{ 'active-sidebar-item': isActive },
{ 'transitioning': props.isTransitioning }
],
onClick: (e: Event) => {
e.preventDefault()
props.handleTransitionNavigation(path)
},
onMouseenter: (e: Event) => props.onItemMouseEnter(path, e),
onMouseleave: props.onItemMouseLeave
}, {
default: () => [
sub,
isNew ? h('span', { class: 'new-tag' }, 'New') : null,
isUpdated ? h('span', { class: 'updated-tag' }, 'Updated') : null
].filter(Boolean)
return () =>
h('div', { class: 'category' }, [
h('p', { class: 'category-name' }, props.category.name),
h(
'div',
{ class: 'category-items' },
items.value.map(({ sub, path, isActive, isNew, isUpdated }: ItemType) => {
return h(
'router-link',
{
key: path,
to: path,
class: ['sidebar-item', { 'active-sidebar-item': isActive }, { transitioning: props.isTransitioning }],
onClick: (e: Event) => {
e.preventDefault();
props.handleTransitionNavigation(path);
},
onMouseenter: (e: Event) => props.onItemMouseEnter(path, e),
onMouseleave: props.onItemMouseLeave
},
{
default: () =>
[
sub,
isNew ? h('span', { class: 'new-tag' }, 'New') : null,
isUpdated ? h('span', { class: 'updated-tag' }, 'Updated') : null
].filter(Boolean)
}
);
})
})
)
])
)
]);
}
})
});
onMounted(() => {
document.addEventListener('keydown', handleKeyDown)
})
document.addEventListener('keydown', handleKeyDown);
});
onUnmounted(() => {
document.removeEventListener('keydown', handleKeyDown)
})
</script>
document.removeEventListener('keydown', handleKeyDown);
});
</script>

View File

@@ -1,5 +1,6 @@
<template>
<!-- Mobile Drawer -->
<div v-if="isDrawerOpen" class="drawer-overlay" @click="closeDrawer">
<div class="drawer-content" :class="{ 'drawer-open': isDrawerOpen }" @click.stop>
<div class="drawer-header sidebar-logo">
@@ -7,6 +8,7 @@
<router-link to="/" @click="closeDrawer">
<img :src="Logo" alt="Logo" class="drawer-logo" />
</router-link>
<button class="icon-button" aria-label="Close" @click="closeDrawer">
<i class="pi pi-times"></i>
</button>
@@ -15,26 +17,41 @@
<div class="drawer-body">
<div class="categories-container">
<Category v-for="cat in CATEGORIES" :key="cat.name" :category="cat" :location="route"
:pending-active-path="pendingActivePath ?? undefined" :handle-click="onNavClick"
:handle-transition-navigation="handleMobileTransitionNavigation" :on-item-mouse-enter="() => { }"
:on-item-mouse-leave="() => { }" :is-transitioning="isTransitioning" />
<Category
v-for="cat in CATEGORIES"
:key="cat.name"
:category="cat"
:location="route"
:pending-active-path="pendingActivePath ?? undefined"
:handle-click="onNavClick"
:handle-transition-navigation="handleMobileTransitionNavigation"
:on-item-mouse-enter="() => {}"
:on-item-mouse-leave="() => {}"
:is-transitioning="isTransitioning"
/>
</div>
<div class="separator"></div>
<div class="useful-links">
<p class="useful-links-title">Useful Links</p>
<div class="links-container">
<a href="https://github.com/DavidHDev/vue-bits" target="_blank" @click="closeDrawer" class="useful-link">
<span>GitHub</span>
<i class="pi pi-arrow-up-right arrow-icon"></i>
</a>
<router-link to="/text-animations/split-text" @click="closeDrawer" class="useful-link">
<span>Docs</span>
<i class="pi pi-arrow-up-right arrow-icon"></i>
</router-link>
<a href="https://davidhaz.com/" target="_blank" @click="closeDrawer" class="useful-link">
<span>Who made this?</span>
<i class="pi pi-arrow-up-right arrow-icon"></i>
</a>
</div>
@@ -44,174 +61,192 @@
</div>
<!-- Desktop Sidebar -->
<nav ref="sidebarContainerRef" class="sidebar" :class="{ 'sidebar-no-fade': isScrolledToBottom }"
@scroll="handleScroll">
<nav
ref="sidebarContainerRef"
class="sidebar"
:class="{ 'sidebar-no-fade': isScrolledToBottom }"
@scroll="handleScroll"
>
<div ref="sidebarRef" class="sidebar-content">
<!-- Active line indicator -->
<div class="active-line" :style="{
transform: isLineVisible && linePosition !== null
? `translateY(${linePosition - 8}px)`
: 'translateY(-100px)',
opacity: isLineVisible ? 1 : 0
}"></div>
<div
class="active-line"
:style="{
transform:
isLineVisible && linePosition !== null ? `translateY(${linePosition - 8}px)` : 'translateY(-100px)',
opacity: isLineVisible ? 1 : 0
}"
></div>
<!-- Hover line indicator -->
<div class="hover-line" :style="{
transform: hoverLinePosition !== null
? `translateY(${hoverLinePosition - 8}px)`
: 'translateY(-100px)',
opacity: isHoverLineVisible ? 1 : 0
}"></div>
<div
class="hover-line"
:style="{
transform: hoverLinePosition !== null ? `translateY(${hoverLinePosition - 8}px)` : 'translateY(-100px)',
opacity: isHoverLineVisible ? 1 : 0
}"
></div>
<div class="categories-list">
<Category v-for="cat in CATEGORIES" :key="cat.name" :category="cat" :location="route"
:pending-active-path="pendingActivePath ?? undefined" :handle-click="scrollToTop"
:handle-transition-navigation="handleTransitionNavigation" :on-item-mouse-enter="onItemEnter"
:on-item-mouse-leave="onItemLeave" :is-transitioning="isTransitioning" />
<Category
v-for="cat in CATEGORIES"
:key="cat.name"
:category="cat"
:location="route"
:pending-active-path="pendingActivePath ?? undefined"
:handle-click="scrollToTop"
:handle-transition-navigation="handleTransitionNavigation"
:on-item-mouse-enter="onItemEnter"
:on-item-mouse-leave="onItemLeave"
:is-transitioning="isTransitioning"
/>
</div>
</div>
</nav>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick, watch, defineComponent, h, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { CATEGORIES, NEW, UPDATED } from '../../constants/Categories'
import Logo from '../../assets/logos/vue-bits-logo.svg'
import '../../css/sidebar.css'
import { ref, onMounted, onUnmounted, nextTick, watch, defineComponent, h, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { CATEGORIES, NEW, UPDATED } from '../../constants/Categories';
import Logo from '../../assets/logos/vue-bits-logo.svg';
import '../../css/sidebar.css';
const HOVER_TIMEOUT_DELAY = 150
const HOVER_TIMEOUT_DELAY = 150;
const isDrawerOpen = ref(false)
const linePosition = ref<number | null>(null)
const isLineVisible = ref(false)
const hoverLinePosition = ref<number | null>(null)
const isHoverLineVisible = ref(false)
const pendingActivePath = ref<string | null>(null)
const isScrolledToBottom = ref(false)
const isTransitioning = ref(false)
const isDrawerOpen = ref(false);
const linePosition = ref<number | null>(null);
const isLineVisible = ref(false);
const hoverLinePosition = ref<number | null>(null);
const isHoverLineVisible = ref(false);
const pendingActivePath = ref<string | null>(null);
const isScrolledToBottom = ref(false);
const isTransitioning = ref(false);
const sidebarRef = ref<HTMLDivElement>()
const sidebarContainerRef = ref<HTMLDivElement>()
const sidebarRef = ref<HTMLDivElement>();
const sidebarContainerRef = ref<HTMLDivElement>();
let hoverTimeoutRef: number | null = null
let hoverDelayTimeoutRef: number | null = null
let hoverTimeoutRef: number | null = null;
let hoverDelayTimeoutRef: number | null = null;
const route = useRoute()
const router = useRouter()
const route = useRoute();
const router = useRouter();
const scrollToTop = () => window.scrollTo(0, 0)
const slug = (str: string) => str.replace(/\s+/g, "-").toLowerCase()
const scrollToTop = () => window.scrollTo(0, 0);
const slug = (str: string) => str.replace(/\s+/g, '-').toLowerCase();
const findActiveElement = () => {
const activePath = pendingActivePath.value || route.path
const activePath = pendingActivePath.value || route.path;
for (const category of CATEGORIES) {
const activeItem = category.subcategories.find((sub: string) => {
const expectedPath = `/${slug(category.name)}/${slug(sub)}`
return activePath === expectedPath
})
const expectedPath = `/${slug(category.name)}/${slug(sub)}`;
return activePath === expectedPath;
});
if (activeItem) {
const selector = `.sidebar a[href="${activePath}"]`
const element = document.querySelector(selector) as HTMLElement
return element
const selector = `.sidebar a[href="${activePath}"]`;
const element = document.querySelector(selector) as HTMLElement;
return element;
}
}
return null
}
return null;
};
const updateLinePosition = (el: HTMLElement | null) => {
if (!el || !sidebarRef.value || !sidebarRef.value.offsetParent) return null
const sidebarRect = sidebarRef.value.getBoundingClientRect()
const elRect = el.getBoundingClientRect()
return elRect.top - sidebarRect.top + elRect.height / 2
}
if (!el || !sidebarRef.value || !sidebarRef.value.offsetParent) return null;
const sidebarRect = sidebarRef.value.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
return elRect.top - sidebarRect.top + elRect.height / 2;
};
const closeDrawer = () => {
isDrawerOpen.value = false
}
isDrawerOpen.value = false;
};
const onNavClick = () => {
closeDrawer()
scrollToTop()
}
closeDrawer();
scrollToTop();
};
const handleTransitionNavigation = async (path: string) => {
if (isTransitioning.value || route.path === path) return
if (isTransitioning.value || route.path === path) return;
pendingActivePath.value = path
pendingActivePath.value = path;
// TODO: Implement transition when available
await router.push(path)
scrollToTop()
pendingActivePath.value = null
}
await router.push(path);
scrollToTop();
pendingActivePath.value = null;
};
const handleMobileTransitionNavigation = async (path: string) => {
if (isTransitioning.value || route.path === path) return
if (isTransitioning.value || route.path === path) return;
closeDrawer()
pendingActivePath.value = path
closeDrawer();
pendingActivePath.value = path;
// TODO: Implement transition when available
await router.push(path)
scrollToTop()
pendingActivePath.value = null
}
await router.push(path);
scrollToTop();
pendingActivePath.value = null;
};
const onItemEnter = (path: string, e: Event) => {
if (hoverTimeoutRef) clearTimeout(hoverTimeoutRef)
if (hoverDelayTimeoutRef) clearTimeout(hoverDelayTimeoutRef)
if (hoverTimeoutRef) clearTimeout(hoverTimeoutRef);
if (hoverDelayTimeoutRef) clearTimeout(hoverDelayTimeoutRef);
const targetElement = e.currentTarget as HTMLElement
const pos = updateLinePosition(targetElement)
const targetElement = e.currentTarget as HTMLElement;
const pos = updateLinePosition(targetElement);
if (pos !== null) {
hoverLinePosition.value = pos
hoverLinePosition.value = pos;
}
hoverDelayTimeoutRef = setTimeout(() => {
isHoverLineVisible.value = true
}, 200)
}
isHoverLineVisible.value = true;
}, 200);
};
const onItemLeave = () => {
if (hoverDelayTimeoutRef) clearTimeout(hoverDelayTimeoutRef)
if (hoverDelayTimeoutRef) clearTimeout(hoverDelayTimeoutRef);
hoverTimeoutRef = setTimeout(() => {
isHoverLineVisible.value = false
}, HOVER_TIMEOUT_DELAY)
}
isHoverLineVisible.value = false;
}, HOVER_TIMEOUT_DELAY);
};
const handleScroll = () => {
const sidebarElement = sidebarContainerRef.value
if (!sidebarElement) return
const sidebarElement = sidebarContainerRef.value;
if (!sidebarElement) return;
const { scrollTop, scrollHeight, clientHeight } = sidebarElement
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 10
isScrolledToBottom.value = isAtBottom
}
const { scrollTop, scrollHeight, clientHeight } = sidebarElement;
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 10;
isScrolledToBottom.value = isAtBottom;
};
const updateActiveLine = async () => {
await nextTick()
await nextTick();
setTimeout(() => {
const activeEl = findActiveElement()
const activeEl = findActiveElement();
if (!activeEl) {
isLineVisible.value = false
return
isLineVisible.value = false;
return;
}
const pos = updateLinePosition(activeEl)
const pos = updateLinePosition(activeEl);
if (pos !== null) {
linePosition.value = pos
isLineVisible.value = true
linePosition.value = pos;
isLineVisible.value = true;
} else {
isLineVisible.value = false
isLineVisible.value = false;
}
}, 100)
}
}, 100);
};
const Category = defineComponent({
name: 'Category',
@@ -251,74 +286,78 @@ const Category = defineComponent({
},
setup(props) {
interface ItemType {
sub: string
path: string
isActive: boolean
isNew: boolean
isUpdated: boolean
sub: string;
path: string;
isActive: boolean;
isNew: boolean;
isUpdated: boolean;
}
const items = computed(() =>
props.category.subcategories.map((sub: string): ItemType => {
const path = `/${slug(props.category.name)}/${slug(sub)}`
const activePath = props.pendingActivePath || props.location.path
const path = `/${slug(props.category.name)}/${slug(sub)}`;
const activePath = props.pendingActivePath || props.location.path;
return {
sub,
path,
isActive: activePath === path,
isNew: (NEW as string[]).includes(sub),
isUpdated: (UPDATED as string[]).includes(sub),
}
isUpdated: (UPDATED as string[]).includes(sub)
};
})
)
);
return () => h('div', { class: 'category' }, [
h('p', { class: 'category-name' }, props.category.name),
h('div', { class: 'category-items' },
items.value.map(({ sub, path, isActive, isNew, isUpdated }: ItemType) => {
return h('router-link', {
key: path,
to: path,
class: [
'sidebar-item',
{ 'active-sidebar-item': isActive },
{ 'transitioning': props.isTransitioning }
],
onClick: (e: Event) => {
e.preventDefault()
props.handleTransitionNavigation(path)
},
onMouseenter: (e: Event) => props.onItemMouseEnter(path, e),
onMouseleave: props.onItemMouseLeave
}, {
default: () => [
sub,
isNew ? h('span', { class: 'new-tag' }, 'New') : null,
isUpdated ? h('span', { class: 'updated-tag' }, 'Updated') : null
].filter(Boolean)
return () =>
h('div', { class: 'category' }, [
h('p', { class: 'category-name' }, props.category.name),
h(
'div',
{ class: 'category-items' },
items.value.map(({ sub, path, isActive, isNew, isUpdated }: ItemType) => {
return h(
'router-link',
{
key: path,
to: path,
class: ['sidebar-item', { 'active-sidebar-item': isActive }, { transitioning: props.isTransitioning }],
onClick: (e: Event) => {
e.preventDefault();
props.handleTransitionNavigation(path);
},
onMouseenter: (e: Event) => props.onItemMouseEnter(path, e),
onMouseleave: props.onItemMouseLeave
},
{
default: () =>
[
sub,
isNew ? h('span', { class: 'new-tag' }, 'New') : null,
isUpdated ? h('span', { class: 'updated-tag' }, 'Updated') : null
].filter(Boolean)
}
);
})
})
)
])
)
]);
}
})
});
watch(() => route.path, updateActiveLine)
watch(pendingActivePath, updateActiveLine)
watch(() => route.path, updateActiveLine);
watch(pendingActivePath, updateActiveLine);
onMounted(() => {
updateActiveLine()
updateActiveLine();
if (sidebarContainerRef.value) {
sidebarContainerRef.value.addEventListener('scroll', handleScroll)
handleScroll()
sidebarContainerRef.value.addEventListener('scroll', handleScroll);
handleScroll();
}
})
});
onUnmounted(() => {
if (hoverTimeoutRef) clearTimeout(hoverTimeoutRef)
if (hoverDelayTimeoutRef) clearTimeout(hoverDelayTimeoutRef)
if (hoverTimeoutRef) clearTimeout(hoverTimeoutRef);
if (hoverDelayTimeoutRef) clearTimeout(hoverDelayTimeoutRef);
if (sidebarContainerRef.value) {
sidebarContainerRef.value.removeEventListener('scroll', handleScroll)
sidebarContainerRef.value.removeEventListener('scroll', handleScroll);
}
})
</script>
});
</script>

View File

@@ -1,18 +1,18 @@
import { ref } from 'vue'
import { ref } from 'vue';
/**
* Composable for force re-rendering components
* Useful for demo components that need to restart animations or reset state
*/
export function useForceRerender() {
const rerenderKey = ref(0)
const rerenderKey = ref(0);
const forceRerender = () => {
rerenderKey.value++
}
rerenderKey.value++;
};
return {
rerenderKey,
forceRerender
}
};
}

View File

@@ -1,48 +1,51 @@
import { ref, onMounted } from 'vue'
import { getStarsCount } from '@/utils/utils'
import { ref, onMounted } from 'vue';
import { getStarsCount } from '@/utils/utils';
const CACHE_KEY = 'github_stars_cache'
const CACHE_DURATION = 24 * 60 * 60 * 1000
const CACHE_KEY = 'github_stars_cache';
const CACHE_DURATION = 24 * 60 * 60 * 1000;
export function useStars() {
const stars = ref<number>(0)
const stars = ref<number>(0);
const fetchStars = async () => {
try {
const cachedData = localStorage.getItem(CACHE_KEY)
const cachedData = localStorage.getItem(CACHE_KEY);
if (cachedData) {
const { count, timestamp } = JSON.parse(cachedData)
const now = Date.now()
const { count, timestamp } = JSON.parse(cachedData);
const now = Date.now();
if (now - timestamp < CACHE_DURATION) {
stars.value = count
return
stars.value = count;
return;
}
}
const count = await getStarsCount()
localStorage.setItem(CACHE_KEY, JSON.stringify({
count,
timestamp: Date.now()
}))
stars.value = count
const count = await getStarsCount();
localStorage.setItem(
CACHE_KEY,
JSON.stringify({
count,
timestamp: Date.now()
})
);
stars.value = count;
} catch (error) {
console.error('Error fetching stars:', error)
const cachedData = localStorage.getItem(CACHE_KEY)
console.error('Error fetching stars:', error);
const cachedData = localStorage.getItem(CACHE_KEY);
if (cachedData) {
const { count } = JSON.parse(cachedData)
stars.value = count
const { count } = JSON.parse(cachedData);
stars.value = count;
}
}
}
};
onMounted(() => {
fetchStars()
})
fetchStars();
});
return stars
return stars;
}

View File

@@ -21,7 +21,7 @@ export const CATEGORIES = [
'Decrypted Text',
'True Focus',
'Scroll Float',
'Scroll Reveal',
'Scroll Reveal'
]
},
{
@@ -35,7 +35,7 @@ export const CATEGORIES = [
'Count Up',
'Click Spark',
'Magnet',
'Cubes',
'Cubes'
]
},
{
@@ -56,8 +56,8 @@ export const CATEGORIES = [
'Glass Icons',
'Decay Card',
'Flowing Menu',
'Elastic Slider',
],
'Elastic Slider'
]
},
{
name: 'Backgrounds',
@@ -73,6 +73,7 @@ export const CATEGORIES = [
'Squares',
'Iridescence',
'Threads',
],
'Grid Motion'
]
}
];

View File

@@ -1,13 +1,13 @@
const animations = {
'fade-content': () => import("../demo/Animations/FadeContentDemo.vue"),
'animated-content': () => import("../demo/Animations/AnimatedContentDemo.vue"),
'pixel-transition': () => import("../demo/Animations/PixelTransitionDemo.vue"),
'glare-hover': () => import("../demo/Animations/GlareHoverDemo.vue"),
'magnet-lines': () => import("../demo/Animations/MagnetLinesDemo.vue"),
'click-spark': () => import("../demo/Animations/ClickSparkDemo.vue"),
'magnet': () => import("../demo/Animations/MagnetDemo.vue"),
'cubes': () => import("../demo/Animations/CubesDemo.vue"),
'count-up': () => import("../demo/Animations/CountUpDemo.vue"),
'fade-content': () => import('../demo/Animations/FadeContentDemo.vue'),
'animated-content': () => import('../demo/Animations/AnimatedContentDemo.vue'),
'pixel-transition': () => import('../demo/Animations/PixelTransitionDemo.vue'),
'glare-hover': () => import('../demo/Animations/GlareHoverDemo.vue'),
'magnet-lines': () => import('../demo/Animations/MagnetLinesDemo.vue'),
'click-spark': () => import('../demo/Animations/ClickSparkDemo.vue'),
'magnet': () => import('../demo/Animations/MagnetDemo.vue'),
'cubes': () => import('../demo/Animations/CubesDemo.vue'),
'count-up': () => import('../demo/Animations/CountUpDemo.vue')
};
const textAnimations = {
@@ -25,44 +25,46 @@ const textAnimations = {
'decrypted-text': () => import("../demo/TextAnimations/DecryptedTextDemo.vue"),
'true-focus': () => import("../demo/TextAnimations/TrueFocusDemo.vue"),
'scroll-float': () => import("../demo/TextAnimations/ScrollFloatDemo.vue"),
'scroll-reveal': ()=> import("../demo/TextAnimations/ScrollRevealDemo.vue"),
'scroll-reveal': ()=> import("../demo/TextAnimations/ScrollRevealDemo.vue")
};
const components = {
'masonry': () => import("../demo/Components/MasonryDemo.vue"),
'profile-card': () => import("../demo/Components/ProfileCardDemo.vue"),
'dock': () => import("../demo/Components/DockDemo.vue"),
'gooey-nav': () => import("../demo/Components/GooeyNavDemo.vue"),
'pixel-card': () => import("../demo/Components/PixelCardDemo.vue"),
'carousel': () => import("../demo/Components/CarouselDemo.vue"),
'spotlight-card': () => import("../demo/Components/SpotlightCardDemo.vue"),
'circular-gallery': () => import("../demo/Components/CircularGalleryDemo.vue"),
'flying-posters': () => import("../demo/Components/FlyingPostersDemo.vue"),
'card-swap': () => import("../demo/Components/CardSwapDemo.vue"),
'infinite-scroll': () => import("../demo/Components/InfiniteScrollDemo.vue"),
'glass-icons': () => import("../demo/Components/GlassIconsDemo.vue"),
'decay-card': () => import("../demo/Components/DecayCardDemo.vue"),
'flowing-menu': () => import("../demo/Components/FlowingMenuDemo.vue"),
'elastic-slider': () => import("../demo/Components/ElasticSliderDemo.vue"),
'tilted-card': () => import("../demo/Components/TiltedCardDemo.vue"),
'masonry': () => import('../demo/Components/MasonryDemo.vue'),
'profile-card': () => import('../demo/Components/ProfileCardDemo.vue'),
'dock': () => import('../demo/Components/DockDemo.vue'),
'gooey-nav': () => import('../demo/Components/GooeyNavDemo.vue'),
'pixel-card': () => import('../demo/Components/PixelCardDemo.vue'),
'carousel': () => import('../demo/Components/CarouselDemo.vue'),
'spotlight-card': () => import('../demo/Components/SpotlightCardDemo.vue'),
'circular-gallery': () => import('../demo/Components/CircularGalleryDemo.vue'),
'flying-posters': () => import('../demo/Components/FlyingPostersDemo.vue'),
'card-swap': () => import('../demo/Components/CardSwapDemo.vue'),
'infinite-scroll': () => import('../demo/Components/InfiniteScrollDemo.vue'),
'glass-icons': () => import('../demo/Components/GlassIconsDemo.vue'),
'decay-card': () => import('../demo/Components/DecayCardDemo.vue'),
'flowing-menu': () => import('../demo/Components/FlowingMenuDemo.vue'),
'elastic-slider': () => import('../demo/Components/ElasticSliderDemo.vue'),
'tilted-card': () => import('../demo/Components/TiltedCardDemo.vue')
};
const backgrounds = {
'dot-grid': () => import("../demo/Backgrounds/DotGridDemo.vue"),
'silk': () => import("../demo/Backgrounds/SilkDemo.vue"),
'lightning': () => import("../demo/Backgrounds/LightningDemo.vue"),
'letter-glitch': () => import("../demo/Backgrounds/LetterGlitchDemo.vue"),
'particles': () => import("../demo/Backgrounds/ParticlesDemo.vue"),
'waves': () => import("../demo/Backgrounds/WavesDemo.vue"),
'squares': () => import("../demo/Backgrounds/SquaresDemo.vue"),
'iridescence': () => import("../demo/Backgrounds/IridescenceDemo.vue"),
'threads': () => import("../demo/Backgrounds/ThreadsDemo.vue"),
'aurora': () => import("../demo/Backgrounds/AuroraDemo.vue"),
'beams': () => import("../demo/Backgrounds/BeamsDemo.vue"),
'dot-grid': () => import('../demo/Backgrounds/DotGridDemo.vue'),
'silk': () => import('../demo/Backgrounds/SilkDemo.vue'),
'lightning': () => import('../demo/Backgrounds/LightningDemo.vue'),
'letter-glitch': () => import('../demo/Backgrounds/LetterGlitchDemo.vue'),
'particles': () => import('../demo/Backgrounds/ParticlesDemo.vue'),
'waves': () => import('../demo/Backgrounds/WavesDemo.vue'),
'squares': () => import('../demo/Backgrounds/SquaresDemo.vue'),
'iridescence': () => import('../demo/Backgrounds/IridescenceDemo.vue'),
'threads': () => import('../demo/Backgrounds/ThreadsDemo.vue'),
'aurora': () => import('../demo/Backgrounds/AuroraDemo.vue'),
'beams': () => import('../demo/Backgrounds/BeamsDemo.vue'),
'grid-motion': () => import('../demo/Backgrounds/GridMotionDemo.vue')
};
export const componentMap = {
...animations,
...textAnimations,
...components,
...backgrounds,
...backgrounds
};

View File

@@ -1,5 +1,5 @@
import code from '@/content/Animations/AnimatedContent/AnimatedContent.vue?raw'
import type { CodeObject } from '@/types/code'
import code from '@/content/Animations/AnimatedContent/AnimatedContent.vue?raw';
import type { CodeObject } from '@/types/code';
export const animatedContent: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Animations/AnimatedContent`,
@@ -32,4 +32,4 @@ export const animatedContent: CodeObject = {
};
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Animations/ClickSpark/ClickSpark.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Animations/ClickSpark/ClickSpark.vue?raw';
import type { CodeObject } from '../../../types/code';
export const clickSpark: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Animations/ClickSpark`,
@@ -44,4 +44,4 @@ import ClickSpark from '@/content/Animations/ClickSpark/ClickSpark.vue'
}
</style>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@/content/Animations/CountUp/CountUp.vue?raw'
import type { CodeObject } from '@/types/code'
import code from '@/content/Animations/CountUp/CountUp.vue?raw';
import type { CodeObject } from '@/types/code';
export const countup: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Animations/CountUp`,
@@ -30,4 +30,4 @@ export const countup: CodeObject = {
};
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Animations/Cubes/Cubes.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Animations/Cubes/Cubes.vue?raw';
import type { CodeObject } from '../../../types/code';
export const cubes: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Animations/Cubes`,
@@ -29,4 +29,4 @@ export const cubes: CodeObject = {
import Cubes from "./Cubes.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Animations/FadeContent/FadeContent.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Animations/FadeContent/FadeContent.vue?raw';
import type { CodeObject } from '../../../types/code';
export const fadeContent: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Animations/FadeContent`,
@@ -24,4 +24,4 @@ export const fadeContent: CodeObject = {
import FadeContent from "./FadeContent.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@/content/Animations/GlareHover/GlareHover.vue?raw'
import type { CodeObject } from '@/types/code'
import code from '@/content/Animations/GlareHover/GlareHover.vue?raw';
import type { CodeObject } from '@/types/code';
export const glareHover: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Animations/GlareHover`,
@@ -26,4 +26,4 @@ export const glareHover: CodeObject = {
import GlareHover from "./GlareHover.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Animations/Magnet/Magnet.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Animations/Magnet/Magnet.vue?raw';
import type { CodeObject } from '../../../types/code';
export const magnet: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Animations/Magnet`,
@@ -45,4 +45,4 @@ import Magnet from '@/content/Animations/Magnet/Magnet.vue'
}
</style>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@/content/Animations/MagnetLines/MagnetLines.vue?raw'
import type { CodeObject } from '@/types/code'
import code from '@/content/Animations/MagnetLines/MagnetLines.vue?raw';
import type { CodeObject } from '@/types/code';
export const magnetLines: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Animations/MagnetLines`,
@@ -19,4 +19,4 @@ export const magnetLines: CodeObject = {
import MagnetLines from "./MagnetLines.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@/content/Animations/PixelTransition/PixelTransition.vue?raw'
import type { CodeObject } from '@/types/code'
import code from '@/content/Animations/PixelTransition/PixelTransition.vue?raw';
import type { CodeObject } from '@/types/code';
export const pixelTransition: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Animations/PixelTransition`,
@@ -26,4 +26,4 @@ export const pixelTransition: CodeObject = {
import PixelTransition from './PixelTransition.vue';
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/Aurora/Aurora.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/Aurora/Aurora.vue?raw';
import type { CodeObject } from '../../../types/code';
export const aurora: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Aurora`,
@@ -30,4 +30,4 @@ export const aurora: CodeObject = {
}
</style>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/Beams/Beams.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/Beams/Beams.vue?raw';
import type { CodeObject } from '../../../types/code';
export const beams: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Beams`,
@@ -33,4 +33,4 @@ export const beams: CodeObject = {
}
</style>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/DotGrid/DotGrid.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/DotGrid/DotGrid.vue?raw';
import type { CodeObject } from '../../../types/code';
export const dotGrid: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/DotGrid`,
@@ -36,4 +36,4 @@ export const dotGrid: CodeObject = {
}
</style>`,
code
}
};

View File

@@ -0,0 +1,45 @@
import code from "@content/Backgrounds/GridMotion/GridMotion.vue?raw";
import type { CodeObject } from "../../../types/code";
export const gridMotion: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/GridMotion`,
installation: `npm i gsap`,
usage: `<template>
<GridMotion
:items="items"
/>
</template>
<script setup lang="ts">
import GridMotion from "./GridMotion.vue";
const items = [
"Item 1",
`<div key='item-1'>Custom Content</div>`,
"https://images.unsplash.com/photo-1723403804231-f4e9b515fe9d?q=80&w=3870&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"Item 2",
`<div key='item-1'>Custom Content</div>`,
"Item 4",
`<div key='item-1'>Custom Content</div>`,
"https://images.unsplash.com/photo-1723403804231-f4e9b515fe9d?q=80&w=3870&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"Item 5",
`<div key='item-1'>Custom Content</div>`,
"Item 7",
`<div key='item-1'>Custom Content</div>`,
"https://images.unsplash.com/photo-1723403804231-f4e9b515fe9d?q=80&w=3870&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"Item 8",
`<div key='item-1'>Custom Content</div>`,
"Item 10",
`<div key='item-1'>Custom Content</div>`,
"https://images.unsplash.com/photo-1723403804231-f4e9b515fe9d?q=80&w=3870&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"Item 11",
`<div key='item-1'>Custom Content</div>`,
"Item 13",
`<div key='item-1'>Custom Content</div>`,
"https://images.unsplash.com/photo-1723403804231-f4e9b515fe9d?q=80&w=3870&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"Item 14",
// Add more items as needed
];
</script>`,
code,
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/Iridescence/Iridescence.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/Iridescence/Iridescence.vue?raw';
import type { CodeObject } from '../../../types/code';
export const iridescence: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Iridescence`,
@@ -19,4 +19,4 @@ export const iridescence: CodeObject = {
import Iridescence from "./Iridescence.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/LetterGlitch/LetterGlitch.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/LetterGlitch/LetterGlitch.vue?raw';
import type { CodeObject } from '../../../types/code';
export const letterGlitch: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/LetterGlitch`,
@@ -29,4 +29,4 @@ export const letterGlitch: CodeObject = {
}
</style>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/Lightning/Lightning.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/Lightning/Lightning.vue?raw';
import type { CodeObject } from '../../../types/code';
export const lightning: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Lightning`,
@@ -30,4 +30,4 @@ export const lightning: CodeObject = {
}
</style>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/Particles/Particles.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/Particles/Particles.vue?raw';
import type { CodeObject } from '../../../types/code';
export const particles: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Particles`,
@@ -36,4 +36,4 @@ export const particles: CodeObject = {
}
</style>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/Silk/Silk.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/Silk/Silk.vue?raw';
import type { CodeObject } from '../../../types/code';
export const silk: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Silk`,
@@ -30,4 +30,4 @@ export const silk: CodeObject = {
}
</style>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/Squares/Squares.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/Squares/Squares.vue?raw';
import type { CodeObject } from '../../../types/code';
export const squares: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Squares`,
@@ -19,4 +19,4 @@ export const squares: CodeObject = {
import Squares from "./Squares.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/Threads/Threads.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/Threads/Threads.vue?raw';
import type { CodeObject } from '../../../types/code';
export const threads: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Threads`,
@@ -19,4 +19,4 @@ export const threads: CodeObject = {
import Threads from "./Threads.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Backgrounds/Waves/Waves.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Backgrounds/Waves/Waves.vue?raw';
import type { CodeObject } from '../../../types/code';
export const waves: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Waves`,
@@ -35,4 +35,4 @@ export const waves: CodeObject = {
}
</style>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/CardSwap/CardSwap.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/CardSwap/CardSwap.vue?raw';
import type { CodeObject } from '../../../types/code';
export const cardSwap: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/CardSwap`,
@@ -51,4 +51,4 @@ export const cardSwap: CodeObject = {
};
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/Carousel/Carousel.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/Carousel/Carousel.vue?raw';
import type { CodeObject } from '../../../types/code';
export const carousel: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/Carousel`,
@@ -37,4 +37,4 @@ export const carousel: CodeObject = {
];
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/CircularGallery/CircularGallery.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/CircularGallery/CircularGallery.vue?raw';
import type { CodeObject } from '../../../types/code';
export const circularGallery: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/CircularGallery`,
@@ -24,4 +24,4 @@ export const circularGallery: CodeObject = {
import CircularGallery from "./CircularGallery.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/DecayCard/DecayCard.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/DecayCard/DecayCard.vue?raw';
import type { CodeObject } from '../../../types/code';
export const decayCard: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/DecayCard`,
@@ -20,4 +20,4 @@ export const decayCard: CodeObject = {
import DecayCard from "./DecayCard.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/Dock/Dock.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/Dock/Dock.vue?raw';
import type { CodeObject } from '../../../types/code';
export const dock: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/Dock`,
@@ -44,4 +44,4 @@ export const dock: CodeObject = {
];
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/ElasticSlider/ElasticSlider.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/ElasticSlider/ElasticSlider.vue?raw';
import type { CodeObject } from '../../../types/code';
export const elasticSlider: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/ElasticSlider`,
@@ -27,4 +27,4 @@ export const elasticSlider: CodeObject = {
import ElasticSlider from "./ElasticSlider.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/FlowingMenu/FlowingMenu.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/FlowingMenu/FlowingMenu.vue?raw';
import type { CodeObject } from '../../../types/code';
export const flowingMenu: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/FlowingMenu`,
@@ -19,4 +19,4 @@ export const flowingMenu: CodeObject = {
];
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/FlyingPosters/FlyingPosters.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/FlyingPosters/FlyingPosters.vue?raw';
import type { CodeObject } from '../../../types/code';
export const flyingPosters: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/FlyingPosters`,
@@ -34,4 +34,4 @@ export const flyingPosters: CodeObject = {
];
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/GlassIcons/GlassIcons.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/GlassIcons/GlassIcons.vue?raw';
import type { CodeObject } from '../../../types/code';
export const glassIcons: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/GlassIcons`,
@@ -20,4 +20,4 @@ export const glassIcons: CodeObject = {
];
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/GooeyNav/GooeyNav.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/GooeyNav/GooeyNav.vue?raw';
import type { CodeObject } from '../../../types/code';
export const gooeyNav: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/GooeyNav`,
@@ -37,4 +37,4 @@ export const gooeyNav: CodeObject = {
}
</style>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/InfiniteScroll/InfiniteScroll.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/InfiniteScroll/InfiniteScroll.vue?raw';
import type { CodeObject } from '../../../types/code';
export const infiniteScroll: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/InfiniteScroll`,
@@ -31,4 +31,4 @@ export const infiniteScroll: CodeObject = {
];
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/Masonry/Masonry.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/Masonry/Masonry.vue?raw';
import type { CodeObject } from '../../../types/code';
export const masonry: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/Masonry`,
@@ -29,4 +29,4 @@ const items = ref([
])
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/PixelCard/PixelCard.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/PixelCard/PixelCard.vue?raw';
import type { CodeObject } from '../../../types/code';
export const pixelCard: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/PixelCard`,
@@ -18,4 +18,4 @@ export const pixelCard: CodeObject = {
import PixelCard from "./PixelCard.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/ProfileCard/ProfileCard.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/ProfileCard/ProfileCard.vue?raw';
import type { CodeObject } from '../../../types/code';
export const profileCard: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/ProfileCard`,
@@ -28,4 +28,4 @@ export const profileCard: CodeObject = {
};
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/SpotlightCard/SpotlightCard.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/SpotlightCard/SpotlightCard.vue?raw';
import type { CodeObject } from '../../../types/code';
export const spotlightCard: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/SpotlightCard`,
@@ -16,4 +16,4 @@ export const spotlightCard: CodeObject = {
import SpotlightCard from "./SpotlightCard.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/Components/TiltedCard/TiltedCard.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/Components/TiltedCard/TiltedCard.vue?raw';
import type { CodeObject } from '../../../types/code';
export const tiltedCard: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/TiltedCard`,
@@ -31,4 +31,4 @@ export const tiltedCard: CodeObject = {
import TiltedCard from "./TiltedCard.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/TextAnimations/BlurText/BlurText.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/TextAnimations/BlurText/BlurText.vue?raw';
import type { CodeObject } from '../../../types/code';
export const blurText: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/BlurText`,
@@ -26,4 +26,4 @@ export const blurText: CodeObject = {
};
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/TextAnimations/CircularText/CircularText.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/TextAnimations/CircularText/CircularText.vue?raw';
import type { CodeObject } from '../../../types/code';
export const circularText: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/CircularText`,
@@ -17,4 +17,4 @@ export const circularText: CodeObject = {
import CircularText from "./CircularText.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/TextAnimations/CurvedLoop/CurvedLoop.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/TextAnimations/CurvedLoop/CurvedLoop.vue?raw';
import type { CodeObject } from '../../../types/code';
export const curvedLoop: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/CurvedLoop`,
@@ -17,4 +17,4 @@ export const curvedLoop: CodeObject = {
import CurvedLoop from "./CurvedLoop.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@/content/TextAnimations/DecryptedText/DecryptedText.vue?raw'
import type { CodeObject } from '@/types/code'
import code from '@/content/TextAnimations/DecryptedText/DecryptedText.vue?raw';
import type { CodeObject } from '@/types/code';
export const decryptedText: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/DecryptedText`,
@@ -21,4 +21,4 @@ export const decryptedText: CodeObject = {
import DecryptedText from "./DecryptedText.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@/content/TextAnimations/FallingText/FallingText.vue?raw'
import type { CodeObject } from '@/types/code'
import code from '@/content/TextAnimations/FallingText/FallingText.vue?raw';
import type { CodeObject } from '@/types/code';
export const fallingText: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/FallingText`,
@@ -19,4 +19,4 @@ export const fallingText: CodeObject = {
import FallingText from "./FallingText.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/TextAnimations/FuzzyText/FuzzyText.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/TextAnimations/FuzzyText/FuzzyText.vue?raw';
import type { CodeObject } from '../../../types/code';
export const fuzzyText: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/FuzzyText`,
@@ -19,4 +19,4 @@ export const fuzzyText: CodeObject = {
import FuzzyText from "./FuzzyText.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/TextAnimations/GradientText/GradientText.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/TextAnimations/GradientText/GradientText.vue?raw';
import type { CodeObject } from '../../../types/code';
export const gradientText: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/GradientText`,
@@ -17,4 +17,4 @@ export const gradientText: CodeObject = {
import GradientText from "./GradientText.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/TextAnimations/ScrollFloat/ScrollFloat.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/TextAnimations/ScrollFloat/ScrollFloat.vue?raw';
import type { CodeObject } from '../../../types/code';
export const scrollFloatCode: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/ScrollFloat`,
@@ -22,4 +22,4 @@ export const scrollFloatCode: CodeObject = {
import ScrollFloat from "./ScrollFloat.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/TextAnimations/ShinyText/ShinyText.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/TextAnimations/ShinyText/ShinyText.vue?raw';
import type { CodeObject } from '../../../types/code';
export const shinyText: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/ShinyText`,
@@ -16,4 +16,4 @@ export const shinyText: CodeObject = {
import ShinyText from "./ShinyText.vue";
</script>`,
code
}
};

View File

@@ -1,6 +1,6 @@
// Fun fact: this is the first component ever made for Vue Bits!
import code from '@content/TextAnimations/SplitText/SplitText.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/TextAnimations/SplitText/SplitText.vue?raw';
import type { CodeObject } from '../../../types/code';
export const splitText: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/SplitText`,
@@ -30,4 +30,4 @@ export const splitText: CodeObject = {
};
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@/content/TextAnimations/TextCursor/TextCursor.vue?raw'
import type { CodeObject } from '@/types/code'
import code from '@/content/TextAnimations/TextCursor/TextCursor.vue?raw';
import type { CodeObject } from '@/types/code';
export const textCursor: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/TextCursor`,
@@ -21,4 +21,4 @@ export const textCursor: CodeObject = {
import TextCursor from "./TextCursor.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/TextAnimations/TextPressure/TextPressure.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/TextAnimations/TextPressure/TextPressure.vue?raw';
import type { CodeObject } from '../../../types/code';
export const textPressure: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/TextPressure`,
@@ -22,4 +22,4 @@ export const textPressure: CodeObject = {
import TextPressure from "./TextPressure.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@/content/TextAnimations/TextTrail/TextTrail.vue?raw'
import type { CodeObject } from '@/types/code'
import code from '@/content/TextAnimations/TextTrail/TextTrail.vue?raw';
import type { CodeObject } from '@/types/code';
export const textTrail: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/TextTrail`,
@@ -20,4 +20,4 @@ export const textTrail: CodeObject = {
import TextTrail from "./TextTrail.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from "@/content/TextAnimations/TrueFocus/TrueFocus.vue?raw";
import type { CodeObject } from "../../../types/code";
import code from '@/content/TextAnimations/TrueFocus/TrueFocus.vue?raw';
import type { CodeObject } from '../../../types/code';
export const trueFocus: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/TrueFocus`,
@@ -18,5 +18,5 @@ export const trueFocus: CodeObject = {
<script setup lang="ts">
import TrueFocus from "./TrueFocus.vue";
</script>`,
code,
code
};

View File

@@ -1,22 +1,22 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { gsap } from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger)
gsap.registerPlugin(ScrollTrigger);
interface AnimatedContentProps {
distance?: number
direction?: 'vertical' | 'horizontal'
reverse?: boolean
duration?: number
ease?: string | ((progress: number) => number)
initialOpacity?: number
animateOpacity?: boolean
scale?: number
threshold?: number
delay?: number
className?: string
distance?: number;
direction?: 'vertical' | 'horizontal';
reverse?: boolean;
duration?: number;
ease?: string | ((progress: number) => number);
initialOpacity?: number;
animateOpacity?: boolean;
scale?: number;
threshold?: number;
delay?: number;
className?: string;
}
const props = withDefaults(defineProps<AnimatedContentProps>(), {
@@ -31,27 +31,27 @@ const props = withDefaults(defineProps<AnimatedContentProps>(), {
threshold: 0.1,
delay: 0,
className: ''
})
});
const emit = defineEmits<{
complete: []
}>()
complete: [];
}>();
const containerRef = ref<HTMLDivElement>()
const containerRef = ref<HTMLDivElement>();
onMounted(() => {
const el = containerRef.value
if (!el) return
const el = containerRef.value;
if (!el) return;
const axis = props.direction === 'horizontal' ? 'x' : 'y'
const offset = props.reverse ? -props.distance : props.distance
const startPct = (1 - props.threshold) * 100
const axis = props.direction === 'horizontal' ? 'x' : 'y';
const offset = props.reverse ? -props.distance : props.distance;
const startPct = (1 - props.threshold) * 100;
gsap.set(el, {
[axis]: offset,
scale: props.scale,
opacity: props.animateOpacity ? props.initialOpacity : 1,
})
opacity: props.animateOpacity ? props.initialOpacity : 1
});
gsap.to(el, {
[axis]: 0,
@@ -65,10 +65,10 @@ onMounted(() => {
trigger: el,
start: `top ${startPct}%`,
toggleActions: 'play none none none',
once: true,
},
})
})
once: true
}
});
});
watch(
() => [
@@ -81,24 +81,24 @@ watch(
props.animateOpacity,
props.scale,
props.threshold,
props.delay,
props.delay
],
() => {
const el = containerRef.value
if (!el) return
const el = containerRef.value;
if (!el) return;
ScrollTrigger.getAll().forEach((t) => t.kill())
gsap.killTweensOf(el)
ScrollTrigger.getAll().forEach(t => t.kill());
gsap.killTweensOf(el);
const axis = props.direction === 'horizontal' ? 'x' : 'y'
const offset = props.reverse ? -props.distance : props.distance
const startPct = (1 - props.threshold) * 100
const axis = props.direction === 'horizontal' ? 'x' : 'y';
const offset = props.reverse ? -props.distance : props.distance;
const startPct = (1 - props.threshold) * 100;
gsap.set(el, {
[axis]: offset,
scale: props.scale,
opacity: props.animateOpacity ? props.initialOpacity : 1,
})
opacity: props.animateOpacity ? props.initialOpacity : 1
});
gsap.to(el, {
[axis]: 0,
@@ -112,27 +112,24 @@ watch(
trigger: el,
start: `top ${startPct}%`,
toggleActions: 'play none none none',
once: true,
},
})
once: true
}
});
},
{ deep: true }
)
);
onUnmounted(() => {
const el = containerRef.value
const el = containerRef.value;
if (el) {
ScrollTrigger.getAll().forEach((t) => t.kill())
gsap.killTweensOf(el)
ScrollTrigger.getAll().forEach(t => t.kill());
gsap.killTweensOf(el);
}
})
});
</script>
<template>
<div
ref="containerRef"
:class="`animated-content ${props.className}`"
>
<div ref="containerRef" :class="`animated-content ${props.className}`">
<slot />
</div>
</template>

View File

@@ -1,35 +1,29 @@
<template>
<div
ref="containerRef"
class="relative w-full h-full"
@click="handleClick"
>
<canvas
ref="canvasRef"
class="absolute inset-0 pointer-events-none"
/>
<div ref="containerRef" class="relative w-full h-full" @click="handleClick">
<canvas ref="canvasRef" class="absolute inset-0 pointer-events-none" />
<slot />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
interface Spark {
x: number
y: number
angle: number
startTime: number
x: number;
y: number;
angle: number;
startTime: number;
}
interface Props {
sparkColor?: string
sparkSize?: number
sparkRadius?: number
sparkCount?: number
duration?: number
easing?: "linear" | "ease-in" | "ease-out" | "ease-in-out"
extraScale?: number
sparkColor?: string;
sparkSize?: number;
sparkRadius?: number;
sparkCount?: number;
duration?: number;
easing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';
extraScale?: number;
}
const props = withDefaults(defineProps<Props>(), {
@@ -40,149 +34,152 @@ const props = withDefaults(defineProps<Props>(), {
duration: 400,
easing: 'ease-out',
extraScale: 1.0
})
});
const containerRef = ref<HTMLDivElement | null>(null)
const canvasRef = ref<HTMLCanvasElement | null>(null)
const sparks = ref<Spark[]>([])
const startTimeRef = ref<number | null>(null)
const animationId = ref<number | null>(null)
const containerRef = ref<HTMLDivElement | null>(null);
const canvasRef = ref<HTMLCanvasElement | null>(null);
const sparks = ref<Spark[]>([]);
const startTimeRef = ref<number | null>(null);
const animationId = ref<number | null>(null);
const easeFunc = computed(() => {
return (t: number) => {
switch (props.easing) {
case "linear":
return t
case "ease-in":
return t * t
case "ease-in-out":
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
case 'linear':
return t;
case 'ease-in':
return t * t;
case 'ease-in-out':
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
default:
return t * (2 - t)
return t * (2 - t);
}
}
})
};
});
const handleClick = (e: MouseEvent) => {
const canvas = canvasRef.value
if (!canvas) return
const rect = canvas.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
const canvas = canvasRef.value;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const now = performance.now()
const now = performance.now();
const newSparks: Spark[] = Array.from({ length: props.sparkCount }, (_, i) => ({
x,
y,
angle: (2 * Math.PI * i) / props.sparkCount,
startTime: now,
}))
startTime: now
}));
sparks.value.push(...newSparks)
}
sparks.value.push(...newSparks);
};
const draw = (timestamp: number) => {
if (!startTimeRef.value) {
startTimeRef.value = timestamp
startTimeRef.value = timestamp;
}
const canvas = canvasRef.value
const ctx = canvas?.getContext('2d')
if (!ctx || !canvas) return
ctx.clearRect(0, 0, canvas.width, canvas.height)
const canvas = canvasRef.value;
const ctx = canvas?.getContext('2d');
if (!ctx || !canvas) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
sparks.value = sparks.value.filter((spark: Spark) => {
const elapsed = timestamp - spark.startTime
const elapsed = timestamp - spark.startTime;
if (elapsed >= props.duration) {
return false
return false;
}
const progress = elapsed / props.duration
const eased = easeFunc.value(progress)
const progress = elapsed / props.duration;
const eased = easeFunc.value(progress);
const distance = eased * props.sparkRadius * props.extraScale
const lineLength = props.sparkSize * (1 - eased)
const distance = eased * props.sparkRadius * props.extraScale;
const lineLength = props.sparkSize * (1 - eased);
const x1 = spark.x + distance * Math.cos(spark.angle)
const y1 = spark.y + distance * Math.sin(spark.angle)
const x2 = spark.x + (distance + lineLength) * Math.cos(spark.angle)
const y2 = spark.y + (distance + lineLength) * Math.sin(spark.angle)
const x1 = spark.x + distance * Math.cos(spark.angle);
const y1 = spark.y + distance * Math.sin(spark.angle);
const x2 = spark.x + (distance + lineLength) * Math.cos(spark.angle);
const y2 = spark.y + (distance + lineLength) * Math.sin(spark.angle);
ctx.strokeStyle = props.sparkColor
ctx.lineWidth = 2
ctx.beginPath()
ctx.moveTo(x1, y1)
ctx.lineTo(x2, y2)
ctx.stroke()
ctx.strokeStyle = props.sparkColor;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
return true
})
return true;
});
animationId.value = requestAnimationFrame(draw)
}
animationId.value = requestAnimationFrame(draw);
};
const resizeCanvas = () => {
const canvas = canvasRef.value
if (!canvas) return
const canvas = canvasRef.value;
if (!canvas) return;
const parent = canvas.parentElement
if (!parent) return
const parent = canvas.parentElement;
if (!parent) return;
const { width, height } = parent.getBoundingClientRect()
const { width, height } = parent.getBoundingClientRect();
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width
canvas.height = height
canvas.width = width;
canvas.height = height;
}
}
};
let resizeTimeout: number
let resizeTimeout: number;
const handleResize = () => {
clearTimeout(resizeTimeout)
resizeTimeout = setTimeout(resizeCanvas, 100)
}
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(resizeCanvas, 100);
};
let resizeObserver: ResizeObserver | null = null
let resizeObserver: ResizeObserver | null = null;
onMounted(() => {
const canvas = canvasRef.value
if (!canvas) return
const canvas = canvasRef.value;
if (!canvas) return;
const parent = canvas.parentElement
if (!parent) return
const parent = canvas.parentElement;
if (!parent) return;
resizeObserver = new ResizeObserver(handleResize)
resizeObserver.observe(parent)
resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(parent);
resizeCanvas()
resizeCanvas();
animationId.value = requestAnimationFrame(draw)
})
animationId.value = requestAnimationFrame(draw);
});
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect()
resizeObserver.disconnect();
}
clearTimeout(resizeTimeout)
if (animationId.value) {
cancelAnimationFrame(animationId.value)
}
})
clearTimeout(resizeTimeout);
watch([
() => props.sparkColor,
() => props.sparkSize,
() => props.sparkRadius,
() => props.sparkCount,
() => props.duration,
easeFunc,
() => props.extraScale
], () => {
if (animationId.value) {
cancelAnimationFrame(animationId.value)
cancelAnimationFrame(animationId.value);
}
animationId.value = requestAnimationFrame(draw)
})
});
watch(
[
() => props.sparkColor,
() => props.sparkSize,
() => props.sparkRadius,
() => props.sparkCount,
() => props.duration,
easeFunc,
() => props.extraScale
],
() => {
if (animationId.value) {
cancelAnimationFrame(animationId.value);
}
animationId.value = requestAnimationFrame(draw);
}
);
</script>

View File

@@ -3,161 +3,164 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
interface Props {
to: number
from?: number
direction?: "up" | "down"
delay?: number
duration?: number
className?: string
startWhen?: boolean
separator?: string
onStart?: () => void
onEnd?: () => void
to: number;
from?: number;
direction?: 'up' | 'down';
delay?: number;
duration?: number;
className?: string;
startWhen?: boolean;
separator?: string;
onStart?: () => void;
onEnd?: () => void;
}
const props = withDefaults(defineProps<Props>(), {
from: 0,
direction: "up",
direction: 'up',
delay: 0,
duration: 2,
className: "",
className: '',
startWhen: true,
separator: ""
})
separator: ''
});
const elementRef = ref<HTMLSpanElement | null>(null)
const currentValue = ref(props.direction === "down" ? props.to : props.from)
const isInView = ref(false)
const animationId = ref<number | null>(null)
const hasStarted = ref(false)
const elementRef = ref<HTMLSpanElement | null>(null);
const currentValue = ref(props.direction === 'down' ? props.to : props.from);
const isInView = ref(false);
const animationId = ref<number | null>(null);
const hasStarted = ref(false);
let intersectionObserver: IntersectionObserver | null = null
let intersectionObserver: IntersectionObserver | null = null;
const damping = computed(() => 20 + 40 * (1 / props.duration))
const stiffness = computed(() => 100 * (1 / props.duration))
const damping = computed(() => 20 + 40 * (1 / props.duration));
const stiffness = computed(() => 100 * (1 / props.duration));
let velocity = 0
let startTime = 0
let velocity = 0;
let startTime = 0;
const formatNumber = (value: number) => {
const options = {
useGrouping: !!props.separator,
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}
maximumFractionDigits: 0
};
const formattedNumber = Intl.NumberFormat("en-US", options).format(
Number(value.toFixed(0))
)
const formattedNumber = Intl.NumberFormat('en-US', options).format(Number(value.toFixed(0)));
return props.separator
? formattedNumber.replace(/,/g, props.separator)
: formattedNumber
}
return props.separator ? formattedNumber.replace(/,/g, props.separator) : formattedNumber;
};
const updateDisplay = () => {
if (elementRef.value) {
elementRef.value.textContent = formatNumber(currentValue.value)
elementRef.value.textContent = formatNumber(currentValue.value);
}
}
};
const springAnimation = (timestamp: number) => {
if (!startTime) startTime = timestamp
if (!startTime) startTime = timestamp;
const target = props.direction === "down" ? props.from : props.to
const current = currentValue.value
const target = props.direction === 'down' ? props.from : props.to;
const current = currentValue.value;
const displacement = target - current
const springForce = displacement * stiffness.value
const dampingForce = velocity * damping.value
const acceleration = springForce - dampingForce
const displacement = target - current;
const springForce = displacement * stiffness.value;
const dampingForce = velocity * damping.value;
const acceleration = springForce - dampingForce;
velocity += acceleration * 0.016 // Assuming 60fps
currentValue.value += velocity * 0.016
velocity += acceleration * 0.016; // Assuming 60fps
currentValue.value += velocity * 0.016;
updateDisplay()
updateDisplay();
if (Math.abs(displacement) > 0.01 || Math.abs(velocity) > 0.01) {
animationId.value = requestAnimationFrame(springAnimation)
animationId.value = requestAnimationFrame(springAnimation);
} else {
currentValue.value = target
updateDisplay()
animationId.value = null
currentValue.value = target;
updateDisplay();
animationId.value = null;
if (props.onEnd) {
props.onEnd()
props.onEnd();
}
}
}
};
const startAnimation = () => {
if (hasStarted.value || !isInView.value || !props.startWhen) return
if (hasStarted.value || !isInView.value || !props.startWhen) return;
hasStarted.value = true
hasStarted.value = true;
if (props.onStart) {
props.onStart()
props.onStart();
}
setTimeout(() => {
startTime = 0
velocity = 0
animationId.value = requestAnimationFrame(springAnimation)
}, props.delay * 1000)
}
startTime = 0;
velocity = 0;
animationId.value = requestAnimationFrame(springAnimation);
}, props.delay * 1000);
};
const setupIntersectionObserver = () => {
if (!elementRef.value) return
if (!elementRef.value) return;
intersectionObserver = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !isInView.value) {
isInView.value = true
startAnimation()
isInView.value = true;
startAnimation();
}
},
{
threshold: 0,
rootMargin: "0px"
rootMargin: '0px'
}
)
);
intersectionObserver.observe(elementRef.value)
}
intersectionObserver.observe(elementRef.value);
};
const cleanup = () => {
if (animationId.value) {
cancelAnimationFrame(animationId.value)
animationId.value = null
cancelAnimationFrame(animationId.value);
animationId.value = null;
}
if (intersectionObserver) {
intersectionObserver.disconnect()
intersectionObserver = null
intersectionObserver.disconnect();
intersectionObserver = null;
}
}
};
watch([() => props.from, () => props.to, () => props.direction], () => {
currentValue.value = props.direction === "down" ? props.to : props.from
updateDisplay()
hasStarted.value = false
}, { immediate: true })
watch(
[() => props.from, () => props.to, () => props.direction],
() => {
currentValue.value = props.direction === 'down' ? props.to : props.from;
updateDisplay();
hasStarted.value = false;
},
{ immediate: true }
);
watch(() => props.startWhen, () => {
if (props.startWhen && isInView.value && !hasStarted.value) {
startAnimation()
watch(
() => props.startWhen,
() => {
if (props.startWhen && isInView.value && !hasStarted.value) {
startAnimation();
}
}
})
);
onMounted(() => {
updateDisplay()
setupIntersectionObserver()
})
updateDisplay();
setupIntersectionObserver();
});
onUnmounted(() => {
cleanup()
})
</script>
cleanup();
});
</script>

View File

@@ -2,46 +2,74 @@
<div class="relative w-1/2 max-md:w-11/12 aspect-square" :style="wrapperStyle">
<div ref="sceneRef" class="grid w-full h-full" :style="sceneStyle">
<template v-for="(_, r) in cells" :key="`row-${r}`">
<div v-for="(__, c) in cells" :key="`${r}-${c}`"
class="cube relative w-full h-full aspect-square [transform-style:preserve-3d]" :data-row="r" :data-col="c">
<div
v-for="(__, c) in cells"
:key="`${r}-${c}`"
class="cube relative w-full h-full aspect-square [transform-style:preserve-3d]"
:data-row="r"
:data-col="c"
>
<span class="absolute pointer-events-none -inset-9" />
<div class="cube-face absolute inset-0 flex items-center justify-center" :style="{
background: 'var(--cube-face-bg)',
border: 'var(--cube-face-border)',
boxShadow: 'var(--cube-face-shadow)',
transform: 'translateY(-50%) rotateX(90deg)',
}" />
<div class="cube-face absolute inset-0 flex items-center justify-center" :style="{
background: 'var(--cube-face-bg)',
border: 'var(--cube-face-border)',
boxShadow: 'var(--cube-face-shadow)',
transform: 'translateY(50%) rotateX(-90deg)',
}" />
<div class="cube-face absolute inset-0 flex items-center justify-center" :style="{
background: 'var(--cube-face-bg)',
border: 'var(--cube-face-border)',
boxShadow: 'var(--cube-face-shadow)',
transform: 'translateX(-50%) rotateY(-90deg)',
}" />
<div class="cube-face absolute inset-0 flex items-center justify-center" :style="{
background: 'var(--cube-face-bg)',
border: 'var(--cube-face-border)',
boxShadow: 'var(--cube-face-shadow)',
transform: 'translateX(50%) rotateY(90deg)',
}" />
<div class="cube-face absolute inset-0 flex items-center justify-center" :style="{
background: 'var(--cube-face-bg)',
border: 'var(--cube-face-border)',
boxShadow: 'var(--cube-face-shadow)',
transform: 'rotateY(-90deg) translateX(50%) rotateY(90deg)',
}" />
<div class="cube-face absolute inset-0 flex items-center justify-center" :style="{
background: 'var(--cube-face-bg)',
border: 'var(--cube-face-border)',
boxShadow: 'var(--cube-face-shadow)',
transform: 'rotateY(90deg) translateX(-50%) rotateY(-90deg)',
}" />
<div
class="cube-face absolute inset-0 flex items-center justify-center"
:style="{
background: 'var(--cube-face-bg)',
border: 'var(--cube-face-border)',
boxShadow: 'var(--cube-face-shadow)',
transform: 'translateY(-50%) rotateX(90deg)'
}"
/>
<div
class="cube-face absolute inset-0 flex items-center justify-center"
:style="{
background: 'var(--cube-face-bg)',
border: 'var(--cube-face-border)',
boxShadow: 'var(--cube-face-shadow)',
transform: 'translateY(50%) rotateX(-90deg)'
}"
/>
<div
class="cube-face absolute inset-0 flex items-center justify-center"
:style="{
background: 'var(--cube-face-bg)',
border: 'var(--cube-face-border)',
boxShadow: 'var(--cube-face-shadow)',
transform: 'translateX(-50%) rotateY(-90deg)'
}"
/>
<div
class="cube-face absolute inset-0 flex items-center justify-center"
:style="{
background: 'var(--cube-face-bg)',
border: 'var(--cube-face-border)',
boxShadow: 'var(--cube-face-shadow)',
transform: 'translateX(50%) rotateY(90deg)'
}"
/>
<div
class="cube-face absolute inset-0 flex items-center justify-center"
:style="{
background: 'var(--cube-face-bg)',
border: 'var(--cube-face-border)',
boxShadow: 'var(--cube-face-shadow)',
transform: 'rotateY(-90deg) translateX(50%) rotateY(90deg)'
}"
/>
<div
class="cube-face absolute inset-0 flex items-center justify-center"
:style="{
background: 'var(--cube-face-bg)',
border: 'var(--cube-face-border)',
boxShadow: 'var(--cube-face-shadow)',
transform: 'rotateY(90deg) translateX(-50%) rotateY(-90deg)'
}"
/>
</div>
</template>
</div>
@@ -49,34 +77,34 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, withDefaults } from 'vue'
import gsap from 'gsap'
import { ref, computed, onMounted, onUnmounted, withDefaults } from 'vue';
import gsap from 'gsap';
interface Gap {
row: number
col: number
row: number;
col: number;
}
interface Duration {
enter: number
leave: number
enter: number;
leave: number;
}
interface Props {
gridSize?: number
cubeSize?: number
maxAngle?: number
radius?: number
easing?: gsap.EaseString
duration?: Duration
cellGap?: number | Gap
borderStyle?: string
faceColor?: string
shadow?: boolean | string
autoAnimate?: boolean
rippleOnClick?: boolean
rippleColor?: string
rippleSpeed?: number
gridSize?: number;
cubeSize?: number;
maxAngle?: number;
radius?: number;
easing?: gsap.EaseString;
duration?: Duration;
cellGap?: number | Gap;
borderStyle?: string;
faceColor?: string;
shadow?: boolean | string;
autoAnimate?: boolean;
rippleOnClick?: boolean;
rippleColor?: string;
rippleSpeed?: number;
}
const props = withDefaults(defineProps<Props>(), {
@@ -91,37 +119,37 @@ const props = withDefaults(defineProps<Props>(), {
autoAnimate: true,
rippleOnClick: true,
rippleColor: '#fff',
rippleSpeed: 2,
})
rippleSpeed: 2
});
const sceneRef = ref<HTMLDivElement | null>(null)
const rafRef = ref<number | null>(null)
const idleTimerRef = ref<number | null>(null)
const userActiveRef = ref(false)
const simPosRef = ref<{ x: number; y: number }>({ x: 0, y: 0 })
const simTargetRef = ref<{ x: number; y: number }>({ x: 0, y: 0 })
const simRAFRef = ref<number | null>(null)
const sceneRef = ref<HTMLDivElement | null>(null);
const rafRef = ref<number | null>(null);
const idleTimerRef = ref<number | null>(null);
const userActiveRef = ref(false);
const simPosRef = ref<{ x: number; y: number }>({ x: 0, y: 0 });
const simTargetRef = ref<{ x: number; y: number }>({ x: 0, y: 0 });
const simRAFRef = ref<number | null>(null);
const colGap = computed(() => {
return typeof props.cellGap === 'number'
? `${props.cellGap}px`
: (props.cellGap as Gap)?.col !== undefined
? `${(props.cellGap as Gap).col}px`
: '5%'
})
: '5%';
});
const rowGap = computed(() => {
return typeof props.cellGap === 'number'
? `${props.cellGap}px`
: (props.cellGap as Gap)?.row !== undefined
? `${(props.cellGap as Gap).row}px`
: '5%'
})
: '5%';
});
const enterDur = computed(() => props.duration.enter)
const leaveDur = computed(() => props.duration.leave)
const enterDur = computed(() => props.duration.enter);
const leaveDur = computed(() => props.duration.leave);
const cells = computed(() => Array.from({ length: props.gridSize }))
const cells = computed(() => Array.from({ length: props.gridSize }));
const sceneStyle = computed(() => ({
gridTemplateColumns: props.cubeSize
@@ -133,189 +161,184 @@ const sceneStyle = computed(() => ({
columnGap: colGap.value,
rowGap: rowGap.value,
perspective: '99999999px',
gridAutoRows: '1fr',
}))
gridAutoRows: '1fr'
}));
const wrapperStyle = computed(() => ({
'--cube-face-border': props.borderStyle,
'--cube-face-bg': props.faceColor,
'--cube-face-shadow':
props.shadow === true ? '0 0 6px rgba(0,0,0,.5)' : props.shadow || 'none',
'--cube-face-shadow': props.shadow === true ? '0 0 6px rgba(0,0,0,.5)' : props.shadow || 'none',
...(props.cubeSize
? {
width: `${props.gridSize * props.cubeSize}px`,
height: `${props.gridSize * props.cubeSize}px`,
}
: {}),
}))
width: `${props.gridSize * props.cubeSize}px`,
height: `${props.gridSize * props.cubeSize}px`
}
: {})
}));
const tiltAt = (rowCenter: number, colCenter: number) => {
if (!sceneRef.value) return
if (!sceneRef.value) return;
sceneRef.value.querySelectorAll<HTMLDivElement>('.cube').forEach((cube) => {
const r = +(cube.dataset.row!)
const c = +(cube.dataset.col!)
const dist = Math.hypot(r - rowCenter, c - colCenter)
sceneRef.value.querySelectorAll<HTMLDivElement>('.cube').forEach(cube => {
const r = +cube.dataset.row!;
const c = +cube.dataset.col!;
const dist = Math.hypot(r - rowCenter, c - colCenter);
if (dist <= props.radius) {
const pct = 1 - dist / props.radius
const angle = pct * props.maxAngle
const pct = 1 - dist / props.radius;
const angle = pct * props.maxAngle;
gsap.to(cube, {
duration: enterDur.value,
ease: props.easing,
overwrite: true,
rotateX: -angle,
rotateY: angle,
})
rotateY: angle
});
} else {
gsap.to(cube, {
duration: leaveDur.value,
ease: 'power3.out',
overwrite: true,
rotateX: 0,
rotateY: 0,
})
rotateY: 0
});
}
})
}
});
};
const onPointerMove = (e: PointerEvent) => {
userActiveRef.value = true
if (idleTimerRef.value) clearTimeout(idleTimerRef.value)
userActiveRef.value = true;
if (idleTimerRef.value) clearTimeout(idleTimerRef.value);
const rect = sceneRef.value!.getBoundingClientRect()
const cellW = rect.width / props.gridSize
const cellH = rect.height / props.gridSize
const colCenter = (e.clientX - rect.left) / cellW
const rowCenter = (e.clientY - rect.top) / cellH
const rect = sceneRef.value!.getBoundingClientRect();
const cellW = rect.width / props.gridSize;
const cellH = rect.height / props.gridSize;
const colCenter = (e.clientX - rect.left) / cellW;
const rowCenter = (e.clientY - rect.top) / cellH;
if (rafRef.value) cancelAnimationFrame(rafRef.value)
rafRef.value = requestAnimationFrame(() =>
tiltAt(rowCenter, colCenter)
)
if (rafRef.value) cancelAnimationFrame(rafRef.value);
rafRef.value = requestAnimationFrame(() => tiltAt(rowCenter, colCenter));
idleTimerRef.value = setTimeout(() => {
userActiveRef.value = false
}, 3000)
}
userActiveRef.value = false;
}, 3000);
};
const resetAll = () => {
if (!sceneRef.value) return
sceneRef.value.querySelectorAll<HTMLDivElement>('.cube').forEach((cube) =>
if (!sceneRef.value) return;
sceneRef.value.querySelectorAll<HTMLDivElement>('.cube').forEach(cube =>
gsap.to(cube, {
duration: leaveDur.value,
rotateX: 0,
rotateY: 0,
ease: 'power3.out',
ease: 'power3.out'
})
)
}
);
};
const onClick = (e: MouseEvent) => {
if (!props.rippleOnClick || !sceneRef.value) return
if (!props.rippleOnClick || !sceneRef.value) return;
const rect = sceneRef.value.getBoundingClientRect()
const cellW = rect.width / props.gridSize
const cellH = rect.height / props.gridSize
const colHit = Math.floor((e.clientX - rect.left) / cellW)
const rowHit = Math.floor((e.clientY - rect.top) / cellH)
const rect = sceneRef.value.getBoundingClientRect();
const cellW = rect.width / props.gridSize;
const cellH = rect.height / props.gridSize;
const colHit = Math.floor((e.clientX - rect.left) / cellW);
const rowHit = Math.floor((e.clientY - rect.top) / cellH);
const baseRingDelay = 0.15
const baseAnimDur = 0.3
const baseHold = 0.6
const baseRingDelay = 0.15;
const baseAnimDur = 0.3;
const baseHold = 0.6;
const spreadDelay = baseRingDelay / props.rippleSpeed
const animDuration = baseAnimDur / props.rippleSpeed
const holdTime = baseHold / props.rippleSpeed
const spreadDelay = baseRingDelay / props.rippleSpeed;
const animDuration = baseAnimDur / props.rippleSpeed;
const holdTime = baseHold / props.rippleSpeed;
const rings: Record<number, HTMLDivElement[]> = {}
sceneRef.value.querySelectorAll<HTMLDivElement>('.cube').forEach((cube) => {
const r = +(cube.dataset.row!)
const c = +(cube.dataset.col!)
const dist = Math.hypot(r - rowHit, c - colHit)
const ring = Math.round(dist)
if (!rings[ring]) rings[ring] = []
rings[ring].push(cube)
})
const rings: Record<number, HTMLDivElement[]> = {};
sceneRef.value.querySelectorAll<HTMLDivElement>('.cube').forEach(cube => {
const r = +cube.dataset.row!;
const c = +cube.dataset.col!;
const dist = Math.hypot(r - rowHit, c - colHit);
const ring = Math.round(dist);
if (!rings[ring]) rings[ring] = [];
rings[ring].push(cube);
});
Object.keys(rings)
.map(Number)
.sort((a, b) => a - b)
.forEach((ring) => {
const delay = ring * spreadDelay
const faces = rings[ring].flatMap((cube) =>
Array.from(cube.querySelectorAll<HTMLElement>('.cube-face'))
)
.forEach(ring => {
const delay = ring * spreadDelay;
const faces = rings[ring].flatMap(cube => Array.from(cube.querySelectorAll<HTMLElement>('.cube-face')));
gsap.to(faces, {
backgroundColor: props.rippleColor,
duration: animDuration,
delay,
ease: 'power3.out',
})
ease: 'power3.out'
});
gsap.to(faces, {
backgroundColor: props.faceColor,
duration: animDuration,
delay: delay + animDuration + holdTime,
ease: 'power3.out',
})
})
}
ease: 'power3.out'
});
});
};
const startAutoAnimation = () => {
if (!props.autoAnimate || !sceneRef.value) return
if (!props.autoAnimate || !sceneRef.value) return;
simPosRef.value = {
x: Math.random() * props.gridSize,
y: Math.random() * props.gridSize,
}
y: Math.random() * props.gridSize
};
simTargetRef.value = {
x: Math.random() * props.gridSize,
y: Math.random() * props.gridSize,
}
y: Math.random() * props.gridSize
};
const speed = 0.02
const speed = 0.02;
const loop = () => {
if (!userActiveRef.value) {
const pos = simPosRef.value
const tgt = simTargetRef.value
pos.x += (tgt.x - pos.x) * speed
pos.y += (tgt.y - pos.y) * speed
tiltAt(pos.y, pos.x)
const pos = simPosRef.value;
const tgt = simTargetRef.value;
pos.x += (tgt.x - pos.x) * speed;
pos.y += (tgt.y - pos.y) * speed;
tiltAt(pos.y, pos.x);
if (Math.hypot(pos.x - tgt.x, pos.y - tgt.y) < 0.1) {
simTargetRef.value = {
x: Math.random() * props.gridSize,
y: Math.random() * props.gridSize,
}
y: Math.random() * props.gridSize
};
}
}
simRAFRef.value = requestAnimationFrame(loop)
}
simRAFRef.value = requestAnimationFrame(loop)
}
simRAFRef.value = requestAnimationFrame(loop);
};
simRAFRef.value = requestAnimationFrame(loop);
};
onMounted(() => {
const el = sceneRef.value
if (!el) return
const el = sceneRef.value;
if (!el) return;
el.addEventListener('pointermove', onPointerMove)
el.addEventListener('pointerleave', resetAll)
el.addEventListener('click', onClick)
el.addEventListener('pointermove', onPointerMove);
el.addEventListener('pointerleave', resetAll);
el.addEventListener('click', onClick);
startAutoAnimation()
})
startAutoAnimation();
});
onUnmounted(() => {
const el = sceneRef.value
const el = sceneRef.value;
if (el) {
el.removeEventListener('pointermove', onPointerMove)
el.removeEventListener('pointerleave', resetAll)
el.removeEventListener('click', onClick)
el.removeEventListener('pointermove', onPointerMove);
el.removeEventListener('pointerleave', resetAll);
el.removeEventListener('click', onClick);
}
if (rafRef.value !== null) cancelAnimationFrame(rafRef.value)
if (idleTimerRef.value !== null) clearTimeout(idleTimerRef.value)
if (simRAFRef.value !== null) cancelAnimationFrame(simRAFRef.value)
})
if (rafRef.value !== null) cancelAnimationFrame(rafRef.value);
if (idleTimerRef.value !== null) clearTimeout(idleTimerRef.value);
if (simRAFRef.value !== null) cancelAnimationFrame(simRAFRef.value);
});
</script>

View File

@@ -5,7 +5,7 @@
:style="{
opacity: inView ? 1 : initialOpacity,
transition: `opacity ${duration}ms ${easing}, filter ${duration}ms ${easing}`,
filter: blur ? (inView ? 'blur(0px)' : 'blur(10px)') : 'none',
filter: blur ? (inView ? 'blur(0px)' : 'blur(10px)') : 'none'
}"
>
<slot />
@@ -13,16 +13,16 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue';
interface Props {
blur?: boolean
duration?: number
easing?: string
delay?: number
threshold?: number
initialOpacity?: number
className?: string
blur?: boolean;
duration?: number;
easing?: string;
delay?: number;
threshold?: number;
initialOpacity?: number;
className?: string;
}
const props = withDefaults(defineProps<Props>(), {
@@ -33,34 +33,34 @@ const props = withDefaults(defineProps<Props>(), {
threshold: 0.1,
initialOpacity: 0,
className: ''
})
});
const inView = ref(false)
const elementRef = ref<HTMLDivElement | null>(null)
let observer: IntersectionObserver | null = null
const inView = ref(false);
const elementRef = ref<HTMLDivElement | null>(null);
let observer: IntersectionObserver | null = null;
onMounted(() => {
const element = elementRef.value
if (!element) return
const element = elementRef.value;
if (!element) return;
observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
observer?.unobserve(element)
observer?.unobserve(element);
setTimeout(() => {
inView.value = true
}, props.delay)
inView.value = true;
}, props.delay);
}
},
{ threshold: props.threshold }
)
);
observer.observe(element)
})
observer.observe(element);
});
onUnmounted(() => {
if (observer) {
observer.disconnect()
observer.disconnect();
}
})
</script>
});
</script>

View File

@@ -1,20 +1,20 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed } from 'vue';
interface GlareHoverProps {
width?: string
height?: string
background?: string
borderRadius?: string
borderColor?: string
glareColor?: string
glareOpacity?: number
glareAngle?: number
glareSize?: number
transitionDuration?: number
playOnce?: boolean
className?: string
style?: Record<string, string | number>
width?: string;
height?: string;
background?: string;
borderRadius?: string;
borderColor?: string;
glareColor?: string;
glareOpacity?: number;
glareAngle?: number;
glareSize?: number;
transitionDuration?: number;
playOnce?: boolean;
className?: string;
style?: Record<string, string | number>;
}
const props = withDefaults(defineProps<GlareHoverProps>(), {
@@ -31,28 +31,28 @@ const props = withDefaults(defineProps<GlareHoverProps>(), {
playOnce: false,
className: '',
style: () => ({})
})
});
const overlayRef = ref<HTMLDivElement | null>(null)
const overlayRef = ref<HTMLDivElement | null>(null);
const rgba = computed(() => {
const hex = props.glareColor.replace('#', '')
let result = props.glareColor
const hex = props.glareColor.replace('#', '');
let result = props.glareColor;
if (/^[\dA-Fa-f]{6}$/.test(hex)) {
const r = parseInt(hex.slice(0, 2), 16)
const g = parseInt(hex.slice(2, 4), 16)
const b = parseInt(hex.slice(4, 6), 16)
result = `rgba(${r}, ${g}, ${b}, ${props.glareOpacity})`
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
result = `rgba(${r}, ${g}, ${b}, ${props.glareOpacity})`;
} else if (/^[\dA-Fa-f]{3}$/.test(hex)) {
const r = parseInt(hex[0] + hex[0], 16)
const g = parseInt(hex[1] + hex[1], 16)
const b = parseInt(hex[2] + hex[2], 16)
result = `rgba(${r}, ${g}, ${b}, ${props.glareOpacity})`
const r = parseInt(hex[0] + hex[0], 16);
const g = parseInt(hex[1] + hex[1], 16);
const b = parseInt(hex[2] + hex[2], 16);
result = `rgba(${r}, ${g}, ${b}, ${props.glareOpacity})`;
}
return result
})
return result;
});
const overlayStyle = computed(() => ({
position: 'absolute' as const,
@@ -64,32 +64,32 @@ const overlayStyle = computed(() => ({
backgroundSize: `${props.glareSize}% ${props.glareSize}%, 100% 100%`,
backgroundRepeat: 'no-repeat',
backgroundPosition: '-100% -100%, 0 0',
pointerEvents: 'none' as const,
}))
pointerEvents: 'none' as const
}));
const animateIn = () => {
const el = overlayRef.value
if (!el) return
const el = overlayRef.value;
if (!el) return;
el.style.transition = 'none'
el.style.backgroundPosition = '-100% -100%, 0 0'
void el.offsetHeight
el.style.transition = `${props.transitionDuration}ms ease`
el.style.backgroundPosition = '100% 100%, 0 0'
}
el.style.transition = 'none';
el.style.backgroundPosition = '-100% -100%, 0 0';
void el.offsetHeight;
el.style.transition = `${props.transitionDuration}ms ease`;
el.style.backgroundPosition = '100% 100%, 0 0';
};
const animateOut = () => {
const el = overlayRef.value
if (!el) return
const el = overlayRef.value;
if (!el) return;
if (props.playOnce) {
el.style.transition = 'none'
el.style.backgroundPosition = '-100% -100%, 0 0'
el.style.transition = 'none';
el.style.backgroundPosition = '-100% -100%, 0 0';
} else {
el.style.transition = `${props.transitionDuration}ms ease`
el.style.backgroundPosition = '-100% -100%, 0 0'
el.style.transition = `${props.transitionDuration}ms ease`;
el.style.backgroundPosition = '-100% -100%, 0 0';
}
}
};
</script>
<template>
@@ -101,12 +101,13 @@ const animateOut = () => {
background: props.background,
borderRadius: props.borderRadius,
borderColor: props.borderColor,
...props.style,
...props.style
}"
@mouseenter="animateIn"
@mouseleave="animateOut"
>
<div ref="overlayRef" :style="overlayStyle" />
<slot />
</div>
</template>

View File

@@ -1,27 +1,34 @@
<template>
<div ref="magnetRef" :class="wrapperClassName" :style="{ position: 'relative', display: 'inline-block' }"
v-bind="$attrs">
<div :class="innerClassName" :style="{
transform: `translate3d(${position.x}px, ${position.y}px, 0)`,
transition: transitionStyle,
willChange: 'transform',
}">
<div
ref="magnetRef"
:class="wrapperClassName"
:style="{ position: 'relative', display: 'inline-block' }"
v-bind="$attrs"
>
<div
:class="innerClassName"
:style="{
transform: `translate3d(${position.x}px, ${position.y}px, 0)`,
transition: transitionStyle,
willChange: 'transform'
}"
>
<slot />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
interface Props {
padding?: number
disabled?: boolean
magnetStrength?: number
activeTransition?: string
inactiveTransition?: string
wrapperClassName?: string
innerClassName?: string
padding?: number;
disabled?: boolean;
magnetStrength?: number;
activeTransition?: string;
inactiveTransition?: string;
wrapperClassName?: string;
innerClassName?: string;
}
const props = withDefaults(defineProps<Props>(), {
@@ -32,53 +39,54 @@ const props = withDefaults(defineProps<Props>(), {
inactiveTransition: 'transform 0.5s ease-in-out',
wrapperClassName: '',
innerClassName: ''
})
});
defineOptions({
inheritAttrs: false
})
});
const magnetRef = ref<HTMLDivElement | null>(null)
const isActive = ref(false)
const position = ref({ x: 0, y: 0 })
const magnetRef = ref<HTMLDivElement | null>(null);
const isActive = ref(false);
const position = ref({ x: 0, y: 0 });
const transitionStyle = computed(() =>
isActive.value ? props.activeTransition : props.inactiveTransition
)
const transitionStyle = computed(() => (isActive.value ? props.activeTransition : props.inactiveTransition));
const handleMouseMove = (e: MouseEvent) => {
if (!magnetRef.value || props.disabled) return
if (!magnetRef.value || props.disabled) return;
const { left, top, width, height } = magnetRef.value.getBoundingClientRect()
const centerX = left + width / 2
const centerY = top + height / 2
const { left, top, width, height } = magnetRef.value.getBoundingClientRect();
const centerX = left + width / 2;
const centerY = top + height / 2;
const distX = Math.abs(centerX - e.clientX)
const distY = Math.abs(centerY - e.clientY)
const distX = Math.abs(centerX - e.clientX);
const distY = Math.abs(centerY - e.clientY);
if (distX < width / 2 + props.padding && distY < height / 2 + props.padding) {
isActive.value = true
const offsetX = (e.clientX - centerX) / props.magnetStrength
const offsetY = (e.clientY - centerY) / props.magnetStrength
position.value = { x: offsetX, y: offsetY }
isActive.value = true;
const offsetX = (e.clientX - centerX) / props.magnetStrength;
const offsetY = (e.clientY - centerY) / props.magnetStrength;
position.value = { x: offsetX, y: offsetY };
} else {
isActive.value = false
position.value = { x: 0, y: 0 }
isActive.value = false;
position.value = { x: 0, y: 0 };
}
}
};
onMounted(() => {
window.addEventListener('mousemove', handleMouseMove)
})
window.addEventListener('mousemove', handleMouseMove);
});
onUnmounted(() => {
window.removeEventListener('mousemove', handleMouseMove)
})
window.removeEventListener('mousemove', handleMouseMove);
});
watch(() => props.disabled, (newDisabled) => {
if (newDisabled) {
position.value = { x: 0, y: 0 }
isActive.value = false
watch(
() => props.disabled,
newDisabled => {
if (newDisabled) {
position.value = { x: 0, y: 0 };
isActive.value = false;
}
}
})
);
</script>

View File

@@ -1,16 +1,16 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { ref, onMounted, onUnmounted, computed } from 'vue';
interface MagnetLinesProps {
rows?: number
columns?: number
containerSize?: string
lineColor?: string
lineWidth?: string
lineHeight?: string
baseAngle?: number
className?: string
style?: Record<string, string | number>
rows?: number;
columns?: number;
containerSize?: string;
lineColor?: string;
lineWidth?: string;
lineHeight?: string;
baseAngle?: number;
className?: string;
style?: Record<string, string | number>;
}
const props = withDefaults(defineProps<MagnetLinesProps>(), {
@@ -23,53 +23,53 @@ const props = withDefaults(defineProps<MagnetLinesProps>(), {
baseAngle: -10,
className: '',
style: () => ({})
})
});
const containerRef = ref<HTMLDivElement | null>(null)
const containerRef = ref<HTMLDivElement | null>(null);
const total = computed(() => props.rows * props.columns)
const total = computed(() => props.rows * props.columns);
const onPointerMove = (pointer: { x: number; y: number }) => {
const container = containerRef.value
if (!container) return
const container = containerRef.value;
if (!container) return;
const items = container.querySelectorAll<HTMLSpanElement>('span')
const items = container.querySelectorAll<HTMLSpanElement>('span');
items.forEach((item) => {
const rect = item.getBoundingClientRect()
const centerX = rect.x + rect.width / 2
const centerY = rect.y + rect.height / 2
items.forEach(item => {
const rect = item.getBoundingClientRect();
const centerX = rect.x + rect.width / 2;
const centerY = rect.y + rect.height / 2;
const b = pointer.x - centerX
const a = pointer.y - centerY
const c = Math.sqrt(a * a + b * b) || 1
const r = ((Math.acos(b / c) * 180) / Math.PI) * (pointer.y > centerY ? 1 : -1)
const b = pointer.x - centerX;
const a = pointer.y - centerY;
const c = Math.sqrt(a * a + b * b) || 1;
const r = ((Math.acos(b / c) * 180) / Math.PI) * (pointer.y > centerY ? 1 : -1);
item.style.setProperty('--rotate', `${r}deg`)
})
}
item.style.setProperty('--rotate', `${r}deg`);
});
};
const handlePointerMove = (e: PointerEvent) => {
onPointerMove({ x: e.x, y: e.y })
}
onPointerMove({ x: e.x, y: e.y });
};
onMounted(() => {
const container = containerRef.value
if (!container) return
const container = containerRef.value;
if (!container) return;
window.addEventListener('pointermove', handlePointerMove)
window.addEventListener('pointermove', handlePointerMove);
const items = container.querySelectorAll<HTMLSpanElement>('span')
const items = container.querySelectorAll<HTMLSpanElement>('span');
if (items.length) {
const middleIndex = Math.floor(items.length / 2)
const rect = items[middleIndex].getBoundingClientRect()
onPointerMove({ x: rect.x, y: rect.y })
const middleIndex = Math.floor(items.length / 2);
const rect = items[middleIndex].getBoundingClientRect();
onPointerMove({ x: rect.x, y: rect.y });
}
})
});
onUnmounted(() => {
window.removeEventListener('pointermove', handlePointerMove)
})
window.removeEventListener('pointermove', handlePointerMove);
});
</script>
<template>
@@ -81,7 +81,7 @@ onUnmounted(() => {
gridTemplateRows: `repeat(${props.rows}, 1fr)`,
width: props.containerSize,
height: props.containerSize,
...props.style,
...props.style
}"
>
<span
@@ -94,7 +94,7 @@ onUnmounted(() => {
height: props.lineHeight,
'--rotate': `${props.baseAngle}deg`,
transform: 'rotate(var(--rotate))',
willChange: 'transform',
willChange: 'transform'
}"
/>
</div>

View File

@@ -1,14 +1,14 @@
<script setup lang="ts">
import { ref, onMounted, watch, onUnmounted, nextTick } from 'vue'
import { gsap } from 'gsap'
import { ref, onMounted, watch, onUnmounted, nextTick } from 'vue';
import { gsap } from 'gsap';
interface PixelTransitionProps {
gridSize?: number
pixelColor?: string
animationStepDuration?: number
className?: string
style?: Record<string, string | number>
aspectRatio?: string
gridSize?: number;
pixelColor?: string;
animationStepDuration?: number;
className?: string;
style?: Record<string, string | number>;
aspectRatio?: string;
}
const props = withDefaults(defineProps<PixelTransitionProps>(), {
@@ -18,114 +18,127 @@ const props = withDefaults(defineProps<PixelTransitionProps>(), {
className: '',
style: () => ({}),
aspectRatio: '100%'
})
});
const containerRef = ref<HTMLDivElement | null>(null)
const pixelGridRef = ref<HTMLDivElement | null>(null)
const activeRef = ref<HTMLDivElement | null>(null)
const isActive = ref(false)
let delayedCall: gsap.core.Tween | null = null
const containerRef = ref<HTMLDivElement | null>(null);
const pixelGridRef = ref<HTMLDivElement | null>(null);
const activeRef = ref<HTMLDivElement | null>(null);
const isActive = ref(false);
let delayedCall: gsap.core.Tween | null = null;
const isTouchDevice =
typeof window !== 'undefined' &&
('ontouchstart' in window ||
(navigator && navigator.maxTouchPoints > 0) ||
(window.matchMedia && window.matchMedia('(pointer: coarse)').matches))
(window.matchMedia && window.matchMedia('(pointer: coarse)').matches));
function buildPixelGrid() {
const pixelGridEl = pixelGridRef.value
if (!pixelGridEl) return
pixelGridEl.innerHTML = ''
const pixelGridEl = pixelGridRef.value;
if (!pixelGridEl) return;
pixelGridEl.innerHTML = '';
for (let row = 0; row < props.gridSize; row++) {
for (let col = 0; col < props.gridSize; col++) {
const pixel = document.createElement('div')
pixel.classList.add('pixelated-image-card__pixel', 'absolute', 'hidden')
pixel.style.backgroundColor = props.pixelColor
const size = 100 / props.gridSize
pixel.style.width = `${size}%`
pixel.style.height = `${size}%`
pixel.style.left = `${col * size}%`
pixel.style.top = `${row * size}%`
pixelGridEl.appendChild(pixel)
const pixel = document.createElement('div');
pixel.classList.add('pixelated-image-card__pixel', 'absolute', 'hidden');
pixel.style.backgroundColor = props.pixelColor;
const size = 100 / props.gridSize;
pixel.style.width = `${size}%`;
pixel.style.height = `${size}%`;
pixel.style.left = `${col * size}%`;
pixel.style.top = `${row * size}%`;
pixelGridEl.appendChild(pixel);
}
}
}
async function animatePixels(activate: boolean) {
isActive.value = activate
await nextTick()
const pixelGridEl = pixelGridRef.value
const activeEl = activeRef.value
if (!pixelGridEl || !activeEl) return
const pixels = pixelGridEl.querySelectorAll<HTMLDivElement>('.pixelated-image-card__pixel')
if (!pixels.length) return
gsap.killTweensOf(pixels)
if (delayedCall) delayedCall.kill()
gsap.set(pixels, { display: 'none' })
const totalPixels = pixels.length
const staggerDuration = props.animationStepDuration / totalPixels
isActive.value = activate;
await nextTick();
const pixelGridEl = pixelGridRef.value;
const activeEl = activeRef.value;
if (!pixelGridEl || !activeEl) return;
const pixels = pixelGridEl.querySelectorAll<HTMLDivElement>('.pixelated-image-card__pixel');
if (!pixels.length) return;
gsap.killTweensOf(pixels);
if (delayedCall) delayedCall.kill();
gsap.set(pixels, { display: 'none' });
const totalPixels = pixels.length;
const staggerDuration = props.animationStepDuration / totalPixels;
gsap.to(pixels, {
display: 'block',
duration: 0,
stagger: {
each: staggerDuration,
from: 'random',
},
})
from: 'random'
}
});
delayedCall = gsap.delayedCall(props.animationStepDuration, () => {
activeEl.style.display = activate ? 'block' : 'none'
activeEl.style.pointerEvents = activate ? 'none' : ''
})
activeEl.style.display = activate ? 'block' : 'none';
activeEl.style.pointerEvents = activate ? 'none' : '';
});
gsap.to(pixels, {
display: 'none',
duration: 0,
delay: props.animationStepDuration,
stagger: {
each: staggerDuration,
from: 'random',
},
})
from: 'random'
}
});
}
function handleMouseEnter() {
if (isTouchDevice) return
if (!isActive.value) animatePixels(true)
if (isTouchDevice) return;
if (!isActive.value) animatePixels(true);
}
function handleMouseLeave() {
if (isTouchDevice) return
if (isActive.value) animatePixels(false)
if (isTouchDevice) return;
if (isActive.value) animatePixels(false);
}
function handleClick() {
if (!isTouchDevice) return
animatePixels(!isActive.value)
if (!isTouchDevice) return;
animatePixels(!isActive.value);
}
onMounted(async () => {
await nextTick()
buildPixelGrid()
})
await nextTick();
buildPixelGrid();
});
watch(() => [props.gridSize, props.pixelColor], () => {
buildPixelGrid()
})
watch(
() => [props.gridSize, props.pixelColor],
() => {
buildPixelGrid();
}
);
onUnmounted(() => {
if (delayedCall) delayedCall.kill()
})
if (delayedCall) delayedCall.kill();
});
</script>
<template>
<div ref="containerRef" :class="[
props.className,
'bg-[#222] text-white rounded-[15px] border-2 border-white w-[300px] max-w-full relative overflow-hidden'
]" :style="props.style" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave" @click="handleClick">
<div
ref="containerRef"
:class="[
props.className,
'bg-[#222] text-white rounded-[15px] border-2 border-white w-[300px] max-w-full relative overflow-hidden'
]"
:style="props.style"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@click="handleClick"
>
<div :style="{ paddingTop: props.aspectRatio }" />
<div class="absolute inset-0 w-full h-full">
<slot name="firstContent" />
</div>
<div ref="activeRef" class="absolute inset-0 w-full h-full z-[2]" style="display: none;">
<div ref="activeRef" class="absolute inset-0 w-full h-full z-[2]" style="display: none">
<slot name="secondContent" />
</div>
<div ref="pixelGridRef" class="absolute inset-0 w-full h-full pointer-events-none z-[3]" />
</div>
</template>

View File

@@ -3,18 +3,18 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, type CSSProperties } from 'vue'
import { Renderer, Program, Mesh, Color, Triangle } from 'ogl'
import { ref, onMounted, onUnmounted, watch, type CSSProperties } from 'vue';
import { Renderer, Program, Mesh, Color, Triangle } from 'ogl';
interface AuroraProps {
colorStops?: string[]
amplitude?: number
blend?: number
time?: number
speed?: number
intensity?: number
className?: string
style?: CSSProperties
colorStops?: string[];
amplitude?: number;
blend?: number;
time?: number;
speed?: number;
intensity?: number;
className?: string;
style?: CSSProperties;
}
const props = withDefaults(defineProps<AuroraProps>(), {
@@ -25,16 +25,16 @@ const props = withDefaults(defineProps<AuroraProps>(), {
intensity: 1.0,
className: '',
style: () => ({})
})
});
const containerRef = ref<HTMLDivElement>()
const containerRef = ref<HTMLDivElement>();
const VERT = `#version 300 es
in vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`
`;
const FRAG = `#version 300 es
precision highp float;
@@ -136,56 +136,56 @@ void main() {
fragColor = vec4(auroraColor * finalAlpha, finalAlpha);
}
`
`;
let renderer: Renderer | null = null
let animateId = 0
let renderer: Renderer | null = null;
let animateId = 0;
const initAurora = () => {
const container = containerRef.value
if (!container) return
const container = containerRef.value;
if (!container) return;
renderer = new Renderer({
alpha: true,
premultipliedAlpha: true,
antialias: true,
})
antialias: true
});
const gl = renderer.gl
gl.clearColor(0, 0, 0, 0)
gl.enable(gl.BLEND)
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
gl.canvas.style.backgroundColor = 'transparent'
const gl = renderer.gl;
gl.clearColor(0, 0, 0, 0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.canvas.style.backgroundColor = 'transparent';
// eslint-disable-next-line prefer-const
let program: Program | undefined
let program: Program | undefined;
const resize = () => {
if (!container) return
if (!container) return;
const parentWidth = container.parentElement?.offsetWidth || container.offsetWidth || window.innerWidth
const parentHeight = container.parentElement?.offsetHeight || container.offsetHeight || window.innerHeight
const parentWidth = container.parentElement?.offsetWidth || container.offsetWidth || window.innerWidth;
const parentHeight = container.parentElement?.offsetHeight || container.offsetHeight || window.innerHeight;
const width = Math.max(parentWidth, 300)
const height = Math.max(parentHeight, 300)
const width = Math.max(parentWidth, 300);
const height = Math.max(parentHeight, 300);
renderer!.setSize(width, height)
renderer!.setSize(width, height);
if (program) {
program.uniforms.uResolution.value = [width, height]
program.uniforms.uResolution.value = [width, height];
}
}
};
window.addEventListener('resize', resize)
window.addEventListener('resize', resize);
const geometry = new Triangle(gl)
const geometry = new Triangle(gl);
if (geometry.attributes.uv) {
delete geometry.attributes.uv
delete geometry.attributes.uv;
}
const colorStopsArray = props.colorStops.map((hex) => {
const c = new Color(hex)
return [c.r, c.g, c.b]
})
const colorStopsArray = props.colorStops.map(hex => {
const c = new Color(hex);
return [c.r, c.g, c.b];
});
program = new Program(gl, {
vertex: VERT,
@@ -194,83 +194,88 @@ const initAurora = () => {
uTime: { value: 0 },
uAmplitude: { value: props.amplitude },
uColorStops: { value: colorStopsArray },
uResolution: { value: [Math.max(container.parentElement?.offsetWidth || container.offsetWidth || window.innerWidth, 300), Math.max(container.parentElement?.offsetHeight || container.offsetHeight || window.innerHeight, 300)] },
uResolution: {
value: [
Math.max(container.parentElement?.offsetWidth || container.offsetWidth || window.innerWidth, 300),
Math.max(container.parentElement?.offsetHeight || container.offsetHeight || window.innerHeight, 300)
]
},
uBlend: { value: props.blend },
uIntensity: { value: props.intensity },
},
})
uIntensity: { value: props.intensity }
}
});
const mesh = new Mesh(gl, { geometry, program })
container.appendChild(gl.canvas)
const mesh = new Mesh(gl, { geometry, program });
container.appendChild(gl.canvas);
gl.canvas.style.width = '100%'
gl.canvas.style.height = '100%'
gl.canvas.style.display = 'block'
gl.canvas.style.position = 'absolute'
gl.canvas.style.top = '0'
gl.canvas.style.left = '0'
gl.canvas.style.width = '100%';
gl.canvas.style.height = '100%';
gl.canvas.style.display = 'block';
gl.canvas.style.position = 'absolute';
gl.canvas.style.top = '0';
gl.canvas.style.left = '0';
const update = (t: number) => {
animateId = requestAnimationFrame(update)
const time = props.time ?? t * 0.01
const speed = props.speed ?? 1.0
animateId = requestAnimationFrame(update);
const time = props.time ?? t * 0.01;
const speed = props.speed ?? 1.0;
if (program) {
program.uniforms.uTime.value = time * speed * 0.1
program.uniforms.uAmplitude.value = props.amplitude ?? 1.0
program.uniforms.uBlend.value = props.blend ?? 0.5
program.uniforms.uIntensity.value = props.intensity ?? 1.0
const stops = props.colorStops ?? ['#27FF64', '#7cff67', '#27FF64']
program.uniforms.uTime.value = time * speed * 0.1;
program.uniforms.uAmplitude.value = props.amplitude ?? 1.0;
program.uniforms.uBlend.value = props.blend ?? 0.5;
program.uniforms.uIntensity.value = props.intensity ?? 1.0;
const stops = props.colorStops ?? ['#27FF64', '#7cff67', '#27FF64'];
program.uniforms.uColorStops.value = stops.map((hex: string) => {
const c = new Color(hex)
return [c.r, c.g, c.b]
})
renderer!.render({ scene: mesh })
const c = new Color(hex);
return [c.r, c.g, c.b];
});
renderer!.render({ scene: mesh });
}
}
animateId = requestAnimationFrame(update)
};
animateId = requestAnimationFrame(update);
resize()
resize();
return () => {
cancelAnimationFrame(animateId)
window.removeEventListener('resize', resize)
cancelAnimationFrame(animateId);
window.removeEventListener('resize', resize);
if (container && gl.canvas.parentNode === container) {
container.removeChild(gl.canvas)
container.removeChild(gl.canvas);
}
gl.getExtension('WEBGL_lose_context')?.loseContext()
}
}
gl.getExtension('WEBGL_lose_context')?.loseContext();
};
};
const cleanup = () => {
if (animateId) {
cancelAnimationFrame(animateId)
cancelAnimationFrame(animateId);
}
if (renderer) {
const gl = renderer.gl
const container = containerRef.value
const gl = renderer.gl;
const container = containerRef.value;
if (container && gl.canvas.parentNode === container) {
container.removeChild(gl.canvas)
container.removeChild(gl.canvas);
}
gl.getExtension('WEBGL_lose_context')?.loseContext()
gl.getExtension('WEBGL_lose_context')?.loseContext();
}
renderer = null
}
renderer = null;
};
onMounted(() => {
initAurora()
})
initAurora();
});
onUnmounted(() => {
cleanup()
})
cleanup();
});
watch(
() => [props.amplitude, props.intensity],
() => {
cleanup()
initAurora()
cleanup();
initAurora();
}
)
);
</script>
<style scoped>
@@ -289,4 +294,4 @@ div {
width: 100% !important;
height: 100% !important;
}
</style>
</style>

View File

@@ -3,19 +3,19 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
import * as THREE from 'three'
import { degToRad } from 'three/src/math/MathUtils.js'
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
import * as THREE from 'three';
import { degToRad } from 'three/src/math/MathUtils.js';
interface BeamsProps {
beamWidth?: number
beamHeight?: number
beamNumber?: number
lightColor?: string
speed?: number
noiseIntensity?: number
scale?: number
rotation?: number
beamWidth?: number;
beamHeight?: number;
beamNumber?: number;
lightColor?: string;
speed?: number;
noiseIntensity?: number;
scale?: number;
rotation?: number;
}
const props = withDefaults(defineProps<BeamsProps>(), {
@@ -27,41 +27,41 @@ const props = withDefaults(defineProps<BeamsProps>(), {
noiseIntensity: 1.75,
scale: 0.2,
rotation: 0
})
});
const containerRef = ref<HTMLDivElement>()
const containerRef = ref<HTMLDivElement>();
let renderer: THREE.WebGLRenderer | null = null
let scene: THREE.Scene | null = null
let camera: THREE.PerspectiveCamera | null = null
let beamMesh: THREE.Mesh<THREE.BufferGeometry, THREE.ShaderMaterial> | null = null
let directionalLight: THREE.DirectionalLight | null = null
let ambientLight: THREE.AmbientLight | null = null
let animationId: number | null = null
let renderer: THREE.WebGLRenderer | null = null;
let scene: THREE.Scene | null = null;
let camera: THREE.PerspectiveCamera | null = null;
let beamMesh: THREE.Mesh<THREE.BufferGeometry, THREE.ShaderMaterial> | null = null;
let directionalLight: THREE.DirectionalLight | null = null;
let ambientLight: THREE.AmbientLight | null = null;
let animationId: number | null = null;
type UniformValue = THREE.IUniform<unknown> | unknown
type UniformValue = THREE.IUniform<unknown> | unknown;
interface ExtendMaterialConfig {
header: string
vertexHeader?: string
fragmentHeader?: string
material?: THREE.MeshPhysicalMaterialParameters & { fog?: boolean }
uniforms?: Record<string, UniformValue>
vertex?: Record<string, string>
fragment?: Record<string, string>
header: string;
vertexHeader?: string;
fragmentHeader?: string;
material?: THREE.MeshPhysicalMaterialParameters & { fog?: boolean };
uniforms?: Record<string, UniformValue>;
vertex?: Record<string, string>;
fragment?: Record<string, string>;
}
type ShaderWithDefines = THREE.ShaderLibShader & {
defines?: Record<string, string | number | boolean>
}
defines?: Record<string, string | number | boolean>;
};
const hexToNormalizedRGB = (hex: string): [number, number, number] => {
const clean = hex.replace('#', '')
const r = parseInt(clean.substring(0, 2), 16)
const g = parseInt(clean.substring(2, 4), 16)
const b = parseInt(clean.substring(4, 6), 16)
return [r / 255, g / 255, b / 255]
}
const clean = hex.replace('#', '');
const r = parseInt(clean.substring(0, 2), 16);
const g = parseInt(clean.substring(2, 4), 16);
const b = parseInt(clean.substring(4, 6), 16);
return [r / 255, g / 255, b / 255];
};
const noise = `
float random (in vec2 st) {
@@ -138,53 +138,47 @@ float cnoise(vec3 P){
float n_xyz = mix(n_yz.x,n_yz.y,fade_xyz.x);
return 2.2 * n_xyz;
}
`
`;
function extendMaterial<T extends THREE.Material = THREE.Material>(
BaseMaterial: new (params?: THREE.MaterialParameters) => T,
cfg: ExtendMaterialConfig
): THREE.ShaderMaterial {
const physical = THREE.ShaderLib.physical as ShaderWithDefines
const {
vertexShader: baseVert,
fragmentShader: baseFrag,
uniforms: baseUniforms,
} = physical
const baseDefines = physical.defines ?? {}
const physical = THREE.ShaderLib.physical as ShaderWithDefines;
const { vertexShader: baseVert, fragmentShader: baseFrag, uniforms: baseUniforms } = physical;
const baseDefines = physical.defines ?? {};
const uniforms: Record<string, THREE.IUniform> =
THREE.UniformsUtils.clone(baseUniforms)
const uniforms: Record<string, THREE.IUniform> = THREE.UniformsUtils.clone(baseUniforms);
const defaults = new BaseMaterial(cfg.material || {}) as T & {
color?: THREE.Color
roughness?: number
metalness?: number
envMap?: THREE.Texture
envMapIntensity?: number
}
color?: THREE.Color;
roughness?: number;
metalness?: number;
envMap?: THREE.Texture;
envMapIntensity?: number;
};
if (defaults.color) uniforms.diffuse.value = defaults.color
if ('roughness' in defaults) uniforms.roughness.value = defaults.roughness
if ('metalness' in defaults) uniforms.metalness.value = defaults.metalness
if ('envMap' in defaults) uniforms.envMap.value = defaults.envMap
if ('envMapIntensity' in defaults)
uniforms.envMapIntensity.value = defaults.envMapIntensity
if (defaults.color) uniforms.diffuse.value = defaults.color;
if ('roughness' in defaults) uniforms.roughness.value = defaults.roughness;
if ('metalness' in defaults) uniforms.metalness.value = defaults.metalness;
if ('envMap' in defaults) uniforms.envMap.value = defaults.envMap;
if ('envMapIntensity' in defaults) uniforms.envMapIntensity.value = defaults.envMapIntensity;
Object.entries(cfg.uniforms ?? {}).forEach(([key, u]) => {
uniforms[key] =
u !== null && typeof u === 'object' && 'value' in u
? (u as THREE.IUniform<unknown>)
: ({ value: u } as THREE.IUniform<unknown>)
})
: ({ value: u } as THREE.IUniform<unknown>);
});
let vert = `${cfg.header}\n${cfg.vertexHeader ?? ''}\n${baseVert}`
let frag = `${cfg.header}\n${cfg.fragmentHeader ?? ''}\n${baseFrag}`
let vert = `${cfg.header}\n${cfg.vertexHeader ?? ''}\n${baseVert}`;
let frag = `${cfg.header}\n${cfg.fragmentHeader ?? ''}\n${baseFrag}`;
for (const [inc, code] of Object.entries(cfg.vertex ?? {})) {
vert = vert.replace(inc, `${inc}\n${code}`)
vert = vert.replace(inc, `${inc}\n${code}`);
}
for (const [inc, code] of Object.entries(cfg.fragment ?? {})) {
frag = frag.replace(inc, `${inc}\n${code}`)
frag = frag.replace(inc, `${inc}\n${code}`);
}
const mat = new THREE.ShaderMaterial({
@@ -193,10 +187,10 @@ function extendMaterial<T extends THREE.Material = THREE.Material>(
vertexShader: vert,
fragmentShader: frag,
lights: true,
fog: !!cfg.material?.fog,
})
fog: !!cfg.material?.fog
});
return mat
return mat;
}
function createStackedPlanesBufferGeometry(
@@ -206,54 +200,51 @@ function createStackedPlanesBufferGeometry(
spacing: number,
heightSegments: number
): THREE.BufferGeometry {
const geometry = new THREE.BufferGeometry()
const numVertices = n * (heightSegments + 1) * 2
const numFaces = n * heightSegments * 2
const positions = new Float32Array(numVertices * 3)
const indices = new Uint32Array(numFaces * 3)
const uvs = new Float32Array(numVertices * 2)
const geometry = new THREE.BufferGeometry();
const numVertices = n * (heightSegments + 1) * 2;
const numFaces = n * heightSegments * 2;
const positions = new Float32Array(numVertices * 3);
const indices = new Uint32Array(numFaces * 3);
const uvs = new Float32Array(numVertices * 2);
let vertexOffset = 0
let indexOffset = 0
let uvOffset = 0
const totalWidth = n * width + (n - 1) * spacing
const xOffsetBase = -totalWidth / 2
let vertexOffset = 0;
let indexOffset = 0;
let uvOffset = 0;
const totalWidth = n * width + (n - 1) * spacing;
const xOffsetBase = -totalWidth / 2;
for (let i = 0; i < n; i++) {
const xOffset = xOffsetBase + i * (width + spacing)
const uvXOffset = Math.random() * 300
const uvYOffset = Math.random() * 300
const xOffset = xOffsetBase + i * (width + spacing);
const uvXOffset = Math.random() * 300;
const uvYOffset = Math.random() * 300;
for (let j = 0; j <= heightSegments; j++) {
const y = height * (j / heightSegments - 0.5)
const v0 = [xOffset, y, 0]
const v1 = [xOffset + width, y, 0]
positions.set([...v0, ...v1], vertexOffset * 3)
const y = height * (j / heightSegments - 0.5);
const v0 = [xOffset, y, 0];
const v1 = [xOffset + width, y, 0];
positions.set([...v0, ...v1], vertexOffset * 3);
const uvY = j / heightSegments
uvs.set(
[uvXOffset, uvY + uvYOffset, uvXOffset + 1, uvY + uvYOffset],
uvOffset
)
const uvY = j / heightSegments;
uvs.set([uvXOffset, uvY + uvYOffset, uvXOffset + 1, uvY + uvYOffset], uvOffset);
if (j < heightSegments) {
const a = vertexOffset,
b = vertexOffset + 1,
c = vertexOffset + 2,
d = vertexOffset + 3
indices.set([a, b, c, c, b, d], indexOffset)
indexOffset += 6
d = vertexOffset + 3;
indices.set([a, b, c, c, b, d], indexOffset);
indexOffset += 6;
}
vertexOffset += 2
uvOffset += 4
vertexOffset += 2;
uvOffset += 4;
}
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2))
geometry.setIndex(new THREE.BufferAttribute(indices, 1))
geometry.computeVertexNormals()
return geometry
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
geometry.computeVertexNormals();
return geometry;
}
const beamMaterial = computed(() =>
@@ -290,12 +281,12 @@ const beamMaterial = computed(() =>
fragmentHeader: '',
vertex: {
'#include <begin_vertex>': `transformed.z += getPos(transformed.xyz);`,
'#include <beginnormal_vertex>': `objectNormal = getNormal(position.xyz);`,
'#include <beginnormal_vertex>': `objectNormal = getNormal(position.xyz);`
},
fragment: {
'#include <dithering_fragment>': `
float randomNoise = noise(gl_FragCoord.xy);
gl_FragColor.rgb -= randomNoise / 15. * uNoiseIntensity;`,
gl_FragColor.rgb -= randomNoise / 15. * uNoiseIntensity;`
},
material: { fog: true },
uniforms: {
@@ -306,127 +297,120 @@ const beamMaterial = computed(() =>
uSpeed: { shared: true, mixed: true, linked: true, value: props.speed },
envMapIntensity: 10,
uNoiseIntensity: props.noiseIntensity,
uScale: props.scale,
},
uScale: props.scale
}
})
)
);
const initThreeJS = () => {
if (!containerRef.value) return
if (!containerRef.value) return;
cleanup()
cleanup();
const container = containerRef.value;
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setClearColor(0x000000, 1);
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(30, 1, 0.1, 1000);
camera.position.set(0, 0, 20);
const geometry = createStackedPlanesBufferGeometry(props.beamNumber, props.beamWidth, props.beamHeight, 0, 100);
const material = beamMaterial.value;
beamMesh = new THREE.Mesh(geometry, material);
const group = new THREE.Group();
group.rotation.z = degToRad(props.rotation);
group.add(beamMesh);
scene.add(group);
directionalLight = new THREE.DirectionalLight(new THREE.Color(props.lightColor), 1);
directionalLight.position.set(0, 3, 10);
const shadowCamera = directionalLight.shadow.camera as THREE.OrthographicCamera;
shadowCamera.top = 24;
shadowCamera.bottom = -24;
shadowCamera.left = -24;
shadowCamera.right = 24;
shadowCamera.far = 64;
directionalLight.shadow.bias = -0.004;
scene.add(directionalLight);
ambientLight = new THREE.AmbientLight(0xffffff, 1);
scene.add(ambientLight);
container.appendChild(renderer.domElement);
const container = containerRef.value
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
renderer.setClearColor(0x000000, 1)
scene = new THREE.Scene()
camera = new THREE.PerspectiveCamera(30, 1, 0.1, 1000)
camera.position.set(0, 0, 20)
const geometry = createStackedPlanesBufferGeometry(
props.beamNumber,
props.beamWidth,
props.beamHeight,
0,
100
)
const material = beamMaterial.value
beamMesh = new THREE.Mesh(geometry, material)
const group = new THREE.Group()
group.rotation.z = degToRad(props.rotation)
group.add(beamMesh)
scene.add(group)
directionalLight = new THREE.DirectionalLight(new THREE.Color(props.lightColor), 1)
directionalLight.position.set(0, 3, 10)
const shadowCamera = directionalLight.shadow.camera as THREE.OrthographicCamera
shadowCamera.top = 24
shadowCamera.bottom = -24
shadowCamera.left = -24
shadowCamera.right = 24
shadowCamera.far = 64
directionalLight.shadow.bias = -0.004
scene.add(directionalLight)
ambientLight = new THREE.AmbientLight(0xffffff, 1)
scene.add(ambientLight)
container.appendChild(renderer.domElement)
const resize = () => {
if (!container || !renderer || !camera) return
const width = container.offsetWidth
const height = container.offsetHeight
renderer.setSize(width, height)
camera.aspect = width / height
camera.updateProjectionMatrix()
}
const resizeObserver = new ResizeObserver(resize)
resizeObserver.observe(container)
resize()
if (!container || !renderer || !camera) return;
const width = container.offsetWidth;
const height = container.offsetHeight;
renderer.setSize(width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
};
const resizeObserver = new ResizeObserver(resize);
resizeObserver.observe(container);
resize();
const animate = () => {
animationId = requestAnimationFrame(animate)
animationId = requestAnimationFrame(animate);
if (beamMesh && beamMesh.material) {
beamMesh.material.uniforms.time.value += 0.1 * 0.016
beamMesh.material.uniforms.time.value += 0.1 * 0.016;
}
if (renderer && scene && camera) {
renderer.render(scene, camera)
renderer.render(scene, camera);
}
}
animationId = requestAnimationFrame(animate)
;(container as HTMLDivElement & { _resizeObserver?: ResizeObserver })._resizeObserver = resizeObserver
}
};
animationId = requestAnimationFrame(animate);
(container as HTMLDivElement & { _resizeObserver?: ResizeObserver })._resizeObserver = resizeObserver;
};
const cleanup = () => {
if (animationId) {
cancelAnimationFrame(animationId)
animationId = null
cancelAnimationFrame(animationId);
animationId = null;
}
if (containerRef.value) {
const container = containerRef.value as HTMLDivElement & { _resizeObserver?: ResizeObserver }
const container = containerRef.value as HTMLDivElement & { _resizeObserver?: ResizeObserver };
if (container._resizeObserver) {
container._resizeObserver.disconnect()
delete container._resizeObserver
container._resizeObserver.disconnect();
delete container._resizeObserver;
}
if (renderer && renderer.domElement.parentNode === container) {
container.removeChild(renderer.domElement)
container.removeChild(renderer.domElement);
}
}
if (beamMesh) {
if (beamMesh.geometry) beamMesh.geometry.dispose()
if (beamMesh.material) beamMesh.material.dispose()
beamMesh = null
if (beamMesh.geometry) beamMesh.geometry.dispose();
if (beamMesh.material) beamMesh.material.dispose();
beamMesh = null;
}
if (renderer) {
renderer.dispose()
renderer = null
renderer.dispose();
renderer = null;
}
scene = null
camera = null
directionalLight = null
ambientLight = null
}
scene = null;
camera = null;
directionalLight = null;
ambientLight = null;
};
watch(
() => [
@@ -437,19 +421,19 @@ watch(
props.speed,
props.noiseIntensity,
props.scale,
props.rotation,
props.rotation
],
() => {
initThreeJS()
initThreeJS();
},
{ deep: true }
)
);
onMounted(() => {
initThreeJS()
})
initThreeJS();
});
onUnmounted(() => {
cleanup()
})
cleanup();
});
</script>

View File

@@ -7,45 +7,45 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue'
import { gsap } from 'gsap'
import { InertiaPlugin } from 'gsap/InertiaPlugin'
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue';
import { gsap } from 'gsap';
import { InertiaPlugin } from 'gsap/InertiaPlugin';
gsap.registerPlugin(InertiaPlugin)
gsap.registerPlugin(InertiaPlugin);
const throttle = <T extends unknown[]>(func: (...args: T) => void, limit: number) => {
let lastCall = 0
let lastCall = 0;
return function (this: unknown, ...args: T) {
const now = performance.now()
const now = performance.now();
if (now - lastCall >= limit) {
lastCall = now
func.apply(this, args)
lastCall = now;
func.apply(this, args);
}
}
}
};
};
interface Dot {
cx: number
cy: number
xOffset: number
yOffset: number
_inertiaApplied: boolean
cx: number;
cy: number;
xOffset: number;
yOffset: number;
_inertiaApplied: boolean;
}
export interface DotGridProps {
dotSize?: number
gap?: number
baseColor?: string
activeColor?: string
proximity?: number
speedTrigger?: number
shockRadius?: number
shockStrength?: number
maxSpeed?: number
resistance?: number
returnDuration?: number
className?: string
style?: Record<string, string | number>
dotSize?: number;
gap?: number;
baseColor?: string;
activeColor?: string;
proximity?: number;
speedTrigger?: number;
shockRadius?: number;
shockStrength?: number;
maxSpeed?: number;
resistance?: number;
returnDuration?: number;
className?: string;
style?: Record<string, string | number>;
}
const props = withDefaults(defineProps<DotGridProps>(), {
@@ -62,11 +62,11 @@ const props = withDefaults(defineProps<DotGridProps>(), {
returnDuration: 1.5,
className: '',
style: () => ({})
})
});
const wrapperRef = ref<HTMLDivElement>()
const canvasRef = ref<HTMLCanvasElement>()
const dots = ref<Dot[]>([])
const wrapperRef = ref<HTMLDivElement>();
const canvasRef = ref<HTMLCanvasElement>();
const dots = ref<Dot[]>([]);
const pointer = ref({
x: 0,
y: 0,
@@ -75,146 +75,146 @@ const pointer = ref({
speed: 0,
lastTime: 0,
lastX: 0,
lastY: 0,
})
lastY: 0
});
function hexToRgb(hex: string) {
const m = hex.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i)
if (!m) return { r: 0, g: 0, b: 0 }
const m = hex.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i);
if (!m) return { r: 0, g: 0, b: 0 };
return {
r: parseInt(m[1], 16),
g: parseInt(m[2], 16),
b: parseInt(m[3], 16),
}
b: parseInt(m[3], 16)
};
}
const baseRgb = computed(() => hexToRgb(props.baseColor))
const activeRgb = computed(() => hexToRgb(props.activeColor))
const baseRgb = computed(() => hexToRgb(props.baseColor));
const activeRgb = computed(() => hexToRgb(props.activeColor));
const circlePath = computed(() => {
if (typeof window === 'undefined' || !window.Path2D) return null
if (typeof window === 'undefined' || !window.Path2D) return null;
const p = new Path2D()
p.arc(0, 0, props.dotSize / 2, 0, Math.PI * 2)
return p
})
const p = new Path2D();
p.arc(0, 0, props.dotSize / 2, 0, Math.PI * 2);
return p;
});
const buildGrid = () => {
const wrap = wrapperRef.value
const canvas = canvasRef.value
if (!wrap || !canvas) return
const wrap = wrapperRef.value;
const canvas = canvasRef.value;
if (!wrap || !canvas) return;
const { width, height } = wrap.getBoundingClientRect()
const dpr = window.devicePixelRatio || 1
const { width, height } = wrap.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr
canvas.height = height * dpr
canvas.style.width = `${width}px`
canvas.style.height = `${height}px`
const ctx = canvas.getContext('2d')
if (ctx) ctx.scale(dpr, dpr)
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
const ctx = canvas.getContext('2d');
if (ctx) ctx.scale(dpr, dpr);
const cols = Math.floor((width + props.gap) / (props.dotSize + props.gap))
const rows = Math.floor((height + props.gap) / (props.dotSize + props.gap))
const cell = props.dotSize + props.gap
const cols = Math.floor((width + props.gap) / (props.dotSize + props.gap));
const rows = Math.floor((height + props.gap) / (props.dotSize + props.gap));
const cell = props.dotSize + props.gap;
const gridW = cell * cols - props.gap
const gridH = cell * rows - props.gap
const gridW = cell * cols - props.gap;
const gridH = cell * rows - props.gap;
const extraX = width - gridW
const extraY = height - gridH
const extraX = width - gridW;
const extraY = height - gridH;
const startX = extraX / 2 + props.dotSize / 2
const startY = extraY / 2 + props.dotSize / 2
const startX = extraX / 2 + props.dotSize / 2;
const startY = extraY / 2 + props.dotSize / 2;
const newDots: Dot[] = []
const newDots: Dot[] = [];
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
const cx = startX + x * cell
const cy = startY + y * cell
newDots.push({ cx, cy, xOffset: 0, yOffset: 0, _inertiaApplied: false })
const cx = startX + x * cell;
const cy = startY + y * cell;
newDots.push({ cx, cy, xOffset: 0, yOffset: 0, _inertiaApplied: false });
}
}
dots.value = newDots
}
dots.value = newDots;
};
let rafId: number
let resizeObserver: ResizeObserver | null = null
let rafId: number;
let resizeObserver: ResizeObserver | null = null;
const draw = () => {
const canvas = canvasRef.value
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
ctx.clearRect(0, 0, canvas.width, canvas.height)
const canvas = canvasRef.value;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const { x: px, y: py } = pointer.value
const proxSq = props.proximity * props.proximity
const { x: px, y: py } = pointer.value;
const proxSq = props.proximity * props.proximity;
for (const dot of dots.value) {
const ox = dot.cx + dot.xOffset
const oy = dot.cy + dot.yOffset
const dx = dot.cx - px
const dy = dot.cy - py
const dsq = dx * dx + dy * dy
const ox = dot.cx + dot.xOffset;
const oy = dot.cy + dot.yOffset;
const dx = dot.cx - px;
const dy = dot.cy - py;
const dsq = dx * dx + dy * dy;
let style = props.baseColor
let style = props.baseColor;
if (dsq <= proxSq) {
const dist = Math.sqrt(dsq)
const t = 1 - dist / props.proximity
const r = Math.round(baseRgb.value.r + (activeRgb.value.r - baseRgb.value.r) * t)
const g = Math.round(baseRgb.value.g + (activeRgb.value.g - baseRgb.value.g) * t)
const b = Math.round(baseRgb.value.b + (activeRgb.value.b - baseRgb.value.b) * t)
style = `rgb(${r},${g},${b})`
const dist = Math.sqrt(dsq);
const t = 1 - dist / props.proximity;
const r = Math.round(baseRgb.value.r + (activeRgb.value.r - baseRgb.value.r) * t);
const g = Math.round(baseRgb.value.g + (activeRgb.value.g - baseRgb.value.g) * t);
const b = Math.round(baseRgb.value.b + (activeRgb.value.b - baseRgb.value.b) * t);
style = `rgb(${r},${g},${b})`;
}
if (circlePath.value) {
ctx.save()
ctx.translate(ox, oy)
ctx.fillStyle = style
ctx.fill(circlePath.value)
ctx.restore()
ctx.save();
ctx.translate(ox, oy);
ctx.fillStyle = style;
ctx.fill(circlePath.value);
ctx.restore();
}
}
rafId = requestAnimationFrame(draw)
}
rafId = requestAnimationFrame(draw);
};
const onMove = (e: MouseEvent) => {
const now = performance.now()
const pr = pointer.value
const dt = pr.lastTime ? now - pr.lastTime : 16
const dx = e.clientX - pr.lastX
const dy = e.clientY - pr.lastY
let vx = (dx / dt) * 1000
let vy = (dy / dt) * 1000
let speed = Math.hypot(vx, vy)
const now = performance.now();
const pr = pointer.value;
const dt = pr.lastTime ? now - pr.lastTime : 16;
const dx = e.clientX - pr.lastX;
const dy = e.clientY - pr.lastY;
let vx = (dx / dt) * 1000;
let vy = (dy / dt) * 1000;
let speed = Math.hypot(vx, vy);
if (speed > props.maxSpeed) {
const scale = props.maxSpeed / speed
vx *= scale
vy *= scale
speed = props.maxSpeed
const scale = props.maxSpeed / speed;
vx *= scale;
vy *= scale;
speed = props.maxSpeed;
}
pr.lastTime = now
pr.lastX = e.clientX
pr.lastY = e.clientY
pr.vx = vx
pr.vy = vy
pr.speed = speed
pr.lastTime = now;
pr.lastX = e.clientX;
pr.lastY = e.clientY;
pr.vx = vx;
pr.vy = vy;
pr.speed = speed;
const canvas = canvasRef.value
if (!canvas) return
const rect = canvas.getBoundingClientRect()
pr.x = e.clientX - rect.left
pr.y = e.clientY - rect.top
const canvas = canvasRef.value;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
pr.x = e.clientX - rect.left;
pr.y = e.clientY - rect.top;
for (const dot of dots.value) {
const dist = Math.hypot(dot.cx - pr.x, dot.cy - pr.y)
const dist = Math.hypot(dot.cx - pr.x, dot.cy - pr.y);
if (speed > props.speedTrigger && dist < props.proximity && !dot._inertiaApplied) {
dot._inertiaApplied = true
gsap.killTweensOf(dot)
const pushX = dot.cx - pr.x + vx * 0.005
const pushY = dot.cy - pr.y + vy * 0.005
dot._inertiaApplied = true;
gsap.killTweensOf(dot);
const pushX = dot.cx - pr.x + vx * 0.005;
const pushY = dot.cy - pr.y + vy * 0.005;
gsap.to(dot, {
inertia: { xOffset: pushX, yOffset: pushY, resistance: props.resistance },
onComplete: () => {
@@ -222,29 +222,29 @@ const onMove = (e: MouseEvent) => {
xOffset: 0,
yOffset: 0,
duration: props.returnDuration,
ease: 'elastic.out(1,0.75)',
})
dot._inertiaApplied = false
},
})
ease: 'elastic.out(1,0.75)'
});
dot._inertiaApplied = false;
}
});
}
}
}
};
const onClick = (e: MouseEvent) => {
const canvas = canvasRef.value
if (!canvas) return
const rect = canvas.getBoundingClientRect()
const cx = e.clientX - rect.left
const cy = e.clientY - rect.top
const canvas = canvasRef.value;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const cx = e.clientX - rect.left;
const cy = e.clientY - rect.top;
for (const dot of dots.value) {
const dist = Math.hypot(dot.cx - cx, dot.cy - cy)
const dist = Math.hypot(dot.cx - cx, dot.cy - cy);
if (dist < props.shockRadius && !dot._inertiaApplied) {
dot._inertiaApplied = true
gsap.killTweensOf(dot)
const falloff = Math.max(0, 1 - dist / props.shockRadius)
const pushX = (dot.cx - cx) * props.shockStrength * falloff
const pushY = (dot.cy - cy) * props.shockStrength * falloff
dot._inertiaApplied = true;
gsap.killTweensOf(dot);
const falloff = Math.max(0, 1 - dist / props.shockRadius);
const pushX = (dot.cx - cx) * props.shockStrength * falloff;
const pushY = (dot.cy - cy) * props.shockStrength * falloff;
gsap.to(dot, {
inertia: { xOffset: pushX, yOffset: pushY, resistance: props.resistance },
onComplete: () => {
@@ -252,64 +252,64 @@ const onClick = (e: MouseEvent) => {
xOffset: 0,
yOffset: 0,
duration: props.returnDuration,
ease: 'elastic.out(1,0.75)',
})
dot._inertiaApplied = false
},
})
ease: 'elastic.out(1,0.75)'
});
dot._inertiaApplied = false;
}
});
}
}
}
};
const throttledMove = throttle(onMove, 50)
const throttledMove = throttle(onMove, 50);
onMounted(async () => {
await nextTick()
await nextTick();
buildGrid()
buildGrid();
if (circlePath.value) {
draw()
draw();
}
if ('ResizeObserver' in window) {
resizeObserver = new ResizeObserver(buildGrid)
resizeObserver = new ResizeObserver(buildGrid);
if (wrapperRef.value) {
resizeObserver.observe(wrapperRef.value)
resizeObserver.observe(wrapperRef.value);
}
} else {
(window as Window).addEventListener('resize', buildGrid)
(window as Window).addEventListener('resize', buildGrid);
}
window.addEventListener('mousemove', throttledMove, { passive: true })
window.addEventListener('click', onClick)
})
window.addEventListener('mousemove', throttledMove, { passive: true });
window.addEventListener('click', onClick);
});
onUnmounted(() => {
if (rafId) {
cancelAnimationFrame(rafId)
cancelAnimationFrame(rafId);
}
if (resizeObserver) {
resizeObserver.disconnect()
resizeObserver.disconnect();
} else {
window.removeEventListener('resize', buildGrid)
window.removeEventListener('resize', buildGrid);
}
window.removeEventListener('mousemove', throttledMove)
window.removeEventListener('click', onClick)
})
window.removeEventListener('mousemove', throttledMove);
window.removeEventListener('click', onClick);
});
watch([() => props.dotSize, () => props.gap], () => {
buildGrid()
})
buildGrid();
});
watch([() => props.proximity, () => props.baseColor, activeRgb, baseRgb, circlePath], () => {
if (rafId) {
cancelAnimationFrame(rafId)
cancelAnimationFrame(rafId);
}
if (circlePath.value) {
draw()
draw();
}
})
</script>
});
</script>

View File

@@ -0,0 +1,115 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, computed } from 'vue';
import { gsap } from 'gsap';
interface GridMotionProps {
items?: string[];
gradientColor?: string;
}
const props = withDefaults(defineProps<GridMotionProps>(), {
items: () => [],
gradientColor: 'black'
});
const gridRef = ref<HTMLElement | null>(null);
const rowRefs = ref<HTMLElement[]>([]);
const mouseX = ref(window.innerWidth / 2);
const totalItems = 28;
const defaultItems = Array.from({ length: totalItems }, (_, i) => `Item ${i + 1}`);
const combinedItems = computed(() => (props.items.length > 0 ? props.items.slice(0, totalItems) : defaultItems));
function isImage(item: string) {
return typeof item === 'string' && item.startsWith('http');
}
function isTag(item: string) {
return typeof item === 'string' && item.startsWith('<') && item.endsWith('>');
}
onMounted(() => {
gsap.ticker.lagSmoothing(0);
const handleMouseMove = (e: MouseEvent) => {
mouseX.value = e.clientX;
};
const updateMotion = () => {
const maxMoveAmount = 300;
const baseDuration = 0.8;
const inertiaFactors = [0.6, 0.4, 0.3, 0.2];
rowRefs.value.forEach((row, index) => {
const direction = index % 2 === 0 ? 1 : -1;
const moveAmount = ((mouseX.value / window.innerWidth) * maxMoveAmount - maxMoveAmount / 2) * direction;
gsap.to(row, {
x: moveAmount,
duration: baseDuration + inertiaFactors[index % inertiaFactors.length],
ease: 'power3.out',
overwrite: 'auto'
});
});
};
const removeAnimation = gsap.ticker.add(updateMotion);
window.addEventListener('mousemove', handleMouseMove);
onBeforeUnmount(() => {
window.removeEventListener('mousemove', handleMouseMove);
removeAnimation();
});
});
</script>
<template>
<div ref="gridRef" class="w-full h-full overflow-hidden">
<section
class="relative flex justify-center items-center w-full h-screen overflow-hidden"
:style="{
background: `radial-gradient(circle, ${gradientColor} 0%, transparent 100%)`
}"
>
<div class="z-[4] absolute inset-0 bg-[length:250px] pointer-events-none"></div>
<div
class="z-[2] relative flex-none gap-4 grid grid-cols-1 grid-rows-4 w-[150vw] h-[150vh] rotate-[-15deg] origin-center"
>
<div
v-for="rowIndex in 4"
:key="rowIndex"
class="gap-4 grid grid-cols-7"
:style="{ willChange: 'transform, filter' }"
ref="rowRefs"
>
<div v-for="itemIndex in 7" :key="itemIndex" class="relative">
<div
class="relative flex justify-center items-center bg-[#111] rounded-[10px] w-full h-full overflow-hidden text-[1.5rem] text-white"
>
<div
v-if="isImage(combinedItems[(rowIndex - 1) * 7 + (itemIndex - 1)])"
class="top-0 left-0 absolute bg-cover bg-center w-full h-full"
:style="{
backgroundImage: `url(${combinedItems[(rowIndex - 1) * 7 + (itemIndex - 1)]})`
}"
></div>
<div
v-else-if="isTag(combinedItems[(rowIndex - 1) * 7 + (itemIndex - 1)])"
class="z-[2] p-4 text-center"
v-html="combinedItems[(rowIndex - 1) * 7 + (itemIndex - 1)]"
></div>
<div v-else class="z-[1] p-4 text-center">
{{ combinedItems[(rowIndex - 1) * 7 + (itemIndex - 1)] }}
</div>
</div>
</div>
</div>
</div>
<div class="top-0 left-0 relative w-full h-full pointer-events-none"></div>
</section>
</div>
</template>

View File

@@ -3,15 +3,15 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { Renderer, Program, Mesh, Color, Triangle } from 'ogl'
import type { OGLRenderingContext } from 'ogl'
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { Renderer, Program, Mesh, Color, Triangle } from 'ogl';
import type { OGLRenderingContext } from 'ogl';
interface Props {
color?: [number, number, number]
speed?: number
amplitude?: number
mouseReact?: boolean
color?: [number, number, number];
speed?: number;
amplitude?: number;
mouseReact?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
@@ -19,16 +19,16 @@ const props = withDefaults(defineProps<Props>(), {
speed: 1.0,
amplitude: 0.1,
mouseReact: true
})
});
const containerRef = ref<HTMLDivElement | null>(null)
const mousePos = ref({ x: 0.5, y: 0.5 })
const containerRef = ref<HTMLDivElement | null>(null);
const mousePos = ref({ x: 0.5, y: 0.5 });
let renderer: Renderer | null = null
let gl: OGLRenderingContext | null = null
let program: Program | null = null
let mesh: Mesh | null = null
let animationId: number | null = null
let renderer: Renderer | null = null;
let gl: OGLRenderingContext | null = null;
let program: Program | null = null;
let mesh: Mesh | null = null;
let animationId: number | null = null;
const vertexShader = `
attribute vec2 uv;
@@ -40,7 +40,7 @@ void main() {
vUv = uv;
gl_Position = vec4(position, 0, 1);
}
`
`;
const fragmentShader = `
precision highp float;
@@ -71,57 +71,57 @@ void main() {
col = cos(col * cos(vec3(d, a, 2.5)) * 0.5 + 0.5) * uColor;
gl_FragColor = vec4(col, 1.0);
}
`
`;
const resize = () => {
if (!containerRef.value || !renderer || !program || !gl) return
if (!containerRef.value || !renderer || !program || !gl) return;
const container = containerRef.value
const scale = 1
renderer.setSize(container.offsetWidth * scale, container.offsetHeight * scale)
const container = containerRef.value;
const scale = 1;
renderer.setSize(container.offsetWidth * scale, container.offsetHeight * scale);
if (program) {
program.uniforms.uResolution.value = new Color(
gl.canvas.width,
gl.canvas.height,
gl.canvas.width / gl.canvas.height
)
);
}
}
};
const handleMouseMove = (e: MouseEvent) => {
if (!containerRef.value || !program) return
if (!containerRef.value || !program) return;
const rect = containerRef.value.getBoundingClientRect()
const x = (e.clientX - rect.left) / rect.width
const y = 1.0 - (e.clientY - rect.top) / rect.height
const rect = containerRef.value.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width;
const y = 1.0 - (e.clientY - rect.top) / rect.height;
mousePos.value = { x, y }
mousePos.value = { x, y };
if (program.uniforms.uMouse.value) {
program.uniforms.uMouse.value[0] = x
program.uniforms.uMouse.value[1] = y
program.uniforms.uMouse.value[0] = x;
program.uniforms.uMouse.value[1] = y;
}
}
};
const update = (t: number) => {
if (!program || !renderer || !mesh) return
if (!program || !renderer || !mesh) return;
animationId = requestAnimationFrame(update)
program.uniforms.uTime.value = t * 0.001
renderer.render({ scene: mesh })
}
animationId = requestAnimationFrame(update);
program.uniforms.uTime.value = t * 0.001;
renderer.render({ scene: mesh });
};
const initializeScene = () => {
if (!containerRef.value) return
if (!containerRef.value) return;
cleanup()
cleanup();
const container = containerRef.value
renderer = new Renderer()
gl = renderer.gl
gl.clearColor(1, 1, 1, 1)
const container = containerRef.value;
renderer = new Renderer();
gl = renderer.gl;
gl.clearColor(1, 1, 1, 1);
const geometry = new Triangle(gl)
const geometry = new Triangle(gl);
program = new Program(gl, {
vertex: vertexShader,
fragment: fragmentShader,
@@ -129,76 +129,72 @@ const initializeScene = () => {
uTime: { value: 0 },
uColor: { value: new Color(...props.color) },
uResolution: {
value: new Color(
gl.canvas.width,
gl.canvas.height,
gl.canvas.width / gl.canvas.height
)
value: new Color(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height)
},
uMouse: { value: new Float32Array([mousePos.value.x, mousePos.value.y]) },
uAmplitude: { value: props.amplitude },
uSpeed: { value: props.speed }
}
})
});
mesh = new Mesh(gl, { geometry, program })
mesh = new Mesh(gl, { geometry, program });
const canvas = gl.canvas as HTMLCanvasElement
canvas.style.width = '100%'
canvas.style.height = '100%'
canvas.style.display = 'block'
const canvas = gl.canvas as HTMLCanvasElement;
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.display = 'block';
container.appendChild(canvas)
container.appendChild(canvas);
window.addEventListener('resize', resize)
window.addEventListener('resize', resize);
if (props.mouseReact) {
container.addEventListener('mousemove', handleMouseMove)
container.addEventListener('mousemove', handleMouseMove);
}
resize()
animationId = requestAnimationFrame(update)
}
resize();
animationId = requestAnimationFrame(update);
};
const cleanup = () => {
if (animationId) {
cancelAnimationFrame(animationId)
animationId = null
cancelAnimationFrame(animationId);
animationId = null;
}
window.removeEventListener('resize', resize)
window.removeEventListener('resize', resize);
if (containerRef.value) {
containerRef.value.removeEventListener('mousemove', handleMouseMove)
containerRef.value.removeEventListener('mousemove', handleMouseMove);
const canvas = containerRef.value.querySelector('canvas')
const canvas = containerRef.value.querySelector('canvas');
if (canvas) {
containerRef.value.removeChild(canvas)
containerRef.value.removeChild(canvas);
}
}
if (gl) {
gl.getExtension('WEBGL_lose_context')?.loseContext()
gl.getExtension('WEBGL_lose_context')?.loseContext();
}
renderer = null
gl = null
program = null
mesh = null
}
renderer = null;
gl = null;
program = null;
mesh = null;
};
onMounted(() => {
initializeScene()
})
initializeScene();
});
onUnmounted(() => {
cleanup()
})
cleanup();
});
watch(
[() => props.color, () => props.speed, () => props.amplitude, () => props.mouseReact],
() => {
initializeScene()
initializeScene();
},
{ deep: true }
)
</script>
);
</script>

View File

@@ -1,79 +1,140 @@
<template>
<div class="relative overflow-hidden">
<canvas ref="canvasRef" class="absolute top-0 left-0 w-full h-full" />
<div v-if="outerVignette"
class="absolute top-0 left-0 w-full h-full pointer-events-none bg-[radial-gradient(circle,_rgba(0,0,0,0)_60%,_rgba(0,0,0,1)_100%)]" />
<div v-if="centerVignette"
class="absolute top-0 left-0 w-full h-full pointer-events-none bg-[radial-gradient(circle,_rgba(0,0,0,0.8)_0%,_rgba(0,0,0,0)_60%)]" />
<div
v-if="outerVignette"
class="absolute top-0 left-0 w-full h-full pointer-events-none bg-[radial-gradient(circle,_rgba(0,0,0,0)_60%,_rgba(0,0,0,1)_100%)]"
/>
<div
v-if="centerVignette"
class="absolute top-0 left-0 w-full h-full pointer-events-none bg-[radial-gradient(circle,_rgba(0,0,0,0.8)_0%,_rgba(0,0,0,0)_60%)]"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { ref, onMounted, onUnmounted, watch } from 'vue';
interface Props {
glitchColors?: string[]
glitchSpeed?: number
centerVignette?: boolean
outerVignette?: boolean
smooth?: boolean
glitchColors?: string[];
glitchSpeed?: number;
centerVignette?: boolean;
outerVignette?: boolean;
smooth?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
glitchColors: () => ["#2b4539", "#61dca3", "#61b3dc"],
glitchColors: () => ['#2b4539', '#61dca3', '#61b3dc'],
glitchSpeed: 50,
centerVignette: false,
outerVignette: false,
smooth: true
})
});
const canvasRef = ref<HTMLCanvasElement | null>(null)
const animationRef = ref<number | null>(null)
const letters = ref<{
char: string
color: string
targetColor: string
colorProgress: number
}[]>([])
const grid = ref({ columns: 0, rows: 0 })
const context = ref<CanvasRenderingContext2D | null>(null)
const lastGlitchTime = ref(Date.now())
const canvasRef = ref<HTMLCanvasElement | null>(null);
const animationRef = ref<number | null>(null);
const letters = ref<
{
char: string;
color: string;
targetColor: string;
colorProgress: number;
}[]
>([]);
const grid = ref({ columns: 0, rows: 0 });
const context = ref<CanvasRenderingContext2D | null>(null);
const lastGlitchTime = ref(Date.now());
const fontSize = 16
const charWidth = 10
const charHeight = 20
const fontSize = 16;
const charWidth = 10;
const charHeight = 20;
const lettersAndSymbols = [
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
"!", "@", "#", "$", "&", "*", "(", ")", "-", "_", "+", "=", "/",
"[", "]", "{", "}", ";", ":", "<", ">", ",", "0", "1", "2", "3",
"4", "5", "6", "7", "8", "9"
]
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
'!',
'@',
'#',
'$',
'&',
'*',
'(',
')',
'-',
'_',
'+',
'=',
'/',
'[',
']',
'{',
'}',
';',
':',
'<',
'>',
',',
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9'
];
const getRandomChar = () => {
return lettersAndSymbols[Math.floor(Math.random() * lettersAndSymbols.length)]
}
return lettersAndSymbols[Math.floor(Math.random() * lettersAndSymbols.length)];
};
const getRandomColor = () => {
return props.glitchColors[Math.floor(Math.random() * props.glitchColors.length)]
}
return props.glitchColors[Math.floor(Math.random() * props.glitchColors.length)];
};
const hexToRgb = (hex: string) => {
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, (m, r, g, b) => {
return r + r + g + g + b + b
})
return r + r + g + g + b + b;
});
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null
}
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
}
: null;
};
const interpolateColor = (
start: { r: number; g: number; b: number },
@@ -83,171 +144,167 @@ const interpolateColor = (
const result = {
r: Math.round(start.r + (end.r - start.r) * factor),
g: Math.round(start.g + (end.g - start.g) * factor),
b: Math.round(start.b + (end.b - start.b) * factor),
}
return `rgb(${result.r}, ${result.g}, ${result.b})`
}
b: Math.round(start.b + (end.b - start.b) * factor)
};
return `rgb(${result.r}, ${result.g}, ${result.b})`;
};
const calculateGrid = (width: number, height: number) => {
const columns = Math.ceil(width / charWidth)
const rows = Math.ceil(height / charHeight)
return { columns, rows }
}
const columns = Math.ceil(width / charWidth);
const rows = Math.ceil(height / charHeight);
return { columns, rows };
};
const initializeLetters = (columns: number, rows: number) => {
grid.value = { columns, rows }
const totalLetters = columns * rows
grid.value = { columns, rows };
const totalLetters = columns * rows;
letters.value = Array.from({ length: totalLetters }, () => ({
char: getRandomChar(),
color: getRandomColor(),
targetColor: getRandomColor(),
colorProgress: 1,
}))
}
colorProgress: 1
}));
};
const resizeCanvas = () => {
const canvas = canvasRef.value
if (!canvas) return
const parent = canvas.parentElement
if (!parent) return
const canvas = canvasRef.value;
if (!canvas) return;
const parent = canvas.parentElement;
if (!parent) return;
const dpr = window.devicePixelRatio || 1
const parentWidth = parent.parentElement?.offsetWidth || parent.offsetWidth || window.innerWidth
const parentHeight = parent.parentElement?.offsetHeight || parent.offsetHeight || window.innerHeight
const width = Math.max(parentWidth, 300)
const height = Math.max(parentHeight, 300)
const dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr
canvas.height = height * dpr
const parentWidth = parent.parentElement?.offsetWidth || parent.offsetWidth || window.innerWidth;
const parentHeight = parent.parentElement?.offsetHeight || parent.offsetHeight || window.innerHeight;
canvas.style.width = `${width}px`
canvas.style.height = `${height}px`
const width = Math.max(parentWidth, 300);
const height = Math.max(parentHeight, 300);
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
if (context.value) {
context.value.setTransform(dpr, 0, 0, dpr, 0, 0)
context.value.setTransform(dpr, 0, 0, dpr, 0, 0);
}
const { columns, rows } = calculateGrid(width, height)
initializeLetters(columns, rows)
drawLetters()
}
const { columns, rows } = calculateGrid(width, height);
initializeLetters(columns, rows);
drawLetters();
};
const drawLetters = () => {
if (!context.value || letters.value.length === 0) return
const ctx = context.value
const { width, height } = canvasRef.value!.getBoundingClientRect()
ctx.clearRect(0, 0, width, height)
ctx.font = `${fontSize}px monospace`
ctx.textBaseline = "top"
if (!context.value || letters.value.length === 0) return;
const ctx = context.value;
const { width, height } = canvasRef.value!.getBoundingClientRect();
ctx.clearRect(0, 0, width, height);
ctx.font = `${fontSize}px monospace`;
ctx.textBaseline = 'top';
letters.value.forEach((letter, index) => {
const x = (index % grid.value.columns) * charWidth
const y = Math.floor(index / grid.value.columns) * charHeight
ctx.fillStyle = letter.color
ctx.fillText(letter.char, x, y)
})
}
const x = (index % grid.value.columns) * charWidth;
const y = Math.floor(index / grid.value.columns) * charHeight;
ctx.fillStyle = letter.color;
ctx.fillText(letter.char, x, y);
});
};
const updateLetters = () => {
if (!letters.value || letters.value.length === 0) return
if (!letters.value || letters.value.length === 0) return;
const updateCount = Math.max(1, Math.floor(letters.value.length * 0.05))
const updateCount = Math.max(1, Math.floor(letters.value.length * 0.05));
for (let i = 0; i < updateCount; i++) {
const index = Math.floor(Math.random() * letters.value.length)
if (!letters.value[index]) continue
const index = Math.floor(Math.random() * letters.value.length);
if (!letters.value[index]) continue;
letters.value[index].char = getRandomChar()
letters.value[index].targetColor = getRandomColor()
letters.value[index].char = getRandomChar();
letters.value[index].targetColor = getRandomColor();
if (!props.smooth) {
letters.value[index].color = letters.value[index].targetColor
letters.value[index].colorProgress = 1
letters.value[index].color = letters.value[index].targetColor;
letters.value[index].colorProgress = 1;
} else {
letters.value[index].colorProgress = 0
letters.value[index].colorProgress = 0;
}
}
}
};
const handleSmoothTransitions = () => {
let needsRedraw = false
letters.value.forEach((letter) => {
let needsRedraw = false;
letters.value.forEach(letter => {
if (letter.colorProgress < 1) {
letter.colorProgress += 0.05
if (letter.colorProgress > 1) letter.colorProgress = 1
letter.colorProgress += 0.05;
if (letter.colorProgress > 1) letter.colorProgress = 1;
const startRgb = hexToRgb(letter.color)
const endRgb = hexToRgb(letter.targetColor)
const startRgb = hexToRgb(letter.color);
const endRgb = hexToRgb(letter.targetColor);
if (startRgb && endRgb) {
letter.color = interpolateColor(
startRgb,
endRgb,
letter.colorProgress
)
needsRedraw = true
letter.color = interpolateColor(startRgb, endRgb, letter.colorProgress);
needsRedraw = true;
}
}
})
});
if (needsRedraw) {
drawLetters()
drawLetters();
}
}
};
const animate = () => {
const now = Date.now()
const now = Date.now();
if (now - lastGlitchTime.value >= props.glitchSpeed) {
updateLetters()
drawLetters()
lastGlitchTime.value = now
updateLetters();
drawLetters();
lastGlitchTime.value = now;
}
if (props.smooth) {
handleSmoothTransitions()
handleSmoothTransitions();
}
animationRef.value = requestAnimationFrame(animate)
}
animationRef.value = requestAnimationFrame(animate);
};
let resizeTimeout: number
let resizeTimeout: number;
const handleResize = () => {
clearTimeout(resizeTimeout)
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
if (animationRef.value) {
cancelAnimationFrame(animationRef.value)
cancelAnimationFrame(animationRef.value);
}
resizeCanvas()
animate()
}, 100)
}
resizeCanvas();
animate();
}, 100);
};
onMounted(() => {
const canvas = canvasRef.value
if (!canvas) return
const canvas = canvasRef.value;
if (!canvas) return;
context.value = canvas.getContext("2d")
resizeCanvas()
animate()
context.value = canvas.getContext('2d');
resizeCanvas();
animate();
window.addEventListener("resize", handleResize)
})
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
if (animationRef.value) {
cancelAnimationFrame(animationRef.value)
cancelAnimationFrame(animationRef.value);
}
window.removeEventListener("resize", handleResize)
})
window.removeEventListener('resize', handleResize);
});
watch([() => props.glitchSpeed, () => props.smooth], () => {
if (animationRef.value) {
cancelAnimationFrame(animationRef.value)
cancelAnimationFrame(animationRef.value);
}
animate()
})
animate();
});
</script>
<style scoped>
@@ -266,4 +323,4 @@ div {
width: 100% !important;
height: 100% !important;
}
</style>
</style>

View File

@@ -3,14 +3,14 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { ref, onMounted, onUnmounted, watch } from 'vue';
interface LightningProps {
hue?: number
xOffset?: number
speed?: number
intensity?: number
size?: number
hue?: number;
xOffset?: number;
speed?: number;
intensity?: number;
size?: number;
}
const props = withDefaults(defineProps<LightningProps>(), {
@@ -19,20 +19,20 @@ const props = withDefaults(defineProps<LightningProps>(), {
speed: 1,
intensity: 1,
size: 1
})
});
const canvasRef = ref<HTMLCanvasElement>()
let animationId = 0
let gl: WebGLRenderingContext | null = null
let program: WebGLProgram | null = null
let startTime = 0
const canvasRef = ref<HTMLCanvasElement>();
let animationId = 0;
let gl: WebGLRenderingContext | null = null;
let program: WebGLProgram | null = null;
let startTime = 0;
const vertexShaderSource = `
attribute vec2 aPosition;
void main() {
gl_Position = vec4(aPosition, 0.0, 1.0);
}
`
`;
const fragmentShaderSource = `
precision mediump float;
@@ -112,155 +112,153 @@ void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
void main() {
mainImage(gl_FragColor, gl_FragCoord.xy);
}
`
`;
const compileShader = (source: string, type: number): WebGLShader | null => {
if (!gl) return null
const shader = gl.createShader(type)
if (!shader) return null
gl.shaderSource(shader, source)
gl.compileShader(shader)
if (!gl) return null;
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('Shader compile error:', gl.getShaderInfoLog(shader))
gl.deleteShader(shader)
return null
console.error('Shader compile error:', gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader
}
return shader;
};
const initWebGL = () => {
const canvas = canvasRef.value
if (!canvas) return
const canvas = canvasRef.value;
if (!canvas) return;
const resizeCanvas = () => {
const rect = canvas.getBoundingClientRect()
const dpr = window.devicePixelRatio || 1
let width = rect.width
let height = rect.height
let parent = canvas.parentElement
const rect = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
let width = rect.width;
let height = rect.height;
let parent = canvas.parentElement;
while (parent && (!width || !height)) {
if (parent.offsetWidth && parent.offsetHeight) {
width = parent.offsetWidth
height = parent.offsetHeight
break
width = parent.offsetWidth;
height = parent.offsetHeight;
break;
}
parent = parent.parentElement
parent = parent.parentElement;
}
if (!width || !height) {
width = window.innerWidth
height = window.innerHeight
width = window.innerWidth;
height = window.innerHeight;
}
width = Math.max(width, 300)
height = Math.max(height, 300)
canvas.width = width * dpr
canvas.height = height * dpr
canvas.style.width = '100%'
canvas.style.height = '100%'
canvas.style.display = 'block'
canvas.style.position = 'absolute'
canvas.style.top = '0'
canvas.style.left = '0'
}
resizeCanvas()
window.addEventListener('resize', resizeCanvas)
width = Math.max(width, 300);
height = Math.max(height, 300);
gl = canvas.getContext('webgl')
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.display = 'block';
canvas.style.position = 'absolute';
canvas.style.top = '0';
canvas.style.left = '0';
};
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
gl = canvas.getContext('webgl');
if (!gl) {
console.error('WebGL not supported')
return
console.error('WebGL not supported');
return;
}
const vertexShader = compileShader(vertexShaderSource, gl.VERTEX_SHADER)
const fragmentShader = compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER)
if (!vertexShader || !fragmentShader) return
const vertexShader = compileShader(vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER);
if (!vertexShader || !fragmentShader) return;
program = gl.createProgram()
if (!program) return
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)
gl.linkProgram(program)
program = gl.createProgram();
if (!program) return;
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('Program linking error:', gl.getProgramInfoLog(program))
return
console.error('Program linking error:', gl.getProgramInfoLog(program));
return;
}
gl.useProgram(program)
gl.useProgram(program);
const vertices = new Float32Array([
-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1,
])
const vertexBuffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const aPosition = gl.getAttribLocation(program, 'aPosition')
gl.enableVertexAttribArray(aPosition)
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)
const aPosition = gl.getAttribLocation(program, 'aPosition');
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
startTime = performance.now()
render()
startTime = performance.now();
render();
return () => {
window.removeEventListener('resize', resizeCanvas)
}
}
window.removeEventListener('resize', resizeCanvas);
};
};
const render = () => {
if (!gl || !program || !canvasRef.value) return
if (!gl || !program || !canvasRef.value) return;
const canvas = canvasRef.value
const canvas = canvasRef.value;
const rect = canvas.getBoundingClientRect()
const rect = canvas.getBoundingClientRect();
if (canvas.width !== rect.width || canvas.height !== rect.height) {
canvas.width = rect.width
canvas.height = rect.height
canvas.style.width = rect.width + 'px'
canvas.style.height = rect.height + 'px'
canvas.width = rect.width;
canvas.height = rect.height;
canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';
}
gl.viewport(0, 0, canvas.width, canvas.height)
gl.viewport(0, 0, canvas.width, canvas.height);
const iResolutionLocation = gl.getUniformLocation(program, 'iResolution')
const iTimeLocation = gl.getUniformLocation(program, 'iTime')
const uHueLocation = gl.getUniformLocation(program, 'uHue')
const uXOffsetLocation = gl.getUniformLocation(program, 'uXOffset')
const uSpeedLocation = gl.getUniformLocation(program, 'uSpeed')
const uIntensityLocation = gl.getUniformLocation(program, 'uIntensity')
const uSizeLocation = gl.getUniformLocation(program, 'uSize')
const iResolutionLocation = gl.getUniformLocation(program, 'iResolution');
const iTimeLocation = gl.getUniformLocation(program, 'iTime');
const uHueLocation = gl.getUniformLocation(program, 'uHue');
const uXOffsetLocation = gl.getUniformLocation(program, 'uXOffset');
const uSpeedLocation = gl.getUniformLocation(program, 'uSpeed');
const uIntensityLocation = gl.getUniformLocation(program, 'uIntensity');
const uSizeLocation = gl.getUniformLocation(program, 'uSize');
gl.uniform2f(iResolutionLocation, canvas.width, canvas.height)
const currentTime = performance.now()
gl.uniform1f(iTimeLocation, (currentTime - startTime) / 1000.0)
gl.uniform1f(uHueLocation, props.hue)
gl.uniform1f(uXOffsetLocation, props.xOffset)
gl.uniform1f(uSpeedLocation, props.speed)
gl.uniform1f(uIntensityLocation, props.intensity)
gl.uniform1f(uSizeLocation, props.size)
gl.uniform2f(iResolutionLocation, canvas.width, canvas.height);
const currentTime = performance.now();
gl.uniform1f(iTimeLocation, (currentTime - startTime) / 1000.0);
gl.uniform1f(uHueLocation, props.hue);
gl.uniform1f(uXOffsetLocation, props.xOffset);
gl.uniform1f(uSpeedLocation, props.speed);
gl.uniform1f(uIntensityLocation, props.intensity);
gl.uniform1f(uSizeLocation, props.size);
gl.drawArrays(gl.TRIANGLES, 0, 6)
animationId = requestAnimationFrame(render)
}
gl.drawArrays(gl.TRIANGLES, 0, 6);
animationId = requestAnimationFrame(render);
};
onMounted(() => {
initWebGL()
})
initWebGL();
});
onUnmounted(() => {
if (animationId) {
cancelAnimationFrame(animationId)
cancelAnimationFrame(animationId);
}
})
});
watch(
() => [props.hue, props.xOffset, props.speed, props.intensity, props.size],
() => {}
)
);
</script>
<style scoped>

View File

@@ -3,22 +3,22 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { Renderer, Camera, Geometry, Program, Mesh } from 'ogl'
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { Renderer, Camera, Geometry, Program, Mesh } from 'ogl';
interface ParticlesProps {
particleCount?: number
particleSpread?: number
speed?: number
particleColors?: string[]
moveParticlesOnHover?: boolean
particleHoverFactor?: number
alphaParticles?: boolean
particleBaseSize?: number
sizeRandomness?: number
cameraDistance?: number
disableRotation?: boolean
className?: string
particleCount?: number;
particleSpread?: number;
speed?: number;
particleColors?: string[];
moveParticlesOnHover?: boolean;
particleHoverFactor?: number;
alphaParticles?: boolean;
particleBaseSize?: number;
sizeRandomness?: number;
cameraDistance?: number;
disableRotation?: boolean;
className?: string;
}
const props = withDefaults(defineProps<ParticlesProps>(), {
@@ -34,32 +34,35 @@ const props = withDefaults(defineProps<ParticlesProps>(), {
cameraDistance: 20,
disableRotation: false,
className: ''
})
});
const containerRef = ref<HTMLDivElement>()
const mouseRef = ref({ x: 0, y: 0 })
const containerRef = ref<HTMLDivElement>();
const mouseRef = ref({ x: 0, y: 0 });
let renderer: Renderer | null = null
let camera: Camera | null = null
let particles: Mesh | null = null
let program: Program | null = null
let animationFrameId: number | null = null
let lastTime = 0
let elapsed = 0
let renderer: Renderer | null = null;
let camera: Camera | null = null;
let particles: Mesh | null = null;
let program: Program | null = null;
let animationFrameId: number | null = null;
let lastTime = 0;
let elapsed = 0;
const defaultColors = ['#ffffff', '#ffffff', '#ffffff']
const defaultColors = ['#ffffff', '#ffffff', '#ffffff'];
const hexToRgb = (hex: string): [number, number, number] => {
hex = hex.replace(/^#/, '')
hex = hex.replace(/^#/, '');
if (hex.length === 3) {
hex = hex.split('').map((c) => c + c).join('')
hex = hex
.split('')
.map(c => c + c)
.join('');
}
const int = parseInt(hex, 16)
const r = ((int >> 16) & 255) / 255
const g = ((int >> 8) & 255) / 255
const b = (int & 255) / 255
return [r, g, b]
}
const int = parseInt(hex, 16);
const r = ((int >> 16) & 255) / 255;
const g = ((int >> 8) & 255) / 255;
const b = (int & 255) / 255;
return [r, g, b];
};
const vertex = /* glsl */ `
attribute vec3 position;
@@ -94,7 +97,7 @@ const vertex = /* glsl */ `
gl_PointSize = (uBaseSize * (1.0 + uSizeRandomness * (random.x - 0.5))) / length(mvPos.xyz);
gl_Position = projectionMatrix * mvPos;
}
`
`;
const fragment = /* glsl */ `
precision highp float;
@@ -118,89 +121,89 @@ const fragment = /* glsl */ `
gl_FragColor = vec4(vColor + 0.2 * sin(uv.yxx + uTime + vRandom.y * 6.28), circle);
}
}
`
`;
const handleMouseMove = (e: MouseEvent) => {
const container = containerRef.value
if (!container) return
const container = containerRef.value;
if (!container) return;
const rect = container.getBoundingClientRect()
const x = ((e.clientX - rect.left) / rect.width) * 2 - 1
const y = -(((e.clientY - rect.top) / rect.height) * 2 - 1)
mouseRef.value = { x, y }
}
const rect = container.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
const y = -(((e.clientY - rect.top) / rect.height) * 2 - 1);
mouseRef.value = { x, y };
};
const initParticles = () => {
const container = containerRef.value
if (!container) return
const container = containerRef.value;
if (!container) return;
renderer = new Renderer({ depth: false, alpha: true })
const gl = renderer.gl
container.appendChild(gl.canvas)
gl.clearColor(0, 0, 0, 0)
renderer = new Renderer({ depth: false, alpha: true });
const gl = renderer.gl;
container.appendChild(gl.canvas);
gl.clearColor(0, 0, 0, 0);
gl.canvas.style.width = '100%'
gl.canvas.style.height = '100%'
gl.canvas.style.display = 'block'
gl.canvas.style.position = 'absolute'
gl.canvas.style.top = '0'
gl.canvas.style.left = '0'
gl.canvas.style.width = '100%';
gl.canvas.style.height = '100%';
gl.canvas.style.display = 'block';
gl.canvas.style.position = 'absolute';
gl.canvas.style.top = '0';
gl.canvas.style.left = '0';
camera = new Camera(gl, { fov: 15 })
camera.position.set(0, 0, props.cameraDistance)
camera = new Camera(gl, { fov: 15 });
camera.position.set(0, 0, props.cameraDistance);
const resize = () => {
if (!container) return
const parentWidth = container.parentElement?.offsetWidth || container.offsetWidth || window.innerWidth
const parentHeight = container.parentElement?.offsetHeight || container.offsetHeight || window.innerHeight
const width = Math.max(parentWidth, 300)
const height = Math.max(parentHeight, 300)
renderer!.setSize(width, height)
camera!.perspective({ aspect: width / height })
if (!container) return;
gl.canvas.style.width = '100%'
gl.canvas.style.height = '100%'
gl.canvas.style.display = 'block'
gl.canvas.style.position = 'absolute'
gl.canvas.style.top = '0'
gl.canvas.style.left = '0'
}
window.addEventListener('resize', resize, false)
resize()
const parentWidth = container.parentElement?.offsetWidth || container.offsetWidth || window.innerWidth;
const parentHeight = container.parentElement?.offsetHeight || container.offsetHeight || window.innerHeight;
const width = Math.max(parentWidth, 300);
const height = Math.max(parentHeight, 300);
renderer!.setSize(width, height);
camera!.perspective({ aspect: width / height });
gl.canvas.style.width = '100%';
gl.canvas.style.height = '100%';
gl.canvas.style.display = 'block';
gl.canvas.style.position = 'absolute';
gl.canvas.style.top = '0';
gl.canvas.style.left = '0';
};
window.addEventListener('resize', resize, false);
resize();
if (props.moveParticlesOnHover) {
container.addEventListener('mousemove', handleMouseMove)
container.addEventListener('mousemove', handleMouseMove);
}
const count = props.particleCount
const positions = new Float32Array(count * 3)
const randoms = new Float32Array(count * 4)
const colors = new Float32Array(count * 3)
const palette = props.particleColors && props.particleColors.length > 0 ? props.particleColors : defaultColors
const count = props.particleCount;
const positions = new Float32Array(count * 3);
const randoms = new Float32Array(count * 4);
const colors = new Float32Array(count * 3);
const palette = props.particleColors && props.particleColors.length > 0 ? props.particleColors : defaultColors;
for (let i = 0; i < count; i++) {
let x: number, y: number, z: number, len: number
let x: number, y: number, z: number, len: number;
do {
x = Math.random() * 2 - 1
y = Math.random() * 2 - 1
z = Math.random() * 2 - 1
len = x * x + y * y + z * z
} while (len > 1 || len === 0)
const r = Math.cbrt(Math.random())
positions.set([x * r, y * r, z * r], i * 3)
randoms.set([Math.random(), Math.random(), Math.random(), Math.random()], i * 4)
const col = hexToRgb(palette[Math.floor(Math.random() * palette.length)])
colors.set(col, i * 3)
x = Math.random() * 2 - 1;
y = Math.random() * 2 - 1;
z = Math.random() * 2 - 1;
len = x * x + y * y + z * z;
} while (len > 1 || len === 0);
const r = Math.cbrt(Math.random());
positions.set([x * r, y * r, z * r], i * 3);
randoms.set([Math.random(), Math.random(), Math.random(), Math.random()], i * 4);
const col = hexToRgb(palette[Math.floor(Math.random() * palette.length)]);
colors.set(col, i * 3);
}
const geometry = new Geometry(gl, {
position: { size: 3, data: positions },
random: { size: 4, data: randoms },
color: { size: 3, data: colors },
})
color: { size: 3, data: colors }
});
program = new Program(gl, {
vertex,
@@ -210,105 +213,105 @@ const initParticles = () => {
uSpread: { value: props.particleSpread },
uBaseSize: { value: props.particleBaseSize },
uSizeRandomness: { value: props.sizeRandomness },
uAlphaParticles: { value: props.alphaParticles ? 1 : 0 },
uAlphaParticles: { value: props.alphaParticles ? 1 : 0 }
},
transparent: true,
depthTest: false,
})
depthTest: false
});
particles = new Mesh(gl, { mode: gl.POINTS, geometry, program })
particles = new Mesh(gl, { mode: gl.POINTS, geometry, program });
lastTime = performance.now()
elapsed = 0
lastTime = performance.now();
elapsed = 0;
const update = (t: number) => {
if (!animationFrameId) return
animationFrameId = requestAnimationFrame(update)
const delta = t - lastTime
lastTime = t
elapsed += delta * props.speed
if (!animationFrameId) return;
animationFrameId = requestAnimationFrame(update);
const delta = t - lastTime;
lastTime = t;
elapsed += delta * props.speed;
if (program) {
program.uniforms.uTime.value = elapsed * 0.001
program.uniforms.uSpread.value = props.particleSpread
program.uniforms.uBaseSize.value = props.particleBaseSize
program.uniforms.uSizeRandomness.value = props.sizeRandomness
program.uniforms.uAlphaParticles.value = props.alphaParticles ? 1 : 0
program.uniforms.uTime.value = elapsed * 0.001;
program.uniforms.uSpread.value = props.particleSpread;
program.uniforms.uBaseSize.value = props.particleBaseSize;
program.uniforms.uSizeRandomness.value = props.sizeRandomness;
program.uniforms.uAlphaParticles.value = props.alphaParticles ? 1 : 0;
}
if (particles) {
if (props.moveParticlesOnHover) {
particles.position.x = -mouseRef.value.x * props.particleHoverFactor
particles.position.y = -mouseRef.value.y * props.particleHoverFactor
particles.position.x = -mouseRef.value.x * props.particleHoverFactor;
particles.position.y = -mouseRef.value.y * props.particleHoverFactor;
} else {
particles.position.x = 0
particles.position.y = 0
particles.position.x = 0;
particles.position.y = 0;
}
if (!props.disableRotation) {
particles.rotation.x = Math.sin(elapsed * 0.0002) * 0.1
particles.rotation.y = Math.cos(elapsed * 0.0005) * 0.15
particles.rotation.z += 0.01 * props.speed
particles.rotation.x = Math.sin(elapsed * 0.0002) * 0.1;
particles.rotation.y = Math.cos(elapsed * 0.0005) * 0.15;
particles.rotation.z += 0.01 * props.speed;
}
}
if (renderer && camera && particles) {
renderer.render({ scene: particles, camera })
renderer.render({ scene: particles, camera });
}
}
};
animationFrameId = requestAnimationFrame(update)
animationFrameId = requestAnimationFrame(update);
return () => {
window.removeEventListener('resize', resize)
window.removeEventListener('resize', resize);
if (props.moveParticlesOnHover) {
container.removeEventListener('mousemove', handleMouseMove)
container.removeEventListener('mousemove', handleMouseMove);
}
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
animationFrameId = null
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
if (container.contains(gl.canvas)) {
container.removeChild(gl.canvas)
container.removeChild(gl.canvas);
}
}
}
};
};
const cleanup = () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
animationFrameId = null
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
if (renderer) {
const container = containerRef.value
const gl = renderer.gl
const container = containerRef.value;
const gl = renderer.gl;
if (container && gl.canvas.parentNode === container) {
container.removeChild(gl.canvas)
container.removeChild(gl.canvas);
}
gl.getExtension('WEBGL_lose_context')?.loseContext()
gl.getExtension('WEBGL_lose_context')?.loseContext();
}
renderer = null
camera = null
particles = null
program = null
}
renderer = null;
camera = null;
particles = null;
program = null;
};
onMounted(() => {
initParticles()
})
initParticles();
});
onUnmounted(() => {
cleanup()
})
cleanup();
});
watch(
() => [props.particleCount, props.particleColors],
() => {
cleanup()
initParticles()
cleanup();
initParticles();
},
{ deep: true }
)
);
watch(
() => [
@@ -322,7 +325,7 @@ watch(
props.disableRotation
],
() => {}
)
);
</script>
<style scoped>

Some files were not shown because too many files have changed in this diff Show More