PageSourceSearch

https://beidou.tw/js/pro-animations.js

js beidou.tw collected 2026-10-08 17:26:19 UTC 13,616 bytes, 349 lines download raw bytes

1/**
2 * 北斗數位科技 - 專業動畫效果
3 * Professional Animation Effects
4 * Version: 2.0
5 * 
6 * 功能:
7 * 1. 滾動觸發動畫
8 * 2. 計數器動畫
9 * 3. 打字機效果
10 * 4. 導航滾動效果
11 * 5. 平滑滾動
12 * 6. 視差效果
13 */
14
15(function() {
16    'use strict';
17    
18    // ============================================
19    // 初始化
20    // ============================================
21    document.addEventListener('DOMContentLoaded', function() {
22        initScrollAnimations();
23        initCounters();
24        initTypewriter();
25        initNavbarScroll();
26        initSmoothScroll();
27        initParallax();
28        initTiltEffect();
29        initRippleEffect();
30        initScrollProgress();
31    });
32    
33    // ============================================
34    // 滾動觸發動畫
35    // ============================================
36    function initScrollAnimations() {
37        var animatedElements = document.querySelectorAll('.animate-on-scroll');
38        var staggerContainers = document.querySelectorAll('.stagger-animation');
39        
40        if (!animatedElements.length && !staggerContainers.length) return;
41        
42        // 使用 Intersection Observer API
43        if ('IntersectionObserver' in window) {
44            var observerOptions = {
45                root: null,
46                rootMargin: '0px 0px -50px 0px',
47                threshold: 0.1
48            };
49            
50            var observer = new IntersectionObserver(function(entries) {
51                entries.forEach(function(entry) {
52                    if (entry.isIntersecting) {
53                        entry.target.classList.add('animated');
54                        // 可選:只觸發一次
55                        // observer.unobserve(entry.target);
56                    }
57                });
58            }, observerOptions);
59            
60            animatedElements.forEach(function(el) {
61                observer.observe(el);
62            });
63            
64            staggerContainers.forEach(function(el) {
65                observer.observe(el);
66            });
67        } else {
68            // 降級方案:直接顯示
69            animatedElements.forEach(function(el) {
70                el.classList.add('animated');
71            });
72            staggerContainers.forEach(function(el) {
73                el.classList.add('animated');
74            });
75        }
76    }
77    
78    // ============================================
79    // 計數器動畫
80    // ============================================
81    function initCounters() {
82        var counters = document.querySelectorAll('[data-counter]');
83        if (!counters.length) return;
84        
85        var observerOptions = {
86            root: null,
87            rootMargin: '0px',
88            threshold: 0.5
89        };
90        
91        var counterObserver = new IntersectionObserver(function(entries) {
92            entries.forEach(function(entry) {
93                if (entry.isIntersecting && !entry.target.classList.contains('counted')) {
94                    animateCounter(entry.target);
95                    entry.target.classList.add('counted');
96                }
97            });
98        }, observerOptions);
99        
100        counters.forEach(function(counter) {
101            counterObserver.observe(counter);
102        });
103    }
104    
105    function animateCounter(element) {
106        // 支援 data-counter 和 data-target 兩種屬性
107        var target = parseFloat(element.getAttribute('data-counter')) || parseFloat(element.getAttribute('data-target')) || 0;
108        var suffix = element.getAttribute('data-suffix') || '';
109        var prefix = element.getAttribute('data-prefix') || '';
110        var decimals = parseInt(element.getAttribute('data-decimals')) || 0;
111        var duration = parseInt(element.getAttribute('data-duration')) || 2000;
112        
113        var startTime = null;
114        var startValue = 0;
115        
116        function easeOutExpo(t) {
117            return t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
118        }
119        
120        function animate(currentTime) {
121            if (!startTime) startTime = currentTime;
122            var progress = Math.min((currentTime - startTime) / duration, 1);
123            var easedProgress = easeOutExpo(progress);
124            var currentValue = startValue + (target - startValue) * easedProgress;
125            
126            element.textContent = prefix + currentValue.toFixed(decimals) + suffix;
127            
128            if (progress < 1) {
129                requestAnimationFrame(animate);
130            }
131        }
132        
133        requestAnimationFrame(animate);
134    }
135    
136    // ============================================
137    // 打字機效果 - 由 luxury-animations.js 處理
138    // ============================================
139    function initTypewriter() {
140        // 打字機效果已移至 luxury-animations.js 統一處理
141        // 避免多個腳本同時處理同一元素造成衝突
142        return;
143    }
144    
145    // ============================================
146    // 導航滾動效果
147    // ============================================
148    function initNavbarScroll() {
149        var navbar = document.querySelector('.navbar');
150        if (!navbar) return;
151        
152        var scrollThreshold = 50;
153        var lastScroll = 0;
154        
155        function handleScroll() {
156            var currentScroll = window.pageYOffset || document.documentElement.scrollTop;
157            
158            // 添加/移除滾動類
159            if (currentScroll > scrollThreshold) {
160                navbar.classList.add('scrolled');
161            } else {
162                navbar.classList.remove('scrolled');
163            }
164            
165            // 可選:滾動方向檢測(隱藏/顯示導航)
166            // if (currentScroll > lastScroll && currentScroll > 300) {
167            //     navbar.style.transform = 'translateY(-100%)';
168            // } else {
169            //     navbar.style.transform = 'translateY(0)';
170            // }
171            
172            lastScroll = currentScroll;
173        }
174        
175        // 使用 passive 監聽器提升性能
176        window.addEventListener('scroll', handleScroll, { passive: true });
177        
178        // 初始檢查
179        handleScroll();
180    }
181    
182    // ============================================
183    // 平滑滾動
184    // ============================================
185    function initSmoothScroll() {
186        document.querySelectorAll('a[href^="#"]').forEach(function(anchor) {
187            anchor.addEventListener('click', function(e) {
188                var targetId = this.getAttribute('href');
189                if (targetId === '#') return;
190                
191                // 跳過 Bootstrap dropdown 和 collapse 觸發器
192                if (this.hasAttribute('data-bs-toggle')) return;
193                if (this.classList.contains('dropdown-toggle')) return;
194                if (this.classList.contains('nav-link') && this.closest('.dropdown')) return;
195                
196                var target = document.querySelector(targetId);
197                if (target) {
198                    e.preventDefault();
199                    
200                    var navbarHeight = document.querySelector('.navbar') ? 
201                        document.querySelector('.navbar').offsetHeight : 0;
202                    
203                    var targetPosition = target.getBoundingClientRect().top + window.pageYOffset - navbarHeight;
204                    
205                    window.scrollTo({
206                        top: targetPosition,
207                        behavior: 'smooth'
208                    });
209                }
210            });
211        });
212    }
213    
214    // ============================================
215    // 視差效果
216    // ============================================
217    function initParallax() {
218        var parallaxElements = document.querySelectorAll('[data-parallax]');
219        if (!parallaxElements.length) return;
220        
221        function handleParallax() {
222            var scrollY = window.pageYOffset;
223            
224            parallaxElements.forEach(function(element) {
225                var speed = parseFloat(element.getAttribute('data-parallax')) || 0.5;
226                var offset = scrollY * speed;
227                element.style.transform = 'translateY(' + offset + 'px)';
228            });
229        }
230        
231        window.addEventListener('scroll', handleParallax, { passive: true });
232    }
233    
234    // ============================================
235    // 3D 傾斜效果
236    // ============================================
237    function initTiltEffect() {
238        var tiltElements = document.querySelectorAll('.tilt-card');
239        
240        tiltElements.forEach(function(element) {
241            element.addEventListener('mousemove', function(e) {
242                var rect = element.getBoundingClientRect();
243                var x = e.clientX - rect.left;
244                var y = e.clientY - rect.top;
245                
246                var centerX = rect.width / 2;
247                var centerY = rect.height / 2;
248                
249                var rotateX = (y - centerY) / 20;
250                var rotateY = (centerX - x) / 20;
251                
252                element.style.transform = 'perspective(1000px) rotateX(' + rotateX + 'deg) rotateY(' + rotateY + 'deg) translateZ(10px)';
253            });
254            
255            element.addEventListener('mouseleave', function() {
256                element.style.transform = 'perspective(1000px) rotateX(0) rotateY(0) translateZ(0)';
257            });
258        });
259    }
260    
261    // ============================================
262    // 漣漪效果
263    // ============================================
264    function initRippleEffect() {
265        document.querySelectorAll('.ripple').forEach(function(button) {
266            button.addEventListener('click', function(e) {
267                var rect = button.getBoundingClientRect();
268                var size = Math.max(rect.width, rect.height);
269                var x = e.clientX - rect.left - size / 2;
270                var y = e.clientY - rect.top - size / 2;
271                
272                var ripple = document.createElement('span');
273                ripple.style.cssText = 
274                    'position: absolute;' +
275                    'width: ' + size + 'px;' +
276                    'height: ' + size + 'px;' +
277                    'left: ' + x + 'px;' +
278                    'top: ' + y + 'px;' +
279                    'background: radial-gradient(circle, rgba(255,255,255,0.4) 0%, transparent 70%);' +
280                    'border-radius: 50%;' +
281                    'transform: scale(0);' +
282                    'animation: rippleEffect 0.6s ease-out;' +
283                    'pointer-events: none;';
284                
285                button.style.position = 'relative';
286                button.style.overflow = 'hidden';
287                button.appendChild(ripple);
288                
289                setTimeout(function() {
290                    ripple.remove();
291                }, 600);
292            });
293        });
294        
295        // 添加 CSS 動畫
296        if (!document.getElementById('ripple-style')) {
297            var style = document.createElement('style');
298            style.id = 'ripple-style';
299            style.textContent = '@keyframes rippleEffect { to { transform: scale(2); opacity: 0; } }';
300            document.head.appendChild(style);
301        }
302    }
303    
304    // ============================================
305    // 滾動進度條
306    // ============================================
307    function initScrollProgress() {
308        // 創建進度條元素
309        var progressBar = document.createElement('div');
310        progressBar.className = 'scroll-progress';
311        progressBar.style.cssText = 
312            'position: fixed;' +
313            'top: 0;' +
314            'left: 0;' +
315            'height: 3px;' +
316            'background: linear-gradient(90deg, #ffc107, #ff9800);' +
317            'z-index: 9999;' +
318            'transition: width 0.1s linear;' +
319            'width: 0%;';
320        document.body.appendChild(progressBar);
321        
322        function updateProgress() {
323            var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
324            var scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
325            var progress = (scrollTop / scrollHeight) * 100;
326            progressBar.style.width = progress + '%';
327        }
328        
329        window.addEventListener('scroll', updateProgress, { passive: true });
330    }
331    
332    // ============================================
333    // 浮動徽章動畫
334    // ============================================
335    var floatingBadges = document.querySelectorAll('.floating-badge');
336    floatingBadges.forEach(function(badge, index) {
337        badge.style.animation = 'floatBadge 3s ease-in-out infinite';
338        badge.style.animationDelay = (index * 0.5) + 's';
339    });
340    
341    // 添加浮動動畫 CSS
342    if (!document.getElementById('float-badge-style')) {
343        var floatStyle = document.createElement('style');
344        floatStyle.id = 'float-badge-style';
345        floatStyle.textContent = '@keyframes floatBadge { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }';
346        document.head.appendChild(floatStyle);
347    }
348    
349})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.