feat: redesign spectra lab with editorial brand aesthetic
This commit is contained in:
175
app.js
175
app.js
@@ -1,168 +1,43 @@
|
||||
const pointerGlow = document.getElementById('pointerGlow');
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
const themeLabel = document.getElementById('themeLabel');
|
||||
const sceneCards = document.querySelectorAll('.scene-card');
|
||||
const counters = document.querySelectorAll('[data-counter]');
|
||||
const sceneDetailTitle = document.getElementById('sceneDetailTitle');
|
||||
const sceneDetailCopy = document.getElementById('sceneDetailCopy');
|
||||
const particleCanvas = document.getElementById('particleCanvas');
|
||||
const modeToggle = document.getElementById('modeToggle');
|
||||
const modeLabel = document.getElementById('modeLabel');
|
||||
const workItems = document.querySelectorAll('.work-item');
|
||||
const workIndex = document.getElementById('workIndex');
|
||||
const workTitle = document.getElementById('workTitle');
|
||||
const workCopy = document.getElementById('workCopy');
|
||||
const revealBlocks = document.querySelectorAll('.reveal');
|
||||
|
||||
const themes = [
|
||||
{ className: '', label: 'Aurora' },
|
||||
{ className: 'theme-ember', label: 'Ember' },
|
||||
{ className: 'theme-tide', label: 'Tide' },
|
||||
];
|
||||
|
||||
let themeIndex = 0;
|
||||
|
||||
function setTheme(index) {
|
||||
document.body.classList.remove('theme-ember', 'theme-tide');
|
||||
const theme = themes[index];
|
||||
if (theme.className) {
|
||||
document.body.classList.add(theme.className);
|
||||
}
|
||||
themeLabel.textContent = theme.label;
|
||||
function applyMode() {
|
||||
const isLight = document.body.classList.contains('light-mode');
|
||||
modeToggle.textContent = isLight ? 'Dark Mode' : 'Light Mode';
|
||||
modeLabel.textContent = isLight ? 'Editorial Light' : 'Editorial Dark';
|
||||
}
|
||||
|
||||
function animateCounter(element) {
|
||||
const target = Number(element.dataset.counter || 0);
|
||||
const duration = 1400;
|
||||
const start = performance.now();
|
||||
|
||||
function step(now) {
|
||||
const progress = Math.min((now - start) / duration, 1);
|
||||
const eased = 1 - Math.pow(1 - progress, 3);
|
||||
element.textContent = Math.round(target * eased);
|
||||
if (progress < 1) requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
function mountParticleField() {
|
||||
if (!particleCanvas) return;
|
||||
const ctx = particleCanvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const particles = [];
|
||||
const particleCount = 48;
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
|
||||
function resize() {
|
||||
width = window.innerWidth;
|
||||
height = window.innerHeight;
|
||||
particleCanvas.width = width * window.devicePixelRatio;
|
||||
particleCanvas.height = height * window.devicePixelRatio;
|
||||
particleCanvas.style.width = `${width}px`;
|
||||
particleCanvas.style.height = `${height}px`;
|
||||
ctx.setTransform(window.devicePixelRatio, 0, 0, window.devicePixelRatio, 0, 0);
|
||||
}
|
||||
|
||||
function seed() {
|
||||
particles.length = 0;
|
||||
for (let i = 0; i < particleCount; i += 1) {
|
||||
particles.push({
|
||||
x: Math.random() * width,
|
||||
y: Math.random() * height,
|
||||
r: Math.random() * 1.8 + 0.8,
|
||||
vx: (Math.random() - 0.5) * 0.28,
|
||||
vy: (Math.random() - 0.5) * 0.28,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
particles.forEach((particle, index) => {
|
||||
particle.x += particle.vx;
|
||||
particle.y += particle.vy;
|
||||
|
||||
if (particle.x < -20) particle.x = width + 20;
|
||||
if (particle.x > width + 20) particle.x = -20;
|
||||
if (particle.y < -20) particle.y = height + 20;
|
||||
if (particle.y > height + 20) particle.y = -20;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.38)';
|
||||
ctx.arc(particle.x, particle.y, particle.r, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
for (let j = index + 1; j < particles.length; j += 1) {
|
||||
const other = particles[j];
|
||||
const dx = particle.x - other.x;
|
||||
const dy = particle.y - other.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
if (distance < 130) {
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = `rgba(130, 180, 255, ${0.12 * (1 - distance / 130)})`;
|
||||
ctx.moveTo(particle.x, particle.y);
|
||||
ctx.lineTo(other.x, other.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
resize();
|
||||
seed();
|
||||
draw();
|
||||
window.addEventListener('resize', () => {
|
||||
resize();
|
||||
seed();
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('pointermove', (event) => {
|
||||
pointerGlow.style.transform = `translate(${event.clientX}px, ${event.clientY}px)`;
|
||||
modeToggle?.addEventListener('click', () => {
|
||||
document.body.classList.toggle('light-mode');
|
||||
applyMode();
|
||||
});
|
||||
|
||||
sceneCards.forEach((card) => {
|
||||
card.addEventListener('pointermove', (event) => {
|
||||
const rect = card.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
const rotateX = ((y / rect.height) - 0.5) * -8;
|
||||
const rotateY = ((x / rect.width) - 0.5) * 10;
|
||||
|
||||
card.style.setProperty('--mx', `${(x / rect.width) * 100}%`);
|
||||
card.style.setProperty('--my', `${(y / rect.height) * 100}%`);
|
||||
card.style.transform = `translateY(-6px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
|
||||
workItems.forEach((item) => {
|
||||
item.addEventListener('click', () => {
|
||||
workItems.forEach((button) => button.classList.remove('is-active'));
|
||||
item.classList.add('is-active');
|
||||
workIndex.textContent = item.dataset.index || '';
|
||||
workTitle.textContent = item.dataset.title || '';
|
||||
workCopy.textContent = item.dataset.copy || '';
|
||||
});
|
||||
|
||||
card.addEventListener('pointerleave', () => {
|
||||
card.style.transform = '';
|
||||
});
|
||||
|
||||
card.addEventListener('click', () => {
|
||||
sceneCards.forEach((item) => item.classList.remove('is-active'));
|
||||
card.classList.add('is-active');
|
||||
sceneDetailTitle.textContent = card.dataset.sceneTitle || '';
|
||||
sceneDetailCopy.textContent = card.dataset.sceneCopy || '';
|
||||
});
|
||||
});
|
||||
|
||||
themeToggle.addEventListener('click', () => {
|
||||
themeIndex = (themeIndex + 1) % themes.length;
|
||||
setTheme(themeIndex);
|
||||
});
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
animateCounter(entry.target);
|
||||
entry.target.classList.add('is-visible');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.4 },
|
||||
{ threshold: 0.18 },
|
||||
);
|
||||
|
||||
counters.forEach((counter) => observer.observe(counter));
|
||||
sceneCards[0]?.classList.add('is-active');
|
||||
setTheme(themeIndex);
|
||||
mountParticleField();
|
||||
revealBlocks.forEach((block) => observer.observe(block));
|
||||
applyMode();
|
||||
|
||||
Reference in New Issue
Block a user