// Typewriter Effect for Info Box with Draggable Functionality class TypewriterEffect { constructor(elementId, messages) { this.element = document.getElementById(elementId); this.infoBox = this.element ? this.element.closest('.info-box') : null; this.messages = messages; this.currentMessageIndex = 0; this.currentCharIndex = 0; this.isTyping = false; this.isDeleting = false; this.typingSpeed = 80; // ms per character this.deletingSpeed = 40; // ms per character when deleting this.pauseTime = 1500; // ms to pause at end of message this.deleteDelay = 800; // ms to wait before starting to delete // Draggable properties this.isDragging = false; this.dragOffset = { x: 0, y: 0 }; this.position = { x: 0, y: 0 }; this.clickStartTime = 0; this.startPosition = { x: 0, y: 0 }; if (this.element) { this.init(); this.setupDraggable(); } } init() { // Clear initial content and start typing this.element.textContent = ''; this.loadPosition(); setTimeout(() => this.type(), 500); // Small delay before starting } setupDraggable() { if (!this.infoBox) return; // Mouse events this.infoBox.addEventListener('mousedown', this.handleStart.bind(this)); document.addEventListener('mousemove', this.handleMove.bind(this)); document.addEventListener('mouseup', this.handleEnd.bind(this)); // Touch events for mobile this.infoBox.addEventListener('touchstart', this.handleStart.bind(this), { passive: false }); document.addEventListener('touchmove', this.handleMove.bind(this), { passive: false }); document.addEventListener('touchend', this.handleEnd.bind(this)); // Prevent default drag behavior on images and links this.infoBox.addEventListener('dragstart', (e) => e.preventDefault()); // Add visual feedback this.infoBox.style.cursor = 'grab'; this.infoBox.style.userSelect = 'none'; this.infoBox.style.webkitUserSelect = 'none'; this.infoBox.style.mozUserSelect = 'none'; this.infoBox.style.msUserSelect = 'none'; } handleStart(e) { this.isDragging = true; this.clickStartTime = Date.now(); const clientX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX; const clientY = e.type === 'touchstart' ? e.touches[0].clientY : e.clientY; this.startPosition = { x: clientX, y: clientY }; const rect = this.infoBox.getBoundingClientRect(); this.dragOffset = { x: clientX - rect.left, y: clientY - rect.top }; this.infoBox.style.cursor = 'grabbing'; this.infoBox.classList.add('dragging'); e.preventDefault(); } handleMove(e) { if (!this.isDragging) return; e.preventDefault(); const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX; const clientY = e.type === 'touchmove' ? e.touches[0].clientY : e.clientY; this.position = { x: clientX - this.dragOffset.x, y: clientY - this.dragOffset.y }; // Constrain to viewport const maxX = window.innerWidth - this.infoBox.offsetWidth; const maxY = window.innerHeight - this.infoBox.offsetHeight; this.position.x = Math.max(0, Math.min(this.position.x, maxX)); this.position.y = Math.max(0, Math.min(this.position.y, maxY)); this.updatePosition(); } handleEnd(e) { if (!this.isDragging) return; this.isDragging = false; this.infoBox.style.cursor = 'grab'; this.infoBox.classList.remove('dragging'); // Save position this.savePosition(); // Check if it was a click vs drag const clickDuration = Date.now() - this.clickStartTime; const clientX = e.type === 'touchend' ? e.changedTouches[0].clientX : e.clientX; const clientY = e.type === 'touchend' ? e.changedTouches[0].clientY : e.clientY; const distance = Math.sqrt( Math.pow(clientX - this.startPosition.x, 2) + Math.pow(clientY - this.startPosition.y, 2) ); // If it was a quick click with minimal movement, trigger click behavior if (clickDuration < 300 && distance < 10) { this.handleClick(); } } handleClick() { // Skip to next message on click this.nextMessage(); } updatePosition() { if (this.infoBox) { this.infoBox.style.left = `${this.position.x}px`; this.infoBox.style.top = `${this.position.y}px`; } } savePosition() { localStorage.setItem('typewriterPosition', JSON.stringify(this.position)); // Update CSS variables for immediate positioning on reload document.documentElement.style.setProperty('--typewriter-x', this.position.x + 'px'); document.documentElement.style.setProperty('--typewriter-y', this.position.y + 'px'); } loadPosition() { const saved = localStorage.getItem('typewriterPosition'); if (saved && this.infoBox) { try { this.position = JSON.parse(saved); // Ensure position is still valid for current viewport const maxX = window.innerWidth - this.infoBox.offsetWidth; const maxY = window.innerHeight - this.infoBox.offsetHeight; this.position.x = Math.max(0, Math.min(this.position.x, maxX)); this.position.y = Math.max(0, Math.min(this.position.y, maxY)); this.updatePosition(); } catch (e) { // If parsing fails, use default position this.position = { x: 20, y: 110 }; } } else { this.position = { x: 20, y: 110 }; } } type() { if (this.isDeleting) { this.deleteText(); } else { this.addText(); } } addText() { if (this.currentCharIndex < this.messages[this.currentMessageIndex].length) { const currentMessage = this.messages[this.currentMessageIndex]; this.element.textContent = currentMessage.substring(0, this.currentCharIndex + 1); this.currentCharIndex++; this.element.classList.add('typing'); setTimeout(() => this.type(), this.typingSpeed); } else { // Finished typing current message this.element.classList.remove('typing'); this.element.classList.add('paused'); setTimeout(() => { this.element.classList.remove('paused'); this.isDeleting = true; setTimeout(() => this.type(), this.deleteDelay); }, this.pauseTime); } } deleteText() { if (this.currentCharIndex > 0) { const currentMessage = this.messages[this.currentMessageIndex]; this.element.textContent = currentMessage.substring(0, this.currentCharIndex - 1); this.currentCharIndex--; setTimeout(() => this.type(), this.deletingSpeed); } else { // Finished deleting, move to next message this.isDeleting = false; this.nextMessage(); setTimeout(() => this.type(), this.typingSpeed); } } nextMessage() { if (this.messages && this.messages.length > 0) { this.currentMessageIndex = (this.currentMessageIndex + 1) % this.messages.length; } } // Method to add new messages dynamically addMessage(message) { this.messages.push(message); } // Method to update messages array updateMessages(newMessages) { this.messages = newMessages; this.currentMessageIndex = 0; this.currentCharIndex = 0; this.isDeleting = false; } // Method to pause/resume typing pause() { this.isPaused = true; } resume() { this.isPaused = false; this.type(); } } // Preload emojis to prevent loading issues function preloadEmojis() { const emojis = ['๐Ÿ‡บ๐Ÿ‡ธ', '๐Ÿ‡ฌ๐Ÿ‡ง', '๐Ÿ‡ฎ๐Ÿ‡ช', '๐Ÿ‡ซ๐Ÿ‡ท', '๐Ÿ‡ช๐Ÿ‡ธ', '๐Ÿ‡ฉ๐Ÿ‡ช', '๐Ÿ‡ฎ๐Ÿ‡น', '๐Ÿ‡ง๐Ÿ‡ท', '๐Ÿ‡ฏ๐Ÿ‡ต', '๐Ÿ‡จ๐Ÿ‡ณ', '๐Ÿ‡ท๐Ÿ‡บ', '๐Ÿ‡ต๐Ÿ‡ฑ', '๐Ÿ‡ธ๐Ÿ‡ช', '๐Ÿ‡ณ๐Ÿ‡ฑ', '๐Ÿ‡ซ๐Ÿ‡ฎ', '๐Ÿ‡ณ๐Ÿ‡ด', '๐Ÿ‡ญ๐Ÿ‡บ', '๐Ÿ‡ฌ๐Ÿ‡ท', '๐Ÿ‡ฎ๐Ÿ‡ฑ', '๐Ÿ‡ธ๐Ÿ‡ฆ', '๐Ÿ‡ฎ๐Ÿ‡ณ', '๐Ÿ‡ฐ๐Ÿ‡ท', '๐Ÿ‡ป๐Ÿ‡ณ', '๐Ÿ‡ต๐Ÿ‡ญ', '๐Ÿ‡ฎ๐Ÿ‡ฉ', '๐Ÿ‡น๐Ÿ‡ญ', 'โค๏ธ']; const testDiv = document.createElement('div'); testDiv.style.position = 'absolute'; testDiv.style.left = '-9999px'; testDiv.style.fontSize = '1px'; testDiv.innerHTML = emojis.join(''); document.body.appendChild(testDiv); setTimeout(() => document.body.removeChild(testDiv), 100); } // Initialize typewriter when DOM is loaded document.addEventListener('DOMContentLoaded', () => { // Preload emojis first preloadEmojis(); // Array of multilingual "thanks" messages with flags const messages = [ "๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡ฌ๐Ÿ‡ง Thank you!", "๐Ÿ‡ฎ๐Ÿ‡ช Go raibh maith agat!", "๐Ÿ‡ซ๐Ÿ‡ท Merci!", "๐Ÿ‡ช๐Ÿ‡ธ ยกGracias!", "๐Ÿ‡ฉ๐Ÿ‡ช Danke!", "๐Ÿ‡ฎ๐Ÿ‡น Grazie!", "๐Ÿ‡ง๐Ÿ‡ท Obrigado!", "๐Ÿ‡ฏ๐Ÿ‡ต ใ‚ใ‚ŠใŒใจใ†!", "๐Ÿ‡จ๐Ÿ‡ณ ่ฐข่ฐข!", "๐Ÿ‡ท๐Ÿ‡บ ะกะฟะฐัะธะฑะพ!", "๐Ÿ‡ต๐Ÿ‡ฑ Tak!", "๐Ÿ‡ธ๐Ÿ‡ช Tack!", "๐Ÿ‡ณ๐Ÿ‡ฑ Dank je!", "๐Ÿ‡ซ๐Ÿ‡ฎ Kiitos!", "๐Ÿ‡ณ๐Ÿ‡ด Takk!", "๐Ÿ‡ต๐Ÿ‡ฑ Dziฤ™kujฤ™!", "๐Ÿ‡ญ๐Ÿ‡บ Kรถszรถnรถm!", "๐Ÿ‡ฌ๐Ÿ‡ท ฮ•ฯ…ฯ‡ฮฑฯฮนฯƒฯ„ฯŽ!", "๐Ÿ‡ฎ๐Ÿ‡ฑ ืชื•ื“ื”!", "๐Ÿ‡ธ๐Ÿ‡ฆ ุดูƒุฑุง!", "๐Ÿ‡ฎ๐Ÿ‡ณ เคงเคจเฅเคฏเคตเคพเคฆ!", "๐Ÿ‡ฐ๐Ÿ‡ท ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!", "๐Ÿ‡ป๐Ÿ‡ณ Cแบฃm ฦกn!", "๐Ÿ‡ต๐Ÿ‡ญ Salamat!", "๐Ÿ‡ฎ๐Ÿ‡ฉ Terima kasih!", "๐Ÿ‡น๐Ÿ‡ญ เธ‚เธญเธšเธ„เธธเธ“!", "โค๏ธ Much love!" ]; // Initialize the typewriter effect with a small delay to ensure emojis are loaded setTimeout(() => { window.typewriter = new TypewriterEffect('typewriter-text', messages); }, 200); });