air_pointer 0.2.0
air_pointer: ^0.2.0 copied to clipboard
Platform-agnostic canvas input for Flutter. Unifies mouse, trackpad, and MediaPipe hand-gesture events behind a single CanvasInputController.
0.2.1 #
Bug fixes #
MouseInputSource—_lastTapTimeis now cleared in the drag path of_onScaleEndso a tap followed by a quick drag no longer poisons the double-tap window for the next genuine tap.MouseInputSource—_onPointerCancelnow resets_isPinchZoomingand_lastTapTimeso an OS-interrupted gesture cannot leave the source in pinch-zoom mode (causing the next tap to be swallowed asCanvasScaleEndEvent) or produce a spuriousCanvasDoubleTapEvent.HandGestureRecognizer— the inter-tap timer (_timeSinceLastDwellS) now advances duringGesturePhase.down(pinch-drag) and pointing-finger scroll so time spent in those phases correctly counts toward the double-tap window.HandGestureRecognizer—_timeSinceLastDwellSis reset todouble.infinitywhen grace expires, preventing a stale tap from a previous tracking session from chaining as a double-tap on re-entry.HandGestureRecognizer— after aCanvasDoubleTapEventfires,_timeSinceLastDwellSis reset todouble.infinityinstead of0, matchingMouseInputSourcebehaviour (a third rapid dwell no longer emits a second double-tap).GestureInputSource(native) —maxHandsconstructor parameter is now stored as a readablefinal int maxHandsfield instead of being silently discarded; callers can query it when configuring theirLandmarkProvider.CanvasGestureEvent— newisSecondHandfield (bool, defaultfalse) distinguishes primary-hand gestures from secondary-hand gestures; bothGestureInputSourcebackends now setisSecondHand: trueon secondary-hand emissions.GestureInputSource(web) —classifyGestureis no longer called twice per frame per hand; the already-computed locals are reused when buildingGestureDebugInfo.GestureInputSource(web) —recognized_gesture.dartimport sorted to correct alphabetical position (lint:directives_ordering).
0.2.0 #
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.0 #
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 ImageBitmap transfer. Camera permission / hardware / context errors all
produce typed HandTrackingStatus states.
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 through CanvasInputController:
- 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).