custom-canvas-and-gestures

Installation
SKILL.md

Custom Canvas & Gestures

Hand-painted surfaces (CustomPainter/Canvas) own every pixel, so they look byte-identical across platforms — but only if the painter stays dumb, the coordinate math is shared, and gestures translate rather than decide. This skill is the contract for that layer: how it splits, how pixels and pointers agree, and how a screen reader hears a canvas.

Read the reference for the task at hand:

  • references/painter-and-scene.md — the View/Painter/Scene split, the shared transform, hit-testing by geometry class, zero-allocation paint(), the two-path repaint pitfall.
  • references/gestures-and-semantics.md — gesture-as-translator, arena arbitration, clamp-not-collision drags, custom-canvas Semantics, action/gesture parity.
  • references/text-and-shapes.md — measured TextPainter fitting, optical centring, RoundedSuperellipseBorder, physical-pixel hairlines, concentric radii, painter-over-image-asset.

Run scripts/check_painter_hygiene.sh and scripts/analyze.sh before a PR.

Non-negotiable rules

  1. A canvas surface is three collaborators — a View, a Painter, and an immutable Scene — never one god-widget. The View watches the ViewModel, builds an immutable Scene value type holding everything the painter needs and nothing more, and hands it to CustomPaint. The Painter extends CustomPainter is dumb: no Notifier, no BuildContext, no DateTime, no domain rules — it draws the Scene and never decides state. A dumb painter is testable and re-skinnable.

  2. shouldRepaint is one value compare — old.scene != scene — and nothing else. Because Scene has value equality, this is both correct and cheap. => true repaints every frame; a wrong false freezes the surface — both fail silently. Config-driven repaints go through shouldRepaint; per-frame animation goes through the repaint: Listenable. The two paths must never double-repaint.

  3. Exactly one affine transform maps logical space ↔ canvas pixels, read by BOTH the painter and the hit-tester. Build it once per layout from the incoming Size; expose toCanvas/toLogical as exact inverses. The single most corrosive bug is a painter and hit-tester that disagree by a few pixels; a shared, tested transform forecloses it. The hit-tester never re-derives scale from size — it calls transform.toLogical.

Installs
77
GitHub Stars
34
First Seen
Aug 15, 2026
custom-canvas-and-gestures — zakariaf/flutter-skills