mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
Show HUD backplates only with scope off and preserve original text styling
This commit is contained in:
1 parent
cfbdf22121
commit
40b471f1e3
7 files changed
+210
-63
No files matched your search
@@ -1,5 +1,6 @@
|
||||
- Add a Scope checkbox and extend feathering to 200%, retaining scope settings
|
||||
across HUD layouts, share links and scenes. First run opens with Scope ON at
|
||||
100% feather; explicit settings and historical links retain their values. Give
|
||||
Tactical, Operator and Minimal text darker black backings without halos or
|
||||
overlapping lines, including vertical metadata, while keeping map attribution clear (bilawalsidhu, #1397).
|
||||
Tactical, Operator and Minimal text black backplates only when Scope is OFF,
|
||||
including vertical metadata, without changing the original text color, glow or
|
||||
opacity. Scope ON retains its original HUD presentation (bilawalsidhu, #1397).
|
||||
+11
-11
@@ -197,20 +197,20 @@ hard edge and 200% extends the gradient to the center while keeping its midpoint
|
||||
on the same keyhole edge. First run opens with Scope ON and feather 100%; links
|
||||
predating `scf` retain their historical 35% parse fallback.
|
||||
|
||||
Tactical, Operator and Minimal give every telemetry readout a small 70%-black
|
||||
backing with rounded corners, including vertical metadata and multiline AIS.
|
||||
These darker backings have no shadow halo or overlap between adjacent lines. Glyphs keep
|
||||
the active style hue with stronger alpha, while the red REC dot retains its own
|
||||
color. Readout backing sits outside each glyph's opacity; Operator's corner and
|
||||
bar frames stay transparent rather than stacking another dark panel. Summary
|
||||
ellipsis and desktop single-line coordinates remain intact. The title, tagline,
|
||||
optional FPS and active-style readout use the same small backing. Minimal's
|
||||
lower coordinates clear map attribution. Cyber retains its existing skin.
|
||||
When Scope is OFF, Tactical, Operator and Minimal add small black backplates
|
||||
at 70% opacity behind telemetry, vertical metadata, multiline AIS and scene
|
||||
chrome. They have rounded corners, no halo and no overlap between adjacent lines.
|
||||
Text keeps its original color, glow and opacity in both Scope states. The engine
|
||||
publishes the OFF state as `data-scope-mask="off"` on the document root, so direct
|
||||
setters, share links and scene restoration drive the same treatment. Scope ON
|
||||
uses the original HUD layout, Operator frames and scene chrome, with no added
|
||||
backplates. The OFF-only Minimal coordinate offset clears map attribution.
|
||||
Cyber retains its existing skin.
|
||||
|
||||
`node scripts/qa-scope-hud.mjs` exercises native checkbox click and Space,
|
||||
feather range and keyboard boundaries, all four layout transitions, share/scene
|
||||
restore, text-backing coverage and non-overlapping geometry, multiline AIS, summary clipping and
|
||||
narrow viewport containment. Optional `QA_SCREENSHOT_DIR` saves mode screenshots.
|
||||
restore, original Scope ON styling, OFF-only backing coverage and non-overlapping
|
||||
geometry, multiline AIS, summary clipping and narrow viewport containment. Optional `QA_SCREENSHOT_DIR` saves mode screenshots.
|
||||
|
||||
## Cyber HUD — September 30, 2026
|
||||
|
||||
|
||||
+115
-1
@@ -56,6 +56,45 @@ const readScope = () =>
|
||||
};
|
||||
});
|
||||
|
||||
const readHudGlyphs = () =>
|
||||
page.evaluate(() => {
|
||||
const hud = document.getElementById('intel-hud');
|
||||
const probe = document.createElement('span');
|
||||
probe.style.display = 'none';
|
||||
hud.appendChild(probe);
|
||||
const baseline = {
|
||||
color: getComputedStyle(probe).color,
|
||||
shadow: getComputedStyle(probe).textShadow,
|
||||
};
|
||||
probe.remove();
|
||||
const glyphs = [
|
||||
'.hud-mode',
|
||||
'.hud-system',
|
||||
'.hud-summary-label',
|
||||
'.hud-summary',
|
||||
'.hud-orbital',
|
||||
'#hud-mgrs',
|
||||
'#hud-coll',
|
||||
].map((selector) => {
|
||||
const el = hud.querySelector(selector);
|
||||
const style = getComputedStyle(el);
|
||||
let opacity = 1;
|
||||
for (
|
||||
let current = el;
|
||||
current && current !== hud;
|
||||
current = current.parentElement
|
||||
)
|
||||
opacity *= Number(getComputedStyle(current).opacity);
|
||||
return {
|
||||
selector,
|
||||
color: style.color,
|
||||
shadow: style.textShadow,
|
||||
opacity,
|
||||
};
|
||||
});
|
||||
return { baseline, glyphs };
|
||||
});
|
||||
|
||||
async function openApplication(url) {
|
||||
await page.goto(url.href, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(
|
||||
@@ -245,6 +284,79 @@ try {
|
||||
|
||||
for (const layout of ['minimal', 'operator', 'tactical']) {
|
||||
await page.select('#hud-layout-select', layout);
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.styleManager.applyVisualState({
|
||||
scope: { enabled: true, featherPct: 100 },
|
||||
}),
|
||||
);
|
||||
const originalGlyphs = await readHudGlyphs();
|
||||
const original = await page.evaluate(() => {
|
||||
const hud = document.getElementById('intel-hud');
|
||||
const wrappers = [...hud.querySelectorAll('.hud-readout')];
|
||||
const scene = [...document.querySelectorAll('.scene-text-holdout')];
|
||||
const unbacked = [...wrappers, ...scene].every(
|
||||
(el) =>
|
||||
getComputedStyle(el).backgroundColor === 'rgba(0, 0, 0, 0)' &&
|
||||
getComputedStyle(el).boxShadow === 'none',
|
||||
);
|
||||
return {
|
||||
markerAbsent: !document.documentElement.hasAttribute('data-scope-mask'),
|
||||
unbacked,
|
||||
wrappersTransparent: wrappers.every(
|
||||
(el) => getComputedStyle(el).display === 'contents',
|
||||
),
|
||||
operatorFrame:
|
||||
hud.dataset.variant !== 'operator' ||
|
||||
(getComputedStyle(hud.querySelector('.hud-corner'))
|
||||
.backgroundColor === 'rgba(0, 0, 0, 0.36)' &&
|
||||
getComputedStyle(hud.querySelector('.hud-top-bar'))
|
||||
.backgroundImage !== 'none'),
|
||||
};
|
||||
});
|
||||
check(
|
||||
`${layout}: Scope ON retains the original presentation without new backplates`,
|
||||
original.markerAbsent &&
|
||||
original.unbacked &&
|
||||
original.wrappersTransparent &&
|
||||
original.operatorFrame,
|
||||
original,
|
||||
);
|
||||
check(
|
||||
`${layout}: original HUD color, glow and metadata alpha remain intact`,
|
||||
originalGlyphs.glyphs.every(
|
||||
(glyph) =>
|
||||
glyph.color === originalGlyphs.baseline.color &&
|
||||
glyph.shadow === originalGlyphs.baseline.shadow,
|
||||
) &&
|
||||
originalGlyphs.glyphs.find(
|
||||
(glyph) => glyph.selector === '.hud-summary-label',
|
||||
).opacity === 0.55 &&
|
||||
originalGlyphs.glyphs.find((glyph) => glyph.selector === '.hud-orbital')
|
||||
.opacity === 0.7 &&
|
||||
originalGlyphs.glyphs.find((glyph) => glyph.selector === '#hud-coll')
|
||||
.opacity === 0.5,
|
||||
originalGlyphs,
|
||||
);
|
||||
if (process.env.QA_SCREENSHOT_DIR) {
|
||||
fs.mkdirSync(process.env.QA_SCREENSHOT_DIR, { recursive: true });
|
||||
await page.screenshot({
|
||||
path: path.join(
|
||||
process.env.QA_SCREENSHOT_DIR,
|
||||
`scope-on-${layout}.png`,
|
||||
),
|
||||
});
|
||||
}
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.styleManager.applyVisualState({
|
||||
scope: { enabled: false },
|
||||
}),
|
||||
);
|
||||
const backedGlyphs = await readHudGlyphs();
|
||||
check(
|
||||
`${layout}: Scope OFF adds backing without changing glyph styles`,
|
||||
JSON.stringify(backedGlyphs) === JSON.stringify(originalGlyphs),
|
||||
backedGlyphs,
|
||||
);
|
||||
const coverage = await page.evaluate(() => {
|
||||
const hud = document.getElementById('intel-hud');
|
||||
const walker = document.createTreeWalker(hud, NodeFilter.SHOW_TEXT);
|
||||
@@ -340,6 +452,7 @@ try {
|
||||
getComputedStyle(el).backgroundColor !== 'rgba(0, 0, 0, 0)',
|
||||
);
|
||||
return {
|
||||
scopeOff: document.documentElement.dataset.scopeMask === 'off',
|
||||
visible,
|
||||
missing,
|
||||
empty,
|
||||
@@ -351,7 +464,8 @@ try {
|
||||
});
|
||||
check(
|
||||
`${layout}: all visible HUD text has one backing`,
|
||||
coverage.visible > 0 &&
|
||||
coverage.scopeOff &&
|
||||
coverage.visible > 0 &&
|
||||
coverage.missing.length === 0 &&
|
||||
coverage.empty === 0 &&
|
||||
coverage.broadBackings === 0,
|
||||
|
||||
@@ -429,6 +429,14 @@ function draw() {
|
||||
_painted = true;
|
||||
}
|
||||
|
||||
/** Expose only the OFF state; an absent attribute preserves the original HUD. */
|
||||
function syncScopeMaskAppearance(enabled = _enabled) {
|
||||
const dataset = globalThis.document?.documentElement?.dataset;
|
||||
if (!dataset) return;
|
||||
if (enabled) delete dataset.scopeMask;
|
||||
else dataset.scopeMask = 'off';
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the scope mask into the viewer container. Idempotent.
|
||||
* @param {Cesium.Viewer} viewer
|
||||
@@ -436,6 +444,7 @@ function draw() {
|
||||
*/
|
||||
export function installScopeMask(viewer) {
|
||||
if (_canvas || !viewer?.container) return;
|
||||
syncScopeMaskAppearance();
|
||||
_container = viewer.container;
|
||||
_viewer = viewer;
|
||||
_canvas = document.createElement('canvas');
|
||||
@@ -511,6 +520,7 @@ export function setScopeMaskEnabled(enabled) {
|
||||
const next = Boolean(enabled);
|
||||
const reEnabled = next && !_enabled;
|
||||
_enabled = next;
|
||||
syncScopeMaskAppearance();
|
||||
if (reEnabled) {
|
||||
// The camera moved freely while the scope was off and nothing sampled it,
|
||||
// so the painted terminus can be a whole altitude band stale. Re-sync once
|
||||
@@ -549,6 +559,7 @@ export function getScopeMaskFeather() {
|
||||
* @returns {void}
|
||||
*/
|
||||
export function destroyScopeMask() {
|
||||
syncScopeMaskAppearance(true);
|
||||
_resizeObserver?.disconnect();
|
||||
_resizeObserver = null;
|
||||
teardownDevicePixelRatioWatch();
|
||||
@@ -562,6 +573,7 @@ export function destroyScopeMask() {
|
||||
|
||||
/** Test seam. */
|
||||
export function _resetScopeMaskForTest() {
|
||||
syncScopeMaskAppearance(true);
|
||||
_resizeObserver?.disconnect();
|
||||
_resizeObserver = null;
|
||||
teardownDevicePixelRatioWatch();
|
||||
|
||||
+26
-1
@@ -157,12 +157,14 @@ function stubScopeMaskDom({ width = 1000, height = 800, dpr = 1 } = {}) {
|
||||
removeEventListener: (type, fn) => listeners.delete(fn),
|
||||
}),
|
||||
};
|
||||
globalThis.document = { createElement: () => canvas };
|
||||
const documentElement = { dataset: {} };
|
||||
globalThis.document = { documentElement, createElement: () => canvas };
|
||||
globalThis.ResizeObserver = class { observe() {} disconnect() {} };
|
||||
const container = { clientWidth: width, clientHeight: height, appendChild() {} };
|
||||
return {
|
||||
canvas,
|
||||
container,
|
||||
documentElement,
|
||||
/** Canvas work counters: backing-store resizes, clears, fills. */
|
||||
ops,
|
||||
/** Every fillStyle string assigned since install (hard-crop path). */
|
||||
@@ -181,6 +183,29 @@ function stubScopeMaskDom({ width = 1000, height = 800, dpr = 1 } = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
test('scope appearance follows direct state changes and install/destroy lifecycles', () => {
|
||||
const dom = stubScopeMaskDom();
|
||||
try {
|
||||
setScopeMaskEnabled(false);
|
||||
assert.equal(dom.documentElement.dataset.scopeMask, 'off');
|
||||
delete dom.documentElement.dataset.scopeMask;
|
||||
installScopeMask({ container: dom.container });
|
||||
assert.equal(dom.documentElement.dataset.scopeMask, 'off', 'install publishes retained OFF state');
|
||||
destroyScopeMask();
|
||||
assert.equal(dom.documentElement.dataset.scopeMask, undefined, 'destroy clears the document marker');
|
||||
installScopeMask({ container: dom.container });
|
||||
assert.equal(dom.documentElement.dataset.scopeMask, 'off', 'reinstall retains the preference');
|
||||
setScopeMaskEnabled(true);
|
||||
assert.equal(dom.documentElement.dataset.scopeMask, undefined, 'ON uses the original presentation');
|
||||
setScopeMaskEnabled(false);
|
||||
_resetScopeMaskForTest();
|
||||
assert.equal(dom.documentElement.dataset.scopeMask, undefined, 'test reset clears the marker');
|
||||
} finally {
|
||||
destroyScopeMask();
|
||||
dom.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('a DPR change with no resize still repaints the backing store', () => {
|
||||
const dom = stubScopeMaskDom({ width: 1000, height: 800, dpr: 1 });
|
||||
try {
|
||||
|
||||
@@ -475,6 +475,12 @@ body:not(.ui-clean-view):not(.recording-mode):has(
|
||||
}
|
||||
|
||||
#intel-hud[data-variant='minimal'] .hud-bottom-left {
|
||||
bottom: calc(2vh + 5.4rem);
|
||||
}
|
||||
|
||||
:root[data-scope-mask='off']
|
||||
#intel-hud[data-variant='minimal']
|
||||
.hud-bottom-left {
|
||||
bottom: calc(2vh + 6.5rem);
|
||||
}
|
||||
|
||||
|
||||
+37
-48
@@ -372,20 +372,19 @@
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
/* Text-sized black backing keeps bright imagery from washing out telemetry.
|
||||
The backing wraps each readout outside its existing opacity, so dim metadata
|
||||
keep their hierarchy as contrast increases; the REC dot stays red. */
|
||||
/* Scope OFF adds text-sized backing; Scope ON retains the original HUD. */
|
||||
.hud-readout {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
#intel-hud:is(
|
||||
:root[data-scope-mask='off']
|
||||
#intel-hud:is(
|
||||
[data-variant='minimal'],
|
||||
[data-variant='operator'],
|
||||
[data-variant='tactical']
|
||||
)
|
||||
.hud-readout,
|
||||
:root:not([data-ui-theme='cyber']) .scene-text-holdout {
|
||||
:root[data-scope-mask='off']:not([data-ui-theme='cyber']) .scene-text-holdout {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
box-shadow: none;
|
||||
border-radius: 2px;
|
||||
@@ -399,7 +398,8 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#intel-hud:is(
|
||||
:root[data-scope-mask='off']
|
||||
#intel-hud:is(
|
||||
[data-variant='minimal'],
|
||||
[data-variant='operator'],
|
||||
[data-variant='tactical']
|
||||
@@ -412,72 +412,53 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#intel-hud:is(
|
||||
[data-variant='minimal'],
|
||||
[data-variant='operator'],
|
||||
[data-variant='tactical']
|
||||
)
|
||||
.hud-readout
|
||||
:where(:not(#hud-rec-dot)) {
|
||||
color: rgb(from var(--hud-color) r g b / 0.86);
|
||||
text-shadow:
|
||||
0 1px 2px rgba(0, 0, 0, 0.8),
|
||||
0 0 4px var(--hud-glow);
|
||||
}
|
||||
|
||||
#intel-hud:is(
|
||||
:root[data-scope-mask='off']
|
||||
#intel-hud:is(
|
||||
[data-variant='minimal'],
|
||||
[data-variant='operator'],
|
||||
[data-variant='tactical']
|
||||
)
|
||||
:is(.hud-top-right, .hud-bottom-right)
|
||||
.hud-readout,
|
||||
:root:not([data-ui-theme='cyber']) #style-indicator .scene-text-holdout {
|
||||
:root[data-scope-mask='off']:not([data-ui-theme='cyber'])
|
||||
#style-indicator
|
||||
.scene-text-holdout {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
/* Rotating the backing with its text also covers both vertical annotations. */
|
||||
#intel-hud:is([data-variant='operator'], [data-variant='tactical']) .hud-edge {
|
||||
:root[data-scope-mask='off']
|
||||
#intel-hud:is([data-variant='operator'], [data-variant='tactical'])
|
||||
.hud-edge {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#intel-hud:is([data-variant='operator'], [data-variant='tactical'])
|
||||
:root[data-scope-mask='off']
|
||||
#intel-hud:is([data-variant='operator'], [data-variant='tactical'])
|
||||
.hud-edge
|
||||
.hud-readout
|
||||
> * {
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
#intel-hud:is(
|
||||
[data-variant='minimal'],
|
||||
[data-variant='operator'],
|
||||
[data-variant='tactical']
|
||||
)
|
||||
:is(
|
||||
.hud-top-bar-left,
|
||||
.hud-top-bar-right,
|
||||
.hud-summary-label,
|
||||
.hud-orbital,
|
||||
.hud-ais-vessel:not(.active)
|
||||
) {
|
||||
opacity: 0.78;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* One backing per line; the operator's frames stay light and transparent. */
|
||||
#intel-hud[data-variant='operator'] .hud-corner,
|
||||
#intel-hud:is([data-variant='minimal'], [data-variant='operator'])
|
||||
:root[data-scope-mask='off'] #intel-hud[data-variant='operator'] .hud-corner,
|
||||
:root[data-scope-mask='off']
|
||||
#intel-hud:is([data-variant='minimal'], [data-variant='operator'])
|
||||
:is(.hud-top-bar, .hud-bottom-bar) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#intel-hud:is([data-variant='minimal'], [data-variant='operator'])
|
||||
:root[data-scope-mask='off']
|
||||
#intel-hud:is([data-variant='minimal'], [data-variant='operator'])
|
||||
:is(.hud-top-bar, .hud-bottom-bar) {
|
||||
width: fit-content;
|
||||
max-width: calc(100vw - 120px);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
#intel-hud:is([data-variant='minimal'], [data-variant='operator'])
|
||||
:root[data-scope-mask='off']
|
||||
#intel-hud:is([data-variant='minimal'], [data-variant='operator'])
|
||||
:is(.hud-top-bar, .hud-bottom-bar)
|
||||
.hud-readout {
|
||||
overflow: hidden;
|
||||
@@ -486,23 +467,31 @@
|
||||
}
|
||||
|
||||
/* The title's inline text box needs a little vertical room around its glyphs. */
|
||||
:root:not([data-ui-theme='cyber']) #title-bar h1 > .scene-text-holdout {
|
||||
:root[data-scope-mask='off']:not([data-ui-theme='cyber'])
|
||||
#title-bar
|
||||
h1
|
||||
> .scene-text-holdout {
|
||||
padding-block: 0.12em;
|
||||
}
|
||||
|
||||
:root:not([data-ui-theme='cyber']) #title-bar .subtitle {
|
||||
:root[data-scope-mask='off']:not([data-ui-theme='cyber']) #title-bar .subtitle {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
:root:not([data-ui-theme='cyber']) #style-indicator .indicator-value {
|
||||
:root[data-scope-mask='off']:not([data-ui-theme='cyber'])
|
||||
#style-indicator
|
||||
.indicator-value {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
#intel-hud[data-variant='minimal']
|
||||
:root[data-scope-mask='off']
|
||||
#intel-hud[data-variant='minimal']
|
||||
.hud-readout:has(> :is(.hud-classification, .hud-system)) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:root:not([data-ui-theme='cyber']) #title-bar .frame-rate-readout {
|
||||
:root[data-scope-mask='off']:not([data-ui-theme='cyber'])
|
||||
#title-bar
|
||||
.frame-rate-readout {
|
||||
margin-top: 6px;
|
||||
}
|
||||
Reference in new issue
Block a user