hm
This commit is contained in:
295
static/js/typewriter.js
Normal file
295
static/js/typewriter.js
Normal file
@@ -0,0 +1,295 @@
|
||||
// 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);
|
||||
});
|
||||
Reference in New Issue
Block a user