diff --git a/openpilot/starpilot/galaxy/tests/test_ambient_stars.mjs b/openpilot/starpilot/galaxy/tests/test_ambient_stars.mjs index 17baef574a..79bcb49cd2 100644 --- a/openpilot/starpilot/galaxy/tests/test_ambient_stars.mjs +++ b/openpilot/starpilot/galaxy/tests/test_ambient_stars.mjs @@ -1,19 +1,40 @@ import assert from "node:assert/strict" +import { readFile } from "node:fs/promises" import { test } from "node:test" import { spawnAmbientStars } from "../web/js/ambient-stars.js" -test("ambient stars populate the decorative layer once without changing app content", () => { +test("ambient stars populate one lightweight texture per star", () => { const children = [] const background = { - ownerDocument: { createElement: (tag) => ({ tag, style: {} }) }, + ownerDocument: { createElement: (tag) => { + const style = {} + Object.defineProperty(style, "setProperty", { value: (name, value) => { style[name] = value } }) + return { tag, style } + } }, querySelector: () => children.find((child) => child.className === "galaxy-hero") || null, appendChild: (child) => children.push(child), } assert.equal(spawnAmbientStars(background, () => 0.5), 14) assert.equal(children.length, 14) assert.deepEqual(children[0], { tag: "i", className: "galaxy-hero", - style: { left: "50.00%", top: "50.00%", animationDelay: "2.00s", width: "2px", height: "2px" } }) + style: { left: "50.00%", top: "50.00%", animationDelay: "2.00s", "--galaxy-star-core": "2px" } }) assert.equal(spawnAmbientStars(background, () => 0), 0) assert.equal(children.length, 14) assert.equal(spawnAmbientStars(null), 0) }) +test("ambient stars use a stable texture without promoting the static backdrop", async () => { + const css = await readFile(new URL("../web/css/galaxy.css", import.meta.url), "utf8") + const backdrop = css.slice(css.indexOf("#galaxy-bg {"), css.indexOf("#galaxy-bg::before")) + const starField = css.slice(css.indexOf("#galaxy-bg::before"), css.indexOf("#galaxy-bg .galaxy-hero")) + const star = css.slice(css.indexOf("#galaxy-bg .galaxy-hero {"), css.indexOf("@keyframes galaxy-twinkle")) + + assert.doesNotMatch(backdrop, /will-change|translateZ/) + assert.match(starField, /inset: 0/) + assert.doesNotMatch(starField, /height:\s*200%/) + assert.match(star, /radial-gradient/) + assert.doesNotMatch(star, /box-shadow/) + assert.match(css, /rotateZ\(0\.05deg\) scale\(0\.9\)/) + assert.match(css, /rotateZ\(0\.05deg\) scale\(1\.2\)/) + assert.match(css, /body:has\([^)]*\.gx-route-workspace[^)]*\) #galaxy-bg \.galaxy-hero\s*{\s*display: none;/) + assert.doesNotMatch(css, /animation-play-state/) +}) diff --git a/openpilot/starpilot/galaxy/web/css/galaxy.css b/openpilot/starpilot/galaxy/web/css/galaxy.css index 50342282f4..01b235bcd1 100644 --- a/openpilot/starpilot/galaxy/web/css/galaxy.css +++ b/openpilot/starpilot/galaxy/web/css/galaxy.css @@ -105,7 +105,6 @@ --content-width: 1200px; --panel-padding: var(--sp-5); - --gx-para-x: 0px; } [data-theme="light"] { @@ -222,16 +221,18 @@ body { radial-gradient(ellipse at 85% 75%, rgba(85, 45, 180, 0.18) 0%, transparent 60%), radial-gradient(circle at 75% 15%, rgba(77, 232, 232, 0.12) 0%, transparent 48%); pointer-events: none; - transform: translateZ(0); - will-change: transform; isolation: isolate; contain: strict; } -#galaxy-bg::before { +#galaxy-bg::before, +#galaxy-bg::after { content: ""; position: absolute; - top: 0; left: 0; width: 100%; height: 200%; + inset: 0; +} + +#galaxy-bg::before { background-image: radial-gradient(1px 1px at 30px 40px, #ffffff, rgba(255,255,255,0)), radial-gradient(1px 1px at 110px 220px, rgba(220, 200, 255, 0.8), rgba(255,255,255,0)), @@ -242,14 +243,9 @@ body { radial-gradient(1px 1px at 890px 180px, #ffffff, rgba(255,255,255,0)); background-size: 800px 400px; opacity: 0.5; - will-change: transform; - transform: translate3d(var(--gx-para-x), 0, 0); } #galaxy-bg::after { - content: ""; - position: absolute; - top: 0; left: 0; width: 100%; height: 200%; background-image: radial-gradient(1.5px 1.5px at 80px 140px, #ffffff, rgba(255,255,255,0)), radial-gradient(2px 2px at 240px 390px, rgba(190, 235, 255, 0.9), rgba(255,255,255,0)), @@ -258,29 +254,29 @@ body { radial-gradient(1.5px 1.5px at 730px 220px, rgba(255, 255, 255, 0.9), rgba(255,255,255,0)); background-size: 900px 450px; opacity: 0.6; - will-change: transform; - transform: translate3d(var(--gx-para-x), 0, 0); } #galaxy-bg .galaxy-hero { position: absolute; - width: 3px; - height: 3px; + width: 8px; + height: 8px; + margin: calc((var(--galaxy-star-core) - 8px) / 2); border-radius: 50%; - background: #ffffff; - box-shadow: 0 0 6px rgba(255, 255, 255, 0.9); + background: radial-gradient(circle, + #fff 0 calc(var(--galaxy-star-core) / 2), + rgba(255, 255, 255, 0.15) calc(var(--galaxy-star-core) / 2 + 0.3px), + rgba(255, 255, 255, 0.04) calc(var(--galaxy-star-core) / 2 + 1.5px), + transparent 4px); animation: galaxy-twinkle 6s ease-in-out infinite alternate; } @keyframes galaxy-twinkle { - 0%, 100% { opacity: 0.3; transform: scale(0.9); } - 50% { opacity: 1; transform: scale(1.2); } + 0%, 100% { opacity: 0.3; transform: rotateZ(0.05deg) scale(0.9); } + 50% { opacity: 1; transform: rotateZ(0.05deg) scale(1.2); } } @media (prefers-reduced-motion: reduce) { - #galaxy-bg, #galaxy-bg::before, #galaxy-bg::after, #galaxy-bg .galaxy-hero { - animation: none !important; - } + #galaxy-bg .galaxy-hero { animation: none !important; } } /* Keep decorative animation out of route viewers, modal backdrops and drawer content. */ diff --git a/openpilot/starpilot/galaxy/web/js/ambient-stars.js b/openpilot/starpilot/galaxy/web/js/ambient-stars.js index 044d4e5964..2e7d1cecdd 100644 --- a/openpilot/starpilot/galaxy/web/js/ambient-stars.js +++ b/openpilot/starpilot/galaxy/web/js/ambient-stars.js @@ -1,13 +1,15 @@ +const STAR_COUNT = 14 + export function spawnAmbientStars(background, random = Math.random) { if (!background || background.querySelector(".galaxy-hero")) return 0 - for (let index = 0; index < 14; index++) { + for (let index = 0; index < STAR_COUNT; index++) { const star = background.ownerDocument.createElement("i") star.className = "galaxy-hero" star.style.left = `${(random() * 100).toFixed(2)}%` star.style.top = `${(random() * 100).toFixed(2)}%` star.style.animationDelay = `${(random() * 4).toFixed(2)}s` - star.style.width = star.style.height = random() > 0.6 ? "3px" : "2px" + star.style.setProperty("--galaxy-star-core", random() > 0.6 ? "3px" : "2px") background.appendChild(star) } - return 14 + return STAR_COUNT }