Skip to content

fix+feat(signal-viewer): remove WebGL white-edge fringe; dampen zoom + shift-drag zoom-to-range#33

Merged
kabaka merged 2 commits into
mainfrom
claude/signal-viewer-zoom-edge
Jun 17, 2026
Merged

fix+feat(signal-viewer): remove WebGL white-edge fringe; dampen zoom + shift-drag zoom-to-range#33
kabaka merged 2 commits into
mainfrom
claude/signal-viewer-zoom-edge

Conversation

@kabaka

@kabaka kabaka commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Two production follow-ups after the WebGL2 renderer went live (#31/#32), both reported by the product owner.

1. Remove the white edge on the waveform lines (fix(charts):)

The anti-aliased edges of the WebGL lines (e.g. the pink Pressure line) showed a bright white fringe the Canvas2D reference doesn't have. Root cause: a premultiplied-alpha mismatch — the context is premultipliedAlpha:true and blends (ONE, ONE_MINUS_SRC_ALPHA) (correct), but the line/envelope fragment shaders wrote straight colour (vec4(rgb, a*coverage) with full-brightness RGB), so feathered edge pixels were un-premultiplied by the compositor and bloomed toward white. Fix: shaders now output premultiplied colour (rgb*a, a), so edges fade to transparent dark exactly like the Canvas2D AA. Opaque interiors are mathematically unchanged; line width / round joins / envelope min-thickness clamp untouched. Adds a shader-source regression test. This moves WebGL output toward the reference, so the fidelity gate should improve or stay within tolerance.

2. Fix zoom feel + add shift-drag zoom-to-range (feat(signal-viewer):)

Now that zoom is smooth, its sensitivity was glaring.

  • Dampened wheel-zoom: was ~1.5× span per notch (~50% — overshoots); now an exp(normalizedDelta · WHEEL_ZOOM_RATE) curve at WHEEL_ZOOM_RATE = 0.0625 (~6%/notch). Device-aware (line vs trackpad-pixel deltas normalized + clamped per event so a fling can't teleport), cursor-anchored (time under the pointer stays put), and reuses the existing rAF/WebGL hot path. The rate is a single named, commented constant for the owner to tune by feel.
  • Shift-drag zoom-to-range: Shift+drag draws a themed selection band over the time range and zooms to it on release; plain drag still pans. Pixel→time conversion clamps to session bounds and floors at the max zoom-in span; Shift-clicks / <5px drags are no-ops; col-resize cursor while Shift is held; pointer-capture tracks drags outside the canvas. Keyboard navigation/data-cursor untouched.
  • New pure signalZoom.ts helper (wheelDeltaToZoomFactor, normalizeWheelDelta, applyCursorAnchoredZoom, pixelRangeToTimeRange) with 21 unit tests (the in-sandbox correctness proof; feel is tuned in prod).

Tuning knobs for the owner

WHEEL_ZOOM_RATE (sensitivity), PIXEL_TO_NORMALIZED (trackpad feel), MIN_SELECTION_PX (shift-zoom dead-zone). Wheel-zoom still gates on Ctrl/Cmd (existing pinch behavior, unchanged).

Tests

Full suite green (2,575) + new shader-source and zoom-helper tests; typecheck/lint/prettier clean. (Interaction feel can't be validated in CI — that's the owner's prod check.)

🤖 Generated with Claude Code

https://claude.ai/code/session_012CzEJ1kUhwobqVTnVusLcb


Generated by Claude Code

claude added 2 commits June 17, 2026 00:31
The WebGL2 context is created with premultipliedAlpha:true and the renderer
blends with blendFunc(ONE, ONE_MINUS_SRC_ALPHA), so the drawing buffer must
hold premultiplied colour. The line fragment shader instead output straight
alpha (vec4(u_color.rgb, u_color.a * coverage)) — full-brightness RGB while
only alpha feathered — so the compositor un-premultiplied the low edge alpha
and anti-aliased edges bloomed toward white. The envelope shader output the
raw straight u_color, with the same latent issue at MSAA silhouette edges and
for any translucent lane.

Both fragment shaders now output premultiplied colour (rgb * a, a) where a =
u_color.a * edge-coverage. Edges fade toward transparent black, revealing the
dark chart cleanly and matching the Canvas2D drawLine/drawEnvelope AA. The host
colour resolution, blend func, 1.2px line width, round-join feel, and the
geometry-side envelope min-thickness clamp are unchanged.

Adds a pure string-level regression test asserting both shaders emit
premultiplied output; real rendered fidelity remains covered by the CI
pixel-diff gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzEJ1kUhwobqVTnVusLcb
Wheel/pinch zoom was far too sensitive (50% span change per notch), so
users overshot and hunted. Replace the fixed per-notch factor with a
device-aware, gentle curve: factor = exp(normalizedDelta * WHEEL_ZOOM_RATE).
DOM_DELTA_LINE notches and DOM_DELTA_PIXEL trackpad streams are normalized to
a common magnitude and clamped per event so neither feels jumpy and one fat
delta can't teleport the zoom. Cursor-anchored zoom is preserved (the time
under the pointer stays put). Sensitivity is a single tunable constant for
the product owner to tune in production (feel can't be validated in CI).

Add a Shift+drag rubber-band: Shift+pointerdown starts a zoom-selection
(distinct from a pan), drawing a theme-tokened semi-transparent band (a cheap
DOM element — no waveform repaint) across the dragged x-range; release
converts pixels -> time, clamps to session bounds, floors at the max zoom-in
limit, and applies it. Shift-clicks and <5px drags are no-ops. Pointer
capture keeps the drag tracking outside the canvas; a col-resize cursor
arms while Shift is held for discoverability. Keyboard zoom (presets) is
unchanged.

Extract the math into a pure, unit-tested helper (signalZoom.ts):
wheelDeltaToZoomFactor / normalizeWheelDelta / applyCursorAnchoredZoom /
pixelRangeToTimeRange, with 21 tests covering device modes, the per-event
clamp, multiplicative symmetry, cursor anchoring, and selection clamping.

Does not touch the WebGL shader files edited elsewhere on this branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzEJ1kUhwobqVTnVusLcb
@kabaka kabaka merged commit a81d5bf into main Jun 17, 2026
12 checks passed
kabaka added a commit that referenced this pull request Jun 17, 2026
…deploys (#34)

The post-#33 main CI failed on a flaky Firefox E2E console-error assertion
(analysis.spec.ts) that caught a benign NS_BINDING_ABORTED — a navigation/
worker-load abort surfaced by Playwright's juggler harness when the test
rapidly goto's between analysis routes. It is not an app fault and it blocked
the Build/Deploy pipeline. Add it to the existing benign-noise allow-list
(alongside React Router and DevTools). Test-only change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants