/** * FollowEyes - A customizable animated eyes module that follows cursor/touch * @param {Object} options - Configuration options * @param {string} options.container - CSS selector for container to place eyes in * @param {string} options.imagePath - Path to head image (optional) * @param {string} options.position - CSS position value (default: 'relative') * @param {string} options.width - Width of head (default: 'min(300px, 80vw)') * @param {string} options.height - Height of head (default: 'min(300px, 80vw)') * @param {number} options.eyeSize - Eye size in pixels or CSS value (default: 'min(60px, 18vw)') * @param {string} options.eyeColor - Eye color (default: 'white') * @param {string} options.pupilColor - Pupil color (default: '#333') * @param {Object} options.customStyles - Additional custom styles for elements */ class FollowEyes { constructor(options = {}) { this.options = { container: 'body', imagePath: null, position: 'relative', width: 'min(300px, 80vw)', height: 'min(300px, 80vw)', eyeSize: 'min(60px, 18vw)', eyeColor: 'white', pupilColor: '#333', customStyles: {}, ...options }; this.initialized = false; this.init(); } init() { // Create elements this.head = document.createElement('div'); this.leftEye = document.createElement('div'); this.rightEye = document.createElement('div'); this.leftPupil = document.createElement('div'); this.rightPupil = document.createElement('div'); // Add classes this.head.className = 'follow-eyes-head'; this.leftEye.className = 'follow-eyes-eye follow-eyes-eye-left'; this.rightEye.className = 'follow-eyes-eye follow-eyes-eye-right'; this.leftPupil.className = 'follow-eyes-pupil'; this.rightPupil.className = 'follow-eyes-pupil'; this.leftPupil.id = 'follow-eyes-left-pupil'; this.rightPupil.id = 'follow-eyes-right-pupil'; // Build structure this.leftEye.appendChild(this.leftPupil); this.rightEye.appendChild(this.rightPupil); this.head.appendChild(this.leftEye); this.head.appendChild(this.rightEye); // Add to container const container = document.querySelector(this.options.container); if (!container) { console.error(`FollowEyes: Container "${this.options.container}" not found`); return; } container.appendChild(this.head); // Apply styles this.applyStyles(); // Set up event listeners this.setupEventListeners(); this.initialized = true; } applyStyles() { // Apply base styles const styles = document.createElement('style'); styles.innerHTML = ` .follow-eyes-head { position: ${this.options.position}; width: ${this.options.width}; height: ${this.options.height}; ${this.options.imagePath ? `background-image: url('${this.options.imagePath}');` : 'background-color: #ffdbac;'} background-size: cover; background-position: center; border-radius: 0 50% 0 0; box-shadow: 0 10px 25px rgba(0,0,0,0.1); ${this.options.imagePath ? '' : 'border-radius: 100px 100px 80px 80px;'} ${this.options.customStyles.head || ''} } .follow-eyes-eye { position: absolute; width: ${this.options.eyeSize}; height: ${this.options.eyeSize}; background-color: ${this.options.eyeColor}; border-radius: 50%; top: 35%; overflow: hidden; box-shadow: inset 0 0 10px rgba(0,0,0,0.2); z-index: 2; ${this.options.customStyles.eye || ''} } .follow-eyes-eye-left { left: 25%; ${this.options.customStyles.leftEye || ''} } .follow-eyes-eye-right { right: 25%; ${this.options.customStyles.rightEye || ''} } .follow-eyes-pupil { position: relative; width: 50%; height: 50%; background-color: ${this.options.pupilColor}; border-radius: 50%; top: 25%; left: 25%; transition: transform 0.05s ease-out; box-shadow: 0 0 5px rgba(0,0,0,0.8); ${this.options.customStyles.pupil || ''} } `; document.head.appendChild(styles); } setupEventListeners() { // Handle mouse movement on desktop document.addEventListener('mousemove', (e) => { this.updateEyes(e.clientX, e.clientY); }); // Handle touch movement on mobile document.addEventListener('touchmove', (e) => { e.preventDefault(); // Prevent scrolling while touching const touch = e.touches[0]; this.updateEyes(touch.clientX, touch.clientY); }, { passive: false }); // Center eyes when page loads window.addEventListener('DOMContentLoaded', () => { this.updateEyes(window.innerWidth / 2, window.innerHeight / 2); }); // Update when window is resized window.addEventListener('resize', () => { this.updateEyes(window.innerWidth / 2, window.innerHeight / 2); }); } updateEyes(x, y) { const leftPupil = document.getElementById('follow-eyes-left-pupil'); const rightPupil = document.getElementById('follow-eyes-right-pupil'); if (!leftPupil || !rightPupil) return; // Get the position of each eye const leftEye = leftPupil.parentElement.getBoundingClientRect(); const rightEye = rightPupil.parentElement.getBoundingClientRect(); // Calculate the center of each eye const leftEyeCenter = { x: leftEye.left + leftEye.width / 2, y: leftEye.top + leftEye.height / 2 }; const rightEyeCenter = { x: rightEye.left + rightEye.width / 2, y: rightEye.top + rightEye.height / 2 }; // Calculate angle for each eye const leftAngle = Math.atan2(y - leftEyeCenter.y, x - leftEyeCenter.x); const rightAngle = Math.atan2(y - rightEyeCenter.y, x - rightEyeCenter.x); // Maximum distance the pupil can move (in pixels) const maxDistance = Math.min(leftEye.width, rightEye.width) * 0.15; // Calculate the position of each pupil const leftPupilX = Math.cos(leftAngle) * maxDistance; const leftPupilY = Math.sin(leftAngle) * maxDistance; const rightPupilX = Math.cos(rightAngle) * maxDistance; const rightPupilY = Math.sin(rightAngle) * maxDistance; // Apply the new position leftPupil.style.transform = `translate(${leftPupilX}px, ${leftPupilY}px)`; rightPupil.style.transform = `translate(${rightPupilX}px, ${rightPupilY}px)`; } // Public method to update options updateOptions(newOptions) { this.options = { ...this.options, ...newOptions }; if (this.initialized) { // Remove existing elements this.head.remove(); // Reinitialize with new options this.init(); } } } // Export for use in browser or module system if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { module.exports = FollowEyes; } else { window.FollowEyes = FollowEyes; }