Observer & track
The observer and track systems provide advanced viewport detection and scroll tracking capabilities with efficient memory management.
Overview
Section titled “Overview”The system provides:
- Observer System: Advanced Intersection Observer with grouping and management
- Track System: Scroll-based tracking with precise bounds calculation
- Memory Management: Efficient observer grouping and automatic cleanup
- Direction Detection: Enhanced scroll direction detection for animations
- Performance Optimization: Optimized observer management and subscription handling
Observer System
Section titled “Observer System”Core Components
Section titled “Core Components”ObserverManager
Section titled “ObserverManager”A singleton that manages all Intersection Observers efficiently:
import { ObserverManager } from "@/modules/_/observe";
// Get singleton instanceconst manager = ObserverManager.getInstance();Observe Class
Section titled “Observe Class”The main observer class for viewport detection:
import { Observe } from "@/modules/_/observe";
const observer = new Observe(element, { root: null, // Root element (null = viewport) rootMargin: "0px", // Margin around root threshold: 0, // Visibility threshold autoStart: false, // Start automatically once: false, // Trigger only once callback: (data) => { // Custom callback console.log("Observer event:", data); },});Configuration Options
Section titled “Configuration Options”interface ObserveConfig { root?: HTMLElement | null; // Root element for intersection rootMargin?: string; // Margin around root (CSS-like) threshold?: number; // Visibility threshold (0-1) autoStart?: boolean; // Start observer automatically once?: boolean; // Trigger only once callback?: (data: ObserveEventData) => void; // Custom callback}
interface ObserveEventData { entry: IntersectionObserverEntry; // Native intersection entry direction: number; // Scroll direction (-1 = up, 1 = down) isIn: boolean; // Whether element is in view}Basic Usage
Section titled “Basic Usage”export default function (element: HTMLElement, dataset: DOMStringMap) { const observer = new Observe(element, { threshold: 0.1, // Trigger when 10% visible rootMargin: "50px", // 50px margin around viewport autoStart: true, // Start immediately callback: ({ isIn, direction }) => { if (isIn) { element.classList.add("in-view"); console.log("Element entered viewport"); } else { element.classList.remove("in-view"); console.log("Element left viewport"); } }, });
// Manual control observer.start(); observer.stop(); observer.destroy();
// Auto-cleanup with lifecycle hooks onDestroy(() => { observer.destroy(); });}Advanced Observer Patterns
Section titled “Advanced Observer Patterns”Direction-Based Animations
Section titled “Direction-Based Animations”const observer = new Observe(element, { threshold: 0.1, callback: ({ isIn, direction }) => { if (isIn) { // Animate in based on direction if (direction > 0) { // Scrolling down - animate from bottom gsap.fromTo( element, { y: 50, opacity: 0 }, { y: 0, opacity: 1, duration: 0.6 }, ); } else { // Scrolling up - animate from top gsap.fromTo( element, { y: -50, opacity: 0 }, { y: 0, opacity: 1, duration: 0.6 }, ); } } },});Staggered Animations
Section titled “Staggered Animations”export default function (element: HTMLElement, dataset: DOMStringMap) { const children = Array.from(element.children) as HTMLElement[];
children.forEach((child, index) => { const childObserver = new Observe(child, { threshold: 0.1, callback: ({ isIn }) => { if (isIn) { gsap.to(child, { opacity: 1, y: 0, duration: 0.5, delay: index * 0.1, // Stagger effect ease: "power2.out", }); } }, });
onDestroy(() => childObserver.destroy()); });}Track System
Section titled “Track System”Overview
Section titled “Overview”The Track system extends the Observer system to provide scroll-based tracking with precise bounds calculation:
import { Track } from "@/modules/_/track";
const track = new Track(element, { bounds: [0, 1], // Output range top: "bottom", // When to start tracking bottom: "top", // When to stop tracking callback: (value) => { // Progress callback console.log("Track progress:", value); },});Configuration Options
Section titled “Configuration Options”interface TrackConfig { bounds: [number, number]; // Output range [min, max] top: "top" | "center" | "bottom"; // Start trigger point bottom: "top" | "center" | "bottom"; // End trigger point callback?: (value: number) => void; // Progress callback}Trigger Points
Section titled “Trigger Points”The track system supports three trigger points:
- “top”: Element’s top edge
- “center”: Element’s center point
- “bottom”: Element’s bottom edge
Basic Usage
Section titled “Basic Usage”export default function (element: HTMLElement, dataset: DOMStringMap) { const track = new Track(element, { bounds: [0, 1], // Output 0-1 top: "bottom", // Start when bottom hits viewport top bottom: "top", // End when top hits viewport bottom callback: (value) => { // value goes from 0 to 1 as element scrolls through viewport element.style.setProperty("--scroll-progress", value.toString()); }, });
// Auto-cleanup onDestroy(() => { track.destroy(); });}Advanced Track Patterns
Section titled “Advanced Track Patterns”Parallax Effects
Section titled “Parallax Effects”const track = new Track(element, { bounds: [0, 200], // Move 200px top: "bottom", bottom: "top", callback: (value) => { // Parallax effect element.style.transform = `translateY(${value}px)`; },});Rotation Effects
Section titled “Rotation Effects”const track = new Track(element, { bounds: [0, 360], // Rotate 360 degrees top: "center", bottom: "center", callback: (value) => { element.style.transform = `rotate(${value}deg)`; },});Scale Effects
Section titled “Scale Effects”const track = new Track(element, { bounds: [0.5, 1], // Scale from 50% to 100% top: "bottom", bottom: "top", callback: (value) => { element.style.transform = `scale(${value})`; },});Complex Animations
Section titled “Complex Animations”const track = new Track(element, { bounds: [0, 1], top: "bottom", bottom: "top", callback: (value) => { // Multiple properties element.style.transform = ` translateY(${value * 100}px) rotate(${value * 180}deg) scale(${0.5 + value * 0.5}) `;
// Opacity element.style.opacity = value.toString();
// CSS custom properties element.style.setProperty("--progress", value.toString()); },});Bounds Calculation
Section titled “Bounds Calculation”The track system automatically calculates bounds based on:
- Element Position: Current element position in viewport
- Trigger Points: Start and end trigger points
- Viewport Size: Current viewport dimensions
- Scroll Position: Current scroll position
// Manual bounds calculation (for reference)function computeBounds(el: HTMLElement, config: TrackConfig) { const bounds = clientRect(el); const { top: topPos, bottom: bottomPos, wh } = bounds; const centerOffset = wh / 2;
bounds.top = topPos - (config.top === "center" ? centerOffset : config.top === "bottom" ? wh : 0);
bounds.bottom = bottomPos - (config.bottom === "center" ? centerOffset : config.bottom === "bottom" ? wh : 0);
return bounds;}Lifecycle Integration
Section titled “Lifecycle Integration”Using with Lifecycle Hooks
Section titled “Using with Lifecycle Hooks”export default function (element: HTMLElement, dataset: DOMStringMap) { const observer = new Observe(element, { threshold: 0.1, callback: ({ isIn }) => { element.classList.toggle("in-view", isIn); }, });
const track = new Track(element, { bounds: [0, 1], callback: (value) => { element.style.setProperty("--progress", value.toString()); }, });
onDestroy(() => { observer.destroy(); track.destroy(); });}Using Convenience Functions
Section titled “Using Convenience Functions”import { onView, onTrack } from "@/modules/_";
export default function (element: HTMLElement, dataset: DOMStringMap) { // Auto-cleanup observers const observer = onView(element, { threshold: 0.1, callback: ({ isIn }) => { element.classList.toggle("in-view", isIn); }, });
const track = onTrack(element, { bounds: [0, 1], callback: (value) => { element.style.setProperty("--progress", value.toString()); }, });
// No manual cleanup needed - handled automatically}Performance Optimization
Section titled “Performance Optimization”Observer Grouping
Section titled “Observer Grouping”The ObserverManager automatically groups observers with the same configuration:
// These observers share the same IntersectionObserver instanceconst observer1 = new Observe(element1, { threshold: 0.1 });const observer2 = new Observe(element2, { threshold: 0.1 });const observer3 = new Observe(element3, { threshold: 0.1 });
// This observer uses a separate instance (different config)const observer4 = new Observe(element4, { threshold: 0.5 });Memory Management
Section titled “Memory Management”export default function (element: HTMLElement, dataset: DOMStringMap) { const observers: Observe[] = [];
// Create multiple observers element.querySelectorAll(".animate").forEach((child) => { const observer = new Observe(child as HTMLElement, { threshold: 0.1, callback: ({ isIn }) => { if (isIn) { child.classList.add("animated"); } }, }); observers.push(observer); });
// Clean up all observers onDestroy(() => { observers.forEach((observer) => observer.destroy()); });}Conditional Tracking
Section titled “Conditional Tracking”export default function (element: HTMLElement, dataset: DOMStringMap) { let track: Track | null = null; let isActive = false;
// Only track when element is in view const observer = new Observe(element, { threshold: 0.1, callback: ({ isIn }) => { if (isIn && !isActive) { // Start tracking track = new Track(element, { bounds: [0, 1], callback: (value) => { element.style.setProperty("--progress", value.toString()); }, }); isActive = true; } else if (!isIn && isActive) { // Stop tracking track?.destroy(); track = null; isActive = false; } }, });
onDestroy(() => { observer.destroy(); track?.destroy(); });}Best Practices
Section titled “Best Practices”Observer Usage
Section titled “Observer Usage”- Threshold Selection: Use appropriate thresholds for your use case
- Root Margin: Use rootMargin for early triggering
- Direction Detection: Use direction for scroll-based animations
- Auto Cleanup: Always clean up observers in
onDestroy
Track Usage
Section titled “Track Usage”- Bounds Planning: Plan your bounds carefully for smooth animations
- Trigger Points: Choose appropriate trigger points for your effect
- Performance: Keep track callbacks lightweight
- Responsive: Consider viewport changes in your calculations
Memory Management
Section titled “Memory Management”- Observer Grouping: Let the system group observers automatically
- Cleanup: Always destroy observers and tracks
- Conditional Creation: Only create observers when needed
- Lifecycle Integration: Use lifecycle hooks for automatic cleanup
API Reference
Section titled “API Reference”Observe Class
Section titled “Observe Class”new Observe(element, config)- Create new observerobserver.start()- Start observingobserver.stop()- Stop observingobserver.destroy()- Destroy observer and cleanup
Track Class
Section titled “Track Class”new Track(element, config)- Create new tracktrack.value- Current track valuetrack.bounds- Current boundstrack.destroy()- Destroy track and cleanup
ObserverManager
Section titled “ObserverManager”ObserverManager.getInstance()- Get singleton instancemanager.addElement(element, config, callbacks)- Add element to observermanager.removeElement(element)- Remove element from observer
Convenience Functions
Section titled “Convenience Functions”onView(element, config)- Create auto-cleanup observeronTrack(element, config)- Create auto-cleanup track