Landing Page
@@ -17,4 +17,12 @@ export default defineConfigWithVueTs(
|
|||||||
|
|
||||||
pluginVue.configs['flat/essential'],
|
pluginVue.configs['flat/essential'],
|
||||||
vueTsConfigs.recommended,
|
vueTsConfigs.recommended,
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'app/vue-rules',
|
||||||
|
files: ['**/*.vue'],
|
||||||
|
rules: {
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<html lang="">
|
<html lang="">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
<meta name="theme-color" content="#0e0e0e">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Vue Bits</title>
|
<title>Vue Bits</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
21
package-lock.json
generated
@@ -8,6 +8,9 @@
|
|||||||
"name": "vue-bits",
|
"name": "vue-bits",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"gsap": "^3.13.0",
|
||||||
|
"ogl": "^1.0.11",
|
||||||
|
"primeicons": "^7.0.0",
|
||||||
"vue": "^3.5.17",
|
"vue": "^3.5.17",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
@@ -3652,6 +3655,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/gsap": {
|
||||||
|
"version": "3.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.13.0.tgz",
|
||||||
|
"integrity": "sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw==",
|
||||||
|
"license": "Standard 'no charge' license: https://gsap.com/standard-license."
|
||||||
|
},
|
||||||
"node_modules/has-flag": {
|
"node_modules/has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
@@ -4574,6 +4583,12 @@
|
|||||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ogl": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/ogl/-/ogl-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-kUpC154AFfxi16pmZUK4jk3J+8zxwTWGPo03EoYA8QPbzikHoaC82n6pNTbd+oEaJonaE8aPWBlX7ad9zrqLsA==",
|
||||||
|
"license": "Unlicense"
|
||||||
|
},
|
||||||
"node_modules/open": {
|
"node_modules/open": {
|
||||||
"version": "10.1.2",
|
"version": "10.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz",
|
||||||
@@ -4817,6 +4832,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/primeicons": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
"lint": "eslint . --fix"
|
"lint": "eslint . --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"gsap": "^3.13.0",
|
||||||
|
"ogl": "^1.0.11",
|
||||||
|
"primeicons": "^7.0.0",
|
||||||
"vue": "^3.5.17",
|
"vue": "^3.5.17",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
public/assets/components.gif
Normal file
|
After Width: | Height: | Size: 7.2 MiB |
BIN
public/assets/grain.webp
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
public/assets/messages.gif
Normal file
|
After Width: | Height: | Size: 424 KiB |
26
src/App.vue
@@ -1,3 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<h1>Vue Bits</h1>
|
<div>
|
||||||
|
<DisplayHeader
|
||||||
|
v-if="!isCategoryPage"
|
||||||
|
:activeItem="activeItem"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import DisplayHeader from '@/components/landing/DisplayHeader/DisplayHeader.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const activeItem = computed(() => {
|
||||||
|
if (route.path === '/') return 'home'
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
const isCategoryPage = computed(() => {
|
||||||
|
return /^\/[^/]+\/[^/]+$/.test(route.path)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|||||||
3
src/assets/common/star.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="6" height="6" viewBox="0 0 6 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2.52894 2.0306L3.11194 0.794686C3.12764 0.759297 3.15328 0.729227 3.18574 0.708121C3.2182 0.687015 3.25608 0.675781 3.2948 0.675781C3.33352 0.675781 3.3714 0.687015 3.40386 0.708121C3.43632 0.729227 3.46196 0.759297 3.47766 0.794686L4.06066 2.0306L5.36388 2.22992C5.53104 2.25535 5.59761 2.47074 5.47682 2.59415L4.53371 3.55521L4.75621 4.91303C4.785 5.08767 4.60999 5.22042 4.46041 5.13815L3.2948 4.49683L2.12919 5.13815C1.97961 5.22042 1.8046 5.08767 1.83339 4.91303L2.05589 3.55558L1.11278 2.59415C0.991994 2.47074 1.05856 2.25535 1.22572 2.22992L2.52894 2.0306Z" fill="white" stroke="white" stroke-width="0.56093" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 775 B |
4
src/assets/logos/vue-bits-logo-small-dark.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="67" height="58" viewBox="0 0 67 58" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0.948242 0.144043H22.837C22.837 0.144043 32.0299 17.1763 38.658 27.6444C38.9372 28.0853 39.0747 28.3453 39.3749 28.7722C39.6552 29.1709 39.813 29.3963 40.1365 29.7609C40.4407 30.1038 40.3868 30.0701 40.7353 30.4551C40.9256 30.6654 41.0901 30.8243 41.4999 31.0893C41.8709 31.3291 42.5065 31.4903 42.9886 31.3276C43.3618 31.2017 43.4628 31.1547 43.7801 30.9768C44.8913 30.354 46.0324 28.0858 46.0324 28.0858L58.7905 4.81785H47.6028V0.144994L66.7607 0.144043L51.406 26.7942C51.406 26.7942 49.8437 29.2994 48.7246 31.2443C48.5422 31.5613 47.7633 32.7078 47.46 33.0943C46.8867 33.825 46.5664 34.2616 46.0324 34.7887C45.5209 35.2936 45.0161 35.5409 44.6578 35.6848C44.1037 35.9073 43.511 36.0194 42.9158 36.068C42.4088 36.1093 42.0672 36.1359 41.563 36.068C40.9342 35.9833 40.8891 35.9616 40.1365 35.6848C39.82 35.5684 39.2914 35.2543 38.7932 34.8951C38.2398 34.496 37.8624 34.1401 37.2402 33.482C36.6925 32.9026 36.4509 32.5591 36.008 31.8962C35.543 31.2003 35.1813 30.8154 34.7421 30.103C28.7742 20.4232 20.2999 4.81785 20.2999 4.81785H9.0302L33.8982 48.0406L38.658 39.8352C38.658 39.8352 39.3105 40.7251 41.6 40.6509C43.7041 40.5826 44.2102 39.8352 44.2102 39.8352L33.8982 57.384L5.50866 8.16941L3.60187 4.81785L0.948242 0.144043Z" fill="#0e0e0e"/>
|
||||||
|
<circle cx="42.4365" cy="14.686" r="4.78514" fill="#0e0e0e"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
4
src/assets/logos/vue-bits-logo-small.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="67" height="58" viewBox="0 0 67 58" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0.54834 0.144043H22.4371C22.4371 0.144043 31.63 17.1763 38.2581 27.6444C38.5373 28.0853 38.6748 28.3453 38.975 28.7722C39.2553 29.1709 39.4131 29.3963 39.7366 29.7609C40.0408 30.1038 39.9869 30.0701 40.3354 30.4551C40.5257 30.6654 40.6902 30.8243 41.1 31.0893C41.471 31.3291 42.1066 31.4903 42.5886 31.3276C42.9619 31.2017 43.0629 31.1547 43.3802 30.9768C44.4914 30.354 45.6325 28.0858 45.6325 28.0858L58.3906 4.81785H47.2029V0.144994L66.3608 0.144043L51.0061 26.7942C51.0061 26.7942 49.4438 29.2994 48.3247 31.2443C48.1423 31.5613 47.3634 32.7078 47.0601 33.0943C46.4868 33.825 46.1665 34.2616 45.6325 34.7887C45.121 35.2936 44.6162 35.5409 44.2579 35.6848C43.7038 35.9073 43.1111 36.0194 42.5159 36.068C42.0089 36.1093 41.6673 36.1359 41.1631 36.068C40.5343 35.9833 40.4892 35.9616 39.7366 35.6848C39.4201 35.5684 38.8915 35.2543 38.3933 34.8951C37.8399 34.496 37.4625 34.1401 36.8403 33.482C36.2926 32.9026 36.051 32.5591 35.608 31.8962C35.1431 31.2003 34.7814 30.8154 34.3422 30.103C28.3743 20.4232 19.9 4.81785 19.9 4.81785H8.6303L33.4983 48.0406L38.2581 39.8352C38.2581 39.8352 38.9106 40.7251 41.2001 40.6509C43.3042 40.5826 43.8103 39.8352 43.8103 39.8352L33.4983 57.384L5.10875 8.16941L3.20196 4.81785L0.54834 0.144043Z" fill="white"/>
|
||||||
|
<circle cx="42.0366" cy="14.686" r="4.78514" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
11
src/assets/logos/vue-bits-logo.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="193" height="41" 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="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"/>
|
||||||
|
<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"/>
|
||||||
|
<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"/>
|
||||||
|
<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"/>
|
||||||
|
<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"/>
|
||||||
|
<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"/>
|
||||||
|
<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"/>
|
||||||
|
<circle cx="29.3282" cy="10.6089" r="3.34281" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.8 KiB |
11
src/assets/logos/vuebits-gh-black.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="849" height="178" viewBox="0 0 849 178" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M292.054 150.096L247.575 30.3442H266.051L302.318 132.304L338.928 30.3442H357.062L312.583 150.096H292.054Z" fill="#0e0e0e"/>
|
||||||
|
<path d="M388.782 152.149C382.053 152.149 376.179 150.78 371.161 148.043C366.257 145.306 362.436 141.2 359.699 135.726C357.076 130.251 355.765 123.351 355.765 115.026V63.8746H372.872V113.144C372.872 121.241 374.64 127.343 378.175 131.449C381.711 135.554 386.786 137.607 393.401 137.607C397.849 137.607 401.84 136.524 405.376 134.357C409.025 132.19 411.877 129.054 413.93 124.948C415.982 120.842 417.009 115.824 417.009 109.893V63.8746H434.116V150.096H418.891L417.693 135.383C415.07 140.63 411.249 144.735 406.231 147.701C401.213 150.666 395.397 152.149 388.782 152.149Z" fill="#0e0e0e"/>
|
||||||
|
<path d="M486.973 152.149C478.762 152.149 471.463 150.267 465.076 146.503C458.689 142.74 453.671 137.493 450.022 130.764C446.486 124.035 444.718 116.223 444.718 107.327C444.718 98.2034 446.486 90.277 450.022 83.5481C453.671 76.7051 458.689 71.4019 465.076 67.6383C471.463 63.7606 478.876 61.8218 487.316 61.8218C495.755 61.8218 502.997 63.7036 509.042 67.4672C515.087 71.2308 519.763 76.249 523.07 82.5217C526.377 88.6803 528.031 95.5232 528.031 103.05C528.031 104.191 527.974 105.445 527.86 106.814C527.86 108.069 527.803 109.494 527.689 111.091H457.207V98.9447H510.924C510.582 91.7596 508.187 86.1712 503.739 82.1795C499.291 78.0737 493.759 76.0209 487.145 76.0209C482.468 76.0209 478.192 77.1043 474.314 79.2712C470.436 81.3241 467.3 84.4035 464.905 88.5092C462.624 92.5009 461.483 97.5761 461.483 103.735V108.525C461.483 114.912 462.624 120.329 464.905 124.777C467.3 129.111 470.436 132.418 474.314 134.699C478.192 136.866 482.411 137.949 486.973 137.949C492.448 137.949 496.953 136.752 500.488 134.357C504.024 131.962 506.647 128.712 508.358 124.606H525.465C523.982 129.852 521.473 134.585 517.938 138.805C514.402 142.911 510.011 146.161 504.765 148.556C499.633 150.951 493.702 152.149 486.973 152.149Z" fill="#0e0e0e"/>
|
||||||
|
<path d="M575.061 150.096V30.3442H619.369C627.694 30.3442 634.651 31.7128 640.24 34.45C645.828 37.0731 649.991 40.7227 652.728 45.3987C655.579 49.9607 657.005 55.1499 657.005 60.9664C657.005 67.011 655.693 72.0862 653.07 76.1919C650.447 80.2977 646.968 83.4911 642.635 85.772C638.415 87.939 633.853 89.1935 628.949 89.5357L631.344 87.8249C636.59 87.939 641.38 89.3646 645.714 92.1018C650.048 94.7249 653.469 98.2604 655.978 102.708C658.487 107.156 659.742 112.06 659.742 117.421C659.742 123.579 658.259 129.168 655.294 134.186C652.329 139.09 647.938 142.968 642.121 145.819C636.305 148.67 629.177 150.096 620.737 150.096H575.061ZM592.168 135.897H618.513C626.04 135.897 631.857 134.186 635.963 130.764C640.183 127.229 642.292 122.268 642.292 115.881C642.292 109.608 640.125 104.59 635.792 100.827C631.572 97.0629 625.698 95.1811 618.171 95.1811H592.168V135.897ZM592.168 82.0084H617.487C624.672 82.0084 630.146 80.3547 633.91 77.0473C637.673 73.6258 639.555 68.9498 639.555 63.0193C639.555 57.3168 637.673 52.8119 633.91 49.5045C630.146 46.083 624.501 44.3722 616.974 44.3722H592.168V82.0084Z" fill="#0e0e0e"/>
|
||||||
|
<path d="M677.064 150.096V63.8746H694.172V150.096H677.064ZM685.789 47.6226C682.482 47.6226 679.745 46.5962 677.578 44.5433C675.525 42.4904 674.498 39.8673 674.498 36.6739C674.498 33.5946 675.525 31.0855 677.578 29.1467C679.745 27.0938 682.482 26.0674 685.789 26.0674C688.983 26.0674 691.663 27.0938 693.83 29.1467C695.997 31.0855 697.08 33.5946 697.08 36.6739C697.08 39.8673 695.997 42.4904 693.83 44.5433C691.663 46.5962 688.983 47.6226 685.789 47.6226Z" fill="#0e0e0e"/>
|
||||||
|
<path d="M750.797 150.096C745.323 150.096 740.59 149.24 736.598 147.53C732.606 145.819 729.527 142.968 727.36 138.976C725.193 134.984 724.11 129.567 724.11 122.724V78.4159H709.226V63.8746H724.11L726.163 42.3194H741.217V63.8746H765.681V78.4159H741.217V122.895C741.217 127.799 742.243 131.164 744.296 132.988C746.349 134.699 749.885 135.554 754.903 135.554H764.825V150.096H750.797Z" fill="#0e0e0e"/>
|
||||||
|
<path d="M814.483 152.149C807.184 152.149 800.797 150.951 795.323 148.556C789.849 146.161 785.515 142.797 782.321 138.463C779.128 134.129 777.189 129.054 776.505 123.237H793.954C794.525 125.974 795.608 128.483 797.205 130.764C798.916 133.045 801.197 134.87 804.048 136.239C807.013 137.607 810.492 138.292 814.483 138.292C818.247 138.292 821.326 137.778 823.721 136.752C826.23 135.611 828.055 134.129 829.196 132.304C830.336 130.365 830.906 128.312 830.906 126.145C830.906 122.952 830.108 120.557 828.511 118.96C827.029 117.25 824.748 115.938 821.668 115.026C818.703 113.999 815.111 113.087 810.891 112.288C806.899 111.604 803.021 110.692 799.258 109.551C795.608 108.297 792.301 106.757 789.335 104.932C786.484 103.108 784.203 100.827 782.493 98.0893C780.782 95.2381 779.926 91.7596 779.926 87.6539C779.926 82.7497 781.238 78.3589 783.861 74.4812C786.484 70.4895 790.191 67.4102 794.981 65.2432C799.885 62.9622 805.644 61.8218 812.259 61.8218C821.839 61.8218 829.538 64.1027 835.354 68.6647C841.171 73.2267 844.592 79.6704 845.619 87.996H829.025C828.568 84.1183 826.858 81.1531 823.892 79.1002C820.927 76.9332 816.992 75.8498 812.088 75.8498C807.184 75.8498 803.421 76.8192 800.797 78.758C798.174 80.6969 796.863 83.263 796.863 86.4563C796.863 88.5092 797.604 90.334 799.087 91.9307C800.569 93.5274 802.736 94.896 805.587 96.0365C808.553 97.0629 812.145 98.0323 816.365 98.9447C822.41 100.085 827.827 101.511 832.617 103.222C837.407 104.932 841.228 107.441 844.079 110.749C846.93 114.056 848.356 118.789 848.356 124.948C848.47 130.308 847.101 135.041 844.25 139.147C841.513 143.253 837.578 146.446 832.446 148.727C827.428 151.008 821.44 152.149 814.483 152.149Z" fill="#0e0e0e"/>
|
||||||
|
<path d="M0.345215 0.90332H67.8054C67.8054 0.90332 96.1375 53.396 116.565 85.658C117.425 87.017 117.849 87.8181 118.774 89.1338C119.638 90.3625 120.125 91.0574 121.121 92.1809C122.059 93.2378 121.893 93.1339 122.967 94.3206C123.553 94.9687 124.06 95.4584 125.323 96.275C126.467 97.0143 128.426 97.5109 129.911 97.0097C131.062 96.6216 131.373 96.4766 132.351 95.9285C135.776 94.0089 139.292 87.0185 139.292 87.0185L178.612 15.3078H144.132V0.90625L203.176 0.90332L155.854 83.038C155.854 83.038 151.039 90.7587 147.59 96.7529C147.028 97.7296 144.627 101.263 143.692 102.454C141.925 104.706 140.938 106.052 139.292 107.677C137.716 109.233 136.16 109.995 135.056 110.438C133.348 111.124 131.522 111.47 129.687 111.619C128.124 111.747 127.072 111.828 125.518 111.619C123.58 111.358 123.441 111.291 121.121 110.438C120.146 110.079 118.517 109.111 116.982 108.004C115.276 106.774 114.113 105.678 112.195 103.649C110.507 101.864 109.763 100.805 108.398 98.7619C106.965 96.6173 105.85 95.4309 104.496 93.2355C86.1035 63.4026 59.9861 15.3078 59.9861 15.3078H25.2534L101.895 148.518L116.565 123.23C116.565 123.23 118.576 125.972 125.632 125.743C132.117 125.533 133.676 123.23 133.676 123.23L101.895 177.314L14.4002 25.6371L8.52356 15.3078L0.345215 0.90332Z" fill="#0e0e0e"/>
|
||||||
|
<circle cx="128.21" cy="45.7212" r="14.7476" fill="#0e0e0e"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.9 KiB |
11
src/assets/logos/vuebits-gh-white.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="849" height="178" viewBox="0 0 849 178" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M292.054 150.029L247.575 30.2773H266.051L302.318 132.237L338.928 30.2773H357.062L312.583 150.029H292.054Z" fill="white"/>
|
||||||
|
<path d="M388.782 152.082C382.053 152.082 376.179 150.713 371.161 147.976C366.257 145.239 362.436 141.133 359.699 135.659C357.076 130.184 355.765 123.284 355.765 114.959V63.8077H372.872V113.077C372.872 121.174 374.64 127.276 378.175 131.382C381.711 135.488 386.786 137.54 393.401 137.54C397.849 137.54 401.84 136.457 405.376 134.29C409.025 132.123 411.877 128.987 413.93 124.881C415.982 120.775 417.009 115.757 417.009 109.827V63.8077H434.116V150.029H418.891L417.693 135.316C415.07 140.563 411.249 144.668 406.231 147.634C401.213 150.599 395.397 152.082 388.782 152.082Z" fill="white"/>
|
||||||
|
<path d="M486.973 152.082C478.762 152.082 471.463 150.2 465.076 146.436C458.689 142.673 453.671 137.426 450.022 130.697C446.486 123.969 444.718 116.156 444.718 107.26C444.718 98.1365 446.486 90.2101 450.022 83.4812C453.671 76.6383 458.689 71.335 465.076 67.5714C471.463 63.6937 478.876 61.7549 487.316 61.7549C495.755 61.7549 502.997 63.6367 509.042 67.4003C515.087 71.1639 519.763 76.1821 523.07 82.4548C526.377 88.6134 528.031 95.4563 528.031 102.984C528.031 104.124 527.974 105.379 527.86 106.747C527.86 108.002 527.803 109.427 527.689 111.024H457.207V98.8778H510.924C510.582 91.6927 508.187 86.1043 503.739 82.1126C499.291 78.0068 493.759 75.954 487.145 75.954C482.468 75.954 478.192 77.0374 474.314 79.2044C470.436 81.2572 467.3 84.3366 464.905 88.4423C462.624 92.434 461.483 97.5092 461.483 103.668V108.458C461.483 114.845 462.624 120.262 464.905 124.71C467.3 129.044 470.436 132.351 474.314 134.632C478.192 136.799 482.411 137.883 486.973 137.883C492.448 137.883 496.953 136.685 500.488 134.29C504.024 131.895 506.647 128.645 508.358 124.539H525.465C523.982 129.785 521.473 134.518 517.938 138.738C514.402 142.844 510.011 146.094 504.765 148.489C499.633 150.884 493.702 152.082 486.973 152.082Z" fill="white"/>
|
||||||
|
<path d="M575.061 150.029V30.2773H619.369C627.694 30.2773 634.651 31.6459 640.24 34.3831C645.828 37.0062 649.991 40.6558 652.728 45.3318C655.579 49.8938 657.005 55.083 657.005 60.8995C657.005 66.9441 655.693 72.0193 653.07 76.125C650.447 80.2308 646.968 83.4242 642.635 85.7052C638.415 87.8721 633.853 89.1266 628.949 89.4688L631.344 87.758C636.59 87.8721 641.38 89.2977 645.714 92.0349C650.048 94.658 653.469 98.1935 655.978 102.641C658.487 107.089 659.742 111.993 659.742 117.354C659.742 123.512 658.259 129.101 655.294 134.119C652.329 139.023 647.938 142.901 642.121 145.752C636.305 148.603 629.177 150.029 620.737 150.029H575.061ZM592.168 135.83H618.513C626.04 135.83 631.857 134.119 635.963 130.697C640.183 127.162 642.292 122.201 642.292 115.814C642.292 109.541 640.125 104.523 635.792 100.76C631.572 96.996 625.698 95.1142 618.171 95.1142H592.168V135.83ZM592.168 81.9415H617.487C624.672 81.9415 630.146 80.2878 633.91 76.9804C637.673 73.5589 639.555 68.8829 639.555 62.9524C639.555 57.2499 637.673 52.745 633.91 49.4376C630.146 46.0161 624.501 44.3054 616.974 44.3054H592.168V81.9415Z" fill="white"/>
|
||||||
|
<path d="M677.064 150.029V63.8077H694.172V150.029H677.064ZM685.789 47.5558C682.482 47.5558 679.745 46.5293 677.578 44.4764C675.525 42.4235 674.498 39.8004 674.498 36.607C674.498 33.5277 675.525 31.0186 677.578 29.0798C679.745 27.0269 682.482 26.0005 685.789 26.0005C688.983 26.0005 691.663 27.0269 693.83 29.0798C695.997 31.0186 697.08 33.5277 697.08 36.607C697.08 39.8004 695.997 42.4235 693.83 44.4764C691.663 46.5293 688.983 47.5558 685.789 47.5558Z" fill="white"/>
|
||||||
|
<path d="M750.797 150.029C745.323 150.029 740.59 149.173 736.598 147.463C732.606 145.752 729.527 142.901 727.36 138.909C725.193 134.917 724.11 129.5 724.11 122.657V78.349H709.226V63.8077H724.11L726.163 42.2525H741.217V63.8077H765.681V78.349H741.217V122.828C741.217 127.732 742.243 131.097 744.296 132.921C746.349 134.632 749.885 135.488 754.903 135.488H764.825V150.029H750.797Z" fill="white"/>
|
||||||
|
<path d="M814.483 152.082C807.184 152.082 800.797 150.884 795.323 148.489C789.849 146.094 785.515 142.73 782.321 138.396C779.128 134.062 777.189 128.987 776.505 123.17H793.954C794.525 125.907 795.608 128.417 797.205 130.697C798.916 132.978 801.197 134.803 804.048 136.172C807.013 137.54 810.492 138.225 814.483 138.225C818.247 138.225 821.326 137.712 823.721 136.685C826.23 135.545 828.055 134.062 829.196 132.237C830.336 130.298 830.906 128.245 830.906 126.079C830.906 122.885 830.108 120.49 828.511 118.893C827.029 117.183 824.748 115.871 821.668 114.959C818.703 113.932 815.111 113.02 810.891 112.222C806.899 111.537 803.021 110.625 799.258 109.484C795.608 108.23 792.301 106.69 789.335 104.865C786.484 103.041 784.203 100.76 782.493 98.0224C780.782 95.1712 779.926 91.6927 779.926 87.587C779.926 82.6829 781.238 78.292 783.861 74.4143C786.484 70.4226 790.191 67.3433 794.981 65.1763C799.885 62.8953 805.644 61.7549 812.259 61.7549C821.839 61.7549 829.538 64.0358 835.354 68.5978C841.171 73.1598 844.592 79.6035 845.619 87.9291H829.025C828.568 84.0514 826.858 81.0862 823.892 79.0333C820.927 76.8664 816.992 75.7829 812.088 75.7829C807.184 75.7829 803.421 76.7523 800.797 78.6911C798.174 80.63 796.863 83.1961 796.863 86.3894C796.863 88.4423 797.604 90.2671 799.087 91.8638C800.569 93.4605 802.736 94.8291 805.587 95.9696C808.553 96.996 812.145 97.9654 816.365 98.8778C822.41 100.018 827.827 101.444 832.617 103.155C837.407 104.865 841.228 107.374 844.079 110.682C846.93 113.989 848.356 118.722 848.356 124.881C848.47 130.241 847.101 134.974 844.25 139.08C841.513 143.186 837.578 146.379 832.446 148.66C827.428 150.941 821.44 152.082 814.483 152.082Z" fill="white"/>
|
||||||
|
<path d="M0.345215 0.836426H67.8054C67.8054 0.836426 96.1375 53.3291 116.565 85.5911C117.425 86.9501 117.849 87.7512 118.774 89.0669C119.638 90.2956 120.125 90.9905 121.121 92.114C122.059 93.1709 121.893 93.067 122.967 94.2537C123.553 94.9018 124.06 95.3915 125.323 96.2081C126.467 96.9474 128.426 97.444 129.911 96.9428C131.062 96.5547 131.373 96.4097 132.351 95.8616C135.776 93.942 139.292 86.9516 139.292 86.9516L178.612 15.2409H144.132V0.839355L203.176 0.836426L155.854 82.9711C155.854 82.9711 151.039 90.6918 147.59 96.686C147.028 97.6627 144.627 101.196 143.692 102.388C141.925 104.639 140.938 105.985 139.292 107.61C137.716 109.166 136.16 109.928 135.056 110.371C133.348 111.057 131.522 111.403 129.687 111.552C128.124 111.68 127.072 111.762 125.518 111.552C123.58 111.291 123.441 111.224 121.121 110.371C120.146 110.012 118.517 109.045 116.982 107.937C115.276 106.708 114.113 105.611 112.195 103.582C110.507 101.797 109.763 100.738 108.398 98.695C106.965 96.5504 105.85 95.364 104.496 93.1686C86.1035 63.3357 59.9861 15.2409 59.9861 15.2409H25.2534L101.895 148.451L116.565 123.163C116.565 123.163 118.576 125.905 125.632 125.677C132.117 125.466 133.676 123.163 133.676 123.163L101.895 177.247L14.4002 25.5702L8.52356 15.2409L0.345215 0.836426Z" fill="white"/>
|
||||||
|
<circle cx="128.21" cy="45.6543" r="14.7476" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.8 KiB |
34
src/components/common/Logo.vue
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<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="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" />
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
<circle cx="29.3282" cy="10.6089" r="3.34281" fill="white" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({
|
||||||
|
name: 'VueBitsLogo'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
310
src/components/landing/DisplayHeader/DisplayHeader.css
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 100;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100vw;
|
||||||
|
padding: 0 4em;
|
||||||
|
height: 160px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: linear-gradient(to bottom, #0e0e0e, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-cta-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 200px;
|
||||||
|
height: 140px;
|
||||||
|
background: transparent;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
mask: radial-gradient(ellipse at center, black 0%, black 20%, transparent 80%);
|
||||||
|
-webkit-mask: radial-gradient(ellipse at center, black 0%, black 20%, transparent 80%);
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo svg {
|
||||||
|
height: 26px;
|
||||||
|
width: auto;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-nav-items {
|
||||||
|
display: none;
|
||||||
|
color: #fff;
|
||||||
|
height: 60px;
|
||||||
|
padding: 0 2.4rem 0 calc(2.4rem + 6px);
|
||||||
|
border-radius: 50px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||||
|
background: rgba(255, 255, 255, 0.01);
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(31, 135, 62, 0.15);
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
-webkit-backdrop-filter: blur(15px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
position: relative;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.3s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-link {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-link::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
left: -12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button {
|
||||||
|
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-size: 200% 200%;
|
||||||
|
backdrop-filter: blur(25px);
|
||||||
|
-webkit-backdrop-filter: blur(25px);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
transition: .3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button span {
|
||||||
|
background-color: #0e0e0e;
|
||||||
|
margin-left: 1em;
|
||||||
|
margin-right: calc(1em - 8px);
|
||||||
|
padding-top: .1em;
|
||||||
|
height: 45px;
|
||||||
|
border-radius: 50px;
|
||||||
|
width: 100px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button span img {
|
||||||
|
margin-right: 6px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
transition: .3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button:hover {
|
||||||
|
transition: .3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button:hover span img {
|
||||||
|
transform: scale(1.2);
|
||||||
|
transition: .3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.header {
|
||||||
|
padding: 0 2em;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo svg {
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo::before {
|
||||||
|
width: 180px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button {
|
||||||
|
padding: 0 0 0 1rem;
|
||||||
|
height: 50px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button span {
|
||||||
|
height: 38px;
|
||||||
|
width: 80px;
|
||||||
|
margin-left: 0.8em;
|
||||||
|
margin-right: calc(0.8em - 6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button span img {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.header {
|
||||||
|
padding: 0 1.5em;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo svg {
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo::before {
|
||||||
|
width: 160px;
|
||||||
|
height: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button {
|
||||||
|
padding: 0 0 0 0.8rem;
|
||||||
|
height: 45px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button span {
|
||||||
|
height: 35px;
|
||||||
|
width: 60px !important;
|
||||||
|
margin-left: 0.6em;
|
||||||
|
margin-right: calc(0.6em - 4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button span img {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.header {
|
||||||
|
padding: 0 1rem;
|
||||||
|
height: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo svg {
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo::before {
|
||||||
|
width: 140px;
|
||||||
|
height: 95px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button {
|
||||||
|
padding: 0 0 0 0.6rem;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button span {
|
||||||
|
height: 32px;
|
||||||
|
width: 60px;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
margin-right: calc(0.5em - 3px);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button span img {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 375px) {
|
||||||
|
.header {
|
||||||
|
padding: 0 0.8rem;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo svg {
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo::before {
|
||||||
|
width: 120px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button {
|
||||||
|
padding: 0 0 0 0.5rem;
|
||||||
|
height: 36px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button span {
|
||||||
|
height: 28px;
|
||||||
|
width: 50px;
|
||||||
|
margin-left: 0.4em;
|
||||||
|
margin-right: calc(0.4em - 2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button span img {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
margin-right: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 900px) {
|
||||||
|
.landing-nav-items {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-cta-group {
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src/components/landing/DisplayHeader/DisplayHeader.vue
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<header class="header">
|
||||||
|
<div class="header-container">
|
||||||
|
<router-link to="/" class="logo">
|
||||||
|
<VueBitsLogo />
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="cta-button"
|
||||||
|
@click="openGitHub"
|
||||||
|
>
|
||||||
|
Star On GitHub
|
||||||
|
<span ref="starCountRef" :style="{ opacity: 0 }">
|
||||||
|
<img :src="starIcon" alt="Star Icon" />
|
||||||
|
{{ stars }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</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'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
activeItem?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
662
src/components/landing/FeatureCards/FeatureCards.css
Normal file
@@ -0,0 +1,662 @@
|
|||||||
|
.features-section {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 12em;
|
||||||
|
padding: 8rem 2rem 4em;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
z-index: 22;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-title {
|
||||||
|
font-size: 4rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -2px;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: .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;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
text-align: center;
|
||||||
|
animation: gradientShift 4s ease-in-out infinite;
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-title * {
|
||||||
|
background: inherit;
|
||||||
|
background-size: inherit;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
animation: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradientShift {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-subtitle {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow:
|
||||||
|
0 0 2px rgba(255, 255, 255, 0.1),
|
||||||
|
0 0 4px rgba(255, 255, 255, 0.3),
|
||||||
|
0 0 8px rgba(255, 255, 255, 0.4),
|
||||||
|
0 0 136px rgba(0, 255, 98, 0.9);
|
||||||
|
font-weight: 400;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.features-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-subtitle {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-header {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.features-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-subtitle {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bento-grid {
|
||||||
|
max-width: 1200px;
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5em;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-auto-rows: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 480px) and (max-width: 767px) {
|
||||||
|
.bento-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card1 {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
grid-row: 1 / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card2 {
|
||||||
|
grid-column: 1 / 2;
|
||||||
|
grid-row: 2 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card4 {
|
||||||
|
grid-column: 2 / 3;
|
||||||
|
grid-row: 2 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 50rem) {
|
||||||
|
.bento-grid {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
grid-template-rows: repeat(3, auto);
|
||||||
|
gap: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card1 {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
grid-row: 1 / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card2 {
|
||||||
|
grid-column: 3 / 5;
|
||||||
|
grid-row: 1 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card4 {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
grid-row: 2 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) and (max-width: 49.99rem) {
|
||||||
|
.bento-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card1 {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
grid-row: 1 / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card2 {
|
||||||
|
grid-column: 3 / 4;
|
||||||
|
grid-row: 1 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card4 {
|
||||||
|
grid-column: 1 / 2;
|
||||||
|
grid-row: 2 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
user-select: none;
|
||||||
|
background: #0e0e0e;
|
||||||
|
border: 1px solid rgba(148, 184, 154, 0.2);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: left;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-end;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 220px;
|
||||||
|
--glow-x: 50%;
|
||||||
|
--glow-y: 50%;
|
||||||
|
--glow-intensity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(163, 148, 184, 0.3), transparent);
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card::after {
|
||||||
|
content: '';
|
||||||
|
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%);
|
||||||
|
border-radius: inherit;
|
||||||
|
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-composite: xor;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover {
|
||||||
|
box-shadow: 0 4px 40px -15px rgba(46, 24, 78, 0.4) !important;
|
||||||
|
background: #07160a;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.feature-card:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h3 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h2 {
|
||||||
|
font-size: 6rem;
|
||||||
|
position: relative;
|
||||||
|
top: 22px;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 800;
|
||||||
|
background: linear-gradient(135deg, #fff 0%, #60fa7f 20%, #55f783 40%, #00ff48 60%, #58f755 80%, #fff 100%);
|
||||||
|
background-size: 200% 200%;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
animation: gradientShift 4s ease-in-out infinite;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card p {
|
||||||
|
color: rgba(161, 148, 184, 0.9);
|
||||||
|
line-height: 1.4;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
text-align: left;
|
||||||
|
max-width: 100%;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.particle-container {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.particle {
|
||||||
|
position: absolute;
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
background: rgba(0, 255, 98, 0.8);
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: 0 0 6px rgba(0, 255, 68, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.particle::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
left: -2px;
|
||||||
|
right: -2px;
|
||||||
|
bottom: -2px;
|
||||||
|
background: rgba(0, 255, 81, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card.particle-container {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
background: #07160b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-spotlight {
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
will-change: transform, opacity;
|
||||||
|
z-index: 200 !important;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-gif-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover .components-gif-wrapper {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-gif {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
opacity: 0.2;
|
||||||
|
mix-blend-mode: lighten;
|
||||||
|
display: block;
|
||||||
|
filter: grayscale(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-gif-wrapper::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 20%;
|
||||||
|
background: linear-gradient(to top, #0e0e0e 0%, transparent 100%);
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-gif-wrapper::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 20%;
|
||||||
|
background: linear-gradient(to bottom, #0e0e0e 0%, transparent 100%);
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-gif-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
mix-blend-mode: lighten;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 1rem;
|
||||||
|
bottom: 1rem;
|
||||||
|
width: 50%;
|
||||||
|
border-radius: 8px;
|
||||||
|
z-index: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover .messages-gif-wrapper {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-gif {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 0;
|
||||||
|
opacity: 0.3;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-gif-wrapper::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 20%;
|
||||||
|
background: linear-gradient(to top, #0e0e0e 0%, transparent 100%);
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-gif-wrapper::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 20%;
|
||||||
|
background: linear-gradient(to bottom, #0e0e0e 0%, transparent 100%);
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-gif-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 1rem;
|
||||||
|
width: 40%;
|
||||||
|
mix-blend-mode: lighten;
|
||||||
|
border-radius: 8px;
|
||||||
|
z-index: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover .switch-gif-wrapper {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-gif {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 0;
|
||||||
|
opacity: 0.3;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-gif-wrapper::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 20%;
|
||||||
|
background: linear-gradient(to top, #0e0e0e 0%, transparent 100%);
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-gif-wrapper::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 20%;
|
||||||
|
background: linear-gradient(to bottom, #0e0e0e 0%, transparent 100%);
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
.features-section {
|
||||||
|
padding: 4rem 1rem 2rem;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-top: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bento-grid {
|
||||||
|
gap: 1rem;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
min-height: 160px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h2 {
|
||||||
|
font-size: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.feature-card h3 {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card p {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-gif-wrapper,
|
||||||
|
.messages-gif-wrapper,
|
||||||
|
.switch-gif-wrapper {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 360px) {
|
||||||
|
.features-section {
|
||||||
|
padding: 2.5rem 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bento-grid {
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
min-height: 150px;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h2 {
|
||||||
|
font-size: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.feature-card h3 {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card p {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-gif-wrapper,
|
||||||
|
.messages-gif-wrapper,
|
||||||
|
.switch-gif-wrapper {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.features-section {
|
||||||
|
padding: 6rem 2rem 3rem;
|
||||||
|
margin-top: 6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
min-height: 200px;
|
||||||
|
padding: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h3 {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card p {
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-gif-wrapper,
|
||||||
|
.messages-gif-wrapper,
|
||||||
|
.switch-gif-wrapper {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 50rem) {
|
||||||
|
.features-section {
|
||||||
|
padding: 8rem 2rem 4rem;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-top: 8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
min-height: 220px;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h3 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card p {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-gif-wrapper,
|
||||||
|
.messages-gif-wrapper,
|
||||||
|
.switch-gif-wrapper {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 500px) and (orientation: landscape) {
|
||||||
|
.features-section {
|
||||||
|
margin-top: 2em;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
min-height: 140px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h3 {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card p {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
286
src/components/landing/FeatureCards/FeatureCards.vue
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
<template>
|
||||||
|
<div class="features-section">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<GlobalSpotlight v-if="gridRef" :grid-ref="gridRef" :disable-animations="isMobile" />
|
||||||
|
|
||||||
|
<div class="bento-grid" ref="gridRef">
|
||||||
|
<ParticleCard class="feature-card card1" :disable-animations="isMobile">
|
||||||
|
<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" />%
|
||||||
|
</h2>
|
||||||
|
<h3>Free & Open Source</h3>
|
||||||
|
<p>Loved by developers around the world</p>
|
||||||
|
</ParticleCard>
|
||||||
|
|
||||||
|
<ParticleCard class="feature-card card2" :disable-animations="isMobile">
|
||||||
|
<div className="components-gif-wrapper">
|
||||||
|
<img src="/assets/components.gif" alt="Components animation" className="components-gif" />
|
||||||
|
</div>
|
||||||
|
<h2>
|
||||||
|
<template v-if="isMobile">80</template>
|
||||||
|
<CountUp v-else :to="80" />+
|
||||||
|
</h2>
|
||||||
|
<h3>Curated Components</h3>
|
||||||
|
<p>Growing weekly & only getting better</p>
|
||||||
|
</ParticleCard>
|
||||||
|
|
||||||
|
<ParticleCard class="feature-card card4" :disable-animations="isMobile">
|
||||||
|
<h2>
|
||||||
|
Modern
|
||||||
|
</h2>
|
||||||
|
<h3>Technologies</h3>
|
||||||
|
<p>TypeScript + Tailwind, ready to ship</p>
|
||||||
|
</ParticleCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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'
|
||||||
|
|
||||||
|
const isMobile = ref(false)
|
||||||
|
const gridRef = ref<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
const checkIsMobile = () => {
|
||||||
|
isMobile.value = window.innerWidth <= 768
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkIsMobile()
|
||||||
|
window.addEventListener('resize', checkIsMobile)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', checkIsMobile)
|
||||||
|
})
|
||||||
|
|
||||||
|
const ParticleCard = defineComponent({
|
||||||
|
name: 'ParticleCard',
|
||||||
|
props: {
|
||||||
|
disableAnimations: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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 createParticle = (x: number, y: number): HTMLDivElement => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
const memoizeParticles = () => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearParticles = () => {
|
||||||
|
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)",
|
||||||
|
onComplete: () => {
|
||||||
|
if (p.parentNode) {
|
||||||
|
p.parentNode.removeChild(p)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
particlesRef.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const animateParticles = () => {
|
||||||
|
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)
|
||||||
|
|
||||||
|
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",
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
isHoveredRef.value = true
|
||||||
|
animateParticles()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
isHoveredRef.value = false
|
||||||
|
clearParticles()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.disableAnimations || !cardRef.value) return
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
isHoveredRef.value = false
|
||||||
|
clearParticles()
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => h('div', {
|
||||||
|
ref: cardRef,
|
||||||
|
class: 'particle-container',
|
||||||
|
style: { position: 'relative', overflow: 'hidden' }
|
||||||
|
}, slots.default?.())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const GlobalSpotlight = defineComponent({
|
||||||
|
name: 'GlobalSpotlight',
|
||||||
|
props: {
|
||||||
|
gridRef: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
disableAnimations: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
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()
|
||||||
|
const inside =
|
||||||
|
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')
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
isInsideSectionRef.value = false
|
||||||
|
props.gridRef.value
|
||||||
|
?.querySelectorAll('.feature-card')
|
||||||
|
.forEach((card: HTMLElement) => card.style.setProperty('--glow-intensity', '0'))
|
||||||
|
if (spotlightRef.value) {
|
||||||
|
gsap.to(spotlightRef.value, { opacity: 0, duration: 0.3, ease: "power2.out" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.disableAnimations || !props.gridRef?.value) return
|
||||||
|
|
||||||
|
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.addEventListener('mousemove', handleMouseMove)
|
||||||
|
document.addEventListener('mouseleave', handleMouseLeave)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove)
|
||||||
|
document.removeEventListener('mouseleave', handleMouseLeave)
|
||||||
|
if (spotlightRef.value?.parentNode) {
|
||||||
|
spotlightRef.value.parentNode.removeChild(spotlightRef.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
155
src/components/landing/Footer/Footer.css
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
.landing-footer {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 8rem;
|
||||||
|
padding: 2.4rem;
|
||||||
|
border-top: 1px solid rgba(149, 184, 148, 0.1);
|
||||||
|
background: linear-gradient(to bottom, transparent, #0e0e0e);
|
||||||
|
z-index: 220;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-left {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
text-align: left;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-logo {
|
||||||
|
height: 28px;
|
||||||
|
width: auto;
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-logo:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-description {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: rgba(161, 148, 184, 0.9);
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 400;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-heart {
|
||||||
|
color: #27FF64;
|
||||||
|
font-size: 1em;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-creator-link {
|
||||||
|
color: #27FF64;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-creator-link:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-copyright {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: rgba(161, 148, 184, 0.7);
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.landing-footer {
|
||||||
|
margin-top: 6rem;
|
||||||
|
padding: 3rem 1.5rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-left {
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.landing-footer {
|
||||||
|
margin-top: 4rem;
|
||||||
|
padding: 2rem 1rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
gap: 1rem;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-description {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-copyright {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/components/landing/Footer/Footer.vue
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<template>
|
||||||
|
<FadeContent :blur="true" :duration="600">
|
||||||
|
<footer class="landing-footer">
|
||||||
|
<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 href="https://davidhaz.com/" target="_blank" class="footer-creator-link">this guy</a>
|
||||||
|
</p>
|
||||||
|
<p class="footer-copyright">© {{ currentYear }} Vue Bits</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-links">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</FadeContent>
|
||||||
|
</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'
|
||||||
|
|
||||||
|
const currentYear = computed(() => new Date().getFullYear())
|
||||||
|
</script>
|
||||||
122
src/components/landing/Hero/Hero.vue
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<template>
|
||||||
|
<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)" />
|
||||||
|
<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)" />
|
||||||
|
</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" />
|
||||||
|
|
||||||
|
<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="#4c1d95" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!isMobile" class="hero-cards-container">
|
||||||
|
<div class="hero-card hero-card-1" @click="openUrl('https://vuebits.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" />
|
||||||
|
<div class="placeholder-card"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hero-cards-row">
|
||||||
|
<div class="hero-card hero-card-2" @click="openUrl('https://vuebits.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://vuebits.dev/backgrounds/squares')">
|
||||||
|
<Squares border-color="#fff" :speed="0.2" direction="diagonal" hover-fill-color="#fff" />
|
||||||
|
<div class="placeholder-card"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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'
|
||||||
|
|
||||||
|
const ResponsiveSplitText = defineComponent({
|
||||||
|
props: {
|
||||||
|
isMobile: { type: Boolean, required: true },
|
||||||
|
text: { type: String, required: true },
|
||||||
|
className: { type: String, default: '' },
|
||||||
|
splitType: { type: String as () => 'chars' | 'words' | 'lines' | 'words, chars', default: 'chars' },
|
||||||
|
delay: { type: Number, default: 100 },
|
||||||
|
duration: { type: Number, default: 0.6 },
|
||||||
|
ease: { type: String, default: 'power3.out' },
|
||||||
|
from: { type: Object, default: () => ({ opacity: 0, y: 40 }) },
|
||||||
|
to: { type: Object, default: () => ({ opacity: 1, y: 0 }) },
|
||||||
|
threshold: { type: Number, default: 0.1 },
|
||||||
|
rootMargin: { type: String, default: '-100px' },
|
||||||
|
textAlign: { type: String as () => 'left' | 'center' | 'right' | 'justify', default: 'center' },
|
||||||
|
onLetterAnimationComplete: { type: Function, default: undefined }
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
if (this.isMobile) {
|
||||||
|
return h('span', { class: this.className }, this.text)
|
||||||
|
} else {
|
||||||
|
return h(SplitText, {
|
||||||
|
text: this.text,
|
||||||
|
className: this.className,
|
||||||
|
splitType: this.splitType,
|
||||||
|
delay: this.delay,
|
||||||
|
duration: this.duration,
|
||||||
|
ease: this.ease,
|
||||||
|
from: this.from,
|
||||||
|
to: this.to,
|
||||||
|
threshold: this.threshold,
|
||||||
|
rootMargin: this.rootMargin,
|
||||||
|
textAlign: this.textAlign,
|
||||||
|
onLetterAnimationComplete: this.onLetterAnimationComplete as (() => void) | undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const openUrl = (url: string) => {
|
||||||
|
window.open(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMobile = ref(false)
|
||||||
|
|
||||||
|
const checkIsMobile = () => {
|
||||||
|
isMobile.value = window.innerWidth <= 768
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkIsMobile()
|
||||||
|
window.addEventListener('resize', checkIsMobile)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', checkIsMobile)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.placeholder-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
335
src/components/landing/PlasmaWave/PlasmaWave.vue
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
<template>
|
||||||
|
<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'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
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>(), {
|
||||||
|
xOffset: 0,
|
||||||
|
yOffset: 0,
|
||||||
|
rotationDeg: 0,
|
||||||
|
focalLength: 0.8,
|
||||||
|
speed1: 0.1,
|
||||||
|
speed2: 0.1,
|
||||||
|
dir2: 1.0,
|
||||||
|
bend1: 0.9,
|
||||||
|
bend2: 0.6,
|
||||||
|
fadeInDuration: 2000
|
||||||
|
})
|
||||||
|
|
||||||
|
const vertex = /* glsl */ `
|
||||||
|
attribute vec2 position;
|
||||||
|
varying vec2 vUv;
|
||||||
|
void main() {
|
||||||
|
vUv = position * 0.5 + 0.5;
|
||||||
|
gl_Position = vec4(position, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const fragment = /* glsl */ `
|
||||||
|
precision mediump float;
|
||||||
|
uniform float iTime;
|
||||||
|
uniform vec2 iResolution;
|
||||||
|
uniform vec2 uOffset;
|
||||||
|
uniform float uRotation;
|
||||||
|
uniform float focalLength;
|
||||||
|
uniform float speed1;
|
||||||
|
uniform float speed2;
|
||||||
|
uniform float dir2;
|
||||||
|
uniform float bend1;
|
||||||
|
uniform float bend2;
|
||||||
|
uniform float bendAdj1;
|
||||||
|
uniform float bendAdj2;
|
||||||
|
uniform float uOpacity;
|
||||||
|
|
||||||
|
const float lt = 0.05;
|
||||||
|
const float pi = 3.141592653589793;
|
||||||
|
const float pi2 = pi * 2.0;
|
||||||
|
const float pi_2 = pi * 0.5;
|
||||||
|
#define MAX_STEPS 15
|
||||||
|
#define A(v) mat2(cos(m.v + radians(vec4(0.0,-90.0,90.0,0.0))))
|
||||||
|
|
||||||
|
void mainImage(out vec4 C, in vec2 U) {
|
||||||
|
float t = iTime * pi;
|
||||||
|
float s = 1.0;
|
||||||
|
float d = 0.0;
|
||||||
|
vec2 R = iResolution;
|
||||||
|
vec2 m = vec2(0.0);
|
||||||
|
|
||||||
|
vec3 o = vec3(0.0, 0.0, -7.0);
|
||||||
|
vec3 u = normalize(vec3((U - 0.5 * R) / R.y, focalLength));
|
||||||
|
vec3 k = vec3(0.0);
|
||||||
|
vec3 p;
|
||||||
|
|
||||||
|
mat2 v = A(y), h = A(x);
|
||||||
|
|
||||||
|
float t1 = t * 0.7;
|
||||||
|
float t2 = t * 0.9;
|
||||||
|
float tSpeed1 = t * speed1;
|
||||||
|
float tSpeed2 = t * speed2 * dir2;
|
||||||
|
|
||||||
|
for (int step = 0; step < MAX_STEPS; ++step) {
|
||||||
|
p = o + u * d;
|
||||||
|
p.yz *= v;
|
||||||
|
p.xz *= h;
|
||||||
|
p.x -= 15.0;
|
||||||
|
|
||||||
|
float px = p.x;
|
||||||
|
float wob1 = bend1 + bendAdj1 + sin(t1 + px * 0.8) * 0.1;
|
||||||
|
float wob2 = bend2 + bendAdj2 + cos(t2 + px * 1.1) * 0.1;
|
||||||
|
|
||||||
|
vec2 baseOffset = vec2(px, px + pi_2);
|
||||||
|
vec2 sinOffset = sin(baseOffset + tSpeed1) * wob1;
|
||||||
|
vec2 cosOffset = cos(baseOffset + tSpeed2) * wob2;
|
||||||
|
|
||||||
|
float wSin = length(p.yz - sinOffset) - lt;
|
||||||
|
float wCos = length(p.yz - cosOffset) - lt;
|
||||||
|
|
||||||
|
k.x = max(px + lt, wSin);
|
||||||
|
k.y = max(px + lt, wCos);
|
||||||
|
|
||||||
|
s = min(s, min(k.x, k.y));
|
||||||
|
if (s < 0.001 || d > 400.0) break;
|
||||||
|
d += s * 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 c = max(cos(d * pi2) - s * sqrt(d) - k, 0.0);
|
||||||
|
|
||||||
|
// Vue.js colors: #42B883 (green) and #35495e (dark gray)
|
||||||
|
vec3 vueGreen = vec3(0.259, 0.722, 0.514); // #42B883
|
||||||
|
vec3 vueDark = vec3(0.208, 0.286, 0.369); // #35495e
|
||||||
|
|
||||||
|
// Use different colors for different wave components
|
||||||
|
vec3 finalColor = vec3(0.0);
|
||||||
|
if (k.x < k.y) {
|
||||||
|
// First wave component - Vue green
|
||||||
|
finalColor = vueGreen * c.x;
|
||||||
|
} else {
|
||||||
|
// Second wave component - Vue dark gray
|
||||||
|
finalColor = vueDark * c.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
float intensity = max(finalColor.r, max(finalColor.g, finalColor.b));
|
||||||
|
if (intensity < 0.15) discard;
|
||||||
|
C = vec4(finalColor * (0.4 + intensity * 0.6), uOpacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 coord = gl_FragCoord.xy + uOffset;
|
||||||
|
coord -= 0.5 * iResolution;
|
||||||
|
float c = cos(uRotation), s = sin(uRotation);
|
||||||
|
coord = mat2(c, -s, s, c) * coord;
|
||||||
|
coord += 0.5 * iResolution;
|
||||||
|
|
||||||
|
vec4 color;
|
||||||
|
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 checkIsMobile = () => {
|
||||||
|
isMobile.value = window.innerWidth <= 768
|
||||||
|
}
|
||||||
|
|
||||||
|
const resize = () => {
|
||||||
|
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 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
|
||||||
|
|
||||||
|
const renderer = new Renderer({
|
||||||
|
alpha: true,
|
||||||
|
dpr: Math.min(window.devicePixelRatio, 1),
|
||||||
|
antialias: false,
|
||||||
|
depth: false,
|
||||||
|
stencil: false,
|
||||||
|
powerPreference: 'high-performance',
|
||||||
|
})
|
||||||
|
rendererRef.value = renderer
|
||||||
|
|
||||||
|
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 geometry = new Geometry(gl, {
|
||||||
|
position: { size: 2, data: new Float32Array([-1, -1, 3, -1, -1, 3]) },
|
||||||
|
})
|
||||||
|
|
||||||
|
const program = new Program(gl, {
|
||||||
|
vertex,
|
||||||
|
fragment,
|
||||||
|
uniforms: {
|
||||||
|
iTime: { value: 0 },
|
||||||
|
iResolution: { value: uniformResolution.value },
|
||||||
|
uOffset: { value: uniformOffset.value },
|
||||||
|
uRotation: { value: 0 },
|
||||||
|
focalLength: { value: props.focalLength },
|
||||||
|
speed1: { value: props.speed1 },
|
||||||
|
speed2: { value: props.speed2 },
|
||||||
|
dir2: { value: props.dir2 },
|
||||||
|
bend1: { value: props.bend1 },
|
||||||
|
bend2: { value: props.bend2 },
|
||||||
|
bendAdj1: { value: 0 },
|
||||||
|
bendAdj2: { value: 0 },
|
||||||
|
uOpacity: { value: 0 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
new Mesh(gl, { geometry, program }).setParent(scene)
|
||||||
|
|
||||||
|
resize()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = (now - lastTimeRef.value) * 0.001
|
||||||
|
|
||||||
|
if (fadeStartTime.value === null && t > 0.1) {
|
||||||
|
fadeStartTime.value = now
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
renderer.render({ scene, camera })
|
||||||
|
} else {
|
||||||
|
if (lastTimeRef.value !== 0) {
|
||||||
|
pausedTimeRef.value = now - lastTimeRef.value
|
||||||
|
lastTimeRef.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rafId.value = requestAnimationFrame(loop)
|
||||||
|
}
|
||||||
|
|
||||||
|
rafId.value = requestAnimationFrame(loop)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupIntersectionObserver = () => {
|
||||||
|
if (!containerRef.value || isMobile.value) return
|
||||||
|
|
||||||
|
intersectionObserver.value = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
isVisible.value = entry.isIntersecting
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rootMargin: '50px',
|
||||||
|
threshold: 0.1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
intersectionObserver.value.observe(containerRef.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if (rafId.value) {
|
||||||
|
cancelAnimationFrame(rafId.value)
|
||||||
|
rafId.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resizeObserver.value) {
|
||||||
|
resizeObserver.value.disconnect()
|
||||||
|
resizeObserver.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intersectionObserver.value) {
|
||||||
|
intersectionObserver.value.disconnect()
|
||||||
|
intersectionObserver.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rendererRef.value) {
|
||||||
|
rendererRef.value.gl.canvas.remove()
|
||||||
|
rendererRef.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
window.removeEventListener('resize', checkIsMobile)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkIsMobile()
|
||||||
|
window.addEventListener('resize', checkIsMobile)
|
||||||
|
|
||||||
|
if (!isMobile.value) {
|
||||||
|
initWebGL()
|
||||||
|
setupIntersectionObserver()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(isMobile, (newIsMobile) => {
|
||||||
|
if (newIsMobile) {
|
||||||
|
cleanup()
|
||||||
|
} else {
|
||||||
|
initWebGL()
|
||||||
|
setupIntersectionObserver()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
162
src/components/landing/StartBuilding/StartBuilding.css
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
.start-building-section {
|
||||||
|
width: 100%;
|
||||||
|
padding: 80px 0;
|
||||||
|
background: transparent;
|
||||||
|
position: relative;
|
||||||
|
z-index: 22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-building-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-building-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
user-select: none;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
#3aed6d,
|
||||||
|
rgba(24, 255, 93, 0.6));
|
||||||
|
background-size: 200% 200%;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 4rem 3rem;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-building-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image: url('/assets/grain.webp');
|
||||||
|
background-size: 500px 500px;
|
||||||
|
filter: invert(100%);
|
||||||
|
mix-blend-mode: multiply;
|
||||||
|
background-repeat: repeat;
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-building-card > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-building-title {
|
||||||
|
color: #0e0e0e;
|
||||||
|
font-size: 2.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-building-subtitle {
|
||||||
|
color: #0e0e0e;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: -1rem 0 0 0;
|
||||||
|
opacity: 0.9;
|
||||||
|
max-width: 600px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-building-button {
|
||||||
|
background: transparent;
|
||||||
|
color: #0e0e0e;
|
||||||
|
border: 2px solid #0e0e0e;
|
||||||
|
padding: .6rem 1.6rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 50px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-building-button:hover {
|
||||||
|
background: #0e0e0e;
|
||||||
|
color: #27FF64;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1280px) {
|
||||||
|
.start-building-container {
|
||||||
|
max-width: 1000px;
|
||||||
|
padding: 0 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-building-card {
|
||||||
|
padding: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.start-building-section {
|
||||||
|
padding: 60px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-building-container {
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-building-card {
|
||||||
|
padding: 3rem 2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-building-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-building-subtitle {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-building-button {
|
||||||
|
padding: 0.875rem 1.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.start-building-section {
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-building-container {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-building-card {
|
||||||
|
padding: 2.5rem 1.5rem;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-building-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-building-subtitle {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-building-button {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/components/landing/StartBuilding/StartBuilding.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<section class="start-building-section">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import './StartBuilding.css'
|
||||||
|
</script>
|
||||||
34
src/components/layouts/CategoryLayout.vue
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<main class="app-container">
|
||||||
|
<header>
|
||||||
|
<h2>Header Component</h2>
|
||||||
|
</header>
|
||||||
|
<section class="category-wrapper">
|
||||||
|
<aside>
|
||||||
|
<h3>Sidebar Component</h3>
|
||||||
|
</aside>
|
||||||
|
<router-view />
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// Layout component for category pages
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-wrapper {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside {
|
||||||
|
width: 250px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
49
src/composables/useStars.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { getStarsCount } from '@/utils/utils'
|
||||||
|
|
||||||
|
const CACHE_KEY = 'github_stars_cache'
|
||||||
|
const CACHE_DURATION = 24 * 60 * 60 * 1000 // 24 hours
|
||||||
|
|
||||||
|
export function useStars() {
|
||||||
|
const stars = ref<number>(0)
|
||||||
|
|
||||||
|
const fetchStars = async () => {
|
||||||
|
try {
|
||||||
|
const cachedData = localStorage.getItem(CACHE_KEY)
|
||||||
|
|
||||||
|
if (cachedData) {
|
||||||
|
const { count, timestamp } = JSON.parse(cachedData)
|
||||||
|
const now = Date.now()
|
||||||
|
|
||||||
|
if (now - timestamp < CACHE_DURATION) {
|
||||||
|
stars.value = count
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Fall back to cached data if available
|
||||||
|
const cachedData = localStorage.getItem(CACHE_KEY)
|
||||||
|
if (cachedData) {
|
||||||
|
const { count } = JSON.parse(cachedData)
|
||||||
|
stars.value = count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchStars()
|
||||||
|
})
|
||||||
|
|
||||||
|
return stars
|
||||||
|
}
|
||||||
163
src/content/Animations/CountUp/CountUp.vue
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<span ref="elementRef" :class="className" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
from: 0,
|
||||||
|
direction: "up",
|
||||||
|
delay: 0,
|
||||||
|
duration: 2,
|
||||||
|
className: "",
|
||||||
|
startWhen: true,
|
||||||
|
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)
|
||||||
|
|
||||||
|
let intersectionObserver: IntersectionObserver | null = null
|
||||||
|
|
||||||
|
const damping = computed(() => 20 + 40 * (1 / props.duration))
|
||||||
|
const stiffness = computed(() => 100 * (1 / props.duration))
|
||||||
|
|
||||||
|
let velocity = 0
|
||||||
|
let startTime = 0
|
||||||
|
|
||||||
|
const formatNumber = (value: number) => {
|
||||||
|
const options = {
|
||||||
|
useGrouping: !!props.separator,
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedNumber = Intl.NumberFormat("en-US", options).format(
|
||||||
|
Number(value.toFixed(0))
|
||||||
|
)
|
||||||
|
|
||||||
|
return props.separator
|
||||||
|
? formattedNumber.replace(/,/g, props.separator)
|
||||||
|
: formattedNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateDisplay = () => {
|
||||||
|
if (elementRef.value) {
|
||||||
|
elementRef.value.textContent = formatNumber(currentValue.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const springAnimation = (timestamp: number) => {
|
||||||
|
if (!startTime) startTime = timestamp
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
velocity += acceleration * 0.016 // Assuming 60fps
|
||||||
|
currentValue.value += velocity * 0.016
|
||||||
|
|
||||||
|
updateDisplay()
|
||||||
|
|
||||||
|
if (Math.abs(displacement) > 0.01 || Math.abs(velocity) > 0.01) {
|
||||||
|
animationId.value = requestAnimationFrame(springAnimation)
|
||||||
|
} else {
|
||||||
|
currentValue.value = target
|
||||||
|
updateDisplay()
|
||||||
|
animationId.value = null
|
||||||
|
|
||||||
|
if (props.onEnd) {
|
||||||
|
props.onEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startAnimation = () => {
|
||||||
|
if (hasStarted.value || !isInView.value || !props.startWhen) return
|
||||||
|
|
||||||
|
hasStarted.value = true
|
||||||
|
|
||||||
|
if (props.onStart) {
|
||||||
|
props.onStart()
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
startTime = 0
|
||||||
|
velocity = 0
|
||||||
|
animationId.value = requestAnimationFrame(springAnimation)
|
||||||
|
}, props.delay * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupIntersectionObserver = () => {
|
||||||
|
if (!elementRef.value) return
|
||||||
|
|
||||||
|
intersectionObserver = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting && !isInView.value) {
|
||||||
|
isInView.value = true
|
||||||
|
startAnimation()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
threshold: 0,
|
||||||
|
rootMargin: "0px"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
intersectionObserver.observe(elementRef.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if (animationId.value) {
|
||||||
|
cancelAnimationFrame(animationId.value)
|
||||||
|
animationId.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intersectionObserver) {
|
||||||
|
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.startWhen, () => {
|
||||||
|
if (props.startWhen && isInView.value && !hasStarted.value) {
|
||||||
|
startAnimation()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateDisplay()
|
||||||
|
setupIntersectionObserver()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
66
src/content/Animations/FadeContent/FadeContent.vue
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="elementRef"
|
||||||
|
:class="className"
|
||||||
|
:style="{
|
||||||
|
opacity: inView ? 1 : initialOpacity,
|
||||||
|
transition: `opacity ${duration}ms ${easing}, filter ${duration}ms ${easing}`,
|
||||||
|
filter: blur ? (inView ? 'blur(0px)' : 'blur(10px)') : 'none',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
blur?: boolean
|
||||||
|
duration?: number
|
||||||
|
easing?: string
|
||||||
|
delay?: number
|
||||||
|
threshold?: number
|
||||||
|
initialOpacity?: number
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
blur: false,
|
||||||
|
duration: 1000,
|
||||||
|
easing: 'ease-out',
|
||||||
|
delay: 0,
|
||||||
|
threshold: 0.1,
|
||||||
|
initialOpacity: 0,
|
||||||
|
className: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const inView = ref(false)
|
||||||
|
const elementRef = ref<HTMLDivElement | null>(null)
|
||||||
|
let observer: IntersectionObserver | null = null
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const element = elementRef.value
|
||||||
|
if (!element) return
|
||||||
|
|
||||||
|
observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
observer?.unobserve(element)
|
||||||
|
setTimeout(() => {
|
||||||
|
inView.value = true
|
||||||
|
}, props.delay)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: props.threshold }
|
||||||
|
)
|
||||||
|
|
||||||
|
observer.observe(element)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (observer) {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
315
src/content/Backgrounds/DotGrid/DotGrid.vue
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
<template>
|
||||||
|
<section :class="`flex items-center justify-center h-full w-full relative ${className}`" :style="style">
|
||||||
|
<div ref="wrapperRef" class="w-full h-full relative">
|
||||||
|
<canvas ref="canvasRef" class="absolute inset-0 w-full h-full pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue'
|
||||||
|
import { gsap } from 'gsap'
|
||||||
|
import { InertiaPlugin } from 'gsap/InertiaPlugin'
|
||||||
|
|
||||||
|
gsap.registerPlugin(InertiaPlugin)
|
||||||
|
|
||||||
|
const throttle = <T extends unknown[]>(func: (...args: T) => void, limit: number) => {
|
||||||
|
let lastCall = 0
|
||||||
|
return function (this: unknown, ...args: T) {
|
||||||
|
const now = performance.now()
|
||||||
|
if (now - lastCall >= limit) {
|
||||||
|
lastCall = now
|
||||||
|
func.apply(this, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Dot {
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<DotGridProps>(), {
|
||||||
|
dotSize: 16,
|
||||||
|
gap: 32,
|
||||||
|
baseColor: '#5227FF',
|
||||||
|
activeColor: '#5227FF',
|
||||||
|
proximity: 150,
|
||||||
|
speedTrigger: 100,
|
||||||
|
shockRadius: 250,
|
||||||
|
shockStrength: 5,
|
||||||
|
maxSpeed: 5000,
|
||||||
|
resistance: 750,
|
||||||
|
returnDuration: 1.5,
|
||||||
|
className: '',
|
||||||
|
style: () => ({})
|
||||||
|
})
|
||||||
|
|
||||||
|
const wrapperRef = ref<HTMLDivElement>()
|
||||||
|
const canvasRef = ref<HTMLCanvasElement>()
|
||||||
|
const dots = ref<Dot[]>([])
|
||||||
|
const pointer = ref({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
vx: 0,
|
||||||
|
vy: 0,
|
||||||
|
speed: 0,
|
||||||
|
lastTime: 0,
|
||||||
|
lastX: 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 }
|
||||||
|
return {
|
||||||
|
r: parseInt(m[1], 16),
|
||||||
|
g: parseInt(m[2], 16),
|
||||||
|
b: parseInt(m[3], 16),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseRgb = computed(() => hexToRgb(props.baseColor))
|
||||||
|
const activeRgb = computed(() => hexToRgb(props.activeColor))
|
||||||
|
|
||||||
|
const circlePath = computed(() => {
|
||||||
|
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 buildGrid = () => {
|
||||||
|
const wrap = wrapperRef.value
|
||||||
|
const canvas = canvasRef.value
|
||||||
|
if (!wrap || !canvas) return
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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 extraX = width - gridW
|
||||||
|
const extraY = height - gridH
|
||||||
|
|
||||||
|
const startX = extraX / 2 + props.dotSize / 2
|
||||||
|
const startY = extraY / 2 + props.dotSize / 2
|
||||||
|
|
||||||
|
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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dots.value = newDots
|
||||||
|
}
|
||||||
|
|
||||||
|
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 { 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
|
||||||
|
|
||||||
|
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})`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (circlePath.value) {
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(ox, oy)
|
||||||
|
ctx.fillStyle = style
|
||||||
|
ctx.fill(circlePath.value)
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
if (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
|
||||||
|
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
gsap.to(dot, {
|
||||||
|
inertia: { xOffset: pushX, yOffset: pushY, resistance: props.resistance },
|
||||||
|
onComplete: () => {
|
||||||
|
gsap.to(dot, {
|
||||||
|
xOffset: 0,
|
||||||
|
yOffset: 0,
|
||||||
|
duration: props.returnDuration,
|
||||||
|
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
|
||||||
|
for (const dot of dots.value) {
|
||||||
|
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
|
||||||
|
gsap.to(dot, {
|
||||||
|
inertia: { xOffset: pushX, yOffset: pushY, resistance: props.resistance },
|
||||||
|
onComplete: () => {
|
||||||
|
gsap.to(dot, {
|
||||||
|
xOffset: 0,
|
||||||
|
yOffset: 0,
|
||||||
|
duration: props.returnDuration,
|
||||||
|
ease: 'elastic.out(1,0.75)',
|
||||||
|
})
|
||||||
|
dot._inertiaApplied = false
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const throttledMove = throttle(onMove, 50)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
buildGrid()
|
||||||
|
|
||||||
|
if (circlePath.value) {
|
||||||
|
draw()
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('ResizeObserver' in window) {
|
||||||
|
resizeObserver = new ResizeObserver(buildGrid)
|
||||||
|
if (wrapperRef.value) {
|
||||||
|
resizeObserver.observe(wrapperRef.value)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(window as Window).addEventListener('resize', buildGrid)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', throttledMove, { passive: true })
|
||||||
|
window.addEventListener('click', onClick)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (rafId) {
|
||||||
|
cancelAnimationFrame(rafId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resizeObserver) {
|
||||||
|
resizeObserver.disconnect()
|
||||||
|
} else {
|
||||||
|
window.removeEventListener('resize', buildGrid)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.removeEventListener('mousemove', throttledMove)
|
||||||
|
window.removeEventListener('click', onClick)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch([() => props.dotSize, () => props.gap], () => {
|
||||||
|
buildGrid()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch([() => props.proximity, () => props.baseColor, activeRgb, baseRgb, circlePath], () => {
|
||||||
|
if (rafId) {
|
||||||
|
cancelAnimationFrame(rafId)
|
||||||
|
}
|
||||||
|
if (circlePath.value) {
|
||||||
|
draw()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
246
src/content/Backgrounds/LetterGlitch/LetterGlitch.vue
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative w-full h-full overflow-hidden">
|
||||||
|
<canvas ref="canvasRef" class="block 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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
glitchColors?: string[]
|
||||||
|
glitchSpeed?: number
|
||||||
|
centerVignette?: boolean
|
||||||
|
outerVignette?: boolean
|
||||||
|
smooth?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
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 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"
|
||||||
|
]
|
||||||
|
|
||||||
|
const getRandomChar = () => {
|
||||||
|
return lettersAndSymbols[Math.floor(Math.random() * lettersAndSymbols.length)]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRandomColor = () => {
|
||||||
|
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
|
||||||
|
hex = hex.replace(shorthandRegex, (m, r, g, 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)
|
||||||
|
return result
|
||||||
|
? {
|
||||||
|
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 },
|
||||||
|
end: { r: number; g: number; b: number },
|
||||||
|
factor: number
|
||||||
|
) => {
|
||||||
|
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})`
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculateGrid = (width: number, height: number) => {
|
||||||
|
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
|
||||||
|
letters.value = Array.from({ length: totalLetters }, () => ({
|
||||||
|
char: getRandomChar(),
|
||||||
|
color: getRandomColor(),
|
||||||
|
targetColor: getRandomColor(),
|
||||||
|
colorProgress: 1,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeCanvas = () => {
|
||||||
|
const canvas = canvasRef.value
|
||||||
|
if (!canvas) return
|
||||||
|
const parent = canvas.parentElement
|
||||||
|
if (!parent) return
|
||||||
|
|
||||||
|
const dpr = window.devicePixelRatio || 1
|
||||||
|
const rect = parent.getBoundingClientRect()
|
||||||
|
|
||||||
|
canvas.width = rect.width * dpr
|
||||||
|
canvas.height = rect.height * dpr
|
||||||
|
|
||||||
|
canvas.style.width = `${rect.width}px`
|
||||||
|
canvas.style.height = `${rect.height}px`
|
||||||
|
|
||||||
|
if (context.value) {
|
||||||
|
context.value.setTransform(dpr, 0, 0, dpr, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { columns, rows } = calculateGrid(rect.width, rect.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"
|
||||||
|
|
||||||
|
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 updateLetters = () => {
|
||||||
|
if (!letters.value || letters.value.length === 0) return
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
} else {
|
||||||
|
letters.value[index].colorProgress = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSmoothTransitions = () => {
|
||||||
|
let needsRedraw = false
|
||||||
|
letters.value.forEach((letter) => {
|
||||||
|
if (letter.colorProgress < 1) {
|
||||||
|
letter.colorProgress += 0.05
|
||||||
|
if (letter.colorProgress > 1) letter.colorProgress = 1
|
||||||
|
|
||||||
|
const startRgb = hexToRgb(letter.color)
|
||||||
|
const endRgb = hexToRgb(letter.targetColor)
|
||||||
|
if (startRgb && endRgb) {
|
||||||
|
letter.color = interpolateColor(
|
||||||
|
startRgb,
|
||||||
|
endRgb,
|
||||||
|
letter.colorProgress
|
||||||
|
)
|
||||||
|
needsRedraw = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (needsRedraw) {
|
||||||
|
drawLetters()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - lastGlitchTime.value >= props.glitchSpeed) {
|
||||||
|
updateLetters()
|
||||||
|
drawLetters()
|
||||||
|
lastGlitchTime.value = now
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.smooth) {
|
||||||
|
handleSmoothTransitions()
|
||||||
|
}
|
||||||
|
|
||||||
|
animationRef.value = requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
|
||||||
|
let resizeTimeout: number
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
clearTimeout(resizeTimeout)
|
||||||
|
resizeTimeout = setTimeout(() => {
|
||||||
|
if (animationRef.value) {
|
||||||
|
cancelAnimationFrame(animationRef.value)
|
||||||
|
}
|
||||||
|
resizeCanvas()
|
||||||
|
animate()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const canvas = canvasRef.value
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
context.value = canvas.getContext("2d")
|
||||||
|
resizeCanvas()
|
||||||
|
animate()
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (animationRef.value) {
|
||||||
|
cancelAnimationFrame(animationRef.value)
|
||||||
|
}
|
||||||
|
window.removeEventListener("resize", handleResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch([() => props.glitchSpeed, () => props.smooth], () => {
|
||||||
|
if (animationRef.value) {
|
||||||
|
cancelAnimationFrame(animationRef.value)
|
||||||
|
}
|
||||||
|
animate()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
197
src/content/Backgrounds/Squares/Squares.vue
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
<template>
|
||||||
|
<canvas ref="canvasRef" class="w-full h-full border-none block" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
|
||||||
|
type CanvasStrokeStyle = string | CanvasGradient | CanvasPattern
|
||||||
|
|
||||||
|
interface GridOffset {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
direction?: "diagonal" | "up" | "right" | "down" | "left"
|
||||||
|
speed?: number
|
||||||
|
borderColor?: CanvasStrokeStyle
|
||||||
|
squareSize?: number
|
||||||
|
hoverFillColor?: CanvasStrokeStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
direction: "right",
|
||||||
|
speed: 1,
|
||||||
|
borderColor: "#999",
|
||||||
|
squareSize: 40,
|
||||||
|
hoverFillColor: "#222"
|
||||||
|
})
|
||||||
|
|
||||||
|
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||||
|
const requestRef = ref<number | null>(null)
|
||||||
|
const numSquaresX = ref<number>(0)
|
||||||
|
const numSquaresY = ref<number>(0)
|
||||||
|
const gridOffset = ref<GridOffset>({ x: 0, y: 0 })
|
||||||
|
const hoveredSquareRef = ref<GridOffset | null>(null)
|
||||||
|
|
||||||
|
let ctx: CanvasRenderingContext2D | null = null
|
||||||
|
|
||||||
|
const resizeCanvas = () => {
|
||||||
|
const canvas = canvasRef.value
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
canvas.width = canvas.offsetWidth
|
||||||
|
canvas.height = canvas.offsetHeight
|
||||||
|
numSquaresX.value = Math.ceil(canvas.width / props.squareSize) + 1
|
||||||
|
numSquaresY.value = Math.ceil(canvas.height / props.squareSize) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const drawGrid = () => {
|
||||||
|
const canvas = canvasRef.value
|
||||||
|
if (!ctx || !canvas) return
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
const startX = Math.floor(gridOffset.value.x / props.squareSize) * props.squareSize
|
||||||
|
const startY = Math.floor(gridOffset.value.y / props.squareSize) * props.squareSize
|
||||||
|
|
||||||
|
for (let x = startX; x < canvas.width + props.squareSize; x += props.squareSize) {
|
||||||
|
for (let y = startY; y < canvas.height + props.squareSize; y += props.squareSize) {
|
||||||
|
const squareX = x - (gridOffset.value.x % props.squareSize)
|
||||||
|
const squareY = y - (gridOffset.value.y % props.squareSize)
|
||||||
|
|
||||||
|
if (
|
||||||
|
hoveredSquareRef.value &&
|
||||||
|
Math.floor((x - startX) / props.squareSize) === hoveredSquareRef.value.x &&
|
||||||
|
Math.floor((y - startY) / props.squareSize) === hoveredSquareRef.value.y
|
||||||
|
) {
|
||||||
|
ctx.fillStyle = props.hoverFillColor
|
||||||
|
ctx.fillRect(squareX, squareY, props.squareSize, props.squareSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.strokeStyle = props.borderColor
|
||||||
|
ctx.strokeRect(squareX, squareY, props.squareSize, props.squareSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const gradient = ctx.createRadialGradient(
|
||||||
|
canvas.width / 2,
|
||||||
|
canvas.height / 2,
|
||||||
|
0,
|
||||||
|
canvas.width / 2,
|
||||||
|
canvas.height / 2,
|
||||||
|
Math.sqrt(canvas.width ** 2 + canvas.height ** 2) / 2
|
||||||
|
)
|
||||||
|
gradient.addColorStop(0, "rgba(0, 0, 0, 0)")
|
||||||
|
gradient.addColorStop(1, "#0e0e0e")
|
||||||
|
|
||||||
|
ctx.fillStyle = gradient
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateAnimation = () => {
|
||||||
|
const effectiveSpeed = Math.max(props.speed, 0.1)
|
||||||
|
|
||||||
|
switch (props.direction) {
|
||||||
|
case "right":
|
||||||
|
gridOffset.value.x = (gridOffset.value.x - effectiveSpeed + props.squareSize) % props.squareSize
|
||||||
|
break
|
||||||
|
case "left":
|
||||||
|
gridOffset.value.x = (gridOffset.value.x + effectiveSpeed + props.squareSize) % props.squareSize
|
||||||
|
break
|
||||||
|
case "up":
|
||||||
|
gridOffset.value.y = (gridOffset.value.y + effectiveSpeed + props.squareSize) % props.squareSize
|
||||||
|
break
|
||||||
|
case "down":
|
||||||
|
gridOffset.value.y = (gridOffset.value.y - effectiveSpeed + props.squareSize) % props.squareSize
|
||||||
|
break
|
||||||
|
case "diagonal":
|
||||||
|
gridOffset.value.x = (gridOffset.value.x - effectiveSpeed + props.squareSize) % props.squareSize
|
||||||
|
gridOffset.value.y = (gridOffset.value.y - effectiveSpeed + props.squareSize) % props.squareSize
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
drawGrid()
|
||||||
|
requestRef.value = requestAnimationFrame(updateAnimation)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseMove = (event: MouseEvent) => {
|
||||||
|
const canvas = canvasRef.value
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect()
|
||||||
|
const mouseX = event.clientX - rect.left
|
||||||
|
const mouseY = event.clientY - rect.top
|
||||||
|
|
||||||
|
const startX = Math.floor(gridOffset.value.x / props.squareSize) * props.squareSize
|
||||||
|
const startY = Math.floor(gridOffset.value.y / props.squareSize) * props.squareSize
|
||||||
|
|
||||||
|
const hoveredSquareX = Math.floor(
|
||||||
|
(mouseX + gridOffset.value.x - startX) / props.squareSize
|
||||||
|
)
|
||||||
|
const hoveredSquareY = Math.floor(
|
||||||
|
(mouseY + gridOffset.value.y - startY) / props.squareSize
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!hoveredSquareRef.value ||
|
||||||
|
hoveredSquareRef.value.x !== hoveredSquareX ||
|
||||||
|
hoveredSquareRef.value.y !== hoveredSquareY
|
||||||
|
) {
|
||||||
|
hoveredSquareRef.value = { x: hoveredSquareX, y: hoveredSquareY }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
hoveredSquareRef.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const initializeCanvas = () => {
|
||||||
|
const canvas = canvasRef.value
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
ctx = canvas.getContext("2d")
|
||||||
|
resizeCanvas()
|
||||||
|
|
||||||
|
canvas.addEventListener("mousemove", handleMouseMove)
|
||||||
|
canvas.addEventListener("mouseleave", handleMouseLeave)
|
||||||
|
window.addEventListener("resize", resizeCanvas)
|
||||||
|
|
||||||
|
requestRef.value = requestAnimationFrame(updateAnimation)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
const canvas = canvasRef.value
|
||||||
|
|
||||||
|
if (requestRef.value) {
|
||||||
|
cancelAnimationFrame(requestRef.value)
|
||||||
|
requestRef.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canvas) {
|
||||||
|
canvas.removeEventListener("mousemove", handleMouseMove)
|
||||||
|
canvas.removeEventListener("mouseleave", handleMouseLeave)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.removeEventListener("resize", resizeCanvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initializeCanvas()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[() => props.direction, () => props.speed, () => props.borderColor, () => props.hoverFillColor, () => props.squareSize],
|
||||||
|
() => {
|
||||||
|
cleanup()
|
||||||
|
initializeCanvas()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
192
src/content/TextAnimations/SplitText/SplitText.vue
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<template>
|
||||||
|
<p
|
||||||
|
ref="textRef"
|
||||||
|
:class="`split-parent overflow-hidden inline-block whitespace-normal ${className}`"
|
||||||
|
:style="{
|
||||||
|
textAlign,
|
||||||
|
wordWrap: 'break-word',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ text }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||||
|
import { gsap } from 'gsap'
|
||||||
|
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||||||
|
import { SplitText as GSAPSplitText } from 'gsap/SplitText'
|
||||||
|
|
||||||
|
gsap.registerPlugin(ScrollTrigger, GSAPSplitText)
|
||||||
|
|
||||||
|
export interface SplitTextProps {
|
||||||
|
text: string
|
||||||
|
className?: string
|
||||||
|
delay?: number
|
||||||
|
duration?: number
|
||||||
|
ease?: string | ((t: number) => number)
|
||||||
|
splitType?: 'chars' | 'words' | 'lines' | 'words, chars'
|
||||||
|
from?: gsap.TweenVars
|
||||||
|
to?: gsap.TweenVars
|
||||||
|
threshold?: number
|
||||||
|
rootMargin?: string
|
||||||
|
textAlign?: 'left' | 'center' | 'right' | 'justify'
|
||||||
|
onLetterAnimationComplete?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<SplitTextProps>(), {
|
||||||
|
className: '',
|
||||||
|
delay: 100,
|
||||||
|
duration: 0.6,
|
||||||
|
ease: 'power3.out',
|
||||||
|
splitType: 'chars',
|
||||||
|
from: () => ({ opacity: 0, y: 40 }),
|
||||||
|
to: () => ({ opacity: 1, y: 0 }),
|
||||||
|
threshold: 0.1,
|
||||||
|
rootMargin: '-100px',
|
||||||
|
textAlign: 'center'
|
||||||
|
})
|
||||||
|
|
||||||
|
const textRef = ref<HTMLParagraphElement | null>(null)
|
||||||
|
const animationCompletedRef = ref(false)
|
||||||
|
const scrollTriggerRef = ref<ScrollTrigger | null>(null)
|
||||||
|
const timelineRef = ref<gsap.core.Timeline | null>(null)
|
||||||
|
const splitterRef = ref<GSAPSplitText | null>(null)
|
||||||
|
|
||||||
|
const initializeAnimation = async () => {
|
||||||
|
if (typeof window === 'undefined' || !textRef.value || !props.text) return
|
||||||
|
|
||||||
|
// Wait for DOM to be fully updated
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const el = textRef.value
|
||||||
|
|
||||||
|
animationCompletedRef.value = false
|
||||||
|
|
||||||
|
const absoluteLines = props.splitType === 'lines'
|
||||||
|
if (absoluteLines) el.style.position = 'relative'
|
||||||
|
|
||||||
|
let splitter: GSAPSplitText
|
||||||
|
try {
|
||||||
|
splitter = new GSAPSplitText(el, {
|
||||||
|
type: props.splitType,
|
||||||
|
absolute: absoluteLines,
|
||||||
|
linesClass: 'split-line',
|
||||||
|
})
|
||||||
|
splitterRef.value = splitter
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create SplitText:', error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let targets: Element[]
|
||||||
|
switch (props.splitType) {
|
||||||
|
case 'lines':
|
||||||
|
targets = splitter.lines
|
||||||
|
break
|
||||||
|
case 'words':
|
||||||
|
targets = splitter.words
|
||||||
|
break
|
||||||
|
case 'chars':
|
||||||
|
targets = splitter.chars
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
targets = splitter.chars
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targets || targets.length === 0) {
|
||||||
|
console.warn('No targets found for SplitText animation')
|
||||||
|
splitter.revert()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targets.forEach((t) => {
|
||||||
|
;(t as HTMLElement).style.willChange = 'transform, opacity'
|
||||||
|
})
|
||||||
|
|
||||||
|
const startPct = (1 - props.threshold) * 100
|
||||||
|
const marginMatch = /^(-?\d+(?:\.\d+)?)(px|em|rem|%)?$/.exec(props.rootMargin)
|
||||||
|
const marginValue = marginMatch ? parseFloat(marginMatch[1]) : 0
|
||||||
|
const marginUnit = marginMatch ? (marginMatch[2] || 'px') : 'px'
|
||||||
|
const sign = marginValue < 0 ? `-=${Math.abs(marginValue)}${marginUnit}` : `+=${marginValue}${marginUnit}`
|
||||||
|
const start = `top ${startPct}%${sign}`
|
||||||
|
|
||||||
|
const tl = gsap.timeline({
|
||||||
|
scrollTrigger: {
|
||||||
|
trigger: el,
|
||||||
|
start,
|
||||||
|
toggleActions: 'play none none none',
|
||||||
|
once: true,
|
||||||
|
onToggle: (self) => {
|
||||||
|
scrollTriggerRef.value = self
|
||||||
|
},
|
||||||
|
},
|
||||||
|
smoothChildTiming: true,
|
||||||
|
onComplete: () => {
|
||||||
|
animationCompletedRef.value = true
|
||||||
|
gsap.set(targets, {
|
||||||
|
...props.to,
|
||||||
|
clearProps: 'willChange',
|
||||||
|
immediateRender: true,
|
||||||
|
})
|
||||||
|
props.onLetterAnimationComplete?.()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
timelineRef.value = tl
|
||||||
|
|
||||||
|
tl.set(targets, { ...props.from, immediateRender: false, force3D: true })
|
||||||
|
tl.to(targets, {
|
||||||
|
...props.to,
|
||||||
|
duration: props.duration,
|
||||||
|
ease: props.ease,
|
||||||
|
stagger: props.delay / 1000,
|
||||||
|
force3D: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if (timelineRef.value) {
|
||||||
|
timelineRef.value.kill()
|
||||||
|
timelineRef.value = null
|
||||||
|
}
|
||||||
|
if (scrollTriggerRef.value) {
|
||||||
|
scrollTriggerRef.value.kill()
|
||||||
|
scrollTriggerRef.value = null
|
||||||
|
}
|
||||||
|
if (splitterRef.value) {
|
||||||
|
gsap.killTweensOf(textRef.value)
|
||||||
|
splitterRef.value.revert()
|
||||||
|
splitterRef.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initializeAnimation()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch for prop changes and reinitialize animation
|
||||||
|
watch(
|
||||||
|
[
|
||||||
|
() => props.text,
|
||||||
|
() => props.delay,
|
||||||
|
() => props.duration,
|
||||||
|
() => props.ease,
|
||||||
|
() => props.splitType,
|
||||||
|
() => props.from,
|
||||||
|
() => props.to,
|
||||||
|
() => props.threshold,
|
||||||
|
() => props.rootMargin,
|
||||||
|
() => props.onLetterAnimationComplete,
|
||||||
|
],
|
||||||
|
() => {
|
||||||
|
cleanup()
|
||||||
|
initializeAnimation()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
@import 'primeicons/primeicons.css';
|
||||||
|
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
@@ -7,6 +8,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
background-color: #0e0e0e;
|
||||||
|
color: #fff;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
|
|||||||
922
src/css/landing.css
Normal file
@@ -0,0 +1,922 @@
|
|||||||
|
.landing-wrapper {
|
||||||
|
min-height: 100dvh;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-wrapper::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 300px;
|
||||||
|
height: 100vh;
|
||||||
|
background: linear-gradient(to right, #0e0e0e, transparent);
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-wrapper::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 300px;
|
||||||
|
height: 100vh;
|
||||||
|
background: linear-gradient(to left, #0e0e0e, transparent);
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-content {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 100%;
|
||||||
|
max-width: calc(1200px + 6em);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 4em;
|
||||||
|
margin-top: 250px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-title {
|
||||||
|
user-select: none;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
font-size: 3.6rem;
|
||||||
|
font-weight: 500;
|
||||||
|
position: relative;
|
||||||
|
z-index: 6;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
max-width: 22ch;
|
||||||
|
letter-spacing: -3px;
|
||||||
|
line-height: 1;
|
||||||
|
text-shadow:
|
||||||
|
0 0 2px rgba(255, 255, 255, 0.1),
|
||||||
|
0 0 4px rgba(255, 255, 255, 0.3),
|
||||||
|
0 0 8px rgba(255, 255, 255, 0.4),
|
||||||
|
0 0 136px rgba(60, 255, 79, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-subtitle {
|
||||||
|
user-select: none;
|
||||||
|
overflow: visible !important;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
text-align: left !important;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #A7EF9E;
|
||||||
|
max-width: 30ch;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
text-shadow:
|
||||||
|
0 0 2px rgba(255, 255, 255, 0.1),
|
||||||
|
0 0 4px rgba(255, 255, 255, 0.3),
|
||||||
|
0 0 8px rgba(255, 255, 255, 0.4),
|
||||||
|
0 0 136px rgba(60, 255, 79, 0.8);
|
||||||
|
z-index: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-split {
|
||||||
|
white-space: nowrap !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-button {
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
rgb(30, 160, 63),
|
||||||
|
rgba(24, 47, 255, 0.6));
|
||||||
|
background-size: 200% 200%;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
border-radius: 50px;
|
||||||
|
border: none;
|
||||||
|
padding: 1rem 1rem 1rem 2.5rem;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
isolation: isolate;
|
||||||
|
z-index: 15;
|
||||||
|
box-shadow:
|
||||||
|
0 0 40px rgba(58, 237, 112, 0.4),
|
||||||
|
0 0 80px rgba(92, 246, 138, 0.3),
|
||||||
|
0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
animation: glow-pulse 3s ease-in-out infinite alternate;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-arrow-circle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #1d9559;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-button:hover .button-arrow-circle {
|
||||||
|
background: rgba(255, 255, 255, 1);
|
||||||
|
transform: translateX(4px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-button span {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
display: inline-block;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-button:hover span {
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-button::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.4),
|
||||||
|
transparent);
|
||||||
|
transition: left 0.6s ease;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-button::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -2px;
|
||||||
|
background: linear-gradient(45deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.1),
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.1),
|
||||||
|
transparent);
|
||||||
|
background-size: 200% 200%;
|
||||||
|
border-radius: 50px;
|
||||||
|
z-index: -1;
|
||||||
|
animation: border-dance 4s linear infinite;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-button:hover {
|
||||||
|
box-shadow:
|
||||||
|
0 0 60px rgba(58, 237, 109, 0.2),
|
||||||
|
0 0 120px rgba(92, 246, 138, 0.2),
|
||||||
|
0 0 180px rgba(40, 217, 90, 0.2),
|
||||||
|
0 12px 40px rgba(0, 0, 0, 0.4),
|
||||||
|
inset 0 2px 0 rgba(255, 255, 255, 0.4),
|
||||||
|
inset 0 -2px 0 rgba(0, 0, 0, 0.3);
|
||||||
|
transform: translateY(-4px) scale(1.01);
|
||||||
|
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
animation-duration: 1.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-button:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-button:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-button:active {
|
||||||
|
transform: translateY(-2px) scale(1.02);
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow-pulse {
|
||||||
|
0% {
|
||||||
|
filter: brightness(1) saturate(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
filter: brightness(1.1) saturate(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes border-dance {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px) scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUpRotate1 {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px) scale(0.9) rotate(-13deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1) rotate(-13deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUpRotate2 {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px) scale(0.9) rotate(10deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1) rotate(10deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUpRotate3 {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px) scale(0.9) rotate(-5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1) rotate(-5deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUpMobile {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px) scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUpMobileRotate1 {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px) scale(0.9) rotate(-13deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1) rotate(-13deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUpMobileRotate2 {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px) scale(0.9) rotate(10deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1) rotate(10deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUpMobileRotate3 {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px) scale(0.9) rotate(-5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1) rotate(-5deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
max-width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-cards-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2.4rem;
|
||||||
|
max-width: 40%;
|
||||||
|
margin-left: .6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-cards-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 2.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-glitch {
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
border-radius: 30px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-dot-grid {
|
||||||
|
overflow: hidden !important;
|
||||||
|
transform: translateY(5px) translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-metaballs {
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-falling-text .falling-text-canvas {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-lines {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
background: linear-gradient(135deg, rgba(58, 237, 109, 0.2), rgba(40, 217, 72, 0.2));
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 30px;
|
||||||
|
box-shadow:
|
||||||
|
0 30px 100px rgba(0, 16, 5, 0.95),
|
||||||
|
0 20px 70px rgba(0, 16, 5, 0.9),
|
||||||
|
0 15px 50px rgba(0, 16, 5, 0.8),
|
||||||
|
0 10px 30px rgba(0, 16, 7, 0.7),
|
||||||
|
0 8px 32px rgba(58, 237, 121, 0.4),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card-1 {
|
||||||
|
animation: fadeInUpRotate1 0.8s ease-out forwards;
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
transform: translateY(0) scale(1) rotate(-13deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card-2 {
|
||||||
|
animation: fadeInUpRotate2 0.8s ease-out forwards;
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
transform: translateY(0) scale(1) rotate(10deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card-3 {
|
||||||
|
animation: fadeInUpRotate3 0.8s ease-out forwards;
|
||||||
|
animation-delay: 0.6s;
|
||||||
|
transform: translateY(0) scale(1) rotate(-5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card:hover {
|
||||||
|
filter: grayscale(50%);
|
||||||
|
box-shadow:
|
||||||
|
0 50px 140px rgba(0, 16, 5, 0.98),
|
||||||
|
0 35px 100px rgba(0, 16, 4, 0.95),
|
||||||
|
0 25px 70px rgba(0, 16, 4, 0.9),
|
||||||
|
0 15px 50px rgba(0, 16, 4, 0.8),
|
||||||
|
0 12px 40px rgba(58, 237, 115, 0.6),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||||
|
backdrop-filter: blur(25px);
|
||||||
|
-webkit-backdrop-filter: blur(25px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
transition: left 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1440px) {
|
||||||
|
.landing-title {
|
||||||
|
font-size: 3.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-subtitle {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-content {
|
||||||
|
margin-top: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1366px) {
|
||||||
|
.landing-title {
|
||||||
|
font-size: 3.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-subtitle {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-content {
|
||||||
|
margin-top: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.landing-title {
|
||||||
|
font-size: 3.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-subtitle {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-content {
|
||||||
|
margin-top: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.landing-title {
|
||||||
|
font-size: 3.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-subtitle {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-content {
|
||||||
|
margin-top: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.landing-title {
|
||||||
|
font-size: 3.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-subtitle {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-content {
|
||||||
|
margin-top: 300px;
|
||||||
|
padding: 0 2em;
|
||||||
|
max-width: calc(1200px + 2em);
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-main-content {
|
||||||
|
max-width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-cards-container {
|
||||||
|
max-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-top: 2rem;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1.5rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-cards-row {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
flex: 1;
|
||||||
|
height: 120px;
|
||||||
|
max-width: calc(33.333% - 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card-1 {
|
||||||
|
animation: fadeInUpMobileRotate1 0.8s ease-out forwards;
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
transform: translateY(0) scale(1) rotate(-13deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card-2 {
|
||||||
|
animation: fadeInUpMobileRotate2 0.8s ease-out forwards;
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
transform: translateY(0) scale(1) rotate(10deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card-3 {
|
||||||
|
animation: fadeInUpMobileRotate3 0.8s ease-out forwards;
|
||||||
|
animation-delay: 0.6s;
|
||||||
|
transform: translateY(0) scale(1) rotate(-5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-wrapper>div[style*="position: absolute"][style*="width: 100vw"][style*="height: 100vh"] {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 950px) {
|
||||||
|
.landing-title {
|
||||||
|
font-size: 3.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-subtitle {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-content {
|
||||||
|
margin-top: 290px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.landing-title {
|
||||||
|
font-size: 3.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-subtitle {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-content {
|
||||||
|
margin-top: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-wrapper::before {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-wrapper::after {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.landing-title {
|
||||||
|
font-size: 3.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-subtitle {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-content {
|
||||||
|
margin-top: 270px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.landing-title {
|
||||||
|
font-size: 3.2rem;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-subtitle {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-align: center !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-content {
|
||||||
|
margin-top: 260px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-button {
|
||||||
|
padding: 0.8rem 0.8rem 0.8rem 1.8rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
gap: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-arrow-circle {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-cards-container {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1.2rem;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-cards-row {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
flex: 1;
|
||||||
|
height: 120px;
|
||||||
|
max-width: calc(33.333% - 0.8rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card-1 {
|
||||||
|
animation: fadeInUpMobileRotate1 0.8s ease-out forwards;
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
transform: translateY(0) scale(1) rotate(-13deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card-2 {
|
||||||
|
animation: fadeInUpMobileRotate2 0.8s ease-out forwards;
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
transform: translateY(0) scale(1) rotate(10deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card-3 {
|
||||||
|
animation: fadeInUpMobileRotate3 0.8s ease-out forwards;
|
||||||
|
animation-delay: 0.6s;
|
||||||
|
transform: translateY(0) scale(1) rotate(-5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-wrapper>div[style*="position: absolute"][style*="width: 100vw"][style*="height: 100vh"] {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.landing-title {
|
||||||
|
font-size: 2.6rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-subtitle {
|
||||||
|
font-size: 1rem;
|
||||||
|
text-align: center !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-content {
|
||||||
|
margin-top: 250px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.landing-title {
|
||||||
|
font-size: 2.4rem;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-subtitle {
|
||||||
|
text-align: center !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-content {
|
||||||
|
margin-top: 240px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 580px) {
|
||||||
|
.landing-title {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-subtitle {
|
||||||
|
text-align: center !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-content {
|
||||||
|
margin-top: 230px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.landing-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-subtitle {
|
||||||
|
text-align: center !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-content {
|
||||||
|
margin-top: 220px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.landing-title {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-subtitle {
|
||||||
|
text-align: center !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-content {
|
||||||
|
margin-top: 210px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-button {
|
||||||
|
padding: 0.7rem 0.7rem 0.7rem 1.5rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-arrow-circle {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-cards-container {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-cards-row {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
flex: 1;
|
||||||
|
height: 120px;
|
||||||
|
max-width: calc(33.333% - 0.666rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card-1 {
|
||||||
|
animation: fadeInUpMobileRotate1 0.8s ease-out forwards;
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
transform: translateY(0) scale(1) rotate(-13deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card-2 {
|
||||||
|
animation: fadeInUpMobileRotate2 0.8s ease-out forwards;
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
transform: translateY(0) scale(1) rotate(10deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card-3 {
|
||||||
|
animation: fadeInUpMobileRotate3 0.8s ease-out forwards;
|
||||||
|
animation-delay: 0.6s;
|
||||||
|
transform: translateY(0) scale(1) rotate(-5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-wrapper>div[style*="position: absolute"][style*="width: 100vw"][style*="height: 100vh"] {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 430px) {
|
||||||
|
.landing-title {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-subtitle {
|
||||||
|
text-align: center !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-content {
|
||||||
|
margin-top: 205px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 390px) {
|
||||||
|
.landing-title {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-subtitle {
|
||||||
|
text-align: center !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-content {
|
||||||
|
margin-top: 202px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 375px) {
|
||||||
|
.landing-title {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-subtitle {
|
||||||
|
text-align: center !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-content {
|
||||||
|
margin-top: 150px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-button {
|
||||||
|
padding: 0.6rem 0.6rem 0.6rem 1.2rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-arrow-circle {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile hero background styles */
|
||||||
|
.mobile-hero-background-container {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-hero-background-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
@import './base.css';
|
@import './base.css';
|
||||||
|
@import './landing.css';
|
||||||
|
|
||||||
|
|||||||
11
src/pages/CategoryPage.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Category Page</h1>
|
||||||
|
<p>Category: {{ $route.params.category }}</p>
|
||||||
|
<p>Subcategory: {{ $route.params.subcategory }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
38
src/pages/LandingPage.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<section class="landing-wrapper">
|
||||||
|
<div v-if="isMobile" class="mobile-hero-background-container">
|
||||||
|
<!-- Hero image will be added when available -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PlasmaWave :y-offset="-300" :x-offset="100" :rotation-deg="-30" />
|
||||||
|
<Hero />
|
||||||
|
<FeatureCards />
|
||||||
|
<StartBuilding />
|
||||||
|
<Footer />
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import Hero from '../components/landing/Hero/Hero.vue'
|
||||||
|
import PlasmaWave from '@/components/landing/PlasmaWave/PlasmaWave.vue'
|
||||||
|
import Footer from '@/components/landing/Footer/Footer.vue'
|
||||||
|
import FeatureCards from '@/components/landing/FeatureCards/FeatureCards.vue'
|
||||||
|
import StartBuilding from '@/components/landing/StartBuilding/StartBuilding.vue'
|
||||||
|
const isMobile = ref(false)
|
||||||
|
|
||||||
|
const checkIsMobile = () => {
|
||||||
|
isMobile.value = window.innerWidth <= 768
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.title = 'Vue Bits - Animated UI Components For Vue'
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
checkIsMobile()
|
||||||
|
window.addEventListener('resize', checkIsMobile)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', checkIsMobile)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import App from '@/App.vue'
|
import LandingPage from '@/pages/LandingPage.vue'
|
||||||
|
import CategoryPage from '@/pages/CategoryPage.vue'
|
||||||
|
import CategoryLayout from '@/components/layouts/CategoryLayout.vue'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
@@ -7,7 +9,18 @@ const router = createRouter({
|
|||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'home',
|
name: 'home',
|
||||||
component: App,
|
component: LandingPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:category/:subcategory',
|
||||||
|
name: 'category',
|
||||||
|
component: CategoryLayout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: CategoryPage,
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
20
src/utils/utils.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Fetches the star count for a GitHub repository
|
||||||
|
* @param repo - Repository in format "owner/repo"
|
||||||
|
* @returns Promise<number> - The star count
|
||||||
|
*/
|
||||||
|
export const getStarsCount = async (repo: string = 'DavidHDev/vue-bits'): Promise<number> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://api.github.com/repos/${repo}`)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`GitHub API error: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return data.stargazers_count || 0
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching GitHub stars:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,19 @@
|
|||||||
{
|
{
|
||||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
"include": [
|
||||||
"exclude": ["src/**/__tests__/*"],
|
"env.d.ts",
|
||||||
|
"src/**/*",
|
||||||
|
"src/**/*.vue"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"src/**/__tests__/*"
|
||||||
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||