Skip to content

Commit 05bfd97

Browse files
feat(web): update Web Demo with Anime.js liquid morphing and gliding animations
1 parent c0e484a commit 05bfd97

5 files changed

Lines changed: 202 additions & 117 deletions

File tree

demo/index.html

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,8 @@ <h1 class="hero-title" data-i18n="hero.title">壢中成績 app</h1>
5252
<p class="cta-sub" data-i18n="hero.cta.sub">適用於 Android 10+</p>
5353
</div>
5454
</div>
55-
<div class="hero-visual fade-in">
56-
<div class="phone-mockup phone-hero">
57-
<div class="phone-screen">
58-
<img src="./assets/icon.webp" alt="壢中成績 App 總覽頁面" loading="eager" width="512" height="512">
59-
</div>
60-
</div>
55+
<div class="hero-visual">
56+
<img src="./assets/icon.webp" alt="壢中成績 App Icon" class="hero-icon-3d" loading="eager" width="512" height="512">
6157
</div>
6258
</div>
6359
</section>

demo/main.js

Lines changed: 105 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import anime from 'animejs';
2+
13
// ===== i18n Translations =====
24
const 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) =====
115150
function 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 =====
160256
document.addEventListener('DOMContentLoaded', () => {
161257
setLanguage(currentLang);
258+
initHeroIcon();
259+
initHeroContent();
162260
initScrollAnimations();
163261
initNavScroll();
164262

demo/package-lock.json

Lines changed: 71 additions & 47 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

demo/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
"preview": "vite preview"
1010
},
1111
"devDependencies": {
12-
"vite": "^6.0.0"
12+
"vite": "6.4.2"
13+
},
14+
"dependencies": {
15+
"animejs": "3.2.2"
1316
}
1417
}

0 commit comments

Comments
 (0)