Files
blinds_express/public/js/main.js
2026-03-22 15:34:09 -05:00

202 lines
6.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================
BlindMaster Landing Page — main.js
- Time-based accent color (mirrors Flutter app theming)
- Nav scroll behavior
- Scroll reveal animations
- Hero blind animation
- Interactive demo slider
- Early access form
============================================================ */
(function () {
'use strict';
// ----------------------------------------------------------
// 1. Time-based theme (matches Flutter app color system)
// Orange 510am | Blue 10am6pm | Purple 6pm+
// ----------------------------------------------------------
function getThemeForHour(hour) {
if (hour >= 5 && hour < 10) {
return {
name: 'morning',
accent: '#F97316',
accentRgb: '249, 115, 22',
accentDark: '#C2410C',
accentGlow: 'rgba(249, 115, 22, 0.25)',
};
} else if (hour >= 10 && hour < 18) {
return {
name: 'day',
accent: '#3B82F6',
accentRgb: '59, 130, 246',
accentDark: '#1D4ED8',
accentGlow: 'rgba(59, 130, 246, 0.25)',
};
} else {
return {
name: 'evening',
accent: '#7C3AED',
accentRgb: '124, 58, 237',
accentDark: '#5B21B6',
accentGlow: 'rgba(124, 58, 237, 0.25)',
};
}
}
function applyTheme(theme) {
const root = document.documentElement;
root.style.setProperty('--accent', theme.accent);
root.style.setProperty('--accent-rgb', theme.accentRgb);
root.style.setProperty('--accent-dark', theme.accentDark);
root.style.setProperty('--accent-glow', theme.accentGlow);
}
const hour = new Date().getHours();
applyTheme(getThemeForHour(hour));
// ----------------------------------------------------------
// 2. Nav — scrolled state
// ----------------------------------------------------------
const nav = document.getElementById('nav');
function updateNav() {
if (window.scrollY > 40) {
nav.classList.add('scrolled');
} else {
nav.classList.remove('scrolled');
}
}
window.addEventListener('scroll', updateNav, { passive: true });
updateNav();
// ----------------------------------------------------------
// 3. Mobile nav toggle
// ----------------------------------------------------------
const hamburger = document.getElementById('hamburger');
const mobileNav = document.getElementById('mobileNav');
let mobileOpen = false;
hamburger.addEventListener('click', () => {
mobileOpen = !mobileOpen;
mobileNav.classList.toggle('open', mobileOpen);
});
document.querySelectorAll('.mobile-nav-link').forEach(link => {
link.addEventListener('click', () => {
mobileOpen = false;
mobileNav.classList.remove('open');
});
});
// ----------------------------------------------------------
// 4. Scroll reveal
// ----------------------------------------------------------
const revealEls = document.querySelectorAll('.reveal');
const revealObserver = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
revealObserver.unobserve(entry.target);
}
});
},
{ threshold: 0.12, rootMargin: '0px 0px -40px 0px' }
);
revealEls.forEach(el => revealObserver.observe(el));
// ----------------------------------------------------------
// 5. Hero blind animation
// Cycle through positions to demo the 010 position model
// ----------------------------------------------------------
const slats = document.querySelectorAll('.blind-slat');
const sliderThumb = document.getElementById('sliderThumb');
// Position 010: map to slat rotation angle
// 0 = fully closed (slats flat, blocking light)
// 5 = fully open (slats perpendicular, maximum light)
// 10 = closed from opposite direction
function positionToAngle(pos) {
if (pos <= 5) {
return (pos / 5) * 75; // 0° → 75°
} else {
return 75 - ((pos - 5) / 5) * 75; // 75° → 0°
}
}
function positionToSliderPercent(pos) {
return (pos / 10) * 100;
}
function setBlindPosition(pos) {
const angle = positionToAngle(pos);
slats.forEach(slat => {
slat.style.transform = `rotateX(${angle}deg)`;
});
sliderThumb.style.left = `${positionToSliderPercent(pos)}%`;
}
// Animate through positions: 0 → 5 → 10 → 5 → 0, looping
const demoPositions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1];
let demoIndex = 0;
setBlindPosition(demoPositions[0]);
setInterval(() => {
demoIndex = (demoIndex + 1) % demoPositions.length;
setBlindPosition(demoPositions[demoIndex]);
}, 700);
// ----------------------------------------------------------
// 6. Interactive slider on demo window (drag/click)
// ----------------------------------------------------------
const sliderTrack = document.querySelector('.app-slider-track');
function handleSliderInteraction(clientX) {
const rect = sliderTrack.getBoundingClientRect();
const pct = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
const pos = Math.round(pct * 10);
setBlindPosition(pos);
}
sliderTrack.addEventListener('mousedown', (e) => {
handleSliderInteraction(e.clientX);
const onMove = (e) => handleSliderInteraction(e.clientX);
const onUp = () => {
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
};
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
});
sliderTrack.addEventListener('touchstart', (e) => {
handleSliderInteraction(e.touches[0].clientX);
const onMove = (e) => handleSliderInteraction(e.touches[0].clientX);
const onEnd = () => {
window.removeEventListener('touchmove', onMove);
window.removeEventListener('touchend', onEnd);
};
window.addEventListener('touchmove', onMove);
window.addEventListener('touchend', onEnd);
}, { passive: true });
// ----------------------------------------------------------
// 7. Early access form
// ----------------------------------------------------------
const form = document.getElementById('earlyAccessForm');
const emailInput = document.getElementById('emailInput');
form.addEventListener('submit', (e) => {
e.preventDefault();
const email = emailInput.value.trim();
if (!email) return;
// Replace form with success message
const successMsg = document.createElement('p');
successMsg.className = 'cs-success';
successMsg.textContent = `You're on the list! We'll notify ${email} at launch.`;
form.replaceWith(successMsg);
});
})();