Velocity-weighted region opacity replaces the (sub-perceptual) frame fade

The linear prev→cur bitmap blend between rebuilds was verified working
but made no visible difference — the 3-frame bitmap cadence was already
below perceptual threshold for large blurred blobs; the churn during
fast drags is the SHAPE, not the cadence. Rolled back entirely (no
extra per-frame blits).

In its place, per the owner: while nodes move fast, their blob fades
out and fades back in on settling. Speed per node = screen px per
frame from position stamps between rebuilds, EMA-smoothed. The blob is
a CLUSTER attribute (still neighbors' skeleton strokes pass right
through a moving node's area — per-node weighting measurably did
nothing), so each cluster's weight = f(max member speed): linear 1→0
between 1.5 and 8 px/frame. Strokes and discs paint at that alpha —
before the blur + final edgeAlpha multiply, so coverage semantics are
unchanged at rest (all weights 1 = the 2026-09-14 no-stacking fix).

Stamps reset on data-object change, NOT geoVersion (the live sim bumps
geoVersion every frame — resetting on it zeroed all speeds; caught
live). regionMotionPending keeps rebuilding after the sim stops until
weights converge back to 1, so a blob never freezes at partial opacity.
This commit is contained in:
amosblomqvist
2026-09-16 11:28:39 +03:00
parent 173059eb8f
commit 74c14931af
2 changed files with 133 additions and 117 deletions
+27
View File
@@ -17,6 +17,7 @@ import {
fitTransform,
labelAlpha,
pickNearest,
regionVelocityWeight,
zoomRatioOf,
LABELS_FADE_SPAN,
} from "../components/graph-canvas.ts";
@@ -226,3 +227,29 @@ describe("sanitizePhysicsTuning (physics controls)", () => {
assert.equal(t.velocityDecay, 0.9);
});
});
describe("regionVelocityWeight", () => {
test("still nodes: at or below the still threshold → full opacity", () => {
assert.equal(regionVelocityWeight(0), 1);
assert.equal(regionVelocityWeight(1.5), 1);
});
test("fast nodes: at or above the moving threshold → blob off", () => {
assert.equal(regionVelocityWeight(8), 0);
assert.equal(regionVelocityWeight(100), 0);
});
test("linear ramp between the thresholds, monotone decreasing", () => {
const mid = regionVelocityWeight((1.5 + 8) / 2);
assert.ok(Math.abs(mid - 0.5) < 1e-12);
// strictly decreasing across the ramp
for (let s = 1.5; s <= 8; s += 0.25) {
assert.ok(regionVelocityWeight(s) >= regionVelocityWeight(s + 0.25));
}
// never leaves [0, 1]
for (let s = -10; s <= 50; s += 1.7) {
const w = regionVelocityWeight(s);
assert.ok(w >= 0 && w <= 1);
}
});
});