PointerTracker
getCoalescedEvents() and getPredictedEvents(): the input samples the browser merged away between two frames, and its guess at the next few - what a drawing or annotation surface needs to stop losing input.
@inject Bit.Butil.PointerTracker pointerTrackerMDN reference
pointermove events a pen's stream turns into is up to the implementation, and it is
typically far fewer than the pen produced. Blazor's PointerEventArgs has no way back
to the DOM event, so the coalesced list cannot be reached from it - this service attaches its own
listener instead.
IsSupported reports getCoalescedEvents; where it is false tracking still works, each frame just carries a single sample - so it is a fidelity check, not a gate. Prediction is Chromium only.
@inject Bit.Butil.PointerTracker pointerTracker
var coalesced = await pointerTracker.IsSupported();
var predicted = await pointerTracker.SupportsPrediction();Drag across the pad below. 'Samples this frame' is how many positions the browser had merged into the one event it delivered - draw through all of them and a fast stroke stays a curve instead of becoming a polyline. Dispose the returned subscription when the gesture is over.
IAsyncDisposable
Bit.Butil.PointerTracker pointerTracker
<div @ref="_surface" style="touch-action:none">...</div>
{
private ElementReference _surface;
private ButilSubscription? _sub;
private readonly List<(double X, double Y)> _path = [];
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender is false) return;
_sub = await pointerTracker.Track(_surface, frame =>
{
// Coalesced carries every sample the device produced between frames, not only the one
// the browser dispatched - which is the difference between a smooth line and a polygon.
foreach (var sample in frame.Coalesced)
{
_path.Add((sample.X, sample.Y));
}
InvokeAsync(StateHasChanged);
});
}
public async ValueTask DisposeAsync()
{
if (_sub is not null) await _sub.DisposeAsync();
}
}- · 0 coalesced · 0 predicted · pressure 0.000 frames · 0 samples - the gap between these two is what an ordinary pointermove handler throws away.Predicted samples are guesses, and they are wrong often enough to matter. The usual use is a provisional 'ink ahead' segment that is thrown away and redrawn on the next real frame - it removes perceived latency without ever committing a predicted point to the document.
// committed ink
foreach (var sample in frame.Coalesced) CommitPoint(sample);
// provisional ink, discarded next frame
ClearProvisional();
foreach (var sample in frame.Predicted) DrawProvisional(sample);API reference
ValueTask<bool> IsSupported()ValueTask<bool> SupportsPrediction()Task<ButilSubscription> Track(ElementReference element, Action<PointerFrame> handler, bool includePredicted = false, params string[] events)ValueTask DisposeAsync()