The scene stops heating the MacBook
"Terribly high CPU usage, the MacBook gets really hot" - a fanless MacBook Air on a 144Hz screen. Measured on that machine with headed engines (process CPU plus the per-process GPU time macOS keeps): an idle Aero library cost LibreWolf ~110% CPU and ~230ms/s of GPU, the study feed ~130%, Chromium up to ~57% CPU and ~350ms/s of GPU, Safari ~220-250ms/s of WindowServer GPU. Priced one by one: - the gradient mask on .ice-floats re-rendered the animated layer offscreen on every frame (~75 points of Gecko CPU). The floats now rise through a plain rectangular band clear of the header, the bottom chrome and the rail, and fade in and out inside it, so the clip never cuts a visible one; - the overlay grain forced a backdrop readback per frame for a texture that measured a mean <=0.11/255 difference: removed; - the invisible blob drift made every frame a full-screen redraw: the blobs are painted once; - static glass (answer tiles, feedback panel, chips, inputs, dropzones, lists, segmented controls, Aero hint chips) re-blurred the moving scene every frame: only floating glass blurs now, and Aero's baked chat chrome drops a live blur that never showed under the bake; - the scene also parks while the window has no focus and under any dialog or sheet scrim (.scene-pause); the loading icon is a compositor-run CSS animation instead of a framer-motion main-thread loop. After, focused and animating: LibreWolf library 109% -> 33% CPU, study 134% -> 34%, chat 130% -> 42%; Chromium GPU 2.5-4.4x lower with 7-22 points less CPU; Safari's WindowServer GPU -20-26% in Aero, about half in dark. Unfocused or under a sheet: nothing. bubbles-test sweeps every float through 41 phases against the band and pins the parking and the cost contract; tests/heat-probe.mjs (macOS) repeats the measurement; the suites find Playwright's Chromium on macOS. All 15 suites pass, lint clean, the contrast scan lost a finding. Version 0.6.7, SW v89. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b5c4f237cd
commit
bfc0f806b9
27 changed files with 613 additions and 249 deletions
277
app/globals.css
277
app/globals.css
|
|
@ -197,14 +197,12 @@
|
|||
--ice-base-bottom: #e6f1fb;
|
||||
--ice-blob-opacity: 0.045;
|
||||
--ice-orb-opacity: 0.22;
|
||||
--ice-grain-opacity: 0.05;
|
||||
}
|
||||
.dark {
|
||||
--ice-base-top: #070b16;
|
||||
--ice-base-bottom: #0c1426;
|
||||
--ice-blob-opacity: 0.1;
|
||||
--ice-orb-opacity: 0.38;
|
||||
--ice-grain-opacity: 0.045;
|
||||
}
|
||||
|
||||
/* Frutiger Aero — the third theme (Settings → Theme). An underwater scene:
|
||||
|
|
@ -265,7 +263,6 @@
|
|||
--ice-base-bottom: #0a5490;
|
||||
--ice-blob-opacity: 0.5;
|
||||
--ice-orb-opacity: 0.6;
|
||||
--ice-grain-opacity: 0.02;
|
||||
}
|
||||
|
||||
/* The scene is the wallpaper itself (a Frutiger Aero scene made with Gemini,
|
||||
|
|
@ -321,14 +318,17 @@
|
|||
}
|
||||
|
||||
/* Rising bubbles. Hidden entirely in the other themes, so they cost nothing
|
||||
there; under reduced motion they sit still instead of rising. */
|
||||
there; under reduced motion they sit still instead of rising. Each one
|
||||
rises from the bottom of the open band to its top (see .ice-floats) and
|
||||
fades in and out inside it, so the band's clip never cuts a visible
|
||||
bubble. */
|
||||
.ice-bubble {
|
||||
display: none;
|
||||
}
|
||||
.aero .ice-bubble {
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: -14vh;
|
||||
bottom: 0;
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
background:
|
||||
|
|
@ -351,16 +351,13 @@
|
|||
animation-delay: var(--bubble-delay, 0s);
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.ice-paused .ice-bubble {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
@keyframes aero-bubble-rise {
|
||||
0% { transform: translate3d(0, 0, 0) scale(0.85); opacity: 0; }
|
||||
6% { opacity: var(--bubble-opacity, 0.9); }
|
||||
50% { transform: translate3d(var(--bubble-sway, 3vw), -52vh, 0) scale(1); }
|
||||
94% { opacity: var(--bubble-opacity, 0.9); }
|
||||
100% { transform: translate3d(calc(var(--bubble-sway, 3vw) * -0.5), -104vh, 0) scale(1.06); opacity: 0; }
|
||||
18% { opacity: var(--bubble-opacity, 0.9); }
|
||||
50% { transform: translate3d(var(--bubble-sway, 3vw), calc(var(--ice-rise) * 0.5), 0) scale(1); }
|
||||
90% { opacity: var(--bubble-opacity, 0.9); }
|
||||
100% { transform: translate3d(calc(var(--bubble-sway, 3vw) * -0.5), calc(var(--ice-rise) + 3%), 0) scale(1.06); opacity: 0; }
|
||||
}
|
||||
|
||||
.aero .ice-bubble-1 { width: 104px; height: 104px; left: 7%; --bubble-duration: 19s; --bubble-delay: -2s; --bubble-sway: 4vw; }
|
||||
|
|
@ -384,15 +381,14 @@
|
|||
|
||||
/* Helper lines that sit straight on the wallpaper (the keyboard tip, the AI
|
||||
note) need their own surface in Aero: dark text on the grass measured
|
||||
2.5:1. A small glass chip fixes it without hiding the wallpaper. */
|
||||
2.5:1. A small glass chip fixes it without hiding the wallpaper — the
|
||||
shared frosted recipe below, not a live blur. */
|
||||
.aero .hint-line {
|
||||
display: inline-block;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 0.75rem;
|
||||
background: var(--glass-bg);
|
||||
background-color: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
/* Same trap as the buttons above: use the Tailwind utilities. */
|
||||
@apply backdrop-blur-md backdrop-saturate-150;
|
||||
}
|
||||
|
||||
/* Bright white rims on the study/homework segments (the frosted fill itself
|
||||
|
|
@ -454,6 +450,7 @@
|
|||
.aero .chat-empty,
|
||||
.aero .chat-composer-band,
|
||||
.aero .segmented,
|
||||
.aero .hint-line,
|
||||
/* Hand-rolled frost fills in the study, exam, chat and homework flows:
|
||||
white/xx utilities and the surface tokens — without this they render as
|
||||
flat white chips next to the glass. */
|
||||
|
|
@ -484,6 +481,15 @@
|
|||
background-repeat: no-repeat, no-repeat, no-repeat;
|
||||
}
|
||||
|
||||
/* The chat header and composer are floating glass (a live backdrop blur),
|
||||
but the bake above is opaque: the blur under it never shows, yet it still
|
||||
re-ran on every frame. The touch fallback below drops the bake, so it
|
||||
brings the blur back there. */
|
||||
.aero .chat-header,
|
||||
.aero .chat-composer-band {
|
||||
@apply backdrop-filter-none;
|
||||
}
|
||||
|
||||
/* The wizard's phone action bar: Aero's page token is the saturated water
|
||||
blue, which read as a heavy slab over the photo — give it the same thin
|
||||
glass as everything else (it already has a real backdrop blur). */
|
||||
|
|
@ -680,10 +686,15 @@
|
|||
.aero .chat-header,
|
||||
.aero .chat-empty,
|
||||
.aero .chat-composer-band,
|
||||
.aero .segmented {
|
||||
.aero .segmented,
|
||||
.aero .hint-line {
|
||||
background-image: none;
|
||||
background-color: rgb(255 255 255 / 0.32);
|
||||
}
|
||||
.aero .chat-header,
|
||||
.aero .chat-composer-band {
|
||||
@apply backdrop-blur-md;
|
||||
}
|
||||
/* Reading surfaces keep a touch more body than the chrome — long answers
|
||||
have to stay comfortable over grass and water — but they are still glass,
|
||||
not white panes. */
|
||||
|
|
@ -899,8 +910,9 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) {
|
|||
|
||||
/* The key gate rides on the shared ice scene (see .ice-scene). */
|
||||
|
||||
/* The fixed ice scene: navy/pale base, drifting glacier blobs, glossy ice
|
||||
orbs and a whisper of grain. Purely decorative. */
|
||||
/* The fixed ice scene: navy/pale base, soft glacier blobs and glossy ice
|
||||
orbs. Purely decorative. Only the orbs (and Aero's bubbles) move; the base
|
||||
and the blobs are painted once. */
|
||||
.ice-scene {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
|
@ -914,61 +926,45 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) {
|
|||
position: absolute;
|
||||
border-radius: 50%;
|
||||
opacity: var(--ice-blob-opacity);
|
||||
will-change: transform;
|
||||
/* No filter: blur() here on purpose. A radial gradient fading to
|
||||
transparent is already a soft blob, and blurring a huge layer that is
|
||||
also being animated forces a re-rasterisation every frame (measured
|
||||
150ms vs 33ms per frame in a software renderer). The falloff stops below
|
||||
keep the same softness. */
|
||||
transparent is already a soft blob, and blurring a huge layer forces a
|
||||
re-rasterisation (measured 150ms vs 33ms per frame in a software
|
||||
renderer). The falloff stops below keep the same softness.
|
||||
No drift either: a few vw over 44-60s never showed at this opacity, but
|
||||
four viewport-sized moving layers turned every frame into a full-screen
|
||||
redraw. */
|
||||
}
|
||||
.ice-blob-a {
|
||||
width: 52vw; height: 52vw; min-width: 24rem; min-height: 24rem;
|
||||
top: -14vh; left: -10vw;
|
||||
background: radial-gradient(circle, rgb(56 189 248 / 0.9) 0%, rgb(56 189 248 / 0.45) 38%, transparent 72%);
|
||||
animation: ice-drift-a 44s ease-in-out infinite;
|
||||
}
|
||||
.ice-blob-b {
|
||||
width: 46vw; height: 46vw; min-width: 22rem; min-height: 22rem;
|
||||
top: 4vh; right: -12vw;
|
||||
background: radial-gradient(circle, rgb(96 165 250 / 0.9) 0%, rgb(96 165 250 / 0.45) 38%, transparent 72%);
|
||||
animation: ice-drift-b 52s ease-in-out infinite;
|
||||
}
|
||||
.ice-blob-c {
|
||||
width: 48vw; height: 48vw; min-width: 22rem; min-height: 22rem;
|
||||
bottom: -16vh; left: 14vw;
|
||||
background: radial-gradient(circle, rgb(129 140 248 / 0.9) 0%, rgb(129 140 248 / 0.42) 38%, transparent 74%);
|
||||
animation: ice-drift-c 60s ease-in-out infinite;
|
||||
}
|
||||
.ice-blob-d {
|
||||
width: 34vw; height: 34vw; min-width: 16rem; min-height: 16rem;
|
||||
bottom: 2vh; right: 6vw;
|
||||
background: radial-gradient(circle, rgb(56 189 248 / 0.85) 0%, rgb(56 189 248 / 0.4) 38%, transparent 74%);
|
||||
animation: ice-drift-a 48s ease-in-out infinite reverse;
|
||||
}
|
||||
|
||||
@keyframes ice-drift-a {
|
||||
0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
|
||||
50% { transform: translate3d(4vw, 3vh, 0) scale(1.08); }
|
||||
}
|
||||
@keyframes ice-drift-b {
|
||||
0%, 100% { transform: translate3d(0, 0, 0) scale(1.05); }
|
||||
50% { transform: translate3d(-5vw, 4vh, 0) scale(1); }
|
||||
}
|
||||
@keyframes ice-drift-c {
|
||||
0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
|
||||
50% { transform: translate3d(3vw, -4vh, 0) scale(1.1); }
|
||||
}
|
||||
|
||||
/* Glossy ice orbs: slow bubbles that rise from just below the bottom edge to
|
||||
above the top edge, swaying gently and fading in/out near the edges so the
|
||||
loop never pops. Only transform + opacity animate, each orb on its own
|
||||
/* Glossy ice orbs: slow bubbles that rise through the open band (see
|
||||
.ice-floats), swaying gently and fading in/out at its edges so the loop
|
||||
never pops. Only transform + opacity animate, each orb on its own
|
||||
composited layer (will-change: transform): no filter/backdrop-filter, and no
|
||||
animated size, offsets or shadow. Per-orb duration, delay (negative, so the
|
||||
scene is already populated on load), horizontal position and sway keep them
|
||||
out of sync. */
|
||||
.ice-orb {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
bottom: 0;
|
||||
border-radius: 50%;
|
||||
opacity: var(--ice-orb-opacity);
|
||||
will-change: transform;
|
||||
|
|
@ -989,120 +985,58 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) {
|
|||
.ice-orb-4 { width: 52px; height: 52px; right: 28%; --ice-orb-duration: 64s; --ice-orb-delay: -52s; --ice-sway: -4vw; }
|
||||
.ice-orb-5 { width: 120px; height: 120px; left: 38%; --ice-orb-duration: 52s; --ice-orb-delay: -27s; --ice-sway: 3vw; }
|
||||
|
||||
/* The rise: -100vh - 100% clears the top edge (100% = the orb's own height);
|
||||
the sway crosses the path twice and the scale drifts slightly. Opacity
|
||||
reaches the token value by 12% and holds it until 88%, so the fades happen
|
||||
while the orb is still partly outside the scene. */
|
||||
/* The rise: --ice-rise lifts an orb from the bottom of the band until its
|
||||
top edge meets the band's top (100% = the orb's own height; the extra 3%
|
||||
at the end takes back what the 1.06 scale adds above it); the sway crosses
|
||||
the path twice and the scale drifts slightly. Opacity reaches the token
|
||||
value by 18% and holds it until 90%, so a visible orb never touches the
|
||||
band's edges. */
|
||||
@keyframes ice-rise {
|
||||
0% { transform: translate3d(0, 0, 0) scale(0.94); opacity: 0; }
|
||||
12% { opacity: var(--ice-orb-opacity); }
|
||||
32% { transform: translate3d(var(--ice-sway, 0px), calc(-32vh - 32%), 0) scale(0.97); }
|
||||
58% { transform: translate3d(calc(var(--ice-sway, 0px) * -0.55), calc(-58vh - 58%), 0) scale(1.02); }
|
||||
80% { transform: translate3d(calc(var(--ice-sway, 0px) * 0.4), calc(-80vh - 80%), 0) scale(1.04); }
|
||||
88% { opacity: var(--ice-orb-opacity); }
|
||||
100% { transform: translate3d(0, calc(-100vh - 100%), 0) scale(1.06); opacity: 0; }
|
||||
18% { opacity: var(--ice-orb-opacity); }
|
||||
32% { transform: translate3d(var(--ice-sway, 0px), calc(var(--ice-rise) * 0.32), 0) scale(0.97); }
|
||||
58% { transform: translate3d(calc(var(--ice-sway, 0px) * -0.55), calc(var(--ice-rise) * 0.58), 0) scale(1.02); }
|
||||
80% { transform: translate3d(calc(var(--ice-sway, 0px) * 0.4), calc(var(--ice-rise) * 0.8), 0) scale(1.04); }
|
||||
90% { opacity: var(--ice-orb-opacity); }
|
||||
100% { transform: translate3d(0, calc(var(--ice-rise) + 3%), 0) scale(1.06); opacity: 0; }
|
||||
}
|
||||
|
||||
/* The rising orbs and bubbles are clipped to the scene, but the floating
|
||||
glass panels (the rail, the chat header/composer, the tab bar) sit on top of
|
||||
it — a bubble reaching one of those edges looked sliced by a straight line.
|
||||
This layer keeps them inside the open middle band: they fade in below the
|
||||
top chrome, out above the bottom chrome (the values come from where the
|
||||
header and the composer actually end) and, on desktop, stay out of the rail
|
||||
strip. Static under reduced motion: nothing moves there, and the parked
|
||||
orbs keep their spots. */
|
||||
/* The open band the orbs and bubbles rise through. The floating glass
|
||||
panels (the rail, the chat header/composer, the tab bar) sit on top of the
|
||||
scene, and a bubble reaching one of their edges looked sliced by a straight
|
||||
line — so the floats stay clear of them: below the top chrome, above the
|
||||
bottom chrome (the values come from where the header and the composer
|
||||
actually end) and, on desktop, right of the rail strip. Every float starts
|
||||
and ends inside the band at zero opacity, so this clip never cuts a visible
|
||||
one. It is a plain rectangular clip on purpose: the gradient mask it
|
||||
replaces re-rendered the whole animated layer offscreen on every frame
|
||||
(Gecko on a 144Hz screen: ~114% CPU idle on the library, ~39% without the
|
||||
mask). Static under reduced motion: nothing moves there, and the parked
|
||||
orbs keep their full-screen spots. */
|
||||
.ice-floats {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
inset: 68px 0 172px;
|
||||
overflow: hidden;
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0,
|
||||
transparent 68px,
|
||||
black 150px,
|
||||
black calc(100% - 330px),
|
||||
transparent calc(100% - 172px)
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0,
|
||||
transparent 68px,
|
||||
black 150px,
|
||||
black calc(100% - 330px),
|
||||
transparent calc(100% - 172px)
|
||||
);
|
||||
--ice-rise: calc(100% - 100vh + 240px);
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.ice-floats {
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0,
|
||||
transparent 68px,
|
||||
black 150px,
|
||||
black calc(100% - 300px),
|
||||
transparent calc(100% - 151px)
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0,
|
||||
transparent 68px,
|
||||
black 150px,
|
||||
black calc(100% - 300px),
|
||||
transparent calc(100% - 151px)
|
||||
);
|
||||
bottom: 151px;
|
||||
--ice-rise: calc(100% - 100vh + 219px);
|
||||
}
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
/* …and the rail's strip (plus its gutter) stays clear too. */
|
||||
.ice-floats {
|
||||
-webkit-mask-image:
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
transparent 0,
|
||||
transparent 68px,
|
||||
black 150px,
|
||||
black calc(100% - 300px),
|
||||
transparent calc(100% - 151px)
|
||||
),
|
||||
linear-gradient(to right, transparent 0, transparent 260px, black 360px);
|
||||
mask-image:
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
transparent 0,
|
||||
transparent 68px,
|
||||
black 150px,
|
||||
black calc(100% - 300px),
|
||||
transparent calc(100% - 151px)
|
||||
),
|
||||
linear-gradient(to right, transparent 0, transparent 260px, black 360px);
|
||||
-webkit-mask-composite: destination-in;
|
||||
mask-composite: intersect;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ice-floats { -webkit-mask-image: none !important; mask-image: none !important; }
|
||||
}
|
||||
html.reduce-motion .ice-floats {
|
||||
-webkit-mask-image: none !important;
|
||||
mask-image: none !important;
|
||||
}
|
||||
|
||||
/* Frosted grain over the whole scene. */
|
||||
.ice-grain {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: var(--ice-grain-opacity);
|
||||
mix-blend-mode: overlay;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||
/* …and the rail's strip plus its gutter (268px, see Sidebar). */
|
||||
.ice-floats { left: 268px; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ice-blob { animation: none; }
|
||||
.ice-floats { inset: 0; }
|
||||
/* Bubbles sit still (and visible) instead of rising. */
|
||||
.aero .ice-bubble { animation: none; opacity: 0.55; transform: translate3d(0, -60vh, 0); }
|
||||
.aero .ice-bubble { animation: none; opacity: 0.55; transform: translate3d(0, -46vh, 0); }
|
||||
/* Orbs go back to the pre-rise static layout (a paused animation is not
|
||||
applied at all in Chrome, which would leave them parked below the fold)
|
||||
and give their composited layers back. */
|
||||
applied at all in Chrome, which would leave them parked at the bottom of
|
||||
the band) and give their composited layers back. */
|
||||
.ice-orb {
|
||||
animation: none;
|
||||
will-change: auto;
|
||||
|
|
@ -1114,15 +1048,19 @@ html.reduce-motion .ice-floats {
|
|||
.ice-orb-5 { top: auto; bottom: 16%; left: 38%; }
|
||||
}
|
||||
|
||||
/* Background tab: park every scene animation, so an invisible page costs no
|
||||
compositor work. */
|
||||
.ice-paused .ice-orb,
|
||||
.ice-paused .ice-blob {
|
||||
/* Park every scene animation while nobody is watching it, so the page costs
|
||||
no compositor work: a background tab or a window without focus (IceBackdrop
|
||||
sets .ice-paused), and any dialog or sheet over the page — its scrim
|
||||
carries .scene-pause, and the blur of the scrim and the sheet would
|
||||
otherwise re-run on every frame of the scene moving behind them. */
|
||||
.ice-paused :is(.ice-orb, .ice-bubble),
|
||||
body:has(.scene-pause) .ice-scene :is(.ice-orb, .ice-bubble) {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
/* Phones: fewer, softer blobs so the drift animation stays cheap. Unlayered
|
||||
on purpose — layered rules would lose to the .ice-blob declarations above. */
|
||||
/* Phones and tablets: fewer blobs and orbs, so the scene stays calm and
|
||||
cheap. Unlayered on purpose — layered rules would lose to the .ice-blob
|
||||
declarations above. */
|
||||
@media (max-width: 1023px) {
|
||||
.ice-blob-c,
|
||||
.ice-blob-d,
|
||||
|
|
@ -1183,13 +1121,16 @@ html.reduce-motion .ice-floats {
|
|||
the tint below plus a backdrop blur — the blur is what reads as glass
|
||||
when content scrolls underneath (the app header, the study chrome, the
|
||||
tab bar, a sheet over the page).
|
||||
- **Static glass** (cards, the rail, the gate, the study prompt card): the
|
||||
exact same tint, rim, highlight and shadow, but **no** backdrop-filter.
|
||||
On a smooth gradient the blur is invisible — and because the ice scene
|
||||
animates, every blurred surface re-runs the filter on every frame.
|
||||
Measured on desktop (2x throttle, software renderer): p50 frame time
|
||||
66ms while the static surfaces blurred the scene, 33ms without them,
|
||||
17ms with the scene's animation also stopped.
|
||||
- **Static glass** (cards, the rail, the gate, the study prompt card, and
|
||||
everything that sits in the page flow: answer tiles, chips, inputs,
|
||||
dropzones, grouped lists, segmented controls): the exact same tint,
|
||||
rim, highlight and shadow, but **no** backdrop-filter. On a smooth
|
||||
gradient the blur is invisible — and because the ice scene animates,
|
||||
every blurred surface re-runs the filter on every frame. Measured on
|
||||
desktop (2x throttle, software renderer): p50 frame time 66ms while the
|
||||
static surfaces blurred the scene, 33ms without them, 17ms with the
|
||||
scene's animation also stopped; on a 144Hz MacBook screen the study
|
||||
feed's answer tiles alone cost Chromium ~20% CPU.
|
||||
|
||||
Nested glass (chips, inputs) never adds a second blur layer. Tailwind's
|
||||
backdrop utilities emit the -webkit- prefix too (hand-written
|
||||
|
|
@ -1300,7 +1241,6 @@ html.reduce-motion .ice-floats {
|
|||
/* Inset grouped list ---------------------------------------------------- */
|
||||
.list-group {
|
||||
border-radius: var(--radius-control);
|
||||
@apply backdrop-blur-lg backdrop-saturate-150;
|
||||
background: color-mix(in oklab, var(--surface) 82%, transparent);
|
||||
border: 1px solid var(--hairline);
|
||||
overflow: hidden;
|
||||
|
|
@ -1349,7 +1289,6 @@ html.reduce-motion .ice-floats {
|
|||
gap: 2px;
|
||||
padding: 3px;
|
||||
border-radius: var(--radius-control);
|
||||
@apply backdrop-blur-sm;
|
||||
background: var(--surface-sunken);
|
||||
border: 1px solid var(--hairline);
|
||||
}
|
||||
|
|
@ -1741,6 +1680,26 @@ html.reduce-motion .ice-floats {
|
|||
.app-shell-in { animation: none; }
|
||||
}
|
||||
|
||||
/* The loading screen's icon: a slow rock and swell for as long as a model
|
||||
runs. CSS on the individual transform properties, so the compositor runs
|
||||
it — the framer-motion loop it replaces rewrote the transform from the
|
||||
main thread on every frame. */
|
||||
@keyframes loading-rock {
|
||||
0%, 100% { rotate: 0deg; }
|
||||
33.333% { rotate: 8deg; }
|
||||
66.667% { rotate: -8deg; }
|
||||
}
|
||||
@keyframes loading-swell {
|
||||
0%, 100% { scale: 1; }
|
||||
50% { scale: 1.08; }
|
||||
}
|
||||
.loading-bob {
|
||||
animation: loading-rock 3.2s ease-in-out infinite, loading-swell 3.2s ease-in-out infinite;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.loading-bob { animation: none; }
|
||||
}
|
||||
|
||||
/* Phones: a quick fade when the study feed takes over (the generation
|
||||
hand-off), so the result doesn't hard-cut in. */
|
||||
@media (max-width: 639px) {
|
||||
|
|
@ -1802,7 +1761,9 @@ html.reduce-motion .ice-floats {
|
|||
every CSS animation/transition collapses to nothing. framer-motion gets
|
||||
told separately (components/MotionPreference).
|
||||
--------------------------------------------------------------------------- */
|
||||
html.reduce-motion .ice-blob,
|
||||
html.reduce-motion .ice-floats {
|
||||
inset: 0;
|
||||
}
|
||||
html.reduce-motion .ice-orb,
|
||||
html.reduce-motion .ice-bubble {
|
||||
animation: none !important;
|
||||
|
|
@ -1810,7 +1771,7 @@ html.reduce-motion .ice-bubble {
|
|||
}
|
||||
html.reduce-motion.aero .ice-bubble {
|
||||
opacity: 0.55;
|
||||
transform: translate3d(0, -60vh, 0);
|
||||
transform: translate3d(0, -46vh, 0);
|
||||
}
|
||||
html.reduce-motion .ice-orb-1 { top: 14%; left: 12%; }
|
||||
html.reduce-motion .ice-orb-2 { top: 58%; left: 4%; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue