Hanvas is a browser-based hand-tracking paint app built with MediaPipe Tasks Vision. It uses your webcam to detect hand landmarks in real time and draws persistent paint strokes on a canvas.
- HTML/CSS/JavaScript (no frontend framework)
- MediaPipe Tasks Vision (
@mediapipe/tasks-vision@0.10.0) - MediaPipe drawing helpers (
@mediapipe/drawing_utils,@mediapipe/hands) - Static hosting via Python
http.serveror Nodeserve
- Accesses webcam through
navigator.mediaDevices.getUserMedia(). - Runs continuous per-frame hand landmark detection.
- Supports up to 2 hands (
numHands: 2).
- Computes a hand midpoint from all landmarks (
getLandmarksMidpoint). - Draws when hand state is:
FistPartial hand
- Does not add new stroke points for
Open hand. - Keeps drawn trails persistent across frames (until cleared).
- Each hand has its own trail bucket (
midpointTrails[handLabel]). - If two points are close enough, intermediate points are inserted.
- This reduces visible gaps when hand motion is fast.
- Processes all detected hands in each frame.
- Uses handedness labels from model output.
- Because video is mirrored for UX, labels are swapped:
- Model
Leftshown asRight - Model
Rightshown asLeft
- Model
CLEAR DRAWING: empties all stored trails.EXPORT: downloads current drawing as PNG.DRAWING: ON/OFF: enables or disables adding new paint points.COLOR MIXING: ON/OFF: enables or disables overlap blending.MIX MODE: SCREEN/MULTIPLY: cycles blend mode used while mixing is ON.- Color selector: changes current paint color for new points.
- Drawing uses canvas compositing (
globalCompositeOperation). - Mixing OFF:
source-over(normal paint layering). - Mixing ON: selected mode from
MIXING_MODES:screen(lighter overlap)multiply(darker overlap)
- Webcam and drawing containers share one CSS aspect ratio variable.
- JS updates
--media-aspect-ratiofrom actual video dimensions. - Result: webcam view and drawing canvas always match proportions.
This section explains the exact inference flow used in script.js.
FilesetResolver.forVisionTasks(...)loads the MediaPipe WASM runtime.HandLandmarker.createFromOptions(...)creates an inference object.- Model options include:
modelAssetPath: hosted hand landmark modeldelegate:GPUfirst, fallback toCPUrunningMode: starts asIMAGEnumHands:2
- App checks secure context (
httpsor localhost). - Calls
getUserMedia({ video: true }). - On
video.onloadeddata, marks webcam active and starts prediction loop.
Each frame in predictWebcam():
- Sync UI aspect ratio from
video.videoWidth/video.videoHeight. - Set
canvasElement.width/heightto match video pixel dimensions.
This keeps coordinate mapping accurate:
- Landmark normalized
xmaps tox * canvas.width - Landmark normalized
ymaps toy * canvas.height
On first prediction frame:
- If running mode is still
IMAGE, callhandLandmarker.setOptions({ runningMode: "VIDEO" }).
Why:
- VIDEO mode is optimized for temporal frame-by-frame inference.
predictWebcam() compares video.currentTime with lastVideoTime.
- If unchanged: skip inference (avoid duplicate work).
- If changed: run
handLandmarker.detectForVideo(video, startTimeMs).
Output (results) includes:
results.landmarks: array of 21 landmarks per handresults.handednesses: left/right confidence categories
For each detected hand:
inferHandState(landmarks)computes how many fingers are extended.- Uses wrist-to-tip and wrist-to-base distance ratios.
- Classifies into:
Open handFistPartial hand
If drawing is enabled and hand state is drawable (Fist or Partial hand):
- Compute midpoint of all landmarks.
- Add to the hand’s trail.
- Interpolate extra points for smooth continuity.
Render order in each loop:
- Clear canvas.
- Draw persistent midpoint trail (with selected color + mixing mode).
- Draw hand connectors and landmarks.
- Update hand-state text.
requestAnimationFrame(predictWebcam)schedules next frame while webcam is running.- This ties inference/render cadence to browser frame timing.
cd /home/martin/dev/Hanvas
/bin/python -m http.server 8000Open:
http://127.0.0.1:8000
npm install
npm start- Use a normal browser tab for webcam permissions.
- Some embedded preview contexts block camera access.
- Exported PNG mirrors horizontally to match displayed drawing orientation.
/api/community-posts is file-backed, so use persistent storage in Render:
- Set
DATA_DIRto a mounted disk path (configured inrender.yamlas/var/data/hanvas). - Keep
community-posts.jsonin that directory so shares survive restarts and cold starts.
Important:
- If your Render instance type does not support persistent disks (for example, some free tiers), use a managed database instead (Render Postgres/Redis or another external DB) because container-local files are not durable.