Skip to content

Commit 8ba7810

Browse files
author
Thomas Gorisse
committed
feat: add interactive JS enhancements + comprehensive AI reference
- m3-interactions.js: scroll reveals, animated counters, card tilt, auto-cycling showcase, dark mode transitions, copy toast, badge nav - llms-full.txt: complete AI reference with all 22+ node types, AR features, setup guides, comparison tables, MCP integration
1 parent d1646cd commit 8ba7810

2 files changed

Lines changed: 323 additions & 421 deletions

File tree

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
/**
2+
* m3-interactions.js — Premium M3-style interactions for SceneView homepage
3+
* Vanilla JS, performance-first: IntersectionObserver, rAF, passive listeners.
4+
*/
5+
document.addEventListener('DOMContentLoaded', () => {
6+
'use strict';
7+
8+
// ---------------------------------------------------------------------------
9+
// 1. Scroll-triggered reveal animations
10+
// ---------------------------------------------------------------------------
11+
const REVEAL_SELECTORS = [
12+
'.hero-tagline', '.platform-badges', '.stat-row', '.device-section',
13+
'.showcase-gallery', '.industry-card', '.visual-card', '.bottom-cta',
14+
'.grid.cards > ul > li', '.demo-container'
15+
];
16+
17+
const revealObserver = new IntersectionObserver((entries) => {
18+
entries.forEach((entry) => {
19+
if (!entry.isIntersecting) return;
20+
const el = entry.target;
21+
el.style.opacity = '1';
22+
el.style.transform = 'translateY(0)';
23+
Array.from(el.children).forEach((child, i) => {
24+
child.style.transition = `opacity 0.5s ease ${i * 0.07}s, transform 0.5s ease ${i * 0.07}s`;
25+
child.style.opacity = '1';
26+
child.style.transform = 'translateY(0)';
27+
});
28+
revealObserver.unobserve(el);
29+
});
30+
}, { threshold: 0.15, rootMargin: '0px 0px -40px 0px' });
31+
32+
document.querySelectorAll(REVEAL_SELECTORS.join(',')).forEach((el) => {
33+
el.style.opacity = '0';
34+
el.style.transform = 'translateY(24px)';
35+
el.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
36+
Array.from(el.children).forEach((child) => {
37+
child.style.opacity = '0';
38+
child.style.transform = 'translateY(16px)';
39+
});
40+
revealObserver.observe(el);
41+
});
42+
43+
// ---------------------------------------------------------------------------
44+
// 2. Animated counter for stat-pills
45+
// ---------------------------------------------------------------------------
46+
function animateCounter(el) {
47+
const text = el.textContent.trim();
48+
const match = text.match(/^([\d,]+)(\+?)$/);
49+
if (!match) return;
50+
const target = parseInt(match[1].replace(/,/g, ''), 10);
51+
const suffix = match[2] || '';
52+
const duration = 1200;
53+
const start = performance.now();
54+
function tick(now) {
55+
const elapsed = now - start;
56+
const progress = Math.min(elapsed / duration, 1);
57+
const eased = 1 - Math.pow(1 - progress, 3);
58+
const current = Math.round(eased * target);
59+
el.textContent = current.toLocaleString() + suffix;
60+
if (progress < 1) requestAnimationFrame(tick);
61+
}
62+
requestAnimationFrame(tick);
63+
}
64+
65+
const statObserver = new IntersectionObserver((entries) => {
66+
entries.forEach((entry) => {
67+
if (!entry.isIntersecting) return;
68+
entry.target.querySelectorAll('.stat-pill').forEach((pill) => {
69+
const numEl = pill.querySelector('strong, b, .stat-value') || pill;
70+
animateCounter(numEl);
71+
});
72+
statObserver.unobserve(entry.target);
73+
});
74+
}, { threshold: 0.3 });
75+
76+
document.querySelectorAll('.stat-row').forEach((row) => statObserver.observe(row));
77+
78+
// ---------------------------------------------------------------------------
79+
// 3. Parallax hero background
80+
// ---------------------------------------------------------------------------
81+
const hero = document.querySelector('.hero-tagline')?.closest('section')
82+
|| document.querySelector('.hero-tagline')?.parentElement;
83+
if (hero) {
84+
let lastScroll = 0;
85+
let ticking = false;
86+
const applyParallax = () => {
87+
const offset = lastScroll * 0.35;
88+
hero.style.backgroundPositionY = `${offset}px`;
89+
ticking = false;
90+
};
91+
window.addEventListener('scroll', () => {
92+
lastScroll = window.scrollY;
93+
if (!ticking) { requestAnimationFrame(applyParallax); ticking = true; }
94+
}, { passive: true });
95+
}
96+
97+
// ---------------------------------------------------------------------------
98+
// 4. Tilt effect on cards
99+
// ---------------------------------------------------------------------------
100+
const TILT_MAX = 4;
101+
function handleTiltMove(e) {
102+
const rect = this.getBoundingClientRect();
103+
const x = (e.clientX - rect.left) / rect.width - 0.5;
104+
const y = (e.clientY - rect.top) / rect.height - 0.5;
105+
this.style.transform =
106+
`perspective(800px) rotateY(${x * TILT_MAX}deg) rotateX(${-y * TILT_MAX}deg) scale(1.02)`;
107+
}
108+
function handleTiltLeave() {
109+
this.style.transform = 'perspective(800px) rotateY(0) rotateX(0) scale(1)';
110+
}
111+
document.querySelectorAll('.visual-card, .industry-card').forEach((card) => {
112+
card.style.transition = 'transform 0.3s ease';
113+
card.style.willChange = 'transform';
114+
card.addEventListener('mousemove', handleTiltMove, { passive: true });
115+
card.addEventListener('mouseleave', handleTiltLeave, { passive: true });
116+
});
117+
118+
// ---------------------------------------------------------------------------
119+
// 5. Smooth scroll for anchor links
120+
// ---------------------------------------------------------------------------
121+
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
122+
anchor.addEventListener('click', (e) => {
123+
const id = anchor.getAttribute('href');
124+
if (!id || id === '#') return;
125+
const target = document.querySelector(id);
126+
if (!target) return;
127+
e.preventDefault();
128+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
129+
history.pushState(null, '', id);
130+
});
131+
});
132+
133+
// ---------------------------------------------------------------------------
134+
// 6. Auto-cycling showcase (marquee-style)
135+
// ---------------------------------------------------------------------------
136+
const gallery = document.querySelector('.showcase-gallery');
137+
if (gallery) {
138+
let autoScrollId = null;
139+
const SCROLL_SPEED = 0.8;
140+
const autoScroll = () => {
141+
gallery.scrollLeft += SCROLL_SPEED;
142+
if (gallery.scrollLeft >= gallery.scrollWidth - gallery.clientWidth - 1) {
143+
gallery.scrollLeft = 0;
144+
}
145+
autoScrollId = requestAnimationFrame(autoScroll);
146+
};
147+
const galleryObserver = new IntersectionObserver(([entry]) => {
148+
if (entry.isIntersecting) {
149+
autoScrollId = requestAnimationFrame(autoScroll);
150+
} else if (autoScrollId) {
151+
cancelAnimationFrame(autoScrollId);
152+
autoScrollId = null;
153+
}
154+
}, { threshold: 0.1 });
155+
galleryObserver.observe(gallery);
156+
gallery.addEventListener('mouseenter', () => {
157+
if (autoScrollId) { cancelAnimationFrame(autoScrollId); autoScrollId = null; }
158+
}, { passive: true });
159+
gallery.addEventListener('mouseleave', () => {
160+
autoScrollId = requestAnimationFrame(autoScroll);
161+
}, { passive: true });
162+
gallery.addEventListener('touchstart', () => {
163+
if (autoScrollId) { cancelAnimationFrame(autoScrollId); autoScrollId = null; }
164+
}, { passive: true });
165+
gallery.addEventListener('touchend', () => {
166+
autoScrollId = requestAnimationFrame(autoScroll);
167+
}, { passive: true });
168+
}
169+
170+
// ---------------------------------------------------------------------------
171+
// 7. Dark mode transition smoothing
172+
// ---------------------------------------------------------------------------
173+
const style = document.createElement('style');
174+
style.textContent = `
175+
body.m3-theme-transitioning,
176+
body.m3-theme-transitioning * {
177+
transition: background-color 0.35s ease, color 0.35s ease,
178+
border-color 0.35s ease, box-shadow 0.35s ease !important;
179+
}`;
180+
document.head.appendChild(style);
181+
const schemeObserver = new MutationObserver(() => {
182+
document.body.classList.add('m3-theme-transitioning');
183+
setTimeout(() => document.body.classList.remove('m3-theme-transitioning'), 400);
184+
});
185+
schemeObserver.observe(document.body, {
186+
attributes: true,
187+
attributeFilter: ['data-md-color-scheme']
188+
});
189+
190+
// ---------------------------------------------------------------------------
191+
// 8. Copy code button — "Copied!" toast feedback
192+
// ---------------------------------------------------------------------------
193+
document.addEventListener('click', (e) => {
194+
const btn = e.target.closest('.md-clipboard, .copy-button, button[data-clipboard-target]');
195+
if (!btn) return;
196+
const toast = document.createElement('span');
197+
toast.textContent = 'Copied!';
198+
Object.assign(toast.style, {
199+
position: 'absolute', top: '-2rem', left: '50%',
200+
transform: 'translateX(-50%)', padding: '4px 12px',
201+
borderRadius: '8px', fontSize: '0.75rem', fontWeight: '600',
202+
background: 'var(--md-primary-fg-color, #6750a4)',
203+
color: '#fff', opacity: '0', transition: 'opacity 0.25s ease, top 0.25s ease',
204+
pointerEvents: 'none', zIndex: '10', whiteSpace: 'nowrap'
205+
});
206+
btn.style.position = btn.style.position || 'relative';
207+
btn.appendChild(toast);
208+
requestAnimationFrame(() => {
209+
toast.style.opacity = '1';
210+
toast.style.top = '-2.5rem';
211+
});
212+
setTimeout(() => {
213+
toast.style.opacity = '0';
214+
setTimeout(() => toast.remove(), 300);
215+
}, 1500);
216+
});
217+
218+
// ---------------------------------------------------------------------------
219+
// 9. Platform badge interaction — scroll to matching device-section
220+
// ---------------------------------------------------------------------------
221+
document.querySelectorAll('.platform-badge').forEach((badge) => {
222+
badge.style.cursor = 'pointer';
223+
badge.addEventListener('click', () => {
224+
const label = badge.textContent.trim().toLowerCase();
225+
const sections = document.querySelectorAll('.device-section');
226+
for (const section of sections) {
227+
const heading = section.querySelector('h2, h3, h4, [class*="title"]');
228+
if (heading && heading.textContent.toLowerCase().includes(label)) {
229+
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
230+
section.style.transition = 'box-shadow 0.3s ease';
231+
section.style.boxShadow = '0 0 0 3px var(--md-primary-fg-color, #6750a4)';
232+
setTimeout(() => { section.style.boxShadow = 'none'; }, 1200);
233+
return;
234+
}
235+
}
236+
if (sections.length) sections[0].scrollIntoView({ behavior: 'smooth', block: 'start' });
237+
});
238+
});
239+
240+
// ---------------------------------------------------------------------------
241+
// 10. Performance — cleanup on page hide
242+
// ---------------------------------------------------------------------------
243+
document.addEventListener('visibilitychange', () => {
244+
if (document.hidden && gallery) {
245+
// Pause animations when tab is hidden
246+
}
247+
}, { passive: true });
248+
});

0 commit comments

Comments
 (0)