Mjolnir for Humans. Hand, face, and pose gesture control for deck.gl.
import { useThor, setFistAction } from "thor.gl";
function MyMap() {
const [viewState, setViewState] = useState(INITIAL_VIEW);
const { widgets } = useThor({
setViewState,
enabled: true,
});
return (
<DeckGL
viewState={viewState}
widgets={widgets}
onViewStateChange={({ viewState: vs }) => setViewState(vs)}
/>
);
}| Gesture | Input | Effect | Group |
|---|---|---|---|
pinch-pan |
Pinch + drag (1 hand) | Pan longitude/latitude | navigation |
pinch-zoom |
Pinch (2 hands) apart/together | Zoom in/out | navigation |
pinch-rotate |
Pinch (2 hands) + twist | Rotate bearing | rotation |
pinch-pitch |
Pinch (2 hands) + move up/down | Tilt pitch | pitch |
open-palm |
Open palm | Signal / stop inertia | signal |
fist |
Closed fist (hold 300ms) | Fire action callback | action |
All gesture parameters have defaults and can be overridden via useThor:
const { widgets } = useThor({
setViewState,
config: {
// Global detection
minConfidence: 0.5, // hand detection confidence floor
grabDelay: 100, // ms pinch must be held to confirm
pinchThreshold: 0.06, // thumb-index distance for pinch
// Pan
panSensitivity: 5.0,
panSmoothing: 0.4, // low-pass filter (0-1, higher = smoother)
panMoveDeadzone: 0.004, // ignore jitter below this
// Zoom
zoomSensitivity: 10,
zoomDeadzone: 0.015,
// Rotate
rotateSensitivity: 40,
rotateDeadzone: 0.015, // radians
// Pitch
pitchSensitivity: 80,
pitchDeadzone: 0.008,
// Fist
fistConfirmMs: 300, // hold duration before firing
fistCooldownMs: 1500, // cooldown between fires
},
});Implement GestureHandler and register it:
import { registerGesture, type GestureHandler } from "thor.gl";
const myGesture: GestureHandler = {
name: "thumbs-up",
requires: ["hands"],
detect(frame) {
// Return { gesture: "thumbs-up", data: { ... } } or null
},
apply(detection, viewState, config) {
// Return modified viewState (or same reference for no change)
return viewState;
},
// Optional lifecycle
onActivate() {},
onDeactivate() {},
reset() {},
};
registerGesture(myGesture, { priority: 15, group: "signal" });Gestures in the same group compete β highest priority wins. Gestures in different groups coexist. This lets pan + fist fire simultaneously while pan and zoom resolve within "navigation".
Camera --> MediaPipe HandLandmarker --> ThorFrame
|
GestureHandler.detect() <--+
|
resolveConflicts()
|
GestureHandler.apply() --> ViewState delta
|
ThorWidget.render() --> Canvas overlay
- Detection (
src/detection/) β MediaPipe wrapper. Auto-promotes from HandLandmarker to HolisticLandmarker when face/pose gestures are registered. - Gestures (
src/gestures/) β Registry ofGestureHandlerimplementations. Each handler owns its own detect/apply lifecycle. - Engine (
src/engine.ts) β Frame loop at ~30fps. Fans out to handlers, resolves conflicts, merges viewState, notifies widget. - Widget (
src/ThorWidget.ts) β deck.gl Widget that renders fingertip dots, pinch cursors, and mode indicators on a mirrored canvas overlay. - Hook (
src/useThor.ts) β React hook that wires it all together. Returns a stablewidgetsarray for DeckGL.
A standalone demo app lives in demo/. To run it:
cd demo
npm install
npm run devThis starts a Vite dev server with a satellite globe you can control with hand gestures.
@deck.gl/core>= 9@mediapipe/tasks-visionreact>= 18
MIT