mirror of
https://github.com/zen-browser/www.git
synced 2025-07-08 17:30:01 +02:00
fix(features): attempt to improve feature section to reduce performance issues
This commit is contained in:
parent
95201bb0f6
commit
1391a517b9
1 changed files with 41 additions and 265 deletions
|
@ -2,6 +2,7 @@
|
||||||
import { motion } from 'motion/react'
|
import { motion } from 'motion/react'
|
||||||
import { getTitleAnimation } from '~/animations'
|
import { getTitleAnimation } from '~/animations'
|
||||||
import Description from '~/components/Description.astro'
|
import Description from '~/components/Description.astro'
|
||||||
|
import Video from './Video.astro'
|
||||||
|
|
||||||
import CompactModeVideo from '~/assets/CompactMode.webm'
|
import CompactModeVideo from '~/assets/CompactMode.webm'
|
||||||
import GlanceVideo from '~/assets/Glance.webm'
|
import GlanceVideo from '~/assets/Glance.webm'
|
||||||
|
@ -9,7 +10,6 @@ import SplitViewsVideo from '~/assets/SplitViews.webm'
|
||||||
import WorkspacesVideo from '~/assets/Workspaces.webm'
|
import WorkspacesVideo from '~/assets/Workspaces.webm'
|
||||||
|
|
||||||
import { getLocale, getUI } from '~/utils/i18n'
|
import { getLocale, getUI } from '~/utils/i18n'
|
||||||
import Video from './Video.astro'
|
|
||||||
|
|
||||||
const locale = getLocale(Astro)
|
const locale = getLocale(Astro)
|
||||||
|
|
||||||
|
@ -21,12 +21,32 @@ const {
|
||||||
|
|
||||||
const { title1 = features.title1, title2 = features.title2, title3 = features.title3 } = Astro.props
|
const { title1 = features.title1, title2 = features.title2, title3 = features.title3 } = Astro.props
|
||||||
|
|
||||||
const descriptions = Object.values(features.featureTabs).map((tab) => tab.description)
|
const featureList = [
|
||||||
|
{
|
||||||
|
key: 'workspaces',
|
||||||
|
video: WorkspacesVideo,
|
||||||
|
...features.featureTabs.workspaces,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'compactMode',
|
||||||
|
video: CompactModeVideo,
|
||||||
|
...features.featureTabs.compactMode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'glance',
|
||||||
|
video: GlanceVideo,
|
||||||
|
...features.featureTabs.glance,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'splitView',
|
||||||
|
video: SplitViewsVideo,
|
||||||
|
...features.featureTabs.splitView,
|
||||||
|
},
|
||||||
|
]
|
||||||
---
|
---
|
||||||
|
|
||||||
<section
|
<section
|
||||||
id="Features"
|
class="relative flex w-full flex-col items-center gap-12 py-12 text-start md:text-center lg:py-36"
|
||||||
class="relative flex w-full flex-col py-12 text-start lg:py-36"
|
|
||||||
>
|
>
|
||||||
<Description class="mb-2 text-4xl sm:text-6xl font-bold">
|
<Description class="mb-2 text-4xl sm:text-6xl font-bold">
|
||||||
<motion.span client:load {...getTitleAnimation(0.2)}>
|
<motion.span client:load {...getTitleAnimation(0.2)}>
|
||||||
|
@ -39,277 +59,33 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
||||||
{title3}
|
{title3}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
</Description>
|
</Description>
|
||||||
<motion.p client:load {...getTitleAnimation(0.6)} className="lg:w-1/2">
|
<motion.p client:load {...getTitleAnimation(0.6)} className="flex items-center justify-center max-w-2xl text-lg opacity-80">
|
||||||
{features.description}
|
{features.description}
|
||||||
</motion.p>
|
</motion.p>
|
||||||
<div class="mt-6 flex flex-col gap-6 lg:flex-row lg:justify-between lg:gap-2">
|
<div class="flex w-full flex-col items-center justify-center gap-12">
|
||||||
<div class="flex w-full flex-col lg:w-1/3">
|
{featureList.map((feature, i) => (
|
||||||
<!-- Mobile tabs -->
|
|
||||||
<div class="flex gap-2 overflow-x-auto overflow-y-clip lg:hidden">
|
|
||||||
<motion.button
|
|
||||||
client:load
|
|
||||||
{...getTitleAnimation()}
|
|
||||||
class="feature-tab whitespace-nowrap"
|
|
||||||
data-active="true"
|
|
||||||
>
|
|
||||||
{features.featureTabs.workspaces.title}
|
|
||||||
</motion.button>
|
|
||||||
<motion.button
|
|
||||||
client:load
|
|
||||||
{...getTitleAnimation(0.2)}
|
|
||||||
class="feature-tab whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{features.featureTabs.compactMode.title}
|
|
||||||
</motion.button>
|
|
||||||
<motion.button
|
|
||||||
client:load
|
|
||||||
{...getTitleAnimation(0.4)}
|
|
||||||
class="feature-tab whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{features.featureTabs.glance.title}
|
|
||||||
</motion.button>
|
|
||||||
<motion.button
|
|
||||||
client:load
|
|
||||||
{...getTitleAnimation(0.6)}
|
|
||||||
class="feature-tab whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{features.featureTabs.splitView.title}
|
|
||||||
</motion.button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Desktop features list -->
|
|
||||||
<div id="features-list" class="hidden lg:flex lg:flex-col lg:gap-3">
|
|
||||||
<motion.div
|
<motion.div
|
||||||
client:load
|
client:load
|
||||||
{...getTitleAnimation(0.8)}
|
{...getTitleAnimation(0.8 + i * 0.2)}
|
||||||
className="feature"
|
className="w-full flex flex-col items-center justify-center gap-4"
|
||||||
data-active="true"
|
|
||||||
>
|
>
|
||||||
<Description class="text-2xl font-bold">
|
<Description class="text-2xl font-bold text-center">
|
||||||
{features.featureTabs.workspaces.title}
|
{feature.title}
|
||||||
</Description>
|
</Description>
|
||||||
<Description>
|
<Description class="flex items-center justify-center max-w-xl text-center">
|
||||||
{features.featureTabs.workspaces.description}
|
{feature.description}
|
||||||
</Description>
|
</Description>
|
||||||
</motion.div>
|
|
||||||
<motion.div client:load {...getTitleAnimation(1)} className="feature">
|
|
||||||
<Description class="text-2xl font-bold">
|
|
||||||
{features.featureTabs.compactMode.title}
|
|
||||||
</Description>
|
|
||||||
<Description>
|
|
||||||
{features.featureTabs.compactMode.description}
|
|
||||||
</Description>
|
|
||||||
</motion.div>
|
|
||||||
<motion.div client:load {...getTitleAnimation(1.2)} className="feature">
|
|
||||||
<Description class="text-2xl font-bold">
|
|
||||||
{features.featureTabs.glance.title}
|
|
||||||
</Description>
|
|
||||||
<Description>
|
|
||||||
{features.featureTabs.glance.description}
|
|
||||||
</Description>
|
|
||||||
</motion.div>
|
|
||||||
<motion.div client:load {...getTitleAnimation(1.4)} className="feature">
|
|
||||||
<Description class="text-2xl font-bold">
|
|
||||||
{features.featureTabs.splitView.title}
|
|
||||||
</Description>
|
|
||||||
<Description>
|
|
||||||
{features.featureTabs.splitView.description}
|
|
||||||
</Description>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Mobile description -->
|
|
||||||
<div
|
|
||||||
class="feature-description mt-4 lg:hidden"
|
|
||||||
data-descriptions={descriptions}
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sticky top-6 w-full lg:w-3/5 h-fit">
|
|
||||||
<div class="relative w-full">
|
|
||||||
<div class="video-stack relative h-full w-full">
|
|
||||||
<Video
|
<Video
|
||||||
|
src={feature.video}
|
||||||
autoplay
|
autoplay
|
||||||
loop
|
loop
|
||||||
muted
|
muted
|
||||||
playsinline
|
playsinline
|
||||||
preload="none"
|
preload="none"
|
||||||
class="feature-video"
|
class="w-full rounded-3xl max-w-3xl shadow-md dark:opacity-80"
|
||||||
src={WorkspacesVideo}
|
|
||||||
data-active="true"
|
|
||||||
/>
|
/>
|
||||||
<Video
|
</motion.div>
|
||||||
autoplay
|
))}
|
||||||
loop
|
|
||||||
muted
|
|
||||||
playsinline
|
|
||||||
preload="none"
|
|
||||||
class="feature-video"
|
|
||||||
src={CompactModeVideo}
|
|
||||||
/>
|
|
||||||
<Video
|
|
||||||
autoplay
|
|
||||||
loop
|
|
||||||
muted
|
|
||||||
playsinline
|
|
||||||
preload="none"
|
|
||||||
class="feature-video"
|
|
||||||
src={GlanceVideo}
|
|
||||||
/>
|
|
||||||
<Video
|
|
||||||
autoplay
|
|
||||||
loop
|
|
||||||
muted
|
|
||||||
playsinline
|
|
||||||
preload="none"
|
|
||||||
class="feature-video"
|
|
||||||
src={SplitViewsVideo}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<script>
|
|
||||||
const features = document.querySelectorAll(
|
|
||||||
'.feature, .feature-tab',
|
|
||||||
) as NodeListOf<HTMLElement>
|
|
||||||
|
|
||||||
// Set initial description
|
|
||||||
const descriptionEl = document.querySelector(
|
|
||||||
'.feature-description',
|
|
||||||
) as HTMLDivElement
|
|
||||||
const descriptions = descriptionEl?.dataset.descriptions?.split(',')
|
|
||||||
if (descriptionEl && descriptions) {
|
|
||||||
descriptionEl.textContent = descriptions[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeToFeature({
|
|
||||||
target,
|
|
||||||
}: {
|
|
||||||
target: HTMLElement | undefined | null
|
|
||||||
}) {
|
|
||||||
target = target?.closest('.feature, .feature-tab')
|
|
||||||
if (!target) return
|
|
||||||
|
|
||||||
const index = Array.from(features).indexOf(target) % 4
|
|
||||||
if (index === -1) return
|
|
||||||
|
|
||||||
// Update both mobile and desktop elements
|
|
||||||
features.forEach((f, i) => {
|
|
||||||
if (i % 4 === index) {
|
|
||||||
f.setAttribute('data-active', 'true')
|
|
||||||
} else {
|
|
||||||
f.removeAttribute('data-active')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update mobile description
|
|
||||||
const descriptionEl = document.querySelector('.feature-description')
|
|
||||||
if (descriptionEl && descriptions) {
|
|
||||||
descriptionEl.textContent = descriptions[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
const videos = document.querySelectorAll(
|
|
||||||
'.feature-video',
|
|
||||||
) as NodeListOf<HTMLVideoElement>
|
|
||||||
videos.forEach((vid, i) => {
|
|
||||||
const yOffset = (i - index) * 20
|
|
||||||
const zOffset = i === index ? 0 : -100 - Math.abs(i - index) * 50
|
|
||||||
const scale = i === index ? 1 : 0.95
|
|
||||||
const rotation = (i - index) * 3
|
|
||||||
|
|
||||||
if (i === index) {
|
|
||||||
vid.setAttribute('data-active', 'true')
|
|
||||||
vid.style.opacity = '1'
|
|
||||||
vid.style.transform = `translate3d(-50%, 0, 0) scale(${scale})`
|
|
||||||
vid.style.zIndex = '10'
|
|
||||||
vid.currentTime = 0
|
|
||||||
vid.play()
|
|
||||||
} else {
|
|
||||||
vid.removeAttribute('data-active')
|
|
||||||
vid.style.transform = `translate3d(-50%, ${yOffset}px, ${zOffset}px)
|
|
||||||
rotate3d(1, 0, 0, ${rotation}deg)
|
|
||||||
scale(${scale})`
|
|
||||||
vid.style.zIndex = String(1 - Math.abs(i - index))
|
|
||||||
vid.pause()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const feature of features) {
|
|
||||||
feature.addEventListener('click', changeToFeature as any)
|
|
||||||
}
|
|
||||||
|
|
||||||
changeToFeature({ target: features[0] as any })
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.feature {
|
|
||||||
@apply w-full cursor-pointer select-none rounded-lg p-4 opacity-0 hover:bg-subtle;
|
|
||||||
transition: background 0.2s ease-in-out;
|
|
||||||
|
|
||||||
&[data-active='true'] {
|
|
||||||
@apply bg-subtle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-tab {
|
|
||||||
@apply rounded-lg px-4 py-2 text-lg font-medium opacity-0 hover:bg-subtle;
|
|
||||||
transition: background 0.2s ease-in-out;
|
|
||||||
|
|
||||||
&[data-active='true'] {
|
|
||||||
@apply bg-subtle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-description {
|
|
||||||
@apply px-4 text-sm text-gray-600 dark:text-gray-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-stack {
|
|
||||||
@apply aspect-video;
|
|
||||||
perspective: 2000px;
|
|
||||||
transform-style: preserve-3d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-video {
|
|
||||||
@apply left-1/2 hidden transform rounded-3xl shadow-md lg:absolute lg:mx-auto lg:block lg:w-full dark:opacity-80;
|
|
||||||
max-width: 800px;
|
|
||||||
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
transform-origin: top center;
|
|
||||||
backface-visibility: hidden;
|
|
||||||
will-change: transform, opacity;
|
|
||||||
transform: translate3d(-50%, 0, -100px) scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dont animate translation on small screens */
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.feature-video {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
transform: none !important;
|
|
||||||
display: none;
|
|
||||||
object-fit: cover;
|
|
||||||
|
|
||||||
&[data-active='true'] {
|
|
||||||
display: block;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-stack {
|
|
||||||
@apply overflow-hidden rounded-xl;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 16/9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue