air_pointer
Platform-agnostic canvas input for Flutter. Unifies mouse, trackpad, and MediaPipe hand-gesture events behind a single CanvasInputController.
What this package is like to depend on
Last release 1 months ago
07 Jul 2026
Too new to tell
only 2 release windows
Nearly every release is documented
notes for 4 of 4 stable releases
Nothing withdrawn
no release was ever pulled
2 months old
4 releases · first in 2026
4 releases in the last 12 months
see the full history below
Release timeline
4 releases · Jun 2026 to Jul 2026Releases
latest 4-
0.2.307 Jul 2026Release notes
Open source →Bug fixes
- Release-build latency logging removed — the per-60-frame latency
debugPrintin the webGestureInputSourcenow only runs in debug builds (kDebugMode); it previously logged to the console in release builds too. - Wasm compilation selects the web implementation — the conditional export
for
GestureInputSourcenow keys ondart.library.js_interopinstead of the legacydart.library.html, soflutter build web --wasmgets the MediaPipe implementation rather than the native stub. - Corrected the
hand_detectionLandmarkProviderdoc example — the sketch inlandmark_provider.dartcalleddetector.detect(image)with a rawCameraImage(that method takes decoded image bytes; camera streams needdetectFromCameraImage) and treatedHand.landmarksas already normalised, whenhand_detectionreturns them in the source image's pixel space. The example now uses the correct camera-streaming call, normalises landmarks by the frame's width/height, and aliaseshand_detection'sHandedness/HandLandmarkTypeto avoid colliding with air_pointer's own. - Worker no longer killed before it can release MediaPipe resources —
GestureInputSource.dispose()(web) posted a'dispose'message and calledWorker.terminate()in the same tick; sincepostMessagedelivery is asynchronous, the worker was always hard-terminated before it processed the message, solandmarker.close()(which releases the WASM/WebGL delegate) never ran.dispose()now gives the worker a short window to shut itself down gracefully before terminating it as a backstop.
Performance
- Skip debug-only work when nobody's listening (web) —
GestureInputSourceparsedworldLandmarks, computed per-hand bounding boxes, and built a fullGestureDebugInfosnapshot on every processed frame regardless of whether anything was subscribed todebugInfo. That work now only runs whendebugInfohas an active listener, avoiding needless per-frame allocation for consumers who don't use the debug stream. - Extend the debug-only skip to the worker itself (web) — the previous
fix only gated main-thread parsing; the inference worker still built
worldHands/handednessand transferred them across the thread boundary on every frame regardless of listener state.GestureInputSourcenow tells the worker (via a newsetDebugEnabledmessage, sent fromdebugInfo'sonListen/onCancel) whether to compute that data at all, and the main thread only dartifies the JS payload fields it actually needs per frame instead of eagerly converting the whole message.
New
GestureInputSource.workerUrl(web, default'hand_tracker_worker.js') — the inference worker script location is now configurable, for apps that serve the worker from a subdirectory. Must be same-origin.
- Release-build latency logging removed — the per-60-frame latency
-
0.2.227 Jun 2026Release notes
Open source →Bug fixes
- Drag-to-scroll interference —
CanvasScrollEventcould fire immediately
after releasing a drag when the hand briefly adopted a pointing-like shape
during the transition. Fixed by adding ascrollConfirmFramesgate (default
2, ~67 ms at 30 fps) that requires N consecutive pointing frames before
scroll activates, consistent with the existingpinchConfirmFramesguard for
drag. SetscrollConfirmFrames: 1to restore the old single-frame-baseline
behaviour.
New
-
handLandmarkConnections— exported constant listing the 21 skeleton
bone pairs for the MediaPipe hand topology. Iterate over it to draw a
skeleton overlay without hand-coding the connections yourself. -
CanvasGestureEvent.confidence(double, default1.0) — ML
confidence of the gesture classification. Always1.0on web (rule-based
heuristic); populated from the backend's score on native when using a
model-basedLandmarkProvidersuch ashand_detection. -
HandDetectionFrame.gestureConfidence/secondHandGestureConfidence
(double, default1.0) — per-gesture confidence fields for native backends
to expose ML scores that flow through toCanvasGestureEvent.confidence. -
GestureInputSourceconfidence thresholds (web) — three new constructor
paramsminHandDetectionConfidence,minHandPresenceConfidence, and
minTrackingConfidence(each default0.5, matching MediaPipe defaults).
Tune these to trade sensitivity against false positives without modifying the
JS worker. -
HandDetectionFrame.worldLandmarks/secondWorldLandmarks—
optional world-space landmark lists (metric scale, hand-centre origin) for
native backends to expose orientation-invariant landmark data. On web,
populated from MediaPipe'sworldLandmarksresult automatically. -
GestureDebugInfo.worldLandmarks/secondWorldLandmarks— world
landmarks forwarded through the debug snapshot so overlays and classifiers
can consume them. -
HandDetectionFrame.boundingBox/secondHandBoundingBox(Rect?)
— axis-aligned bounding box of each detected hand in normalised image
coordinates. Native backends set this from their ML model's output. On web,
computed automatically as the min/max envelope of the 21 image-space
landmarks. -
GestureDebugInfo.boundingBox/secondHandBoundingBox(Rect?)
— bounding box forwarded through the debug snapshot for overlay rendering. -
StylusInputSource— input source for Apple Pencil, Samsung S-Pen, and
anyPointerDeviceKind.stylus/PointerDeviceKind.invertedStylusdevice.
Maps pen contact to element-drag canvas events (Down/Move/Up, tap, double-tap,
hover, cancel). Filters out mouse/touch/trackpad events so it can be combined
withMouseInputSourceorTouchInputSourcewithout duplicates. Exposes an
eraserModeStreamthat emitstrue/falseon eraser-vs-tip mode changes. -
CanvasDownEvent.pressure/CanvasMoveEvent.pressure(double,
default1.0) — hardware pen pressure (0.0–1.0) carried by stylus down and
move events. Backwards-compatible: non-stylus sources emit1.0; existing
construction and pattern-matching compiles unchanged. -
defaultPointerSource()— factory function that returns the best-fit
CanvasInputSourcefor the current platform:TouchInputSourceon Android /
iOS,MouseInputSourceeverywhere else (including web). UseskIsWeband
defaultTargetPlatformfrompackage:flutter/foundation.dart, so it is safe
to call on web without conditional imports.GestureInputSourceis not
included — it requires platform-specific configuration that cannot be inferred
automatically. -
TouchInputSource— mobile-native input source for Android and iOS.
Single-finger drag →CanvasScrollEvent(direct-manipulation pan); two-finger
pinch/spread →CanvasScaleEvent; fling →CanvasScrollEventwith non-zero
velocityfield for consumer momentum animations. Tap detection uses raw
Listenerevents rather thanGestureDetectorso taps are never dropped by
ScaleGestureRecognizer's kPanSlop movement threshold. -
CanvasScrollEvent.velocity(Offset, defaultOffset.zero) — fling
velocity field added toCanvasScrollEvent. Non-zero only on fling events
emitted byTouchInputSource. Backwards-compatible: existing code that
constructs or pattern-matchesCanvasScrollEventcompiles unchanged.
Documentation
- README — Documented three previously undocumented public API surfaces:
CanvasInputControllermuting (muteWhenActive/activeStream),
GestureInputSource.statusStreamlifecycle states, and
GestureInputSource.setFilterParams(). - README — Added self-hosting section documenting
mediaPipeBaseUrland
modelAssetUrlconstructor parameters (Flutter Web only); corrected the
"no self-hosted model" limitation which was inaccurate — self-hosting has
been wired since 0.1.0. - KNOWN_LIMITATIONS — Corrected "No rotation gesture" bullet: rotation IS
emitted byGestureInputSourceviaCanvasScaleEvent.rotation; only
MouseInputSourceomits it (Flutter'sScaleGestureRecognizerlimitation). - CONTRIBUTING — Fixed wrong directory paths (
lib/src/filters/→
lib/src/filter/, removed non-existentlib/src/calibration/); added
development prerequisites table. - SECURITY — Replaced GitHub default template with accurate policy for a
0.2.x package (correct version table, advisory link, SLA, scope table). - CLAUDE.md — Added architecture guide for AI-assisted development,
covering the js_interop boundary invariant, quality gates, testing philosophy,
and breaking-change rules.
Fixes
pubspec.yamlversion bumped from0.2.0to0.2.1(was incorrectly
behindCHANGELOG.md); addedhomepageandissue_trackerfields.GestureInputSource(native) —dispose()now wraps_frameSub?.cancel()
inunawaited(), consistent with the threeStreamController.close()calls
below it.
Example
- 3D Room — Camera now starts inside the room (dist 900 → 250) for an
immersive interior view instead of a dollhouse perspective from outside. - 3D Room — Furniture can now be rotated on its Y-axis: horizontal scroll
while a piece is selected rotates it in-place; two-finger twist
(CanvasScaleEvent.rotation) also rotates the selected piece instead of the
camera.
Release notes
Open source →Bug fixes
- Drag-to-scroll interference —
CanvasScrollEventcould fire immediately after releasing a drag when the hand briefly adopted a pointing-like shape during the transition. Fixed by adding ascrollConfirmFramesgate (default2, ~67 ms at 30 fps) that requires N consecutive pointing frames before scroll activates, consistent with the existingpinchConfirmFramesguard for drag. SetscrollConfirmFrames: 1to restore the old single-frame-baseline behaviour.
New
-
handLandmarkConnections— exported constant listing the 21 skeleton bone pairs for the MediaPipe hand topology. Iterate over it to draw a skeleton overlay without hand-coding the connections yourself. -
CanvasGestureEvent.confidence(double, default1.0) — ML confidence of the gesture classification. Always1.0on web (rule-based heuristic); populated from the backend's score on native when using a model-basedLandmarkProvidersuch ashand_detection. -
HandDetectionFrame.gestureConfidence/secondHandGestureConfidence(double, default1.0) — per-gesture confidence fields for native backends to expose ML scores that flow through toCanvasGestureEvent.confidence. -
GestureInputSourceconfidence thresholds (web) — three new constructor paramsminHandDetectionConfidence,minHandPresenceConfidence, andminTrackingConfidence(each default0.5, matching MediaPipe defaults). Tune these to trade sensitivity against false positives without modifying the JS worker. -
HandDetectionFrame.worldLandmarks/secondWorldLandmarks— optional world-space landmark lists (metric scale, hand-centre origin) for native backends to expose orientation-invariant landmark data. On web, populated from MediaPipe'sworldLandmarksresult automatically. -
GestureDebugInfo.worldLandmarks/secondWorldLandmarks— world landmarks forwarded through the debug snapshot so overlays and classifiers can consume them. -
HandDetectionFrame.boundingBox/secondHandBoundingBox(Rect?) — axis-aligned bounding box of each detected hand in normalised image coordinates. Native backends set this from their ML model's output. On web, computed automatically as the min/max envelope of the 21 image-space landmarks. -
GestureDebugInfo.boundingBox/secondHandBoundingBox(Rect?) — bounding box forwarded through the debug snapshot for overlay rendering. -
StylusInputSource— input source for Apple Pencil, Samsung S-Pen, and anyPointerDeviceKind.stylus/PointerDeviceKind.invertedStylusdevice. Maps pen contact to element-drag canvas events (Down/Move/Up, tap, double-tap, hover, cancel). Filters out mouse/touch/trackpad events so it can be combined withMouseInputSourceorTouchInputSourcewithout duplicates. Exposes aneraserModeStreamthat emitstrue/falseon eraser-vs-tip mode changes. -
CanvasDownEvent.pressure/CanvasMoveEvent.pressure(double, default1.0) — hardware pen pressure (0.0–1.0) carried by stylus down and move events. Backwards-compatible: non-stylus sources emit1.0; existing construction and pattern-matching compiles unchanged. -
defaultPointerSource()— factory function that returns the best-fitCanvasInputSourcefor the current platform:TouchInputSourceon Android / iOS,MouseInputSourceeverywhere else (including web). UseskIsWebanddefaultTargetPlatformfrompackage:flutter/foundation.dart, so it is safe to call on web without conditional imports.GestureInputSourceis not included — it requires platform-specific configuration that cannot be inferred automatically. -
TouchInputSource— mobile-native input source for Android and iOS. Single-finger drag →CanvasScrollEvent(direct-manipulation pan); two-finger pinch/spread →CanvasScaleEvent; fling →CanvasScrollEventwith non-zerovelocityfield for consumer momentum animations. Tap detection uses rawListenerevents rather thanGestureDetectorso taps are never dropped byScaleGestureRecognizer's kPanSlop movement threshold. -
CanvasScrollEvent.velocity(Offset, defaultOffset.zero) — fling velocity field added toCanvasScrollEvent. Non-zero only on fling events emitted byTouchInputSource. Backwards-compatible: existing code that constructs or pattern-matchesCanvasScrollEventcompiles unchanged.
Documentation
- README — Documented three previously undocumented public API surfaces:
CanvasInputControllermuting (muteWhenActive/activeStream),GestureInputSource.statusStreamlifecycle states, andGestureInputSource.setFilterParams(). - README — Added self-hosting section documenting
mediaPipeBaseUrlandmodelAssetUrlconstructor parameters (Flutter Web only); corrected the "no self-hosted model" limitation which was inaccurate — self-hosting has been wired since 0.1.0. - KNOWN_LIMITATIONS — Corrected "No rotation gesture" bullet: rotation IS
emitted by
GestureInputSourceviaCanvasScaleEvent.rotation; onlyMouseInputSourceomits it (Flutter'sScaleGestureRecognizerlimitation). - CONTRIBUTING — Fixed wrong directory paths (
lib/src/filters/→lib/src/filter/, removed non-existentlib/src/calibration/); added development prerequisites table. - SECURITY — Replaced GitHub default template with accurate policy for a 0.2.x package (correct version table, advisory link, SLA, scope table).
- CLAUDE.md — Added architecture guide for AI-assisted development, covering the js_interop boundary invariant, quality gates, testing philosophy, and breaking-change rules.
Fixes
pubspec.yamlversion bumped from0.2.0to0.2.1(was incorrectly behindCHANGELOG.md); addedhomepageandissue_trackerfields.GestureInputSource(native) —dispose()now wraps_frameSub?.cancel()inunawaited(), consistent with the threeStreamController.close()calls below it.
Example
- 3D Room — Camera now starts inside the room (dist 900 → 250) for an immersive interior view instead of a dollhouse perspective from outside.
- 3D Room — Furniture can now be rotated on its Y-axis: horizontal scroll
while a piece is selected rotates it in-place; two-finger twist
(
CanvasScaleEvent.rotation) also rotates the selected piece instead of the camera.
- Drag-to-scroll interference —
-
0.2.022 Jun 2026Release notes
Open source →New events
CanvasDoubleTapEvent— emitted alongside the secondCanvasTapEventwhen two taps occur withindoubleTapWindow(default 300 ms).MouseInputSourcedetects it via a DateTime gap between releases;HandGestureRecognizervia elapsed time between consecutive dwell taps.CanvasLongPressEvent— emitted after the pointer (or cursor) holds still forlongPressDuration.MouseInputSourceusesGestureDetector.onLongPressStart;HandGestureRecognizeruses a configurablelongPressDurationthreshold shared with the dwell timer (shorter threshold always wins).CanvasGestureEvent(gesture)— edge-triggered on eachRecognizedGesturechange (non-none) for both the primary and secondary hand. Fires once when a gesture starts; resets when the hand is lost so the same gesture can fire again on re-entry.
HandGestureRecognizer
longPressDuration— new constructor param;Duration.zero= disabled (default).doubleTapWindow— new constructor param controlling the inter-tap interval that qualifies as a double tap (default 300 ms).longPressDurationS/doubleTapWindowS— public getters for the above.- Pointing-finger scroll now emits both horizontal and vertical deltas
(
Offset(scrollDx, scrollDy)instead ofOffset(0, scrollDy)). _checkDwellrefactored to_checkDwellEventsreturningList<PointerInputEvent>to support multiple simultaneous events (tap + double-tap).
MouseInputSource
CanvasCancelEventemitted fromListener.onPointerCancelwhen the OS interrupts an active drag (context menu, window switch, etc.).CanvasLongPressEventviaGestureDetector.onLongPressStart. If a drag was in progress,CanvasCancelEventis emitted first to close it cleanly.- Double-tap detected via a
DateTimegap between successive tap releases.
GestureInputSource
longPressDurationanddoubleTapWindowforwarded toHandGestureRecognizeron both web and native variants.maxHandsparam (default 2) forwarded to the MediaPipe web worker'snumHandsoption; set to 1 to improve performance in single-hand apps.CanvasGestureEventemitted for secondary hand gestures in addition to the primary hand._lastGestureresets tononewhen tracking is lost, so the same gesture fires again when the hand re-enters the frame.
Breaking changes
OneEuroFilterremoved from the public barrel export (air_pointer.dart). It was an internal smoothing detail; import directly frompackage:air_pointer/src/filter/one_euro_filter.dartif needed.
-
0.1.021 Jun 2026Release notes
Open source →Initial release.
Events
PointerInputEventsealed hierarchy —CanvasTapEvent,CanvasDownEvent,CanvasMoveEvent,CanvasUpEvent,CanvasCancelEvent,CanvasHoverEvent,CanvasScrollEvent(withisTrackpad),CanvasScaleEvent(withrotation),CanvasScaleEndEvent,CanvasSwipeEvent(withSwipeDirectionandvelocity). All events use theCanvasprefix to avoid collisions with Flutter's own pointer events.
Core abstractions
CanvasInputSource— abstract boundary; all input origins implement this contract.CanvasInputController— merges events from multipleCanvasInputSources into a single broadcast stream; foldsbuildSurfacewrappers in order.
MouseInputSource
- Maps Flutter gesture-arena callbacks to canvas events: tap →
CanvasTapEvent, one-finger drag → Down/Move/Up, two-finger pinch →CanvasScaleEvent, scroll wheel and native trackpad pinch (PointerScaleEvent) →CanvasScrollEvent/CanvasScaleEvent, mouse hover →CanvasHoverEvent. scrollMultiplier— scales scroll deltas before emission.tapSlop— configurable tap-slop threshold (default 10 px).isTrackpadonCanvasScrollEvent— true for macOS/iOS two-finger pan; consumers can skip ticker-based inertia since the OS already provides momentum.
GestureInputSource (Flutter Web)
MediaPipe HandLandmarker running in a dedicated web worker (off the main thread). Zero-copy
ImageBitmaptransfer. Camera permission / hardware / context errors all produce typedHandTrackingStatusstates.HandGestureRecognizer— pure-Dart state machine; fully testable without a camera.- Acquisition gate: N consecutive frames (default 3) to confirm hand presence.
- Hysteresis: separate close (default 0.05) and open (default 0.08) thresholds prevent chatter near the boundary.
- Grace window: N frames (default 5) before declaring the hand lost; cursor freezes and dwell progress is preserved through brief occlusions.
- Clutch / Midas-touch guard: pinch is blocked until the hand opens after confirmation, preventing accidental drags when the hand enters the frame already pinched.
CanvasCancelEvent(notCanvasUpEvent) when the hand exits during an active drag.- Two-hand spread →
CanvasScaleEventwith rotation delta;CanvasScaleEndEventwhen the second hand leaves. - Dwell-click: cursor must hold still within
dwellRadiusfordwellDurationto emitCanvasTapEvent. Progress is reported viaGestureDebugInfo.dwellProgress(0–1). Dwell is preserved through the grace window so brief occlusions don't reset progress. - Pointing-finger scroll: index extended + middle curled →
CanvasScrollEventdriven by vertical fingertip movement. Enabled viascrollEnabled: true; scaled byscrollScale. - Swipe gesture: fast directional movement (velocity >
swipeThresholdpx/s) in an open hand emitsCanvasSwipeEvent. 60/40 dominance ratio prevents diagonal false-positives; 400 ms cooldown suppresses repeated firing from one gesture. Disabled by default (swipeThreshold: 0).
OneEuroFilter— adaptive low-pass filter (Casiez et al., CHI 2012) for landmark coordinates. Exposesvelocityfor prediction and swipe detection.GestureCalibrator— accumulates open/closed pose samples fromGestureDebugInfo.pinchDistanceand computes per-userCalibrationResult.LandmarkProvider— platform interface for native landmark sources (e.g. TFLite on iOS/Android); web implementation uses MediaPipe via web worker.- Debug support —
GestureInputSource.debugInfostream ofGestureDebugInfo(phase, pinch distance, dwell progress, pointing flag, landmarks, worker latency, round-trip latency).buildCameraPreviewreturns a live camera widget.
Example
example/ships two demos driven entirely throughCanvasInputController:- Sandbox canvas — draggable boxes with dot-grid background, inertia scrolling, two-finger pinch-to-zoom, dwell-click, pointing-finger scroll, debug overlay, camera preview, calibration dialog, and zoom badge.
- Netflix-style demo — scrollable content grid with hero section, card rows with horizontal scrolling, detail overlay, and full air-pointer interaction (pinch-drag with inertia, dwell-click with progress ring, pointing scroll, swipe navigation).