From fb29c1db9d0bfc73008ad262f63e7780547a3ca1 Mon Sep 17 00:00:00 2001 From: webbeef Date: Sun, 19 Jul 2026 19:24:32 -0700 Subject: [PATCH] ui: reduce animations to lower cpu usage Signed-off-by: webbeef --- ui/system/desktop/dock.css | 26 +++++--------------------- ui/system/mobile/pebble.css | 25 +++++-------------------- ui/system/mobile/pebble.js | 19 +++++++++++++++++++ ui/system/services/notifications.js | 23 +++++++++++++++++++++++ 4 files changed, 52 insertions(+), 41 deletions(-) diff --git a/ui/system/desktop/dock.css b/ui/system/desktop/dock.css index 670b06c..a1a0d0e 100644 --- a/ui/system/desktop/dock.css +++ b/ui/system/desktop/dock.css @@ -23,7 +23,6 @@ var(--workshop-shadow); /* Theme-color halo, refreshed live from the active view. */ filter: drop-shadow(0 4px 16px var(--workshop-theme-tint)); - animation: dock-breath 5.5s ease-in-out infinite; transition: transform var(--workshop-morph-duration) var(--workshop-easing), opacity 0.28s var(--workshop-easing), @@ -31,22 +30,6 @@ max-width: calc(100vw - 28px); } -@keyframes dock-breath { - 0%, - 100% { - box-shadow: - inset 0 1px 0 oklch(99% 0.005 105 / 0.7), - var(--workshop-shadow); - border-radius: 22px 24px 22px 23px; - } - 50% { - box-shadow: - inset 0 1px 0 oklch(99% 0.005 105 / 0.85), - var(--workshop-shadow-hi); - border-radius: 24px 22px 23px 22px; - } -} - /* Mod+. translates the dock down rather than fading */ body[data-dock-hidden] .dock { transform: translateX(-50%) @@ -240,8 +223,11 @@ body[data-dock-hidden] .dock { justify-content: center; line-height: 1; pointer-events: none; - /* Soft pulse so a fresh notification draws the eye. */ - animation: dot-pulse 3.2s ease-out infinite; + /* Pulse a few times so a fresh notification draws the eye, then rest static. + Not `infinite`: an always-on pulse makes Servo rebuild the display list + every frame and pegs CPU. The keyframes end at the resting badge, so no + fill-mode is needed. */ + animation: dot-pulse 3.2s ease-out 3; box-shadow: 0 0 0 0 oklch(58% 0.18 25 / 0.5); } @@ -277,8 +263,6 @@ body[data-dock-position="right"] .dock { height: auto; max-width: none; max-height: calc(100vh - var(--topbar-height) - 28px); - /* Disable the corner-rotation breath in vertical orientation. */ - animation: none; } body[data-dock-position="left"] .dock { diff --git a/ui/system/mobile/pebble.css b/ui/system/mobile/pebble.css index e0e392e..434c5a5 100644 --- a/ui/system/mobile/pebble.css +++ b/ui/system/mobile/pebble.css @@ -96,22 +96,6 @@ :host([data-state="ambient"]) .pebble { touch-action: none; - animation: pebble-breath 5.5s ease-in-out infinite; -} - -@keyframes pebble-breath { - 0%, - 100% { - box-shadow: var(--pebble-shadow); - border-radius: 22px 24px 22px 23px; - } - 50% { - box-shadow: - 0 1px 2px oklch(35% 0.03 105 / 0.22), - 0 8px 22px oklch(35% 0.03 105 / 0.22), - 0 22px 56px oklch(35% 0.03 105 / 0.16); - border-radius: 24px 22px 23px 22px; - } } :host([data-pressing]) .pebble { @@ -144,7 +128,6 @@ transform: translateX(0); cursor: default; touch-action: none; - animation: none; } :host([data-state="command"]) .pebble { @@ -156,7 +139,6 @@ transform: translateX(0); cursor: default; touch-action: none; - animation: none; box-shadow: 0 -2px 6px oklch(35% 0.03 105 / 0.16), 0 -16px 48px oklch(35% 0.03 105 / 0.18); @@ -261,8 +243,11 @@ .meta-notif.active { display: block; - /* Slightly slower base period; the keyframes carry the rhythm. */ - animation: dot-pulse 3.2s ease-out infinite; + /* Pulse a few times to catch the eye when a notification arrives, then rest + static. Not `infinite`: an always-on pulse makes Servo rebuild the display + list every frame and pegs CPU (same reason the ambient pebble no longer + breathes). The keyframes end at the resting dot, so no fill-mode is needed. */ + animation: dot-pulse 3.2s ease-out 3; box-shadow: 0 0 0 0 oklch(48% 0.12 155 / 0.5); } diff --git a/ui/system/mobile/pebble.js b/ui/system/mobile/pebble.js index 2662088..a5ae451 100644 --- a/ui/system/mobile/pebble.js +++ b/ui/system/mobile/pebble.js @@ -237,6 +237,7 @@ export class MobilePebble extends LitElement { // Notifications service `registerPanel` contract: // - sets `panel.notifications` directly (handled by the Lit reactive prop) // - sets `panel.open = false` to dismiss (collapses pebble to ambient) + // - calls `panel.pulseNotification()` on a new arrival to replay the dot get open() { return this.state !== STATE.AMBIENT; } @@ -247,6 +248,24 @@ export class MobilePebble extends LitElement { } } + /** + * Replay the notification dot's finite attention pulse. The notifications + * service calls this when a new notification arrives so it re-grabs the eye + * even when the dot is already showing. Awaits the pending render so the dot + * exists and is `.active`, then restarts the CSS animation via a reflow (the + * same idiom the desktop badge and the topbar `.flash` buttons use). + */ + async pulseNotification() { + await this.updateComplete; + const dot = this.renderRoot?.querySelector(".meta-notif"); + if (!dot || !dot.classList.contains("active")) { + return; + } + dot.style.animation = "none"; + void dot.offsetWidth; + dot.style.animation = ""; + } + // Keyboard-shortcut friendly helpers (used by services/shortcuts.js) toggleCommand() { this._setState( diff --git a/ui/system/services/notifications.js b/ui/system/services/notifications.js index aa3df2c..b613580 100644 --- a/ui/system/services/notifications.js +++ b/ui/system/services/notifications.js @@ -36,6 +36,28 @@ function updatePanels() { updateBadge(); } +/** + * A newly arrived notification should re-grab attention even when the badge is + * already showing. Restart the finite attention pulse on the dot(s): clearing + * the animation, forcing a reflow, then restoring it makes CSS replay it (the + * same reflow idiom the topbar / pin `.flash` buttons use). Called only from + * `add`, never on dismiss, so removing notifications stays quiet. + */ +function pulseBadge() { + // Desktop dock dot lives directly in the main document. + const badge = document.getElementById("notifications-badge"); + if (badge && !badge.classList.contains("hidden")) { + badge.style.animation = "none"; + void badge.offsetWidth; + badge.style.animation = ""; + } + // Other surfaces (e.g. the mobile pebble) own their dot inside their own + // shadow DOM, so let each panel replay it. + for (const panel of panels) { + panel.pulseNotification?.(); + } +} + export function add(notification) { if (!notification.timestamp) { notification.timestamp = Date.now(); @@ -62,6 +84,7 @@ export function add(notification) { } updatePanels(); + pulseBadge(); if (notification.title) { toastManager?.add(notification.title); -- 2.51.2