fix(awareness): stop the subject echoing itself and cover the echo wiring

Remaining review follow-ups for the sonar pulses.

No self-echo: pulses are born 5 km out (startFraction of the radius),
but contacts nearer than that, the subject included, still lit on every
pulse birth. They now never echo. Just past the birth ring, an echo is
scaled with the front's own fade-in, so a bracket no longer flashes
before a visible front reaches it. The ring flash at the full radius is
unchanged.

Older Safari: the reduced-motion listener falls back to the media
query's addListener / removeListener when addEventListener is missing,
as other modules here already do.

Echo wiring test: a detectionHost test now paints through the real
detection lane with an echo field set, and checks the echo stroke takes
the echo colour on the default style and each style's line colour under
CRT, NVG and FLIR. CURRENT-STATE notes the 5 km no-echo zone.
This commit is contained in:
Manjunath committed 2026-11-03 20:04:33 +05:30
1 parent 013cfdf91d
commit 0f56b3147b
6 files changed
+146 -29

No files matched your search

+37 -12
View File
@@ -49,20 +49,30 @@ function luminance(color) {
* test; `motion.set(on)` flips it later and tells its listeners, as the
* browser does.
*/
function ringRendering(t, { reducedMotion = false } = {}) {
function ringRendering(t, { reducedMotion = false, legacyQuery = false } = {}) {
const previous = globalThis.matchMedia;
const listeners = new Set();
const query = {
get matches() {
return reducedMotion;
},
addEventListener(type, listener) {
if (type === 'change') listeners.add(listener);
},
removeEventListener(type, listener) {
if (type === 'change') listeners.delete(listener);
},
};
// `legacyQuery` is a MediaQueryList as Safari before 14 shipped it, with
// only addListener / removeListener.
const query = legacyQuery
? {
get matches() {
return reducedMotion;
},
addListener: (listener) => listeners.add(listener),
removeListener: (listener) => listeners.delete(listener),
}
: {
get matches() {
return reducedMotion;
},
addEventListener(type, listener) {
if (type === 'change') listeners.add(listener);
},
removeEventListener(type, listener) {
if (type === 'change') listeners.delete(listener);
},
};
globalThis.matchMedia = (media) =>
media === '(prefers-reduced-motion: reduce)'
? query
@@ -693,6 +703,21 @@ test('turning reduced motion on mid-session stops the pulses, and off restarts t
assert.equal(motion.listeners(), 0);
});
test('a reduced-motion change reaches older Safari through addListener', (t) => {
const { entities, scene, rendering, motion, state } = ringRendering(t, {
legacyQuery: true,
});
state.subject = flight(3);
rendering.renderVisual(state.subject);
assert.equal(scene.primitives.length, 1);
assert.equal(motion.listeners(), 1);
motion.set(true);
assert.equal(scene.primitives.length, 0);
assert.equal(entities.values[0].polyline.material.color.isConstant, true);
rendering.clearVisual();
assert.equal(motion.listeners(), 0);
});
test('a refresh after reduced motion changed rebuilds the visual too', (t) => {
const { entities, rendering, motion } = ringRendering(t);
rendering.renderVisual(flight(3));
+9 -3
View File
@@ -758,7 +758,10 @@ export function createRendering({
const onMotionChange = () => {
if (layerState.subject) renderVisual(layerState.subject);
};
motionQuery?.addEventListener?.('change', onMotionChange);
// Safari before 14 has only the older addListener / removeListener.
if (motionQuery?.addEventListener)
motionQuery.addEventListener('change', onMotionChange);
else motionQuery?.addListener?.(onMotionChange);
layerState.visual = {
key,
@@ -770,8 +773,11 @@ export function createRendering({
reducedMotion,
wake,
stopPulses,
removeMotionListener: () =>
motionQuery?.removeEventListener?.('change', onMotionChange),
removeMotionListener: () => {
if (motionQuery?.removeEventListener)
motionQuery.removeEventListener('change', onMotionChange);
else motionQuery?.removeListener?.(onMotionChange);
},
};
governorRequestRender('awareness-visual');
}