From c3e092e3058a456b90c266253445275dd8b47374 Mon Sep 17 00:00:00 2001 From: sHa Date: Sun, 28 Dec 2025 04:25:53 +0200 Subject: [PATCH] feat: enhance logo animations with interactive tilt and mixed mode support --- css/styles.css | 177 ++++++++++++++++++++++++++++++++------------ index.html | 23 +++--- js/app.js | 195 ++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 334 insertions(+), 61 deletions(-) diff --git a/css/styles.css b/css/styles.css index 1e7ea9c..aa031a7 100644 --- a/css/styles.css +++ b/css/styles.css @@ -185,6 +185,12 @@ html, body { } } +/* Interactive Tilt - responds to cursor/gyroscope, controlled by JavaScript */ +.logo--interactive { + transition: transform 0.15s ease-out; + will-change: transform; +} + /* All Full Logo Animations Combined */ .logo--all { animation: @@ -357,19 +363,25 @@ html, body { top: 20px; left: 50%; transform: translateX(-50%) translateY(-100px); - background: rgba(0, 0, 0, 0.85); + background: linear-gradient(135deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.08)); color: #fff; padding: 16px 24px; - border-radius: 8px; + border-radius: 16px; font-size: 14px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.3), + 0 2px 8px rgba(0, 0, 0, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.3), + inset 0 -1px 0 rgba(0, 0, 0, 0.1); z-index: 999; opacity: 0; - transition: all 0.3s ease; + transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); pointer-events: none; - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.1); + backdrop-filter: blur(40px) saturate(180%); + -webkit-backdrop-filter: blur(40px) saturate(180%); + border: 1px solid rgba(255, 255, 255, 0.25); + min-width: 200px; } .toast.show { @@ -378,27 +390,29 @@ html, body { } .toast-title { - font-weight: bold; + font-weight: 600; margin-bottom: 4px; - color: var(--gradient-color-1); + color: rgba(255, 255, 255, 0.95); + font-size: 14px; } .toast-message { font-size: 13px; - opacity: 0.9; + color: rgba(255, 255, 255, 0.85); } /* ===== Help Button ===== */ .help-button { position: fixed; - top: 20px; + bottom: 20px; right: 20px; width: 44px; height: 44px; + padding: 0; border-radius: 50%; - background: rgba(0, 0, 0, 0.75); - color: #fff; - border: 2px solid rgba(255, 255, 255, 0.2); + background: transparent; + color: rgba(255, 255, 255, 0.5); + border: 2px solid transparent; font-size: 24px; font-weight: bold; cursor: pointer; @@ -407,15 +421,23 @@ html, body { align-items: center; justify-content: center; transition: all 0.3s ease; - backdrop-filter: blur(10px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + backdrop-filter: none; + box-shadow: none; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); } .help-button:hover { - background: rgba(102, 126, 234, 0.9); - border-color: rgba(255, 255, 255, 0.4); + background: linear-gradient(135deg, rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0.1)); + border-color: rgba(255, 255, 255, 0.3); + color: #fff; transform: scale(1.1); - box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.4), + inset 0 -1px 0 rgba(0, 0, 0, 0.1); + backdrop-filter: blur(40px) saturate(180%); + -webkit-backdrop-filter: blur(40px) saturate(180%); + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); } .help-button:active { @@ -430,16 +452,21 @@ html, body { width: 500px; max-width: calc(100vw - 40px); max-height: calc(100vh - 120px); - background: rgba(20, 20, 30, 0.95); - border: 1px solid rgba(255, 255, 255, 0.15); - border-radius: 12px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); - backdrop-filter: blur(20px); + background: linear-gradient(135deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.05)); + border: 1px solid rgba(255, 255, 255, 0.25); + border-radius: 20px; + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.3), + 0 2px 8px rgba(0, 0, 0, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.3), + inset 0 -1px 0 rgba(0, 0, 0, 0.1); + backdrop-filter: blur(40px) saturate(180%); + -webkit-backdrop-filter: blur(40px) saturate(180%); z-index: 1000; opacity: 0; transform: translateY(-20px) scale(0.95); pointer-events: none; - transition: all 0.3s ease; + transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; } @@ -455,8 +482,10 @@ html, body { justify-content: space-between; align-items: center; padding: 20px 24px; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - background: rgba(102, 126, 234, 0.15); + border-bottom: 1px solid rgba(255, 255, 255, 0.15); + background: linear-gradient(135deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.05)); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); } .help-header h2 { @@ -469,22 +498,26 @@ html, body { .help-close { width: 32px; height: 32px; - border: none; - background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + background: linear-gradient(135deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.05)); color: #fff; font-size: 24px; line-height: 1; - border-radius: 6px; + border-radius: 8px; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); } .help-close:hover { - background: rgba(255, 255, 255, 0.2); + background: linear-gradient(135deg, rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0.1)); + border-color: rgba(255, 255, 255, 0.3); transform: rotate(90deg); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3); } .help-content { @@ -495,21 +528,25 @@ html, body { } .help-content::-webkit-scrollbar { - width: 8px; + width: 10px; } .help-content::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.05); - border-radius: 4px; + border-radius: 5px; + margin: 4px 0; } .help-content::-webkit-scrollbar-thumb { - background: rgba(102, 126, 234, 0.5); - border-radius: 4px; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.15)); + border-radius: 5px; + border: 2px solid transparent; + background-clip: padding-box; } .help-content::-webkit-scrollbar-thumb:hover { - background: rgba(102, 126, 234, 0.7); + background: linear-gradient(135deg, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.25)); + background-clip: padding-box; } .help-section { @@ -523,7 +560,7 @@ html, body { .help-section h3 { font-size: 14px; font-weight: 600; - color: var(--gradient-color-1); + color: rgba(255, 255, 255, 0.9); margin: 0 0 12px 0; text-transform: uppercase; letter-spacing: 0.5px; @@ -541,16 +578,38 @@ html, body { display: inline-block; min-width: 28px; padding: 4px 8px; - margin-right: 12px; - background: rgba(102, 126, 234, 0.2); - border: 1px solid rgba(102, 126, 234, 0.4); - border-radius: 4px; + margin: 0 6px; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.1)); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 6px; font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Courier New', monospace; font-size: 13px; font-weight: 600; text-align: center; - color: var(--gradient-color-1); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + color: rgba(255, 255, 255, 0.95); + box-shadow: + 0 2px 4px rgba(0, 0, 0, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.25); + cursor: pointer; + transition: all 0.2s ease; + user-select: none; + -webkit-user-select: none; +} + +.help-item kbd:hover { + background: linear-gradient(135deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.15)); + border-color: rgba(255, 255, 255, 0.4); + transform: translateY(-1px); + box-shadow: + 0 4px 8px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.35); +} + +.help-item kbd:active { + transform: translateY(0); + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.2); } .help-section-url { @@ -562,13 +621,13 @@ html, body { .help-code { background: rgba(0, 0, 0, 0.4); padding: 12px; - border-radius: 6px; + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.15); font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Courier New', monospace; font-size: 12px; - color: var(--gradient-color-3); + color: rgba(255, 255, 255, 0.85); margin-bottom: 16px; word-break: break-all; - border: 1px solid rgba(249, 147, 251, 0.2); } .help-params { @@ -582,7 +641,7 @@ html, body { } .help-params strong { - color: var(--gradient-color-1); + color: rgba(255, 255, 255, 0.9); font-weight: 600; margin-right: 8px; } @@ -650,6 +709,19 @@ html, body { .footer p { font-size: 12px; } + + .help-button { + bottom: 15px; + right: 15px; + font-size: 20px; + width: 40px; + height: 40px; + } + + .help-button:hover { + width: 40px; + height: 40px; + } } @media (max-width: 480px) { @@ -713,6 +785,19 @@ html, body { .footer p { font-size: 11px; } + + .help-button { + bottom: 12px; + right: 12px; + font-size: 18px; + width: 36px; + height: 36px; + } + + .help-button:hover { + width: 36px; + height: 36px; + } } /* ===== Accessibility ===== */ diff --git a/index.html b/index.html index e1e909d..dfc5f3c 100644 --- a/index.html +++ b/index.html @@ -12,7 +12,6 @@

Background

-
B Toggle background animation (static ↔ animated)
-
G Cycle gradient types (linear → radial → conic)
+
B Toggle background animation (static ↔ animated)
+
G Cycle gradient types (linear → radial → conic)

Logo Animations

-
L Cycle logo states (static → animated → full)
-
A Cycle logo body animations (shake → rotate → tilt → all)
-
E Cycle element animations (neon → color → explosion → fly → all)
+
L Cycle logo states (static → animated → full)
+
A Cycle logo body animations (shake → rotate → tilt → interactive → mixed → all)
+
E Cycle element animations (neon → color → explosion → fly → all)

Animation Speed

-
+ Increase speed (+0.25x)
-
- Decrease speed (-0.25x)
-
0 Reset speed to 1x
+
+ Increase speed (+0.25x)
+
- Decrease speed (-0.25x)
+
0 Reset speed to 1x

Other

-
R Reset all to defaults
-
H or ? Toggle this help
+
R Reset all to defaults
+
H or ? Toggle this help
@@ -68,7 +67,7 @@
bg: static | animated
gradient: linear | radial | conic
logo: static | animated | full
-
logoAnim: shake | rotate | tilt | all
+
logoAnim: shake | rotate | tilt | interactive | mixed | all
elementAnim: neon | color | explosion | fly | all
speed: 0.1 to 5 (default: 1)
diff --git a/js/app.js b/js/app.js index 6b10686..2f23b4e 100644 --- a/js/app.js +++ b/js/app.js @@ -11,7 +11,7 @@ bg: 'animated', // static | animated gradient: 'linear', // linear | radial | conic logo: 'full', // static | animated | full - logoAnim: 'tilt', // shake | rotate | tilt | all + logoAnim: 'mixed', // shake | rotate | tilt | interactive | mixed | all elementAnim: 'neon', // neon | color | explosion | fly | all speed: 0.5 // 0.5 (slow) - 3 (fast) }; @@ -95,6 +95,23 @@ hideHelp(); } }); + + // Make kbd elements clickable for touch devices + const kbdElements = document.querySelectorAll('.help-item kbd[data-key]'); + kbdElements.forEach(kbd => { + kbd.addEventListener('click', (e) => { + e.stopPropagation(); + const key = kbd.getAttribute('data-key'); + // Simulate keyboard event + const event = new KeyboardEvent('keydown', { + key: key, + code: key, + bubbles: true, + cancelable: true + }); + document.dispatchEvent(event); + }); + }); } // ===== Background Configuration ===== @@ -113,12 +130,170 @@ } } + // ===== Interactive Tilt Controller ===== + function createInteractiveTiltController(logoElement, mixedMode = false) { + let isActive = false; + let mode = 'cursor'; // 'cursor' or 'gyro' + let lastGyroTime = 0; + let lastCursorTime = 0; + let idleTimeout = null; + const SWITCH_TIMEOUT = 1000; // ms - time before switching modes + const IDLE_TIMEOUT = 2000; // ms - time with no movement before returning to tilt animation + + // Tilt ranges + const MAX_TILT_X = 20; // degrees + const MAX_TILT_Y = 20; // degrees + + function setInteractiveMode(active) { + if (mixedMode) { + if (active) { + // Switch to interactive control + logoElement.classList.remove('logo--tilt'); + logoElement.classList.add('logo--interactive'); + } else { + // Return to tilt animation + logoElement.classList.remove('logo--interactive'); + logoElement.classList.add('logo--tilt'); + logoElement.style.transform = ''; + } + } + } + + function resetIdleTimer() { + if (!mixedMode) return; + + if (idleTimeout) { + clearTimeout(idleTimeout); + } + + // Switch to interactive mode + setInteractiveMode(true); + + // Set timer to return to tilt animation after idle period + idleTimeout = setTimeout(() => { + setInteractiveMode(false); + }, IDLE_TIMEOUT); + } + + function handleMouseMove(e) { + if (!isActive) return; + + lastCursorTime = Date.now(); + + // Reset idle timer in mixed mode + resetIdleTimer(); + + // Auto-switch to cursor mode if gyro was active + if (mode === 'gyro' && (lastCursorTime - lastGyroTime) > SWITCH_TIMEOUT) { + mode = 'cursor'; + } + + if (mode === 'cursor') { + // Calculate cursor position (-1 to 1) + const x = (e.clientX / window.innerWidth) * 2 - 1; + const y = (e.clientY / window.innerHeight) * 2 - 1; + + // Apply tilt based on cursor position + const rotateY = x * MAX_TILT_Y; + const rotateX = -y * MAX_TILT_X; + + logoElement.style.transform = `perspective(400px) rotateY(${rotateY}deg) rotateX(${rotateX}deg)`; + } + } + + function handleDeviceOrientation(e) { + if (!isActive) return; + + // Check if we have valid gyroscope data + if (e.beta === null || e.gamma === null) return; + + lastGyroTime = Date.now(); + + // Reset idle timer in mixed mode + resetIdleTimer(); + + // Auto-switch to gyro mode if cursor was active + if (mode === 'cursor' && (lastGyroTime - lastCursorTime) > SWITCH_TIMEOUT) { + mode = 'gyro'; + } + + if (mode === 'gyro') { + // beta: front-to-back tilt (-180 to 180) + // gamma: left-to-right tilt (-90 to 90) + + // Normalize and clamp values + let rotateX = Math.max(-MAX_TILT_X, Math.min(MAX_TILT_X, e.beta / 4)); + let rotateY = Math.max(-MAX_TILT_Y, Math.min(MAX_TILT_Y, e.gamma / 3)); + + logoElement.style.transform = `perspective(400px) rotateY(${rotateY}deg) rotateX(${rotateX}deg)`; + } + } + + function requestPermission() { + // Request permission for iOS 13+ devices + if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') { + DeviceOrientationEvent.requestPermission() + .then(permissionState => { + if (permissionState === 'granted') { + window.addEventListener('deviceorientation', handleDeviceOrientation); + } + }) + .catch(console.error); + } else { + // Non-iOS or older browsers + window.addEventListener('deviceorientation', handleDeviceOrientation); + } + } + + return { + start() { + if (isActive) return; + isActive = true; + mode = 'cursor'; + lastCursorTime = Date.now(); + + // In mixed mode, start with tilt animation + if (mixedMode) { + setInteractiveMode(false); + } + + // Add cursor listener + document.addEventListener('mousemove', handleMouseMove); + + // Try to add gyroscope listener + requestPermission(); + }, + stop() { + if (!isActive) return; + isActive = false; + + // Clear idle timer + if (idleTimeout) { + clearTimeout(idleTimeout); + idleTimeout = null; + } + + // Remove listeners + document.removeEventListener('mousemove', handleMouseMove); + window.removeEventListener('deviceorientation', handleDeviceOrientation); + + // Reset transform + logoElement.style.transform = ''; + } + }; + } + // ===== Logo Full-Body Animation Configuration ===== function configureLogoAnimation(config) { const { logo, logoAnim } = config; // Remove all logo animation classes - elements.logo.classList.remove('logo--shake', 'logo--rotate', 'logo--tilt', 'logo--all'); + elements.logo.classList.remove('logo--shake', 'logo--rotate', 'logo--tilt', 'logo--interactive', 'logo--all'); + + // Stop interactive mode if active + if (window.interactiveTiltController) { + window.interactiveTiltController.stop(); + } // Apply logo animation if not static if (logo !== 'static') { @@ -130,6 +305,20 @@ elements.logo.classList.add('logo--rotate'); } else if (logoAnim === 'tilt') { elements.logo.classList.add('logo--tilt'); + } else if (logoAnim === 'interactive') { + elements.logo.classList.add('logo--interactive'); + // Start interactive tilt mode (pure interactive, no mixed mode) + if (!window.interactiveTiltController) { + window.interactiveTiltController = createInteractiveTiltController(elements.logo, false); + } + window.interactiveTiltController.start(); + } else if (logoAnim === 'mixed') { + // Mixed mode: starts with tilt animation, switches to interactive on movement + elements.logo.classList.add('logo--tilt'); + if (!window.interactiveTiltController) { + window.interactiveTiltController = createInteractiveTiltController(elements.logo, true); + } + window.interactiveTiltController.start(); } } } @@ -277,7 +466,7 @@ break; case 'a': // Cycle logo body animations - const logoAnims = ['shake', 'rotate', 'tilt', 'all']; + const logoAnims = ['shake', 'rotate', 'tilt', 'interactive', 'mixed', 'all']; const animIndex = logoAnims.indexOf(currentConfig.logoAnim); currentConfig.logoAnim = logoAnims[(animIndex + 1) % logoAnims.length]; break;