Spaces:
Build error
Build error
| // Particle Animation Background | |
| class ParticleBackground { | |
| constructor(canvasId) { | |
| this.canvas = document.getElementById(canvasId); | |
| this.ctx = this.canvas.getContext('2d'); | |
| this.particles = []; | |
| this.particleCount = 50; | |
| this.init(); | |
| } | |
| init() { | |
| // Set canvas to full window size | |
| this.resizeCanvas(); | |
| window.addEventListener('resize', () => this.resizeCanvas()); | |
| // Create particles | |
| this.createParticles(); | |
| // Start animation loop | |
| this.animate(); | |
| } | |
| resizeCanvas() { | |
| this.canvas.width = window.innerWidth; | |
| this.canvas.height = window.innerHeight; | |
| } | |
| createParticles() { | |
| this.particles = []; | |
| for (let i = 0; i < this.particleCount; i++) { | |
| this.particles.push({ | |
| x: Math.random() * this.canvas.width, | |
| y: Math.random() * this.canvas.height, | |
| radius: Math.random() * 3 + 1, | |
| speed: Math.random() * 1 + 0.2, | |
| directionX: Math.random() * 2 - 1, | |
| directionY: Math.random() * 2 - 1, | |
| opacity: Math.random() * 0.5 + 0.1 | |
| }); | |
| } | |
| } | |
| drawParticles() { | |
| this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); | |
| for (let i = 0; i < this.particles.length; i++) { | |
| const p = this.particles[i]; | |
| // Draw particle | |
| this.ctx.beginPath(); | |
| this.ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); | |
| this.ctx.fillStyle = `rgba(255, 255, 255, ${p.opacity})`; | |
| this.ctx.fill(); | |
| // Update position | |
| p.x += p.directionX * p.speed; | |
| p.y += p.directionY * p.speed; | |
| // Bounce off edges | |
| if (p.x < 0 || p.x > this.canvas.width) p.directionX *= -1; | |
| if (p.y < 0 || p.y > this.canvas.height) p.directionY *= -1; | |
| // Draw connections | |
| for (let j = i + 1; j < this.particles.length; j++) { | |
| const p2 = this.particles[j]; | |
| const distance = Math.sqrt( | |
| Math.pow(p.x - p2.x, 2) + | |
| Math.pow(p.y - p2.y, 2) | |
| ); | |
| if (distance < 150) { | |
| this.ctx.beginPath(); | |
| this.ctx.strokeStyle = `rgba(255, 255, 255, ${0.2 * (1 - distance/150)})`; | |
| this.ctx.lineWidth = 0.5; | |
| this.ctx.moveTo(p.x, p.y); | |
| this.ctx.lineTo(p2.x, p2.y); | |
| this.ctx.stroke(); | |
| } | |
| } | |
| } | |
| } | |
| animate() { | |
| this.drawParticles(); | |
| requestAnimationFrame(() => this.animate()); | |
| } | |
| } | |
| /* ============================================== | |
| Object Detection Handler | |
| ================================================== */ | |
| class VisionAIDetector { | |
| constructor() { | |
| // DOM Elements | |
| this.modelSelect = document.getElementById('modelSelect'); | |
| this.thresholdRange = document.getElementById('thresholdRange'); | |
| this.thresholdValue = document.getElementById('thresholdValue'); | |
| this.uploadBtn = document.getElementById('uploadBtn'); | |
| this.imageUpload = document.getElementById('imageUpload'); | |
| this.liveCaptureBtn = document.getElementById('liveCaptureBtn'); | |
| this.screenshotBtn = document.getElementById('screenshotBtn'); | |
| this.liveVideo = document.getElementById('liveVideo'); | |
| this.detectedCanvas = document.getElementById('detectedCanvas'); | |
| this.loadingOverlay = document.getElementById('loadingOverlay'); | |
| this.modelLabel = document.getElementById('modelLabel'); | |
| this.objectList = document.getElementById('objectList'); | |
| this.objectCounter = document.querySelector('.object-counter'); | |
| this.totalObjects = document.getElementById('totalObjects'); | |
| this.totalCategories = document.getElementById('totalCategories'); | |
| this.avgConfidence = document.getElementById('avgConfidence'); | |
| this.objectTypeChart = document.getElementById('objectTypeChart'); | |
| this.generateAudioBtn = document.getElementById('generateAudioBtn'); | |
| this.voiceTypeSelect = document.getElementById('voiceTypeSelect'); | |
| this.speechRateSelect = document.getElementById('speechRateSelect'); | |
| // Tab panel elements | |
| this.objectsTab = document.querySelector('[data-tab="objects"]'); | |
| this.statsTab = document.querySelector('[data-tab="stats"]'); | |
| this.audioTab = document.querySelector('[data-tab="audio"]'); | |
| this.objectsTabPane = document.getElementById('objectsTab'); | |
| this.statsTabPane = document.getElementById('statsTab'); | |
| this.audioTabPane = document.getElementById('audioTab'); | |
| // Canvas context | |
| this.ctx = this.detectedCanvas.getContext('2d'); | |
| // State variables | |
| this.stream = null; | |
| this.chart = null; | |
| this.detectionResults = null; | |
| this.currentImageDataUrl = null; // Store the current image for reprocessing | |
| this.processingLock = false; // Lock to prevent multiple simultaneous processings | |
| // Backend URL - change this to match your production setup | |
| this.apiUrl = 'http://localhost:5000'; | |
| // Initialize | |
| this.init(); | |
| } | |
| init() { | |
| // Set initial values | |
| this.modelLabel.textContent = this.modelSelect.value.split('.')[0]; | |
| // Event listeners for image input | |
| this.uploadBtn.addEventListener('click', () => this.imageUpload.click()); | |
| this.imageUpload.addEventListener('change', (e) => this.handleImageUpload(e)); | |
| this.liveCaptureBtn.addEventListener('click', () => this.toggleLiveCapture()); | |
| this.screenshotBtn.addEventListener('click', () => this.captureScreenshot()); | |
| // Event listeners for settings changes with real-time processing | |
| this.modelSelect.addEventListener('change', () => { | |
| this.modelLabel.textContent = this.modelSelect.value.split('.')[0]; | |
| this.reprocessCurrentImage(); | |
| }); | |
| this.thresholdRange.addEventListener('input', () => { | |
| this.thresholdValue.textContent = `${this.thresholdRange.value}%`; | |
| // Debounce threshold changes to prevent too many API calls | |
| clearTimeout(this.thresholdTimeout); | |
| this.thresholdTimeout = setTimeout(() => { | |
| this.reprocessCurrentImage(); | |
| }, 300); | |
| }); | |
| // Tab panel handlers - Enhanced for direct tab navigation | |
| this.objectsTab.addEventListener('click', () => this.switchTab('objects')); | |
| this.statsTab.addEventListener('click', () => this.switchTab('stats')); | |
| this.audioTab.addEventListener('click', () => this.switchTab('audio')); | |
| // Initialize charts | |
| this.initChart(); | |
| } | |
| /* ============================================== | |
| Tab Switching Logic under Detection Section | |
| ================================================== */ | |
| switchTab(tabId) { | |
| // Remove active class from all tabs | |
| [this.objectsTab, this.statsTab, this.audioTab].forEach(tab => | |
| tab.classList.remove('active')); | |
| // Hide all panes first | |
| this.objectsTabPane.style.display = 'none'; | |
| this.statsTabPane.style.display = 'none'; | |
| this.audioTabPane.style.display = 'none'; | |
| // Add active class to selected tab and show only its pane | |
| if (tabId === 'objects') { | |
| this.objectsTab.classList.add('active'); | |
| this.objectsTabPane.style.display = 'block'; | |
| } else if (tabId === 'stats') { | |
| this.statsTab.classList.add('active'); | |
| this.statsTabPane.style.display = 'block'; | |
| // Refresh stats content if we have results | |
| if (this.detectionResults) { | |
| this.updateStats(this.detectionResults); | |
| } | |
| } else if (tabId === 'audio') { | |
| this.audioTab.classList.add('active'); | |
| this.objectsTabPane.style.display = 'block'; | |
| this.audioTabPane.style.display = 'block'; | |
| } | |
| } | |
| async handleImageUpload(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| try { | |
| // Show loading overlay | |
| this.loadingOverlay.style.display = 'flex'; | |
| // Read the image file | |
| const imageDataUrl = await this.readFileAsDataURL(file); | |
| this.currentImageDataUrl = imageDataUrl; // Store for later reprocessing | |
| // Load image to get dimensions | |
| const img = await this.loadImage(imageDataUrl); | |
| // Set canvas dimensions | |
| this.detectedCanvas.width = img.width; | |
| this.detectedCanvas.height = img.height; | |
| // Draw original image on canvas | |
| this.ctx.drawImage(img, 0, 0); | |
| // Get selected model and confidence threshold | |
| const model = this.modelSelect.value; | |
| const confidenceThreshold = parseInt(this.thresholdRange.value) / 100; | |
| // Process the image | |
| await this.processImage(imageDataUrl, model, confidenceThreshold); | |
| // Enable screenshot button | |
| this.screenshotBtn.disabled = false; | |
| // Hide loading overlay | |
| this.loadingOverlay.style.display = 'none'; | |
| } catch (error) { | |
| console.error('Error processing image:', error); | |
| this.showError('Failed to process image. Please try again.'); | |
| this.loadingOverlay.style.display = 'none'; | |
| } | |
| } | |
| async reprocessCurrentImage() { | |
| // If no image is loaded or processing is already happening, do nothing | |
| if (!this.currentImageDataUrl || this.processingLock) return; | |
| this.processingLock = true; | |
| try { | |
| // Show loading overlay | |
| this.loadingOverlay.style.display = 'flex'; | |
| // Get current settings | |
| const model = this.modelSelect.value; | |
| const confidenceThreshold = parseInt(this.thresholdRange.value) / 100; | |
| // Reprocess with new settings | |
| await this.processImage(this.currentImageDataUrl, model, confidenceThreshold); | |
| // Hide loading overlay | |
| this.loadingOverlay.style.display = 'none'; | |
| } catch (error) { | |
| console.error('Error reprocessing image:', error); | |
| this.showError('Failed to reprocess image. Please try again.'); | |
| this.loadingOverlay.style.display = 'none'; | |
| } finally { | |
| this.processingLock = false; | |
| } | |
| } | |
| async toggleLiveCapture() { | |
| if (!this.stream) { | |
| // Start camera | |
| try { | |
| this.stream = await navigator.mediaDevices.getUserMedia({ | |
| video: { | |
| facingMode: 'environment', | |
| width: { ideal: 1280 }, | |
| height: { ideal: 720 } | |
| } | |
| }); | |
| // Display video | |
| this.liveVideo.srcObject = this.stream; | |
| this.liveVideo.style.display = 'block'; | |
| this.detectedCanvas.style.display = 'none'; | |
| this.liveVideo.play(); | |
| // Change button text | |
| this.liveCaptureBtn.innerHTML = '<i class="bi bi-camera"></i><span>Capture</span>'; | |
| // Enable screenshot button | |
| this.screenshotBtn.disabled = false; | |
| } catch (error) { | |
| console.error('Error accessing camera:', error); | |
| this.showError('Could not access camera. Please check permissions.'); | |
| } | |
| } else { | |
| // Take a snapshot and process | |
| this.captureScreenshot(); | |
| } | |
| } | |
| captureScreenshot() { | |
| if (!this.stream && this.liveVideo.style.display !== 'block') return; | |
| try { | |
| // Show loading overlay | |
| this.loadingOverlay.style.display = 'flex'; | |
| // Create temporary canvas to capture frame | |
| const tempCanvas = document.createElement('canvas'); | |
| tempCanvas.width = this.liveVideo.videoWidth; | |
| tempCanvas.height = this.liveVideo.videoHeight; | |
| const tempCtx = tempCanvas.getContext('2d'); | |
| tempCtx.drawImage(this.liveVideo, 0, 0); | |
| // Convert to data URL | |
| const imageDataUrl = tempCanvas.toDataURL('image/jpeg'); | |
| this.currentImageDataUrl = imageDataUrl; // Store for later reprocessing | |
| // Set canvas dimensions | |
| this.detectedCanvas.width = tempCanvas.width; | |
| this.detectedCanvas.height = tempCanvas.height; | |
| // Draw captured frame on main canvas | |
| this.ctx.drawImage(tempCanvas, 0, 0); | |
| // Stop video stream | |
| this.stopVideoStream(); | |
| // Show canvas | |
| this.detectedCanvas.style.display = 'block'; | |
| // Get selected model and confidence threshold | |
| const model = this.modelSelect.value; | |
| const confidenceThreshold = parseInt(this.thresholdRange.value) / 100; | |
| // Process the image | |
| this.processImage(imageDataUrl, model, confidenceThreshold); | |
| } catch (error) { | |
| console.error('Error capturing screenshot:', error); | |
| this.showError('Failed to capture image. Please try again.'); | |
| this.loadingOverlay.style.display = 'none'; | |
| } | |
| } | |
| stopVideoStream() { | |
| if (this.stream) { | |
| this.stream.getTracks().forEach(track => track.stop()); | |
| this.stream = null; | |
| this.liveVideo.style.display = 'none'; | |
| this.liveCaptureBtn.innerHTML = '<i class="bi bi-camera-video"></i><span>Live Camera</span>'; | |
| } | |
| } | |
| async processImage(imageDataUrl, selectedModel, confidenceThreshold) { | |
| try { | |
| // Make API request to backend | |
| const response = await fetch(`${this.apiUrl}/detect`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| image: imageDataUrl, | |
| model: selectedModel, | |
| confidence: confidenceThreshold | |
| }) | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`Server returned ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| // Store results for use in other tabs | |
| this.detectionResults = data; | |
| // Get original image from canvas (important to preserve it when reprocessing) | |
| const originalImage = new Image(); | |
| originalImage.src = imageDataUrl; | |
| // Wait for image to load | |
| await new Promise(resolve => { | |
| originalImage.onload = resolve; | |
| }); | |
| // Clear canvas and redraw original image | |
| this.ctx.clearRect(0, 0, this.detectedCanvas.width, this.detectedCanvas.height); | |
| this.ctx.drawImage(originalImage, 0, 0, this.detectedCanvas.width, this.detectedCanvas.height); | |
| // Draw detection results | |
| this.drawDetections(data.detections); | |
| // Update object list | |
| this.updateObjectList(data.grouped_objects); | |
| // Update stats | |
| this.updateStats(data); | |
| // Enable audio generation | |
| this.generateAudioBtn.disabled = false; | |
| this.generateAudioBtn.onclick = () => this.generateAudioDescription(data.grouped_objects); | |
| // Hide loading overlay | |
| this.loadingOverlay.style.display = 'none'; | |
| } catch (error) { | |
| console.error('Detection Error:', error); | |
| this.showError('Detection failed. Please try again.'); | |
| this.loadingOverlay.style.display = 'none'; | |
| } | |
| } | |
| drawDetections(detections) { | |
| // Draw each detection | |
| detections.forEach(detection => { | |
| const [x, y, width, height] = detection.bbox; | |
| // Draw bounding box | |
| this.ctx.beginPath(); | |
| this.ctx.rect(x, y, width, height); | |
| this.ctx.lineWidth = 3; | |
| this.ctx.strokeStyle = 'rgba(255, 0, 0, 0.8)'; | |
| this.ctx.stroke(); | |
| // Create label background | |
| const label = `${detection.class} (${(detection.confidence * 100).toFixed(0)}%)`; | |
| this.ctx.font = '16px Arial'; | |
| const textWidth = this.ctx.measureText(label).width + 10; | |
| this.ctx.fillStyle = 'rgba(255, 0, 0, 0.7)'; | |
| this.ctx.fillRect( | |
| x, | |
| y > 25 ? y - 25 : y, | |
| textWidth, | |
| 25 | |
| ); | |
| // Draw label text | |
| this.ctx.fillStyle = 'white'; | |
| this.ctx.fillText( | |
| label, | |
| x + 5, | |
| y > 25 ? y - 7 : y + 18 | |
| ); | |
| }); | |
| } | |
| updateObjectList(groupedObjects) { | |
| // Clear previous list | |
| this.objectList.innerHTML = ''; | |
| if (groupedObjects.length === 0) { | |
| const li = document.createElement('li'); | |
| li.className = 'no-objects'; | |
| li.textContent = 'No objects detected'; | |
| this.objectList.appendChild(li); | |
| this.objectCounter.textContent = '0'; | |
| return; | |
| } | |
| // Update counter | |
| const totalCount = groupedObjects.reduce((sum, obj) => sum + obj.count, 0); | |
| this.objectCounter.textContent = totalCount; | |
| // Add each object group to the list | |
| groupedObjects.forEach(group => { | |
| const li = document.createElement('li'); | |
| const confidence = this.detectionResults.detections | |
| .filter(d => d.class === group.class) | |
| .reduce((sum, d) => sum + d.confidence, 0) / group.count; | |
| li.innerHTML = ` | |
| <div class="object-info"> | |
| <div class="object-name">${group.class}</div> | |
| <span class="object-confidence">${(confidence * 100).toFixed(0)}% confidence</span> | |
| </div> | |
| <div class="object-count"> | |
| <span>${group.count}</span> | |
| </div> | |
| `; | |
| this.objectList.appendChild(li); | |
| }); | |
| } | |
| initChart() { | |
| if (this.chart) { | |
| this.chart.destroy(); | |
| } | |
| const ctx = this.objectTypeChart.getContext('2d'); | |
| this.chart = new Chart(ctx, { | |
| type: 'doughnut', | |
| data: { | |
| labels: [], | |
| datasets: [{ | |
| data: [], | |
| backgroundColor: [ | |
| 'rgba(255, 99, 132, 0.8)', | |
| 'rgba(54, 162, 235, 0.8)', | |
| 'rgba(255, 206, 86, 0.8)', | |
| 'rgba(75, 192, 192, 0.8)', | |
| 'rgba(153, 102, 255, 0.8)', | |
| 'rgba(255, 159, 64, 0.8)', | |
| 'rgba(199, 199, 199, 0.8)', | |
| 'rgba(83, 102, 255, 0.8)', | |
| 'rgba(40, 159, 64, 0.8)', | |
| 'rgba(210, 199, 199, 0.8)' | |
| ], | |
| borderWidth: 1 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| position: 'right', | |
| labels: { | |
| color: '#fff', | |
| font: { | |
| size: 12 | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| updateStats(data) { | |
| if (!data || !data.grouped_objects) return; | |
| const { detections, grouped_objects } = data; | |
| // Basic stats | |
| const totalCount = grouped_objects.reduce((sum, obj) => sum + obj.count, 0); | |
| const categoryCount = grouped_objects.length; | |
| const avgConfidence = detections.length > 0 | |
| ? detections.reduce((sum, d) => sum + d.confidence, 0) / detections.length * 100 | |
| : 0; | |
| // Update DOM | |
| this.totalObjects.textContent = totalCount; | |
| this.totalCategories.textContent = categoryCount; | |
| this.avgConfidence.textContent = `${avgConfidence.toFixed(1)}%`; | |
| // Update chart | |
| this.updateChart(grouped_objects); | |
| } | |
| updateChart(groupedObjects) { | |
| // Only take top 5 categories if more than 5 | |
| let chartData = [...groupedObjects]; | |
| if (chartData.length > 5) { | |
| chartData.sort((a, b) => b.count - a.count); | |
| const others = chartData.slice(5).reduce( | |
| (sum, obj) => sum + obj.count, 0 | |
| ); | |
| chartData = chartData.slice(0, 5); | |
| if (others > 0) { | |
| chartData.push({ class: 'Others', count: others }); | |
| } | |
| } | |
| // Update chart data | |
| this.chart.data.labels = chartData.map(obj => obj.class); | |
| this.chart.data.datasets[0].data = chartData.map(obj => obj.count); | |
| this.chart.update(); | |
| } | |
| generateAudioDescription(groupedObjects) { | |
| // Cancel any ongoing speech | |
| window.speechSynthesis.cancel(); | |
| if (groupedObjects.length === 0) return; | |
| // Get settings | |
| const voiceType = this.voiceTypeSelect.value; | |
| const speechRate = parseFloat(this.speechRateSelect.value); | |
| // Build description | |
| let description; | |
| if (groupedObjects.length === 1) { | |
| const obj = groupedObjects[0]; | |
| description = `I detected ${obj.count} ${obj.class}${obj.count > 1 ? 's' : ''}.`; | |
| } else { | |
| const lastItem = groupedObjects[groupedObjects.length - 1]; | |
| const itemsExceptLast = groupedObjects.slice(0, -1).map( | |
| obj => `${obj.count} ${obj.class}${obj.count > 1 ? 's' : ''}` | |
| ).join(', '); | |
| description = `I detected ${itemsExceptLast} and ${lastItem.count} ${lastItem.class}${lastItem.count > 1 ? 's' : ''}.`; | |
| } | |
| // Create utterance | |
| const utterance = new SpeechSynthesisUtterance(description); | |
| // Get available voices | |
| const voices = window.speechSynthesis.getVoices(); | |
| if (voices.length === 0) { | |
| // If voices aren't loaded yet, wait and try again | |
| window.speechSynthesis.onvoiceschanged = () => { | |
| this.generateAudioDescription(groupedObjects); | |
| }; | |
| return; | |
| } | |
| // Select voice based on gender preference | |
| let selectedVoice; | |
| if (voiceType === 'male') { | |
| selectedVoice = voices.find(v => | |
| v.name.toLowerCase().includes('male') || | |
| (!v.name.toLowerCase().includes('female') && v.lang.startsWith('en')) | |
| ); | |
| } else { | |
| selectedVoice = voices.find(v => | |
| v.name.toLowerCase().includes('female') || | |
| v.lang.startsWith('en') | |
| ); | |
| } | |
| // Set voice and rate | |
| if (selectedVoice) utterance.voice = selectedVoice; | |
| utterance.rate = speechRate; | |
| // Speak | |
| window.speechSynthesis.speak(utterance); | |
| } | |
| showError(message) { | |
| this.objectList.innerHTML = ` | |
| <li class="no-objects error"> | |
| <i class="bi bi-exclamation-triangle"></i> | |
| ${message} | |
| </li> | |
| `; | |
| } | |
| // Utility methods | |
| readFileAsDataURL(file) { | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onload = e => resolve(e.target.result); | |
| reader.onerror = e => reject(e); | |
| reader.readAsDataURL(file); | |
| }); | |
| } | |
| loadImage(src) { | |
| return new Promise((resolve, reject) => { | |
| const img = new Image(); | |
| img.onload = () => resolve(img); | |
| img.onerror = reject; | |
| img.src = src; | |
| }); | |
| } | |
| } | |
| // Smooth Scrolling for Navigation | |
| function initSmoothScrolling() { | |
| document.querySelectorAll('a[href^="#"]').forEach(anchor => { | |
| anchor.addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| // Don't scroll for modal triggers | |
| if (this.getAttribute('data-bs-toggle') === 'modal') return; | |
| const targetId = this.getAttribute('href'); | |
| const targetElement = document.querySelector(targetId); | |
| if (targetElement) { | |
| const navbarHeight = document.querySelector('.navbar').offsetHeight; | |
| const targetPosition = targetElement.getBoundingClientRect().top + window.pageYOffset - navbarHeight; | |
| window.scrollTo({ | |
| top: targetPosition, | |
| behavior: 'smooth' | |
| }); | |
| } | |
| }); | |
| }); | |
| } | |
| // Animation on scroll using GSAP | |
| function initScrollAnimations() { | |
| // Register ScrollTrigger plugin | |
| gsap.registerPlugin(ScrollTrigger); | |
| // Animate feature cards | |
| const featureCards = document.querySelectorAll('.feature-card'); | |
| featureCards.forEach((card, index) => { | |
| gsap.fromTo( | |
| card, | |
| { y: 50, opacity: 0 }, | |
| { | |
| y: 0, | |
| opacity: 1, | |
| duration: 0.6, | |
| delay: index * 0.1, | |
| scrollTrigger: { | |
| trigger: card, | |
| start: "top 85%", | |
| toggleActions: "play none none none" | |
| } | |
| } | |
| ); | |
| }); | |
| // Animate team cards | |
| const teamCards = document.querySelectorAll('.team-card'); | |
| teamCards.forEach((card, index) => { | |
| gsap.fromTo( | |
| card, | |
| { y: 50, opacity: 0 }, | |
| { | |
| y: 0, | |
| opacity: 1, | |
| duration: 0.6, | |
| delay: index * 0.1, | |
| scrollTrigger: { | |
| trigger: card, | |
| start: "top 85%", | |
| toggleActions: "play none none none" | |
| } | |
| } | |
| ); | |
| }); | |
| // Animate section headers | |
| const sectionHeaders = document.querySelectorAll('.section-header'); | |
| sectionHeaders.forEach((header) => { | |
| gsap.fromTo( | |
| header, | |
| { y: 30, opacity: 0 }, | |
| { | |
| y: 0, | |
| opacity: 1, | |
| duration: 0.8, | |
| scrollTrigger: { | |
| trigger: header, | |
| start: "top 85%", | |
| toggleActions: "play none none none" | |
| } | |
| } | |
| ); | |
| }); | |
| } | |
| // Navbar background on scroll | |
| function initNavbarScroll() { | |
| const navbar = document.querySelector('.navbar'); | |
| window.addEventListener('scroll', () => { | |
| if (window.scrollY > 50) { | |
| navbar.classList.add('scrolled'); | |
| } else { | |
| navbar.classList.remove('scrolled'); | |
| } | |
| }); | |
| } | |
| // Document Ready | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Initialize particle background | |
| new ParticleBackground('particleCanvas'); | |
| // Initialize vision AI detector | |
| const detector = new VisionAIDetector(); | |
| // Initialize smooth scrolling | |
| initSmoothScrolling(); | |
| // Initialize animations | |
| initScrollAnimations(); | |
| // Initialize navbar scroll effect | |
| initNavbarScroll(); | |
| // Handle voice API loading | |
| window.speechSynthesis.onvoiceschanged = () => { | |
| window.speechSynthesis.getVoices(); | |
| }; | |
| }); |