move.gl
Version:
Motion and Animation Library for Stylescape.
1,109 lines (930 loc) • 39.3 kB
text/typescript
// =============================================================================
// move.gl Demo Scripts
// =============================================================================
// Demo-specific JavaScript for interactive component demonstrations.
// These scripts are separate from the main library.
//
// This file contains:
// - Theme toggle functionality
// - Navigation dropdown functionality
// - Utility functions for demos
// - Demo page components (Draggable, Keyboard, Gesture, Screensaver, VideoOverlay)
// -----------------------------------------------------------------------------
// Type Definitions
// -----------------------------------------------------------------------------
type SwipeDirection = 'left' | 'right' | 'up' | 'down';
type DragEventType = 'start' | 'drag' | 'end';
type DragCallback = (type: DragEventType, elementId: string, x: number, y: number) => void;
interface KeyboardLayout {
default: string[][];
shift: string[][];
special: string[][];
}
interface GestureStats {
taps: number;
swipes: number;
pinches: number;
}
interface ScreensaverOptions {
timeout?: number;
fadeDuration?: number;
}
interface VideoOverlayOptions {
opacity?: number;
fadeDuration?: number;
effect?: string;
}
// -----------------------------------------------------------------------------
// Theme Toggle
// -----------------------------------------------------------------------------
/**
* Initialize theme toggle functionality.
* Handles light/dark mode switching with localStorage persistence.
*/
function initThemeToggle(): void {
const themeToggle = document.querySelector<HTMLButtonElement>('[data-toggle="theme"]');
const html = document.documentElement;
// Load saved theme or use system preference
const savedTheme = localStorage.getItem('theme');
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedTheme) {
html.setAttribute('data-theme', savedTheme);
} else if (systemPrefersDark) {
html.setAttribute('data-theme', 'dark');
}
// Handle theme toggle click
themeToggle?.addEventListener('click', () => {
const currentTheme = html.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
});
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) {
html.setAttribute('data-theme', e.matches ? 'dark' : 'light');
}
});
}
// -----------------------------------------------------------------------------
// Navigation Dropdown
// -----------------------------------------------------------------------------
/**
* Initialize navigation dropdown functionality.
* Handles opening, closing, and keyboard navigation.
*/
function initNavDropdown(): void {
const dropdown = document.getElementById('nav-dropdown');
const toggle = dropdown?.querySelector<HTMLButtonElement>('.nav__dropdown-toggle');
if (!dropdown || !toggle) return;
// Toggle dropdown on click
toggle.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = dropdown.classList.toggle('open');
toggle.setAttribute('aria-expanded', String(isOpen));
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!dropdown.contains(e.target as Node)) {
dropdown.classList.remove('open');
toggle.setAttribute('aria-expanded', 'false');
}
});
// Close dropdown on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && dropdown.classList.contains('open')) {
dropdown.classList.remove('open');
toggle.setAttribute('aria-expanded', 'false');
toggle.focus();
}
});
}
// -----------------------------------------------------------------------------
// Tab Navigation
// -----------------------------------------------------------------------------
/**
* Initialize tab navigation functionality.
* Handles tab switching and content visibility.
*/
function initTabs(): void {
const tabContainers = document.querySelectorAll<HTMLElement>('[data-tabs]');
tabContainers.forEach((container) => {
const tabs = container.querySelectorAll<HTMLButtonElement>('.tab');
const contents = container.querySelectorAll<HTMLElement>('.tab-content');
tabs.forEach((tab) => {
tab.addEventListener('click', () => {
const targetId = tab.dataset.target;
// Update active tab
tabs.forEach((t) => t.classList.remove('active'));
tab.classList.add('active');
// Update visible content
contents.forEach((content) => {
content.classList.toggle('active', content.id === targetId);
});
});
});
});
}
// -----------------------------------------------------------------------------
// Log Output
// -----------------------------------------------------------------------------
/**
* Log output manager for demo panels.
*/
class LogOutput {
private container: HTMLElement;
private maxEntries: number;
constructor(selector: string, maxEntries = 50) {
const element = document.querySelector<HTMLElement>(selector);
if (!element) {
throw new Error(`Log output container not found: ${selector}`);
}
this.container = element;
this.maxEntries = maxEntries;
}
/**
* Add a log entry with timestamp.
*/
log(message: string, type: 'info' | 'success' | 'warning' | 'error' = 'info'): void {
const entry = document.createElement('div');
entry.className = `log-entry log-entry--${type}`;
const timestamp = document.createElement('span');
timestamp.className = 'timestamp';
timestamp.textContent = new Date().toLocaleTimeString();
const text = document.createElement('span');
text.textContent = message;
entry.appendChild(timestamp);
entry.appendChild(text);
this.container.appendChild(entry);
// Remove old entries if exceeding max
while (this.container.children.length > this.maxEntries) {
this.container.removeChild(this.container.firstChild!);
}
// Scroll to bottom
this.container.scrollTop = this.container.scrollHeight;
}
/**
* Clear all log entries.
*/
clear(): void {
this.container.innerHTML = '';
}
}
// -----------------------------------------------------------------------------
// Utility Functions
// -----------------------------------------------------------------------------
/**
* Format a number with units (e.g., "1.5s", "100px").
*/
function formatValue(value: number, unit: string): string {
return `${value}${unit}`;
}
/**
* Debounce function for rate-limiting.
*/
function debounce<T extends (...args: unknown[]) => void>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: ReturnType<typeof setTimeout> | null = null;
return (...args: Parameters<T>) => {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
/**
* Throttle function for rate-limiting.
*/
function throttle<T extends (...args: unknown[]) => void>(
func: T,
limit: number
): (...args: Parameters<T>) => void {
let inThrottle = false;
return (...args: Parameters<T>) => {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
// -----------------------------------------------------------------------------
// Initialize on DOM Ready
// -----------------------------------------------------------------------------
function initDemo(): void {
initThemeToggle();
initNavDropdown();
initTabs();
}
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initDemo);
} else {
initDemo();
}
// -----------------------------------------------------------------------------
// Exports
// -----------------------------------------------------------------------------
export {
debounce, formatValue, initNavDropdown,
initTabs, initThemeToggle, LogOutput, throttle
};
// -----------------------------------------------------------------------------
// Demo: Draggable
// -----------------------------------------------------------------------------
/**
* Demo implementation of draggable functionality.
* For production use, import Draggable from the main library.
*/
class DemoDraggable {
private element: HTMLElement | null;
private isDragging = false;
private startX = 0;
private startY = 0;
private onDrag: DragCallback | undefined;
private boundRect: DOMRect | undefined;
constructor(elementId: string, onDrag?: DragCallback) {
this.element = document.getElementById(elementId);
this.onDrag = onDrag;
if (this.element?.parentElement) {
this.boundRect = this.element.parentElement.getBoundingClientRect();
this.attachEventListeners();
}
}
private attachEventListeners(): void {
if (!this.element) return;
this.element.addEventListener('mousedown', this.startDrag.bind(this));
this.element.addEventListener('touchstart', this.startDrag.bind(this), { passive: false });
document.addEventListener('mouseup', this.stopDrag.bind(this));
document.addEventListener('touchend', this.stopDrag.bind(this));
document.addEventListener('mousemove', this.drag.bind(this));
document.addEventListener('touchmove', this.drag.bind(this), { passive: false });
}
private getCoords(event: MouseEvent | TouchEvent): { clientX: number; clientY: number } {
if ('touches' in event && event.touches.length > 0) {
return { clientX: event.touches[0].clientX, clientY: event.touches[0].clientY };
}
return { clientX: (event as MouseEvent).clientX, clientY: (event as MouseEvent).clientY };
}
private startDrag(event: MouseEvent | TouchEvent): void {
if (!this.element) return;
event.preventDefault();
const coords = this.getCoords(event);
this.isDragging = true;
this.startX = coords.clientX - this.element.offsetLeft;
this.startY = coords.clientY - this.element.offsetTop;
this.onDrag?.('start', this.element.id, coords.clientX, coords.clientY);
}
private stopDrag(event: MouseEvent | TouchEvent): void {
if (this.isDragging && this.element) {
this.isDragging = false;
const coords = this.getCoords(event);
this.onDrag?.('end', this.element.id, coords.clientX || 0, coords.clientY || 0);
}
}
private drag(event: MouseEvent | TouchEvent): void {
if (!this.isDragging || !this.element?.parentElement) return;
event.preventDefault();
const coords = this.getCoords(event);
const container = this.element.parentElement.getBoundingClientRect();
let newX = coords.clientX - this.startX;
let newY = coords.clientY - this.startY;
newX = Math.max(0, Math.min(newX, container.width - this.element.offsetWidth));
newY = Math.max(0, Math.min(newY, container.height - this.element.offsetHeight));
this.element.style.left = `${newX}px`;
this.element.style.top = `${newY}px`;
this.element.style.right = 'auto';
this.element.style.bottom = 'auto';
this.element.style.transform = 'none';
this.onDrag?.('drag', this.element.id, Math.round(newX), Math.round(newY));
}
}
// -----------------------------------------------------------------------------
// Demo: Virtual Keyboard
// -----------------------------------------------------------------------------
/**
* Demo virtual keyboard with customizable layouts.
*/
class DemoKeyboard {
private layouts: KeyboardLayout = {
default: [
['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'],
['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'],
['⇧', 'z', 'x', 'c', 'v', 'b', 'n', 'm', '⌫'],
['123', ' ', '.', '↵']
],
shift: [
['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'],
['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L'],
['⇧', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '⌫'],
['123', ' ', '.', '↵']
],
special: [
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'],
['@', '#', '$', '%', '&', '*', '-', '+', '='],
['!', '"', "'", ':', ';', '/', '?', '⌫'],
['ABC', ' ', '.', '↵']
]
};
private currentMode: keyof KeyboardLayout = 'default';
private stats = { chars: 0, words: 0, keys: 0 };
private input: HTMLInputElement | null;
private container: HTMLElement | null;
constructor(inputId: string, containerId: string) {
this.input = document.getElementById(inputId) as HTMLInputElement;
this.container = document.getElementById(containerId);
this.render();
}
private render(): void {
if (!this.container) return;
const layout = this.layouts[this.currentMode];
this.container.innerHTML = layout.map(row =>
`<div class="keyboard-row">${row.map(key => {
let cls = 'key';
if (key === ' ') cls += ' key--space';
if (key === '⇧') cls += ' key--shift';
if (key === '⌫') cls += ' key--backspace';
if (key === '↵') cls += ' key--enter';
if (key === '123' || key === 'ABC') cls += ' key--mode';
return `<button class="${cls}" data-key="${key}">${key === ' ' ? 'space' : key}</button>`;
}).join('')}</div>`
).join('');
this.container.querySelectorAll<HTMLButtonElement>('.key').forEach(btn => {
btn.addEventListener('click', () => this.handleKey(btn.dataset.key ?? ''));
});
}
private handleKey(key: string): void {
if (!this.input) return;
this.stats.keys++;
if (key === '⌫') {
this.input.value = this.input.value.slice(0, -1);
} else if (key === '↵') {
this.input.value += '\n';
} else if (key === '⇧') {
this.switchMode(this.currentMode === 'shift' ? 'default' : 'shift');
return;
} else if (key === '123') {
this.switchMode('special');
return;
} else if (key === 'ABC') {
this.switchMode('default');
return;
} else {
this.input.value += key;
this.stats.chars++;
}
this.stats.words = this.input.value.trim().split(/\s+/).filter(w => w).length;
this.updateStats();
}
switchMode(mode: keyof KeyboardLayout): void {
this.currentMode = mode;
this.render();
document.querySelectorAll<HTMLButtonElement>('.mode-btn').forEach(btn => {
btn.classList.toggle('active',
(mode === 'default' && btn.textContent === 'ABC') ||
(mode === 'shift' && btn.textContent?.includes('SHIFT')) ||
(mode === 'special' && btn.textContent === '123')
);
});
}
private updateStats(): void {
const charEl = document.getElementById('charCount');
const wordEl = document.getElementById('wordCount');
const keyEl = document.getElementById('keypressCount');
if (charEl) charEl.textContent = String(this.stats.chars);
if (wordEl) wordEl.textContent = String(this.stats.words);
if (keyEl) keyEl.textContent = String(this.stats.keys);
}
getStats(): typeof this.stats {
return { ...this.stats };
}
}
// -----------------------------------------------------------------------------
// Demo: Gesture Handler
// -----------------------------------------------------------------------------
/**
* Demo gesture handler for touch and mouse interactions.
*/
class DemoGesture {
private element: HTMLElement | null;
private stats: GestureStats = { taps: 0, swipes: 0, pinches: 0 };
private startX = 0;
private startY = 0;
private startTime = 0;
private isDragging = false;
constructor(elementId: string) {
this.element = document.getElementById(elementId);
if (this.element) {
this.attachEventListeners();
}
}
private attachEventListeners(): void {
if (!this.element) return;
// Mouse events
this.element.addEventListener('mousedown', (e) => this.handleStart(e.clientX, e.clientY));
document.addEventListener('mouseup', (e) => this.handleEnd(e.clientX, e.clientY));
// Touch events
this.element.addEventListener('touchstart', (e) => {
e.preventDefault();
if (e.touches.length === 1) {
this.handleStart(e.touches[0].clientX, e.touches[0].clientY);
} else if (e.touches.length === 2) {
this.handlePinch();
}
});
this.element.addEventListener('touchend', (e) => {
if (e.changedTouches.length === 1) {
this.handleEnd(e.changedTouches[0].clientX, e.changedTouches[0].clientY);
}
});
}
private handleStart(x: number, y: number): void {
this.startX = x;
this.startY = y;
this.startTime = Date.now();
this.isDragging = true;
}
private handleEnd(x: number, y: number): void {
if (!this.isDragging) return;
this.isDragging = false;
const dx = x - this.startX;
const dy = y - this.startY;
const dist = Math.sqrt(dx * dx + dy * dy);
const duration = Date.now() - this.startTime;
if (dist < 10 && duration < 300) {
this.handleTap();
} else if (dist > 50) {
const direction = this.getSwipeDirection(dx, dy);
this.handleSwipe(direction, dx, dy);
}
}
private getSwipeDirection(dx: number, dy: number): SwipeDirection {
if (Math.abs(dx) > Math.abs(dy)) {
return dx > 0 ? 'right' : 'left';
}
return dy > 0 ? 'down' : 'up';
}
private handleTap(): void {
this.stats.taps++;
this.updateUI('tap');
}
private handleSwipe(direction: SwipeDirection, _dx: number, _dy: number): void {
this.stats.swipes++;
this.updateUI('swipe', direction);
}
private handlePinch(): void {
this.stats.pinches++;
this.updateUI('pinch');
}
private updateUI(type: string, direction?: SwipeDirection): void {
const tapCount = document.getElementById('tapCount');
const swipeCount = document.getElementById('swipeCount');
const pinchCount = document.getElementById('pinchCount');
const lastDirection = document.getElementById('lastDirection');
if (tapCount) tapCount.textContent = String(this.stats.taps);
if (swipeCount) swipeCount.textContent = String(this.stats.swipes);
if (pinchCount) pinchCount.textContent = String(this.stats.pinches);
if (direction && lastDirection) lastDirection.textContent = direction;
// Show feedback
const feedback = document.getElementById('gestureFeedback');
if (feedback) {
feedback.textContent = type === 'swipe' ? `Swiped ${direction}!` : `${type.charAt(0).toUpperCase() + type.slice(1)} detected!`;
feedback.classList.add('visible');
setTimeout(() => feedback.classList.remove('visible'), 1500);
}
// Show direction indicator
if (direction) {
this.showDirectionIndicator(direction);
}
}
private showDirectionIndicator(direction: SwipeDirection): void {
document.querySelectorAll('.direction-indicator').forEach(el => el.classList.remove('active'));
const indicator = document.getElementById(`dir${direction.charAt(0).toUpperCase() + direction.slice(1)}`);
if (indicator) {
indicator.classList.add('active');
setTimeout(() => indicator.classList.remove('active'), 300);
}
}
resetStats(): void {
this.stats = { taps: 0, swipes: 0, pinches: 0 };
this.updateUI('reset');
const lastDirection = document.getElementById('lastDirection');
if (lastDirection) lastDirection.textContent = '—';
}
}
// -----------------------------------------------------------------------------
// Demo: Screensaver
// -----------------------------------------------------------------------------
/**
* Demo screensaver with inactivity detection.
*/
class DemoScreensaver {
private timeout: number;
private fadeDuration: number;
private isActive = false;
private timeoutId: ReturnType<typeof setTimeout> | null = null;
private lastActivity: number;
private eventCount = 0;
private screensaverContent: HTMLElement | null;
private placeholder: HTMLElement | null;
private statusDot: HTMLElement | null;
private statusText: HTMLElement | null;
private countdown: HTMLElement | null;
private activityLog: HTMLElement | null;
constructor(options: ScreensaverOptions = {}) {
this.timeout = options.timeout ?? 10000;
this.fadeDuration = options.fadeDuration ?? 500;
this.lastActivity = Date.now();
this.screensaverContent = document.getElementById('screensaverContent');
this.placeholder = document.getElementById('placeholder');
this.statusDot = document.getElementById('statusDot');
this.statusText = document.getElementById('statusText');
this.countdown = document.getElementById('countdown');
this.activityLog = document.getElementById('activityLog');
this.setupEventListeners();
this.startTimeout();
this.updateCountdown();
}
private setupEventListeners(): void {
['mousemove', 'mousedown', 'keydown', 'touchstart', 'scroll'].forEach(event => {
document.addEventListener(event, () => this.resetTimeout(), { passive: true });
});
}
log(message: string): void {
if (!this.activityLog) return;
const time = new Date().toLocaleTimeString();
const entry = document.createElement('div');
entry.className = 'entry';
entry.textContent = `[${time}] ${message}`;
this.activityLog.appendChild(entry);
this.activityLog.scrollTop = this.activityLog.scrollHeight;
}
resetTimeout(): void {
if (this.isActive) this.deactivate();
this.lastActivity = Date.now();
this.eventCount++;
const eventCountEl = document.getElementById('eventCount');
if (eventCountEl) eventCountEl.textContent = String(this.eventCount);
if (this.timeoutId) clearTimeout(this.timeoutId);
this.startTimeout();
}
private startTimeout(): void {
this.timeoutId = setTimeout(() => this.activate(), this.timeout);
}
private updateCountdown(): void {
setInterval(() => {
if (!this.isActive && this.countdown) {
const elapsed = Date.now() - this.lastActivity;
const remaining = Math.max(0, Math.ceil((this.timeout - elapsed) / 1000));
this.countdown.textContent = `${remaining}s`;
}
}, 100);
}
activate(): void {
this.isActive = true;
this.screensaverContent?.classList.add('active');
if (this.placeholder) this.placeholder.style.opacity = '0';
this.statusDot?.classList.add('screensaver-active');
if (this.statusText) this.statusText.textContent = 'Screensaver active';
if (this.countdown) this.countdown.textContent = '💤';
this.log('Screensaver activated');
}
deactivate(): void {
this.isActive = false;
this.screensaverContent?.classList.remove('active');
if (this.placeholder) this.placeholder.style.opacity = '1';
this.statusDot?.classList.remove('screensaver-active');
if (this.statusText) this.statusText.textContent = 'Monitoring activity';
this.log('Screensaver deactivated');
}
setTimeout(timeout: number): void {
this.timeout = timeout;
this.resetTimeout();
}
setFadeDuration(duration: number): void {
this.fadeDuration = duration;
if (this.screensaverContent) {
this.screensaverContent.style.transition = `opacity ${duration}ms ease`;
}
}
getEventCount(): number {
return this.eventCount;
}
resetEventCount(): void {
this.eventCount = 0;
const eventCountEl = document.getElementById('eventCount');
if (eventCountEl) eventCountEl.textContent = '0';
}
}
// -----------------------------------------------------------------------------
// Demo: Video Overlay
// -----------------------------------------------------------------------------
/**
* Demo video overlay with visual effects.
*/
class DemoVideoOverlay {
private overlay: HTMLElement | null;
private particles: HTMLElement | null;
private isVisible = false;
private currentEffect = 'vignette';
private opacity = 1;
private fadeDuration = 300;
private blurAmount = 0;
constructor() {
this.overlay = document.getElementById('videoOverlay');
this.particles = document.getElementById('particles');
this.checkHevcSupport();
this.setupControls();
this.createParticles();
}
private checkHevcSupport(): void {
const indicator = document.getElementById('alphaIndicator');
if (!indicator) return;
const video = document.createElement('video');
const canPlayHevc = video.canPlayType('video/mp4; codecs="hvc1"') !== '';
if (canPlayHevc) {
indicator.className = 'alpha-indicator supported';
indicator.textContent = '✓ HEVC Supported';
} else {
indicator.className = 'alpha-indicator unsupported';
indicator.textContent = '✗ HEVC Not Supported';
}
}
private setupControls(): void {
document.getElementById('opacitySlider')?.addEventListener('input', (e) => {
const target = e.target as HTMLInputElement;
this.opacity = parseInt(target.value) / 100;
const valueEl = document.getElementById('opacityValue');
if (valueEl) valueEl.textContent = `${target.value}%`;
if (this.isVisible && this.overlay) this.overlay.style.opacity = String(this.opacity);
});
document.getElementById('fadeSlider')?.addEventListener('input', (e) => {
const target = e.target as HTMLInputElement;
this.fadeDuration = parseInt(target.value);
const valueEl = document.getElementById('fadeValue');
if (valueEl) valueEl.textContent = target.value;
if (this.overlay) this.overlay.style.transition = `opacity ${this.fadeDuration}ms ease`;
});
document.getElementById('blurSlider')?.addEventListener('input', (e) => {
const target = e.target as HTMLInputElement;
this.blurAmount = parseInt(target.value);
const valueEl = document.getElementById('blurValue');
if (valueEl) valueEl.textContent = target.value;
if (this.overlay) {
this.overlay.style.backdropFilter = this.blurAmount > 0 ? `blur(${this.blurAmount}px)` : 'none';
}
});
document.querySelectorAll<HTMLButtonElement>('.effect-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.effect-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
this.setEffect(btn.dataset.effect ?? 'none');
});
});
document.getElementById('toggleOverlay')?.addEventListener('click', () => this.toggle());
document.getElementById('fadeInOut')?.addEventListener('click', () => this.fadeInOut());
document.getElementById('resetDemo')?.addEventListener('click', () => this.reset());
}
private createParticles(): void {
if (!this.particles) return;
for (let i = 0; i < 30; i++) {
const particle = document.createElement('div');
particle.className = 'particle';
particle.style.left = `${Math.random() * 100}%`;
particle.style.top = `${Math.random() * 100}%`;
particle.style.animationDelay = `${Math.random() * 3}s`;
particle.style.animationDuration = `${2 + Math.random() * 2}s`;
this.particles.appendChild(particle);
}
}
setEffect(effect: string): void {
this.currentEffect = effect;
const overlayEffect = this.overlay?.querySelector<HTMLElement>('.overlay-effect');
if (!overlayEffect || !this.particles) return;
switch (effect) {
case 'none':
overlayEffect.style.background = 'transparent';
this.particles.style.display = 'none';
break;
case 'vignette':
overlayEffect.style.background = 'radial-gradient(circle at center, transparent 30%, rgba(0, 0, 0, 0.8) 100%)';
this.particles.style.display = 'none';
break;
case 'particles':
overlayEffect.style.background = 'transparent';
this.particles.style.display = 'block';
break;
case 'gradient':
overlayEffect.style.background = 'linear-gradient(to bottom, rgba(102, 126, 234, 0.3), rgba(118, 75, 162, 0.3))';
this.particles.style.display = 'none';
break;
case 'scanlines':
overlayEffect.style.background = 'repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.1) 1px, transparent 1px, transparent 2px)';
this.particles.style.display = 'none';
break;
}
}
toggle(): void {
this.isVisible = !this.isVisible;
this.overlay?.classList.toggle('visible', this.isVisible);
if (this.isVisible && this.overlay) this.overlay.style.opacity = String(this.opacity);
const toggleBtn = document.getElementById('toggleOverlay');
if (toggleBtn) toggleBtn.textContent = this.isVisible ? 'Hide Overlay' : 'Show Overlay';
}
fadeInOut(): void {
if (!this.isVisible) {
this.toggle();
setTimeout(() => this.toggle(), this.fadeDuration * 3);
}
}
reset(): void {
this.isVisible = false;
this.overlay?.classList.remove('visible');
const toggleBtn = document.getElementById('toggleOverlay');
if (toggleBtn) toggleBtn.textContent = 'Show Overlay';
const opacitySlider = document.getElementById('opacitySlider') as HTMLInputElement;
const opacityValue = document.getElementById('opacityValue');
const fadeSlider = document.getElementById('fadeSlider') as HTMLInputElement;
const fadeValue = document.getElementById('fadeValue');
const blurSlider = document.getElementById('blurSlider') as HTMLInputElement;
const blurValue = document.getElementById('blurValue');
if (opacitySlider) opacitySlider.value = '100';
if (opacityValue) opacityValue.textContent = '100%';
if (fadeSlider) fadeSlider.value = '300';
if (fadeValue) fadeValue.textContent = '300';
if (blurSlider) blurSlider.value = '0';
if (blurValue) blurValue.textContent = '0';
this.opacity = 1;
this.fadeDuration = 300;
this.blurAmount = 0;
if (this.overlay) {
this.overlay.style.transition = 'opacity 300ms ease';
this.overlay.style.backdropFilter = 'none';
}
}
}
// -----------------------------------------------------------------------------
// Demo Page Initializers
// -----------------------------------------------------------------------------
/**
* Initialize the draggable demo page.
*/
function initDraggableDemo(): void {
const logOutput = document.getElementById('logOutput');
function log(message: string): void {
if (!logOutput) return;
const time = new Date().toLocaleTimeString();
const entry = document.createElement('div');
entry.className = 'log-entry';
entry.innerHTML = `<span class="timestamp">[${time}]</span> ${message}`;
logOutput.appendChild(entry);
logOutput.scrollTop = logOutput.scrollHeight;
}
function handleDrag(type: DragEventType, id: string, x: number, y: number): void {
if (type === 'start') {
log(`<strong>${id}</strong>: Drag started`);
} else if (type === 'end') {
log(`<strong>${id}</strong>: Drag ended`);
} else {
log(`<strong>${id}</strong>: Position (${x}, ${y})`);
}
}
// Initialize draggable boxes
new DemoDraggable('box1', handleDrag);
new DemoDraggable('box2', handleDrag);
new DemoDraggable('box3', handleDrag);
// Global functions for buttons
(window as unknown as Record<string, () => void>).resetPositions = () => {
const box1 = document.getElementById('box1');
const box2 = document.getElementById('box2');
const box3 = document.getElementById('box3');
if (box1) box1.style.cssText = 'top: 50px; left: 50px;';
if (box2) box2.style.cssText = 'top: 50px; right: 50px;';
if (box3) box3.style.cssText = 'bottom: 50px; left: 50%; transform: translateX(-50%);';
log('Positions reset');
};
(window as unknown as Record<string, () => void>).clearLog = () => {
if (logOutput) {
logOutput.innerHTML = '<div class="log-entry"><span class="timestamp">[--:--:--]</span> Log cleared</div>';
}
};
log('Draggable demo initialized. Try dragging the boxes!');
}
/**
* Initialize the keyboard demo page.
*/
function initKeyboardDemo(): void {
const keyboard = new DemoKeyboard('keyboardInput', 'keyboard');
(window as unknown as Record<string, (mode: string) => void>).switchMode = (mode: string) => {
keyboard.switchMode(mode as keyof KeyboardLayout);
};
}
/**
* Initialize the gesture demo page.
*/
function initGestureDemo(): void {
const gesture = new DemoGesture('gestureArea');
function log(type: string, details: string): void {
const gestureLog = document.getElementById('gestureLog');
if (!gestureLog) return;
const time = new Date().toLocaleTimeString();
const entry = document.createElement('div');
entry.className = 'entry';
entry.innerHTML = `<span class="timestamp">${time}</span><span class="type">${type}</span><span class="details">${details}</span>`;
gestureLog.appendChild(entry);
gestureLog.scrollTop = gestureLog.scrollHeight;
}
(window as unknown as Record<string, () => void>).resetStats = () => {
gesture.resetStats();
log('RESET', 'Stats cleared');
};
(window as unknown as Record<string, () => void>).clearLog = () => {
const gestureLog = document.getElementById('gestureLog');
if (gestureLog) {
gestureLog.innerHTML = '<div class="entry"><span class="timestamp">--:--:--</span><span class="type">INIT</span><span class="details">Log cleared</span></div>';
}
};
log('READY', 'Gesture handler initialized');
}
/**
* Initialize the screensaver demo page.
*/
function initScreensaverDemo(): void {
const screensaver = new DemoScreensaver({ timeout: 10000, fadeDuration: 500 });
document.getElementById('timeoutSlider')?.addEventListener('input', (e) => {
const target = e.target as HTMLInputElement;
const value = parseInt(target.value);
const valueEl = document.getElementById('timeoutValue');
if (valueEl) valueEl.textContent = String(value);
screensaver.setTimeout(value * 1000);
screensaver.log(`Timeout set to ${value}s`);
});
document.getElementById('fadeSlider')?.addEventListener('input', (e) => {
const target = e.target as HTMLInputElement;
const value = parseInt(target.value);
const valueEl = document.getElementById('fadeValue');
if (valueEl) valueEl.textContent = String(value);
screensaver.setFadeDuration(value);
screensaver.log(`Fade duration set to ${value}ms`);
});
(window as unknown as Record<string, () => void>).forceActivate = () => screensaver.activate();
(window as unknown as Record<string, () => void>).forceDeactivate = () => {
screensaver.deactivate();
screensaver.resetTimeout();
};
(window as unknown as Record<string, () => void>).resetDemo = () => {
screensaver.resetEventCount();
const activityLog = document.getElementById('activityLog');
if (activityLog) activityLog.innerHTML = '<div class="entry">[--:--:--] Demo reset</div>';
screensaver.deactivate();
screensaver.resetTimeout();
};
}
/**
* Initialize the video overlay demo page.
*/
function initVideoOverlayDemo(): void {
new DemoVideoOverlay();
}
// -----------------------------------------------------------------------------
// Auto-Detection and Initialization
// -----------------------------------------------------------------------------
/**
* Detect which demo page is loaded and initialize accordingly.
*/
function initDemoPage(): void {
const path = window.location.pathname;
if (path.includes('demo_draggable')) {
initDraggableDemo();
} else if (path.includes('demo_keyboard')) {
initKeyboardDemo();
} else if (path.includes('demo_gesture')) {
initGestureDemo();
} else if (path.includes('demo_screensaver')) {
initScreensaverDemo();
} else if (path.includes('demo_video_overlay')) {
initVideoOverlayDemo();
}
}
// Auto-initialize demo pages
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initDemoPage);
} else {
initDemoPage();
}
// -----------------------------------------------------------------------------
// Demo Class Exports
// -----------------------------------------------------------------------------
export {
DemoDraggable,
DemoGesture,
DemoKeyboard,
DemoScreensaver,
DemoVideoOverlay,
initDemoPage,
initDraggableDemo,
initGestureDemo,
initKeyboardDemo,
initScreensaverDemo,
initVideoOverlayDemo
};