mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
Landing Page
This commit is contained in:
34
src/components/common/Logo.vue
Normal file
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
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
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
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
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
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
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
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
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
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
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
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>
|
||||
Reference in New Issue
Block a user