11---
22import ' ./Hero.css' ;
3- import { Image } from ' astro:assets' ;
43import { Code } from ' astro-expressive-code/components' ;
54import { Icon } from ' astro-icon/components' ;
65import { H1 , Button , Container , Flex , BodyMd , Grid , Col } from ' @/components/primitives' ;
76import { getEntry } from ' astro:content' ;
87import { getLangFromUrl , useTranslations } from ' @/i18n/utils' ;
8+ import { Image } from ' astro:assets' ;
99
1010const lang = getLangFromUrl (Astro .url );
1111const t = useTranslations (lang );
@@ -24,73 +24,12 @@ app.get('/', (req, res) => {
2424app.listen(port, () => {
2525 console.log(\` Example app listening on port \$ {port}\` )
2626}) ` ;
27-
28- interface Props {
29- videoSrc: string ;
30- videoSrcLight: string ;
31- posterSrc: string ;
32- posterSrcLight: string ;
33- }
34-
35- const { videoSrc, videoSrcLight, posterSrc, posterSrcLight } = Astro .props ;
3627---
3728
3829<section class =" hero" >
39- <div class =" hero__video-container" >
40- <Image
41- class =" hero__video-poster hero__video-poster--dark"
42- src ={ posterSrc }
43- width ={ 1920 }
44- height ={ 1080 }
45- loading =" eager"
46- priority ={ true }
47- alt =" "
48- aria-hidden =" true"
49- />
50-
51- <Image
52- class =" hero__video-poster hero__video-poster--light"
53- src ={ posterSrcLight }
54- width ={ 1920 }
55- height ={ 1080 }
56- loading =" eager"
57- priority ={ true }
58- alt =" "
59- aria-hidden =" true"
60- />
61-
62- <video
63- class =" hero__video hero__video--dark"
64- autoplay
65- muted
66- loop
67- playsinline
68- preload =" metadata"
69- data-src ={ videoSrc } ></video >
70-
71- <video
72- class =" hero__video hero__video--light"
73- autoplay
74- muted
75- loop
76- playsinline
77- preload =" metadata"
78- data-src ={ videoSrcLight } ></video >
79-
30+ <div class =" hero__bg-container" >
31+ <canvas class =" hero__canvas" aria-hidden =" true" ></canvas >
8032 <div class =" hero__video-overlay" ></div >
81-
82- <button
83- class =" hero__video-control"
84- type =" button"
85- title ={ t (' hero.videoPause' )}
86- aria-label ={ t (' hero.videoPause' )}
87- data-label-pause ={ t (' hero.videoPause' )}
88- data-label-play ={ t (' hero.videoPlay' )}
89- data-state =" paused"
90- >
91- <Icon name =" fluent:pause-16-filled" class =" hero__video-control-pause" />
92- <Icon name =" fluent:play-16-filled" class =" hero__video-control-play" />
93- </button >
9433 </div >
9534 <Container >
9635 <div class =" hero__content" >
@@ -102,6 +41,7 @@ const { videoSrc, videoSrcLight, posterSrc, posterSrcLight } = Astro.props;
10241 class =" hero__logo-kawaii"
10342 width ={ 400 }
10443 height ={ 225 }
44+ loading =" lazy"
10545 />
10646 <Icon name =" logo-express-black" class =" hero__logo-icon--dark hero__logo-default" />
10747 <Icon name =" logo-express-white" class =" hero__logo-icon--light hero__logo-default" />
@@ -133,138 +73,62 @@ const { videoSrc, videoSrcLight, posterSrc, posterSrcLight } = Astro.props;
13373</section >
13474
13575<script >
136- function initHeroVideo() {
137- const container = document.querySelector<HTMLDivElement>('.hero__video-container');
138- if (!container) return;
139-
140- const controlButton = container.querySelector<HTMLButtonElement>('.hero__video-control');
141- if (!controlButton) return;
142-
143- const darkVideo = container.querySelector<HTMLVideoElement>('.hero__video--dark');
144- const lightVideo = container.querySelector<HTMLVideoElement>('.hero__video--light');
145- const darkPoster = container.querySelector<HTMLImageElement>('.hero__video-poster--dark');
146- const lightPoster = container.querySelector<HTMLImageElement>('.hero__video-poster--light');
147-
148- let currentVideo: HTMLVideoElement | null = null;
149- let currentPoster: HTMLImageElement | null = null;
150-
151- const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
152-
153- function getIsDark() {
154- const theme = document.documentElement.getAttribute('data-theme');
155- return (
156- theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)
157- );
158- }
159-
160- function getAssets() {
161- const isDark = getIsDark();
162- return {
163- video: isDark ? darkVideo : lightVideo,
164- poster: isDark ? darkPoster : lightPoster,
165- };
166- }
167-
168- function loadVideo(video: HTMLVideoElement) {
169- const src = video.getAttribute('data-src');
170- if (!src) return;
171-
172- // do not load video if src is loaded (reduce network call)
173- if (!video.src) {
174- video.src = src;
175- video.load();
176- }
177- }
178-
179- function activateVideo(video: HTMLVideoElement | null, poster: HTMLImageElement | null) {
180- if (!video || !poster) return;
181-
182- if (currentPoster) currentPoster.classList.remove('hide_poster');
183-
184- currentPoster = poster;
185-
186- if (currentVideo) {
187- currentVideo.pause();
188- currentVideo.classList.remove('hero__video--active');
189- }
190-
191- currentVideo = video;
192-
193- if (prefersReducedMotion) {
194- currentVideo.pause();
195- currentVideo.removeAttribute('autoplay');
196- currentVideo.classList.remove('hero__video--active');
197- if (controlButton) controlButton.style.display = 'none';
76+ // Import immediately — module is small (~6KB) and shader compilation is async.
77+ // initHeroWebGL renders the first frame during init (in resize()),
78+ // so the arc appears as soon as shaders compile — no image fallback needed.
79+ import('./hero-background').then(({ initHeroWebGL }) => {
80+ const canvas = document.querySelector<HTMLCanvasElement>('.hero__canvas');
81+ if (!canvas) return;
82+
83+ const isMobile = window.matchMedia('(max-width: 768px)');
84+ const staticOnly =
85+ isMobile.matches || window.matchMedia('(prefers-reduced-motion: reduce)').matches;
86+
87+ initHeroWebGL(canvas, staticOnly).then((bg) => {
88+ if (!bg) return;
89+
90+ // On mobile / reduced-motion: render one frame, done
91+ if (staticOnly) {
92+ bg.play();
19893 return;
19994 }
20095
201- loadVideo(video);
202-
203- // check if video is ready to play then just update background animation
204- // returning early to stop repeated listener on theme change
205- // ref: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState#htmlmediaelement.have_future_data
206- if (video.readyState >= video.HAVE_FUTURE_DATA) {
207- updateStatus(video, poster);
208- return;
96+ // Desktop: defer animation loop to after page load + idle
97+ function startAnimation() {
98+ const idle = window.requestIdleCallback || ((cb: IdleRequestCallback) => setTimeout(cb, 1));
99+ idle(() => {
100+ bg.play();
101+
102+ const observer = new IntersectionObserver(
103+ (entries) => {
104+ entries.forEach((entry) => {
105+ if (entry.isIntersecting) {
106+ bg.play();
107+ } else {
108+ bg.pause();
109+ }
110+ });
111+ },
112+ { rootMargin: '80px' }
113+ );
114+
115+ observer.observe(canvas);
116+
117+ document.addEventListener('visibilitychange', () => {
118+ if (document.hidden) {
119+ bg.pause();
120+ } else {
121+ bg.play();
122+ }
123+ });
124+ });
209125 }
210126
211- video.addEventListener('canplay', () => updateStatus(video, poster), { once: true });
212- }
213-
214- function updateVideo() {
215- const { video: nextVideo, poster: hidePoster } = getAssets();
216-
217- activateVideo(nextVideo, hidePoster);
218- }
219-
220- function updateStatus(video: HTMLVideoElement | null, poster: HTMLImageElement | null) {
221- if (!video || !poster) return;
222- // if video is playable then hide poster
223- poster.classList.add('hide_poster');
224- // activate correct video animations
225- video.classList.add('hero__video--active');
226- video.play().catch(() => {});
227- // update controll btn on video load
228- controlButton?.setAttribute('data-state', 'playing');
229- }
230-
231- // Control button
232- controlButton.addEventListener('click', () => {
233- if (!currentVideo) return;
234-
235- if (currentVideo.paused) {
236- currentVideo.play().catch(() => {});
237- controlButton.setAttribute('data-state', 'playing');
127+ if (document.readyState === 'complete') {
128+ startAnimation();
238129 } else {
239- currentVideo.pause();
240- controlButton.setAttribute('data-state', 'paused');
130+ window.addEventListener('load', startAnimation, { once: true });
241131 }
242132 });
243-
244- // stop video on hidden tabs
245- document.addEventListener('visibilitychange', () => {
246- if (!currentVideo) return;
247-
248- if (document.hidden) {
249- currentVideo.pause();
250- } else if (!prefersReducedMotion) {
251- currentVideo.play().catch(() => {});
252- }
253- });
254-
255- // Initial run
256- updateVideo();
257-
258- // React to theme changes
259- const observer = new MutationObserver(() => {
260- updateVideo();
261- });
262-
263- observer.observe(document.documentElement, {
264- attributes: true,
265- attributeFilter: ['data-theme'],
266- });
267- }
268-
269- initHeroVideo();
133+ });
270134</script >
0 commit comments