air_pointer 0.2.2 copy "air_pointer: ^0.2.2" to clipboard
air_pointer: ^0.2.2 copied to clipboard

Platform-agnostic canvas input for Flutter. Unifies mouse, trackpad, and MediaPipe hand-gesture events behind a single CanvasInputController.

0.2.2 #

Bug fixes #

  • Drag-to-scroll interferenceCanvasScrollEvent could fire immediately after releasing a drag when the hand briefly adopted a pointing-like shape during the transition. Fixed by adding a scrollConfirmFrames gate (default 2, ~67 ms at 30 fps) that requires N consecutive pointing frames before scroll activates, consistent with the existing pinchConfirmFrames guard for drag. Set scrollConfirmFrames: 1 to 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, default 1.0) — ML confidence of the gesture classification. Always 1.0 on web (rule-based heuristic); populated from the backend's score on native when using a model-based LandmarkProvider such as hand_detection.

  • HandDetectionFrame.gestureConfidence / secondHandGestureConfidence (double, default 1.0) — per-gesture confidence fields for native backends to expose ML scores that flow through to CanvasGestureEvent.confidence.

  • GestureInputSource confidence thresholds (web) — three new constructor params minHandDetectionConfidence, minHandPresenceConfidence, and minTrackingConfidence (each default 0.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's worldLandmarks result 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 any PointerDeviceKind.stylus / PointerDeviceKind.invertedStylus device. 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 with MouseInputSource or TouchInputSource without duplicates. Exposes an eraserModeStream that emits true / false on eraser-vs-tip mode changes.

  • CanvasDownEvent.pressure / CanvasMoveEvent.pressure (double, default 1.0) — hardware pen pressure (0.0–1.0) carried by stylus down and move events. Backwards-compatible: non-stylus sources emit 1.0; existing construction and pattern-matching compiles unchanged.

  • defaultPointerSource() — factory function that returns the best-fit CanvasInputSource for the current platform: TouchInputSource on Android / iOS, MouseInputSource everywhere else (including web). Uses kIsWeb and defaultTargetPlatform from package:flutter/foundation.dart, so it is safe to call on web without conditional imports. GestureInputSource is 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 → CanvasScrollEvent with non-zero velocity field for consumer momentum animations. Tap detection uses raw Listener events rather than GestureDetector so taps are never dropped by ScaleGestureRecognizer's kPanSlop movement threshold.

  • CanvasScrollEvent.velocity (Offset, default Offset.zero) — fling velocity field added to CanvasScrollEvent. Non-zero only on fling events emitted by TouchInputSource. Backwards-compatible: existing code that constructs or pattern-matches CanvasScrollEvent compiles unchanged.

Documentation #

  • README — Documented three previously undocumented public API surfaces: CanvasInputController muting (muteWhenActive/activeStream), GestureInputSource.statusStream lifecycle states, and GestureInputSource.setFilterParams().
  • README — Added self-hosting section documenting mediaPipeBaseUrl and modelAssetUrl constructor 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 GestureInputSource via CanvasScaleEvent.rotation; only MouseInputSource omits it (Flutter's ScaleGestureRecognizer limitation).
  • CONTRIBUTING — Fixed wrong directory paths (lib/src/filters/lib/src/filter/, removed non-existent lib/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.yaml version bumped from 0.2.0 to 0.2.1 (was incorrectly behind CHANGELOG.md); added homepage and issue_tracker fields.
  • GestureInputSource (native)dispose() now wraps _frameSub?.cancel() in unawaited(), consistent with the three StreamController.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.

0.2.1 #

Bug fixes #

  • MouseInputSource_lastTapTime is now cleared in the drag path of _onScaleEnd so a tap followed by a quick drag no longer poisons the double-tap window for the next genuine tap.
  • MouseInputSource_onPointerCancel now resets _isPinchZooming and _lastTapTime so an OS-interrupted gesture cannot leave the source in pinch-zoom mode (causing the next tap to be swallowed as CanvasScaleEndEvent) or produce a spurious CanvasDoubleTapEvent.
  • HandGestureRecognizer — the inter-tap timer (_timeSinceLastDwellS) now advances during GesturePhase.down (pinch-drag) and pointing-finger scroll so time spent in those phases correctly counts toward the double-tap window.
  • HandGestureRecognizer_timeSinceLastDwellS is reset to double.infinity when grace expires, preventing a stale tap from a previous tracking session from chaining as a double-tap on re-entry.
  • HandGestureRecognizer — after a CanvasDoubleTapEvent fires, _timeSinceLastDwellS is reset to double.infinity instead of 0, matching MouseInputSource behaviour (a third rapid dwell no longer emits a second double-tap).
  • GestureInputSource (native)maxHands constructor parameter is now stored as a readable final int maxHands field instead of being silently discarded; callers can query it when configuring their LandmarkProvider.
  • CanvasGestureEvent — new isSecondHand field (bool, default false) distinguishes primary-hand gestures from secondary-hand gestures; both GestureInputSource backends now set isSecondHand: true on secondary-hand emissions.
  • GestureInputSource (web)classifyGesture is no longer called twice per frame per hand; the already-computed locals are reused when building GestureDebugInfo.
  • GestureInputSource (web)recognized_gesture.dart import sorted to correct alphabetical position (lint: directives_ordering).

0.2.0 #

New events #

  • CanvasDoubleTapEvent — emitted alongside the second CanvasTapEvent when two taps occur within doubleTapWindow (default 300 ms). MouseInputSource detects it via a DateTime gap between releases; HandGestureRecognizer via elapsed time between consecutive dwell taps.
  • CanvasLongPressEvent — emitted after the pointer (or cursor) holds still for longPressDuration. MouseInputSource uses GestureDetector.onLongPressStart; HandGestureRecognizer uses a configurable longPressDuration threshold shared with the dwell timer (shorter threshold always wins).
  • CanvasGestureEvent(gesture) — edge-triggered on each RecognizedGesture change (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 of Offset(0, scrollDy)).
  • _checkDwell refactored to _checkDwellEvents returning List<PointerInputEvent> to support multiple simultaneous events (tap + double-tap).

MouseInputSource #

  • CanvasCancelEvent emitted from Listener.onPointerCancel when the OS interrupts an active drag (context menu, window switch, etc.).
  • CanvasLongPressEvent via GestureDetector.onLongPressStart. If a drag was in progress, CanvasCancelEvent is emitted first to close it cleanly.
  • Double-tap detected via a DateTime gap between successive tap releases.

GestureInputSource #

  • longPressDuration and doubleTapWindow forwarded to HandGestureRecognizer on both web and native variants.
  • maxHands param (default 2) forwarded to the MediaPipe web worker's numHands option; set to 1 to improve performance in single-hand apps.
  • CanvasGestureEvent emitted for secondary hand gestures in addition to the primary hand.
  • _lastGesture resets to none when tracking is lost, so the same gesture fires again when the hand re-enters the frame.

Breaking changes #

  • OneEuroFilter removed from the public barrel export (air_pointer.dart). It was an internal smoothing detail; import directly from package:air_pointer/src/filter/one_euro_filter.dart if needed.

0.1.0 #

Initial release.

Events #

  • PointerInputEvent sealed hierarchyCanvasTapEvent, CanvasDownEvent, CanvasMoveEvent, CanvasUpEvent, CanvasCancelEvent, CanvasHoverEvent, CanvasScrollEvent (with isTrackpad), CanvasScaleEvent (with rotation), CanvasScaleEndEvent, CanvasSwipeEvent (with SwipeDirection and velocity). All events use the Canvas prefix to avoid collisions with Flutter's own pointer events.

Core abstractions #

  • CanvasInputSource — abstract boundary; all input origins implement this contract.
  • CanvasInputController — merges events from multiple CanvasInputSources into a single broadcast stream; folds buildSurface wrappers 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).
  • isTrackpad on CanvasScrollEvent — 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 (not CanvasUpEvent) when the hand exits during an active drag.
    • Two-hand spread → CanvasScaleEvent with rotation delta; CanvasScaleEndEvent when the second hand leaves.
    • Dwell-click: cursor must hold still within dwellRadius for dwellDuration to emit CanvasTapEvent. Progress is reported via GestureDebugInfo.dwellProgress (0–1). Dwell is preserved through the grace window so brief occlusions don't reset progress.
    • Pointing-finger scroll: index extended + middle curled → CanvasScrollEvent driven by vertical fingertip movement. Enabled via scrollEnabled: true; scaled by scrollScale.
    • Swipe gesture: fast directional movement (velocity > swipeThreshold px/s) in an open hand emits CanvasSwipeEvent. 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. Exposes velocity for prediction and swipe detection.
  • GestureCalibrator — accumulates open/closed pose samples from GestureDebugInfo.pinchDistance and computes per-user CalibrationResult.
  • LandmarkProvider — platform interface for native landmark sources (e.g. TFLite on iOS/Android); web implementation uses MediaPipe via web worker.
  • Debug supportGestureInputSource.debugInfo stream of GestureDebugInfo (phase, pinch distance, dwell progress, pointing flag, landmarks, worker latency, round-trip latency). buildCameraPreview returns 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).
4
likes
160
points
380
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Platform-agnostic canvas input for Flutter. Unifies mouse, trackpad, and MediaPipe hand-gesture events behind a single CanvasInputController.

Repository (GitHub)
View/report issues
Contributing

Topics

#canvas #gestures #hand-tracking #mediapipe

License

Apache-2.0 (license)

Dependencies

flutter, web

More

Packages that depend on air_pointer