1+ import anime from 'animejs' ;
2+
13// ===== i18n Translations =====
24const translations = {
35 zh : {
@@ -98,10 +100,11 @@ function setLanguage(lang) {
98100 document . documentElement . lang = safeLang === 'zh' ? 'zh-Hant' : 'en' ;
99101
100102 const langStrings = safeLang === 'zh' ? translations . zh : translations . en ;
103+ const langMap = new Map ( Object . entries ( langStrings ) ) ;
101104 document . querySelectorAll ( '[data-i18n]' ) . forEach ( ( el ) => {
102105 const key = el . getAttribute ( 'data-i18n' ) ;
103- if ( Object . prototype . hasOwnProperty . call ( langStrings , key ) ) {
104- el . textContent = langStrings [ key ] ;
106+ if ( langMap . has ( key ) ) {
107+ el . textContent = langMap . get ( key ) ;
105108 }
106109 } ) ;
107110
@@ -111,21 +114,114 @@ function setLanguage(lang) {
111114 }
112115}
113116
114- // ===== Intersection Observer (Fade-in) =====
117+ // ===== Anime.js: Hero Icon 3D =====
118+ function initHeroIcon ( ) {
119+ const icon = document . querySelector ( '.hero-icon-3d' ) ;
120+ if ( ! icon ) return ;
121+
122+ // Continuous breathing / floating (No sudden jumps, no mouse parallax)
123+ anime ( {
124+ targets : icon ,
125+ translateY : [ - 15 , 15 ] ,
126+ duration : 3500 ,
127+ direction : 'alternate' ,
128+ loop : true ,
129+ easing : 'easeInOutSine'
130+ } ) ;
131+ }
132+
133+ // ===== Anime.js: Hero Content =====
134+ function initHeroContent ( ) {
135+ const heroContent = document . querySelector ( '.hero-content' ) ;
136+ if ( heroContent ) {
137+ heroContent . style . opacity = 1 ;
138+ anime ( {
139+ targets : '.hero-content > *' ,
140+ opacity : [ 0 , 1 ] ,
141+ translateY : [ 30 , 0 ] ,
142+ duration : 1000 ,
143+ delay : anime . stagger ( 200 , { start : 200 } ) ,
144+ easing : 'easeOutCubic'
145+ } ) ;
146+ }
147+ }
148+
149+ // ===== Intersection Observer & Anime.js (Scroll Animations) =====
115150function initScrollAnimations ( ) {
116151 const observer = new IntersectionObserver (
117152 ( entries ) => {
118153 entries . forEach ( ( entry ) => {
119154 if ( entry . isIntersecting ) {
120- entry . target . classList . add ( 'visible' ) ;
121- observer . unobserve ( entry . target ) ;
155+ const row = entry . target ;
156+ const isReversed = row . classList . contains ( 'feature-row--reversed' ) ;
157+
158+ const texts = row . querySelectorAll ( '.feature-tag, .feature-title, .feature-desc' ) ;
159+ const phone = row . querySelector ( '.phone-mockup' ) ;
160+
161+ // Animate texts (Gliding Elegance - Bottom up)
162+ if ( texts . length > 0 ) {
163+ anime ( {
164+ targets : texts ,
165+ opacity : [ 0 , 1 ] ,
166+ translateY : [ 60 , 0 ] ,
167+ duration : 1600 ,
168+ delay : anime . stagger ( 150 ) ,
169+ easing : 'easeOutExpo'
170+ } ) ;
171+ }
172+
173+ // Animate phone (Gliding Elegance - Bottom up)
174+ if ( phone ) {
175+ anime ( {
176+ targets : phone ,
177+ opacity : [ 0 , 1 ] ,
178+ translateY : [ 100 , 0 ] ,
179+ duration : 1800 ,
180+ delay : 300 ,
181+ easing : 'easeOutExpo'
182+ } ) ;
183+ }
184+
185+ observer . unobserve ( row ) ;
122186 }
123187 } ) ;
124188 } ,
125- { threshold : 0.15 , rootMargin : '0px 0px -40px 0px' }
189+ { threshold : 0.2 }
126190 ) ;
127191
128- document . querySelectorAll ( '.fade-in, .reveal-left, .reveal-right' ) . forEach ( ( el ) => observer . observe ( el ) ) ;
192+ document . querySelectorAll ( '.feature-row' ) . forEach ( ( row ) => {
193+ // Hide initially to prevent flash before animation
194+ const texts = row . querySelectorAll ( '.feature-tag, .feature-title, .feature-desc' ) ;
195+ const phone = row . querySelector ( '.phone-mockup' ) ;
196+ texts . forEach ( t => t . style . opacity = 0 ) ;
197+ if ( phone ) phone . style . opacity = 0 ;
198+
199+ observer . observe ( row ) ;
200+ } ) ;
201+
202+ // Handle other simple fade-in elements
203+ const simpleObserver = new IntersectionObserver (
204+ ( entries ) => {
205+ entries . forEach ( ( entry ) => {
206+ if ( entry . isIntersecting ) {
207+ anime ( {
208+ targets : entry . target ,
209+ opacity : [ 0 , 1 ] ,
210+ translateY : [ 20 , 0 ] ,
211+ duration : 1000 ,
212+ easing : 'easeInOutSine'
213+ } ) ;
214+ simpleObserver . unobserve ( entry . target ) ;
215+ }
216+ } ) ;
217+ } ,
218+ { threshold : 0.15 }
219+ ) ;
220+
221+ document . querySelectorAll ( '.fade-in:not(.hero-content)' ) . forEach ( ( el ) => {
222+ el . style . opacity = 0 ;
223+ simpleObserver . observe ( el ) ;
224+ } ) ;
129225}
130226
131227// ===== Nav Scroll Effect =====
@@ -159,6 +255,8 @@ function initNavScroll() {
159255// ===== Init =====
160256document . addEventListener ( 'DOMContentLoaded' , ( ) => {
161257 setLanguage ( currentLang ) ;
258+ initHeroIcon ( ) ;
259+ initHeroContent ( ) ;
162260 initScrollAnimations ( ) ;
163261 initNavScroll ( ) ;
164262
0 commit comments