// ==================== ENHANCED LOADER SYSTEM ====================
// Moved to loader.js
// API URL Configuration
// Automatically select between Localhost and Production
const API_BASE_URL = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
? 'http://localhost:7860'
: 'https://harshasnade-deepfake-detection-model.hf.space';
// ==================== SESSION MANAGEMENT ====================
function getSessionId() {
let sessionId = localStorage.getItem('deepguard_session_id');
if (!sessionId) {
// Generate a new session ID if it doesn't exist
sessionId = typeof crypto.randomUUID === 'function'
? crypto.randomUUID()
: 'session-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
localStorage.setItem('deepguard_session_id', sessionId);
}
return sessionId;
}
// ==================== BACKEND COLD START HANDLER ====================
// ==================== BACKEND COLD START HANDLER ====================
async function checkBackendHealth(retries = 100) { // Increased retries for cold starts (approx 2 mins)
const healthUrl = `${API_BASE_URL}/api/health`;
try {
const response = await fetch(healthUrl, { method: 'GET' });
if (response.ok) {
console.log('✅ Backend is ready!');
// Signal loader to finish
window.isBackendReady = true;
// If loader is already done (rare, or if user navigated away and back), this does nothing harmful
// If loader is stuck at 99%, this will release it.
return true;
}
} catch (error) {
console.warn('Backend sleeping or unreachable...');
}
if (retries > 0) {
// If we are still loading, tell the loader
if (window.updateLoaderStatus) {
window.updateLoaderStatus("STARTING SERVER...");
} else {
// Fallback if loader JS hasn't initialized or strictly separate
console.log("Waiting for server...");
}
// Show toast only if it's taking a while (e.g., after 5s)
if (retries < 95) { // Assuming 30 retries originally, now 100.
// Avoid spamming toasts if the loader is visible covering everything
// showToast('⏳ Model is waking up from sleep. Please wait...', 'info');
}
// Retry every 2 seconds (faster polling)
await new Promise(resolve => setTimeout(resolve, 2000));
return checkBackendHealth(retries - 1);
}
// Failed after all retries
if (window.updateLoaderStatus) {
window.updateLoaderStatus("SERVER ERROR");
}
showToast('❌ Backend failed to start. Please refresh.', 'error');
return false;
}
// Check status immediately on load
document.addEventListener('DOMContentLoaded', () => {
// Start ensuring backend is ready.
// The loader is already running (initLoader called in loader.js).
// checking logic runs in parallel.
checkBackendHealth();
});
// ==================== TOAST NOTIFICATION SYSTEM ====================
function showToast(message, type = 'info') {
const container = document.getElementById('toastContainer');
if (!container) return;
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
// Accessibility: Set role based on importance
if (type === 'error' || type === 'warning') {
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
} else {
toast.setAttribute('role', 'status');
toast.setAttribute('aria-live', 'polite');
}
// Icons based on type with aria-hidden
let icon = 'ℹ️';
if (type === 'success') icon = '✅';
if (type === 'error') icon = '⛔';
if (type === 'warning') icon = '⚠️';
toast.innerHTML = `
`;
container.appendChild(toast);
// Play sound based on type
if (type === 'success') playSound('success');
if (type === 'error') playSound('alert');
// Auto remove
setTimeout(() => {
toast.classList.add('hiding');
toast.addEventListener('animationend', () => {
if (toast.parentElement) toast.remove();
});
}, 4000);
}
// ==================== FILE VALIDATION ====================
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
function validateFile(file) {
if (file.size > MAX_FILE_SIZE) {
showToast(`File "${file.name}" exceeds 100MB limit.`, 'error');
return false;
}
return true;
}
// ==================== AUDIO SYSTEM ====================
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const playSound = (type) => {
if (audioCtx.state === 'suspended') audioCtx.resume();
const osc = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
osc.connect(gainNode);
gainNode.connect(audioCtx.destination);
const now = audioCtx.currentTime;
if (type === 'scan') {
// High-tech scanning sound
osc.type = 'sine';
osc.frequency.setValueAtTime(800, now);
osc.frequency.exponentialRampToValueAtTime(1200, now + 0.1);
osc.frequency.exponentialRampToValueAtTime(800, now + 0.2);
gainNode.gain.setValueAtTime(0.1, now);
gainNode.gain.linearRampToValueAtTime(0, now + 0.2);
osc.start(now);
osc.stop(now + 0.2);
} else if (type === 'alert') {
// Warning sound for fake detection
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(200, now);
osc.frequency.linearRampToValueAtTime(100, now + 0.3);
gainNode.gain.setValueAtTime(0.2, now);
gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc.start(now);
osc.stop(now + 0.3);
} else if (type === 'success') {
// Safe/Authentic sound
osc.type = 'sine';
osc.frequency.setValueAtTime(440, now);
osc.frequency.exponentialRampToValueAtTime(880, now + 0.3);
gainNode.gain.setValueAtTime(0.1, now);
gainNode.gain.linearRampToValueAtTime(0, now + 0.3);
osc.start(now);
osc.stop(now + 0.3);
}
};
// ==================== PARTICLE BACKGROUND ====================
// ==================== INITIALIZATION ====================
document.addEventListener('DOMContentLoaded', () => {
// ==================== THEME MANAGEMENT ====================
const initTheme = () => {
// Create Toggle Button
const themeToggleBtn = document.createElement('button');
themeToggleBtn.className = 'theme-toggle-btn';
themeToggleBtn.title = "Toggle Theme";
// Find navbar content
const navContent = document.querySelector('.nav-content');
if (navContent) {
// We want to group the last element (usually the CTA button) with our new toggle
// to keep them both on the right side if justify-content: space-between is used.
const lastItem = navContent.lastElementChild;
// Check if the last item is a button/link (and not the menu or logo if order differs)
// In standard index.html: Logo, Menu, Button. Button is last.
if (lastItem && !lastItem.classList.contains('nav-menu') && lastItem.tagName !== 'SCRIPT') {
const wrapper = document.createElement('div');
wrapper.style.display = 'flex';
wrapper.style.alignItems = 'center';
// Insert wrapper before the last item
navContent.insertBefore(wrapper, lastItem);
// Move last item into wrapper
wrapper.appendChild(lastItem);
// Append toggle to wrapper
wrapper.appendChild(themeToggleBtn);
} else {
// Fallback if structure is unexpected
navContent.appendChild(themeToggleBtn);
}
}
// Check Logic
const savedTheme = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-theme', savedTheme);
updateThemeIcon(themeToggleBtn, savedTheme);
themeToggleBtn.addEventListener('click', (e) => {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
// Fallback for browsers without View Transitions
if (!document.startViewTransition) {
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeIcon(themeToggleBtn, newTheme);
return;
}
// Get click coordinates
const x = e.clientX;
const y = e.clientY;
// Calculate distance to the furthest corner
const endRadius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y)
);
// Start the view transition
const transition = document.startViewTransition(() => {
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeIcon(themeToggleBtn, newTheme);
});
// Animate the circular clip path
transition.ready.then(() => {
const clipPath = [
`circle(0px at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`
];
document.documentElement.animate(
{
clipPath: clipPath,
},
{
duration: 800, // Slightly slower for dramatic effect
easing: 'ease-in-out',
pseudoElement: '::view-transition-new(root)',
}
);
});
});
};
const updateThemeIcon = (btn, theme) => {
if (theme === 'light') {
btn.innerHTML = '🌙'; // Moon
btn.style.borderColor = 'var(--text-primary)';
btn.style.color = 'var(--text-primary)';
} else {
btn.innerHTML = '☀'; // Sun
btn.style.borderColor = 'rgba(255, 255, 255, 0.2)';
btn.style.color = '#fff';
}
};
initTheme();
// Initialize AOS
if (typeof AOS !== 'undefined') {
AOS.init({
duration: 800,
easing: 'ease-out-cubic',
once: true,
mirror: false,
offset: 100
});
}
// Initialize Particles.js (if element exists)
if (document.getElementById('particles-js') && typeof particlesJS !== 'undefined') {
const theme = localStorage.getItem('theme') || 'dark';
const pColor = theme === 'light' ? '#0044cc' : '#E3F514';
particlesJS('particles-js', {
particles: {
number: { value: 60, density: { enable: true, value_area: 800 } },
color: { value: pColor },
shape: { type: 'circle' },
opacity: { value: 0.3, random: true },
size: { value: 3, random: true },
line_linked: {
enable: true,
distance: 150,
color: pColor,
opacity: 0.2,
width: 1
},
move: {
enable: true,
speed: 2,
direction: 'none',
random: false,
straight: false,
out_mode: 'out',
bounce: false,
}
},
interactivity: {
detect_on: 'canvas',
events: {
onhover: { enable: true, mode: 'repulse' },
onclick: { enable: true, mode: 'push' },
resize: true
},
modes: {
repulse: { distance: 100, duration: 0.4 },
push: { particles_nb: 4 }
}
},
retina_detect: true
});
}
// ==================== MICRO-INTERACTIONS ====================
// 1. Button Ripple Effect
const buttons = document.querySelectorAll('.btn-primary, .btn-hero-primary, .btn-hero-secondary');
buttons.forEach(btn => {
btn.addEventListener('click', function (e) {
let x = e.clientX - e.target.offsetLeft;
let y = e.clientY - e.target.offsetTop;
let ripples = document.createElement('span');
ripples.style.left = x + 'px';
ripples.style.top = y + 'px';
ripples.classList.add('ripple');
this.appendChild(ripples);
setTimeout(() => {
ripples.remove();
}, 600);
});
});
// 2. 3D Card Tilt Effect
const tiltCards = document.querySelectorAll('.feature-card, .tech-card, .showcase-item');
tiltCards.forEach(card => {
card.addEventListener('mousemove', (e) => {
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
// Calculate rotation (max 10 degrees)
const rotateX = ((y - centerY) / centerY) * -10;
const rotateY = ((x - centerX) / centerX) * 10;
card.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale(1.02)`;
});
card.addEventListener('mouseleave', () => {
// Reset position
card.style.transform = 'perspective(1000px) rotateX(0) rotateY(0) scale(1)';
});
});
// ==================== FLOATING 3D OBJECTS PARALLAX ====================
const floatingCube = document.getElementById('floatingCube');
const floatingPyramid = document.getElementById('floatingPyramid');
// Initialize Particles.js for Neural Network Effect
if (typeof particlesJS !== 'undefined') {
particlesJS('loader-particles', {
"particles": {
"number": { "value": 80, "density": { "enable": true, "value_area": 800 } },
"color": { "value": "#E3F514" },
"shape": { "type": "circle" },
"opacity": { "value": 0.5, "random": true },
"size": { "value": 3, "random": true },
"line_linked": {
"enable": true,
"distance": 150,
"color": "#E3F514",
"opacity": 0.4,
"width": 1
},
"move": {
"enable": true,
"speed": 2,
"direction": "none",
"random": false,
"straight": false,
"out_mode": "out",
"bounce": false,
}
},
"interactivity": {
"detect_on": "canvas",
"events": { "onhover": { "enable": true, "mode": "grab" }, "onclick": { "enable": true, "mode": "push" } },
"modes": { "grab": { "distance": 140, "line_linked": { "opacity": 1 } } }
},
"retina_detect": true
});
}
// Simulate loading progress
let progress = 0;
// The following variables are already declared within the next 'if' block.
// Redeclaring them here would cause a SyntaxError.
// let currentX = 0;
// let currentY = 0;
if (floatingCube || floatingPyramid) {
let mouseX = 0;
let mouseY = 0;
let currentX = 0;
let currentY = 0;
// Track mouse position
document.addEventListener('mousemove', (e) => {
mouseX = (e.clientX / window.innerWidth - 0.5) * 2;
mouseY = (e.clientY / window.innerHeight - 0.5) * 2;
});
// Smooth animation loop
function animate3DObjects() {
// Smooth interpolation
currentX += (mouseX - currentX) * 0.05;
currentY += (mouseY - currentY) * 0.05;
if (floatingCube) {
const rotateY = 20 + currentX * 15;
const rotateX = 15 - currentY * 15;
const translateX = currentX * 30;
const translateY = currentY * 30;
floatingCube.style.transform = `
translateY(-30px)
translateX(${translateX}px)
translateY(${translateY}px)
rotateX(${rotateX}deg)
rotateY(${rotateY}deg)
scale(1)
`;
}
if (floatingPyramid) {
const rotateY = -20 + currentX * -20;
const rotateX = 15 - currentY * -10;
const translateX = currentX * -40;
const translateY = currentY * -40;
floatingPyramid.style.transform = `
translateY(0px)
translateX(${translateX}px)
translateY(${translateY}px)
rotateX(${rotateX}deg)
rotateY(${rotateY}deg)
scale(1)
`;
}
requestAnimationFrame(animate3DObjects);
}
animate3DObjects();
}
// ==================== FLUID HOVER REVEAL EFFECT ====================
const revealContainer = document.getElementById('heroRevealContainer');
const revealCanvas = document.getElementById('revealCanvas');
const revealTopImage = document.getElementById('revealTopImage');
const revealBottomImage = document.querySelector('.reveal-bottom');
if (revealContainer && revealCanvas && revealTopImage && revealBottomImage) {
const ctx = revealCanvas.getContext('2d');
// Set canvas size
const updateCanvasSize = () => {
const rect = revealContainer.getBoundingClientRect();
revealCanvas.width = rect.width;
revealCanvas.height = rect.height;
};
updateCanvasSize();
window.addEventListener('resize', updateCanvasSize);
// Physics parameters - optimized to match reference video
const physics = {
mouseX: -1000, // Start off-screen
mouseY: -1000,
targetX: -1000,
targetY: -1000,
velocityX: 0,
velocityY: 0,
prevMouseX: -1000,
prevMouseY: -1000,
damping: 0.18, // Smoother, more buttery motion
stiffness: 0.08, // More responsive following
isHovering: false
};
// Control points for organic shape - 20 points
class ControlPoint {
constructor(angle, baseRadius) {
this.angle = angle;
this.baseRadius = baseRadius;
this.currentRadius = baseRadius;
this.targetRadius = baseRadius;
this.noiseOffset = Math.random() * 1000;
this.noiseSpeed = 0.001 + Math.random() * 0.001;
this.trailStrength = 0;
}
update(centerX, centerY, time, velocityX, velocityY) {
// Calculate velocity magnitude
const speed = Math.sqrt(velocityX * velocityX + velocityY * velocityY);
// Organic noise - constant variation
const noise = Math.sin(time * this.noiseSpeed + this.noiseOffset) * 30;
// Trailing effect - points drag behind based on angle to velocity
const velocityAngle = Math.atan2(velocityY, velocityX);
const angleDiff = this.angle - velocityAngle;
// Dramatic trailing - enhanced for reference video match
const trailingFactor = Math.cos(angleDiff);
const trailing = trailingFactor < 0 ? trailingFactor * speed * 60 : 0; // Increased for more visible trailing
// Perpendicular deformation - squash and stretch
const perpFactor = Math.sin(angleDiff);
const perpDeformation = perpFactor * speed * 15;
// Shape morphing based on velocity
const velocityMorph = speed * 3;
// Combine all effects
this.targetRadius = this.baseRadius + noise + trailing + perpDeformation + velocityMorph;
// Smooth interpolation
this.currentRadius += (this.targetRadius - this.currentRadius) * 0.15;
// Calculate final position
this.x = centerX + Math.cos(this.angle) * this.currentRadius;
this.y = centerY + Math.sin(this.angle) * this.currentRadius;
}
}
// Create 20 control points for smooth organic shape
const controlPoints = [];
const pointCount = 20;
const baseRadius = 350; // Large 350px base - matches reference video
for (let i = 0; i < pointCount; i++) {
const angle = (i / pointCount) * Math.PI * 2;
controlPoints.push(new ControlPoint(angle, baseRadius));
}
// Mouse tracking
revealContainer.addEventListener('mouseenter', () => {
physics.isHovering = true;
});
revealContainer.addEventListener('mouseleave', () => {
physics.isHovering = false;
// Move target off-screen when leaving
physics.targetX = -1000;
physics.targetY = -1000;
});
revealContainer.addEventListener('mousemove', (e) => {
const rect = revealContainer.getBoundingClientRect();
physics.targetX = e.clientX - rect.left;
physics.targetY = e.clientY - rect.top;
});
// Draw smooth organic shape using control points
function drawOrganicShape(centerX, centerY, time, velocityX, velocityY) {
// Update all control points
controlPoints.forEach(point => {
point.update(centerX, centerY, time, velocityX, velocityY);
});
// Create smooth curve through all points using quadratic curves
ctx.beginPath();
// Start at first point
ctx.moveTo(controlPoints[0].x, controlPoints[0].y);
// Draw smooth curve through all points
for (let i = 0; i < pointCount; i++) {
const current = controlPoints[i];
const next = controlPoints[(i + 1) % pointCount];
// Use quadratic curve for smoothness
const midX = (current.x + next.x) / 2;
const midY = (current.y + next.y) / 2;
ctx.quadraticCurveTo(current.x, current.y, midX, midY);
}
ctx.closePath();
ctx.fill();
}
// Animation loop
let animationTime = 0;
function animateReveal() {
animationTime++;
// Track previous position for velocity calculation
const prevX = physics.mouseX;
const prevY = physics.mouseY;
// Spring physics for smooth following
const dx = physics.targetX - physics.mouseX;
const dy = physics.targetY - physics.mouseY;
physics.velocityX += dx * physics.stiffness;
physics.velocityY += dy * physics.stiffness;
physics.velocityX *= (1 - physics.damping);
physics.velocityY *= (1 - physics.damping);
physics.mouseX += physics.velocityX;
physics.mouseY += physics.velocityY;
// Calculate actual velocity for morphing
const actualVelocityX = physics.mouseX - prevX;
const actualVelocityY = physics.mouseY - prevY;
// Clear canvas
ctx.clearRect(0, 0, revealCanvas.width, revealCanvas.height);
// CURSOR WINDOW EFFECT: Reveal bottom image only where cursor is
if (physics.isHovering || physics.mouseX > -500) { // Keep animating for a bit after leaving
// Draw organic shape with all enhancements
ctx.fillStyle = 'white'; // This will be used as alpha mask
drawOrganicShape(physics.mouseX, physics.mouseY, animationTime, actualVelocityX, actualVelocityY);
// Apply mask to BOTTOM image (reveal it only where cursor is)
revealBottomImage.style.maskImage = `url(${revealCanvas.toDataURL()})`;
revealBottomImage.style.webkitMaskImage = `url(${revealCanvas.toDataURL()})`;
revealBottomImage.style.maskSize = 'cover';
revealBottomImage.style.webkitMaskSize = 'cover';
} else {
// No mask when not hovering - bottom image hidden
revealBottomImage.style.maskImage = 'none';
revealBottomImage.style.webkitMaskImage = 'none';
}
requestAnimationFrame(animateReveal);
}
animateReveal();
}
});
// ==================== SCROLL ANIMATIONS ====================
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0) rotateX(0)';
observer.unobserve(entry.target);
}
});
}, observerOptions);
document.querySelectorAll('.feature-card, .tech-card, .showcase-item, .pipeline-step, .model-card').forEach(el => {
el.style.opacity = '0';
el.style.transform = 'translateY(30px)';
el.style.transition = 'all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1)';
observer.observe(el);
});
// ==================== 3D CARD TILT EFFECT ====================
const init3DTilt = () => {
const cards = document.querySelectorAll('.feature-card, .tech-card, .showcase-item');
cards.forEach(card => {
card.addEventListener('mousemove', (e) => {
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const rotateX = (y - centerY) / 10;
const rotateY = (centerX - x) / 10;
card.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale3d(1.05, 1.05, 1.05)`;
});
card.addEventListener('mouseleave', () => {
card.style.transform = 'perspective(1000px) rotateX(0) rotateY(0) scale3d(1, 1, 1)';
});
});
};
// Initialize 3D tilt
init3DTilt();
// Smooth scroll for navigation links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Navbar scroll effect
let lastScroll = 0;
const navbar = document.querySelector('.navbar');
// ==================== COMPARISON SLIDER LOGIC ====================
function initComparisons() {
const overlays = document.getElementsByClassName("img-comp-overlay");
const container = document.querySelector('.img-comp-container');
const handle = document.querySelector('.slider-handle');
if (!container) return;
// Center handle initially
const w = container.offsetWidth;
container.querySelector('.img-comp-overlay').style.width = (w / 2) + "px";
handle.style.left = (w / 2) + "px";
let clicked = 0;
container.addEventListener('mousedown', slideReady);
window.addEventListener('mouseup', slideFinish);
container.addEventListener('touchstart', slideReady);
window.addEventListener('touchend', slideFinish);
function slideReady(e) {
e.preventDefault();
clicked = 1;
window.addEventListener('mousemove', slideMove);
window.addEventListener('touchmove', slideMove);
}
function slideFinish() {
clicked = 0;
}
function slideMove(e) {
if (clicked == 0) return false;
let pos = getCursorPos(e);
if (pos < 0) pos = 0;
if (pos > w) pos = w;
slide(pos);
}
function getCursorPos(e) {
let a, x = 0;
e = (e.changedTouches) ? e.changedTouches[0] : e;
const rect = container.getBoundingClientRect();
x = e.pageX - rect.left - window.scrollX;
return x;
}
function slide(x) {
container.querySelector('.img-comp-overlay').style.width = x + "px";
handle.style.left = (container.getBoundingClientRect().left + x) - container.getBoundingClientRect().left + "px"; // Relative to container
}
}
// Initialize slider on load
window.addEventListener('load', initComparisons);
// ==================== ADVANCED SCROLL STORYTELLING ====================
let scrollY = 0;
let lastScrollY = 0;
let ticking = false;
// Elements
const heroContent = document.querySelector('.hero-content');
const progressBar = document.getElementById('scrollProgress');
const parallaxItems = document.querySelectorAll('.feature-card, .tech-card, .showcase-item');
const textReveals = document.querySelectorAll('p, h2, h3');
// Add classes for text reveal
textReveals.forEach(el => el.classList.add('scroll-reveal'));
// Main Scroll Listener
window.addEventListener('scroll', () => {
scrollY = window.scrollY;
if (!ticking) {
window.requestAnimationFrame(updateScrollStory);
ticking = true;
}
});
function updateScrollStory() {
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
// 1. Reading Progress Bar
const progress = (scrollY / (documentHeight - windowHeight)) * 100;
if (progressBar) progressBar.style.width = `${progress}%`;
// 2. Navigation Bar Logic
if (scrollY > 100) {
navbar.style.background = 'rgba(10, 10, 15, 0.95)';
navbar.style.boxShadow = '0 4px 24px rgba(0, 0, 0, 0.3)';
navbar.style.padding = '15px 0';
} else {
navbar.style.background = 'transparent';
navbar.style.backdropFilter = 'none';
navbar.style.boxShadow = 'none';
navbar.style.padding = '20px 0';
}
// 3. Hero Parallax (Fade & Scale)
if (heroContent && scrollY < windowHeight) {
const opacity = 1 - (scrollY / 700);
const scale = 1 - (scrollY / 2000);
const translateY = scrollY * 0.5;
if (opacity >= 0) {
heroContent.style.opacity = opacity;
heroContent.style.transform = `translateY(${translateY}px) scale(${scale})`;
}
}
// 4. Continuous Parallax for Cards
parallaxItems.forEach((item, index) => {
const rect = item.getBoundingClientRect();
// Check if in view
if (rect.top < windowHeight + 100 && rect.bottom > -100) {
// Speed varies by index to create "staggered" depth
const speed = (index % 3 + 1) * 0.05;
const offset = (scrollY * speed) * 0.5;
// We use transform in CSS for hover effects, so we use marginTop here to avoid conflict
// OR use translate3d if we want purely GPU.
// Better: Applying a subtle Y shift.
// CAUTION: This might conflict with hover transform.
// Let's use a custom property instead if possible, or just skip if hovering.
// item.style.transform = `translateY(${offset}px)`; // Conflict risk
}
});
// 5. Text Reveal & Active State
textReveals.forEach(el => {
const rect = el.getBoundingClientRect();
// Calculate center distance
const centerOffset = (windowHeight / 2) - (rect.top + rect.height / 2);
// Simple entry check
if (rect.top < windowHeight * 0.85) {
el.classList.add('active');
} else {
// Optional: Remove active class to re-trigger?
// el.classList.remove('active'); // Keep purely additive for now
}
});
// 6. Floating Objects Scroll Drift
const floaters = document.querySelectorAll('.floating-3d-object');
floaters.forEach((el, index) => {
const speed = (index + 1) * 0.2;
el.style.marginTop = `${scrollY * speed * 1.5}px`;
});
lastScrollY = scrollY;
ticking = false;
}
// Initial call
updateScrollStory();
// ==================== POLISH & ATMOSPHERE ====================
// 1. Page Transitions
document.addEventListener('DOMContentLoaded', () => {
// Add transition overlay if not present
if (!document.querySelector('.page-transition-overlay')) {
const overlay = document.createElement('div');
overlay.className = 'page-transition-overlay';
document.body.appendChild(overlay);
// Trigger fade in
setTimeout(() => {
overlay.classList.add('loaded');
}, 100);
}
});
// Link Interceptor
document.querySelectorAll('a').forEach(link => {
link.addEventListener('click', e => {
const href = link.getAttribute('href');
// Only intercept internal links
if (href && href.startsWith('#') || href.includes('javascript:') || !href) return;
e.preventDefault();
const overlay = document.querySelector('.page-transition-overlay');
overlay.classList.remove('loaded'); // Fade to black
setTimeout(() => {
window.location.href = href;
}, 600); // Match CSS duration
});
});
// 2. Magnetic Buttons
const magneticBtns = document.querySelectorAll('.btn-primary, .btn-hero-primary, .btn-hero-secondary, .nav-link, .logo');
magneticBtns.forEach(btn => {
btn.addEventListener('mousemove', e => {
const rect = btn.getBoundingClientRect();
const x = e.clientX - rect.left - rect.width / 2;
const y = e.clientY - rect.top - rect.height / 2;
// Magnetic pull strength
btn.style.transform = `translate(${x * 0.2}px, ${y * 0.2}px)`;
});
btn.addEventListener('mouseleave', () => {
btn.style.transform = 'translate(0, 0)';
});
});
// 3. Motion Branding (Logo Animation)
const logo = document.querySelector('.logo-text');
if (logo) {
logo.style.opacity = '0';
logo.style.transform = 'translateY(-20px)';
logo.style.transition = 'all 0.8s ease-out';
setTimeout(() => {
logo.style.opacity = '1';
logo.style.transform = 'translateY(0)';
}, 200);
// Scroll reaction for logo
window.addEventListener('scroll', () => {
if (window.scrollY > 50) {
logo.style.fontSize = '1.5rem'; // Shrink
} else {
logo.style.fontSize = '1.8rem'; // Reset
}
});
}
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const previewArea = document.getElementById('previewArea');
const previewImage = document.getElementById('previewImage');
const resultsSection = document.getElementById('resultsSection');
if (uploadArea) {
// SINGLE FILE LOGIC DISABLED IN FAVOR OF QUEUE SYSTEM
// Drag & Drop listeners removed to prevent conflicts
/*
uploadArea.addEventListener('dragover', (e) => { ... });
uploadArea.addEventListener('drop', (e) => { ... });
fileInput.addEventListener('change', (e) => { ... });
*/
}
async function handleAnalysisUpload(file) {
const isVideo = file.type.startsWith('video/');
const isImage = file.type.startsWith('image/');
if (!isImage && !isVideo) {
alert('Please upload an image or video file');
return;
}
playSound('scan'); // Trigger scan sound
// Show Preview
const reader = new FileReader();
reader.onload = (e) => {
if (isImage) {
previewImage.src = e.target.result;
previewImage.style.display = 'block';
// Disable video preview if any
} else {
// For video, we might show a thumbnail or generic icon
// or create a video element
previewImage.style.display = 'none';
}
uploadArea.style.display = 'none';
previewArea.style.display = 'block';
// Reset previous execution state
document.getElementById('heatmapToggle').style.display = 'none';
document.getElementById('heatmapOverlay').style.display = 'none';
document.getElementById('scanTimeDisplay').textContent = '--';
};
reader.readAsDataURL(file);
// Show Loading State in Results
const analysisResults = document.querySelector('.analysis-results');
const emptyState = document.querySelector('.empty-state');
emptyState.style.display = 'none';
analysisResults.style.display = 'none';
// Create temporary loading element
let loader = document.getElementById('analysisLoader');
if (!loader) {
loader = document.createElement('div');
loader.id = 'analysisLoader';
loader.className = 'empty-state';
loader.innerHTML = `
Running DeepGuard detection pipeline
`; resultsSection.appendChild(loader); } // Update loading text for video if (isVideo) { document.getElementById('loaderText').textContent = "Scanning Video Frames..."; document.getElementById('loaderSubText').textContent = "Processing frame-by-frame analysis"; } loader.style.display = 'block'; // Show enhanced processing overlay with health check showProcessingOverlay(isVideo); // Step 1: Check model health const healthStatus = await checkModelHealth(); const modelStatusText = document.getElementById('modelStatusText'); if (healthStatus.model_status === 'ready') { if (modelStatusText) modelStatusText.textContent = 'Online'; setProcessingStep('connect'); } else if (healthStatus.model_status === 'initializing') { if (modelStatusText) modelStatusText.textContent = 'Initializing'; setProcessingStep('warmup'); } else { if (modelStatusText) modelStatusText.textContent = 'Unavailable'; } try { // Step 2: Upload (already at this step) setProcessingStep('upload'); // Call Backend const formData = new FormData(); formData.append('file', file); const startTime = performance.now(); // Start timer const endpoint = isVideo ? '/api/predict_video' : '/api/predict'; // Step 3: Connecting to AI model setProcessingStep('connect'); const response = await fetch(`${API_BASE_URL}${endpoint}`, { method: 'POST', headers: { 'X-Session-ID': getSessionId() }, body: formData }); // Step 4: Analyzing setProcessingStep('analyze'); if (!response.ok) throw new Error('Analysis failed'); const result = await response.json(); const endTime = performance.now(); // End timer const duration = ((endTime - startTime) / 1000).toFixed(2); if (isVideo) { // Add extra info for the result page // We need the history path to play the video. // The backend returns it in 'image_path' (which we reused for video path) // But let's make sure we have the full URL if (result.avg_fake_prob !== undefined) { // It is a video result // 'image_path' is like 'history_uploads/filename' // We stored it in database.add_scan. // Wait, process_video output (result) doesn't contain 'image_path'. // 'app.py' needs to return it. // Since I cannot edit app.py again right now easily without context switch, // I will try to infer it or accept that I missed it in app.py. // WAIT! app.py returns 'jsonify(result)'. // And result comes from 'video_inference.py'. // 'video_inference.py' doesn't know about the file path in history. // CRITICAL FIX: The result object in localStorage MUST have the URL. // I can construct it from the filename if I knew it. // But I don't easily know the timestamped filename the server made. // Wait, I can't restart app.py edit. // Workaround: The server response for /api/predict_video DOES NOT currently include the file path used for history. // This means the frontend won't know where to load the video from. // I must update app.py to include 'video_path_relative' or similar in the response. // But first let's finish this script.js update, then I might have to do a quick patch on app.py. // Actually, I can do a separate tool call to patch app.py after this. } // Temporary: Save result and redirect localStorage.setItem('video_analysis_result', JSON.stringify(result)); window.location.href = 'video_result.html'; return; } const scanTimeDisplay = document.getElementById('scanTimeDisplay'); if (scanTimeDisplay) { scanTimeDisplay.textContent = `${duration}s`; } // Store heatmap data if (result.heatmap) { const heatmapOverlay = document.getElementById('heatmapOverlay'); const heatmapToggle = document.getElementById('heatmapToggle'); const heatmapSwitch = document.getElementById('heatmapSwitch'); heatmapOverlay.src = `data:image/jpeg;base64,${result.heatmap}`; heatmapOverlay.style.display = 'block'; // Make sure the image element is visible layout-wise heatmapToggle.style.display = 'flex'; heatmapSwitch.checked = false; heatmapOverlay.style.opacity = '0'; heatmapSwitch.onchange = (e) => { heatmapOverlay.style.opacity = e.target.checked ? '1' : '0'; }; } // Store scan_id for feedback if (result.scan_id) { currentScanId = result.scan_id; console.log('Scan ID stored:', currentScanId); } // Update UI with Results updateAnalysisUI(result); loader.style.display = 'none'; analysisResults.style.display = 'block'; // Hide processing overlay hideProcessingOverlay(); } catch (error) { console.error(error); hideProcessingOverlay(); loader.innerHTML = `The AI model is currently unavailable. Please retry in a few moments.
`; // Store file reference for retry window.lastUploadedFile = file; } } function updateAnalysisUI(result) { const isFake = result.prediction === 'FAKE'; const confidence = (result.confidence * 100).toFixed(1); const verdictTitle = document.getElementById('verdictTitle'); const confidenceBar = document.getElementById('confidenceBar'); const confidenceValue = document.getElementById('confidenceValue'); const fakeProb = document.getElementById('fakeProb'); const realProb = document.getElementById('realProb'); const analysisText = document.getElementById('analysisText'); // Update Verdict verdictTitle.textContent = isFake ? 'FAKE DETECTED' : 'REAL IMAGE'; verdictTitle.className = `verdict-title ${isFake ? 'verdict-fake' : 'verdict-real'}`; // Update Badges const badgeContainer = document.getElementById('detectionBadges'); if (badgeContainer) { badgeContainer.innerHTML = ''; // Metadata Check if (result.metadata_check && result.metadata_check.detected) { const badge = document.createElement('div'); badge.className = 'detection-badge badge-critical'; badge.innerHTML = ` Signature: ${result.metadata_check.source || 'Unknown AI'}`; badgeContainer.appendChild(badge); } // Watermark Check if (result.watermark_check && result.watermark_check.detected) { const badge = document.createElement('div'); badge.className = 'detection-badge badge-warning'; badge.innerHTML = ` Watermark: ${result.watermark_check.source || 'Detected'}`; badgeContainer.appendChild(badge); } } // Play Result Sound playSound(isFake ? 'alert' : 'success'); // Update Meter with dynamic colors setTimeout(() => { confidenceBar.style.width = `${confidence}%`; // Remove all confidence classes confidenceBar.classList.remove('confidence-low', 'confidence-medium', 'confidence-high', 'confidence-very-high'); // Add appropriate class based on confidence level if (confidence < 60) { confidenceBar.classList.add('confidence-low'); } else if (confidence < 75) { confidenceBar.classList.add('confidence-medium'); } else if (confidence < 90) { confidenceBar.classList.add('confidence-high'); } else { confidenceBar.classList.add('confidence-very-high'); } }, 100); confidenceValue.textContent = `${confidence}% Confidence`; // Update Metrics // fakeProb.textContent = `${(result.fake_probability * 100).toFixed(1)}%`; // realProb.textContent = `${(result.real_probability * 100).toFixed(1)}%`; // Update Chart const ctx = document.getElementById('probabilityChart').getContext('2d'); // Destroy previous chart if exists if (window.probChartInstance) { window.probChartInstance.destroy(); } const fakeP = result.fake_probability * 100; const realP = result.real_probability * 100; window.probChartInstance = new Chart(ctx, { type: 'doughnut', data: { labels: [`Fake ${fakeP.toFixed(1)}%`, `Real ${realP.toFixed(1)}%`], datasets: [{ data: [fakeP, realP], backgroundColor: [ 'rgba(227, 245, 20, 0.9)', // Fake (Electric Yellow) 'rgba(16, 185, 129, 0.9)' // Real (Green) ], borderColor: [ 'rgba(227, 245, 20, 1)', // Fake border 'rgba(16, 185, 129, 1)' // Real border ], borderWidth: 2, hoverOffset: 8 }] }, options: { responsive: true, maintainAspectRatio: false, cutout: '65%', // Doughnut hole size plugins: { legend: { position: 'right', labels: { color: '#fff', padding: 15, font: { size: 13, weight: '600', family: "'Inter', sans-serif" }, usePointStyle: true, pointStyle: 'circle' } }, tooltip: { backgroundColor: 'rgba(0, 0, 0, 0.8)', titleColor: '#fff', bodyColor: '#fff', borderColor: 'rgba(227, 245, 20, 0.5)', borderWidth: 1, padding: 12, displayColors: true, callbacks: { label: function (context) { const label = context.label || ''; return ' ' + label; } } } }, animation: { animateRotate: true, animateScale: true, duration: 1000, easing: 'easeOutQuart' } } }); // Update Text if (isFake) { analysisText.innerHTML = ` ⚠️ High Risk DetectedNo analyses yet. Upload images to get started!