Ink
Delegated ink trails: while the user draws, the compositor paints ahead of your rendering, so the line keeps up with the stylus instead of trailing a frame behind it.
@inject Bit.Butil.Ink inkMDN reference
Returns true when the runtime exposes navigator.ink.requestPresenter. During prerender/SSR the check returns false rather than throwing, so defer it to OnAfterRenderAsync.
@inject Bit.Butil.Ink ink
var supported = await ink.IsSupported();The trail is started on an element and clipped to it. The pointer listener lives in JavaScript, because the presenter has to be fed the browser's own PointerEvent - by the time .NET saw one it would be a copy, and an untrusted one at that. Draw in the box below with the trail on and off and compare how far the ink lags behind the cursor; the effect is most visible with fast strokes on a high-refresh display.
IAsyncDisposable
Bit.Butil.Ink ink
<canvas @ref="_canvas" width="600" height="300"></canvas>
{
private ElementReference _canvas;
private InkTrailHandle? _trail;
// The trail is drawn by the compositor ahead of the app's own strokes, so it hides the latency
// between the pen and the canvas. The app still renders every stroke itself.
private async Task Start() =>
_trail = await ink.StartTrail(_canvas, color: "#0f6cbd", diameter: 4);
public async ValueTask DisposeAsync()
{
if (_trail is not null) await _trail.DisposeAsync(); // stops the trail
}
}What a colour or brush-size picker calls: the trail changes from the next point onwards, without stopping and restarting it. Keep it in step with your own stroke style - the trail is the part of the line the user sees first.
{
private InkTrailHandle? _trail; // from StartTrail
// Takes effect on the next segment the compositor draws; strokes already on the canvas are the
// app's own and are unaffected.
private async Task SwitchPen() => await _trail!.SetStyle("#c50f1f", diameter: 8);
}4 pxAPI reference
ValueTask<bool> IsSupported()ValueTask<InkTrailHandle?> StartTrail(ElementReference presentationArea, string color = "black", double diameter = 3)ValueTask DisposeAsync()ValueTask<bool> SetStyle(string color, double diameter = 0)ValueTask DisposeAsync()