Refactor ambient stars logic and improve CSS for stability and performance

This commit is contained in:
Prabhaav Pillai
2026-10-18 00:47:18 -04:00
parent 60d02accce
commit 44755c7cf7
3 changed files with 46 additions and 27 deletions
@@ -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/)
})
+17 -21
View File
@@ -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. */
@@ -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
}