Infinity Canvas Logo

High-performance infinite canvas for Flutter

Pub Star on Github License: MIT Flutter Website Dart Version Flutter Version Platform Support Open Issues Pull Requests Contributors Last Commit


  • Mixed layers: positioned widgets, painter passes, and overlays
  • Programmatic camera/item control
  • Built for large scenes (node editors, maps, strategy UIs, visual tooling)

Node editor demo Galaxy trade map demo Database editor demo

Install

dependencies:
  infinity_canvas: ^0.9.0

Quickstart

import 'package:flutter/material.dart';
import 'package:infinity_canvas/infinity_canvas.dart';

class MyCanvasPage extends StatefulWidget {
  const MyCanvasPage({super.key});

  @override
  State<MyCanvasPage> createState() => _MyCanvasPageState();
}

class _MyCanvasPageState extends State<MyCanvasPage> {
  late final CanvasController controller;

  @override
  void initState() {
    super.initState();
    controller = CanvasController(
      initialWorldTopLeft: const Offset(-200, -120),
      initialZoom: 1.1,
    );
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return InfinityCanvas(
      controller: controller,
      enableCulling: true,
      layers: [
        CanvasLayer.positionedItems(
          id: 'nodes',
          items: [
            CanvasItem(
              id: 'node-1',
              worldPosition: const Offset(120, 100),
              behavior: CanvasItemBehavior.nodeEditor(),
              child: const _Card('Node 1'),
            ),
          ],
        ),
      ],
    );
  }
}

class _Card extends StatelessWidget {
  final String title;
  const _Card(this.title);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 180,
      height: 92,
      padding: const EdgeInsets.all(10),
      decoration: BoxDecoration(
        color: const Color(0xFF0F172A),
        borderRadius: BorderRadius.circular(10),
        border: Border.all(color: const Color(0x3347A3FF)),
      ),
      child: Text(title, style: const TextStyle(color: Colors.white)),
    );
  }
}

Use Like This

Move items

controller.items.setWorldPosition('node-1', const Offset(300, 240));

controller.items.setWorldPositions({
  'node-1': const Offset(300, 240),
  'node-2': const Offset(620, 300),
});

To add another item, create another CanvasItem(...) with its own id, worldPosition, and child, then include it in the items: [...] list.

Auto-sized items and reflow

Items default to CanvasItemSize.auto(), sizing themselves to their child. When you need to lay them out relative to each other but don't know their sizes up front (e.g. stacking a column of node-editor cards with variable content), listen for measured sizes instead of estimating or retrying post-frame:

// Reflow a column whenever any item's measured size changes.
void layoutColumn(CanvasApi api, List<String> ids, {double x = 0, double gap = 100}) {
  double y = 0;
  final updates = <String, Offset>{};
  for (final id in ids) {
    final size = api.items.getEffectiveSize(id);
    if (size == null) return; // not measured yet — a later signal will retry
    updates[id] = Offset(x, y);
    y += size.height + gap;
  }
  api.items.setWorldPositions(updates);
}

// Fires after each measurement, and getEffectiveSize already returns the new
// size when it does — no post-frame settle loop needed.
controller.items.measurementRevision.addListener(() {
  layoutColumn(controller, ['a', 'b', 'c']);
});

Use controller.items.measuredSizeListenable(id) instead if you only care about one item. Both guarantee that getMeasuredSize/getEffectiveSize already reflect the new size when the signal fires.

Camera controls

controller.camera.jumpToWorldTopLeft(const Offset(-500, -300), zoom: 1.2);
controller.camera.jumpToWorldCenter(const Offset(0, 0), zoom: 0.8);

await controller.camera.animateToWorldCenter(
  const Offset(1200, 800),
  zoom: 1.0,
  duration: const Duration(milliseconds: 420),
);

controller.camera.fitAllItems();

Layer types

InfinityCanvas(
  controller: controller,
  layers: [
    CanvasLayer.painter(
      id: 'bg',
      painterBuilder: (transform, controller) => MyBackgroundPainter(transform),
    ),
    CanvasLayer.positionedItems(id: 'nodes', items: items),
    CanvasLayer.overlay(
      id: 'hud',
      ignorePointer: false,
      builder: (context, transform, controller) => const MyHudWidget(),
    ),
  ],
);

Hover, drag, and item transform

CanvasItem(
  id: 'node-3',
  worldPosition: const Offset(200, 120),
  onHoverChanged: (hovered) {
    controller.items.setTransform(
      'node-3',
      hovered ? (Matrix4.identity()..scale(1.04)) : null,
    );
  },
  onDragUpdate: (event) {
    // event.worldPosition / event.worldDelta / event.pointerGlobalPosition
  },
  child: const _Card('Hover me'),
);

Performance Defaults

  • Use enableCulling: true for larger scenes
  • Give items a fixed CanvasItemSize where possible
  • Prefer CanvasLayer.painter for very dense static visuals
  • Use controller.items.setWorldPositions(...) for batch updates
  • Keep overlays lean (CanvasLayer.overlay) for HUD/interaction logic

Example Demos

See example/lib/main.dart:

  • Minimal Items
  • Painted Item Widgets
  • Node Canvas (Clean)
  • Grouped Nodes (Linear)
  • Input Smoke
  • Docking Windows
  • Database Schema Designer
  • Massive Multi-Widget Art Scene
  • Galaxy Trade Map
  • Orbital Constellation

API Appendix (Compact)

CanvasController

  • camera: transform, pan, zoom, fit, jump, animate
  • items: read diagnostics + mutate item state
  • layers: show/hide layers

controller.camera

  • jumpToWorldTopLeft(...)
  • jumpToWorldCenter(...)
  • animateToWorldTopLeft(...)
  • animateToWorldCenter(...)
  • fitWorldRect(...)
  • fitAllItems(...)
  • setScale(...)
  • translateWorld(...)
  • screenToWorld(...)
  • worldToScreen(...)
  • renderStatsListenable

controller.items

  • getDiagnostics(id)
  • getWorldPosition(id)
  • getMeasuredSize(id) / getEffectiveSize(id)
  • positionListenable(id)
  • measuredSizeListenable(id)
  • measurementRevision
  • setWorldPosition(id, offset)
  • setWorldPositions({id: offset})
  • setTransform(id, matrixOrNull)
  • mutateTransform(id, mutator)
  • clearTransform(id)
  • setDragEnabled(id, enabled)
  • bringToFront(id)

CanvasLayer

  • CanvasLayer.positionedItems(...)
  • CanvasLayer.painter(...)
  • CanvasLayer.overlay(...)

CanvasInputBehavior

  • CanvasInputBehavior.desktop()
  • CanvasInputBehavior.touch()
  • CanvasInputBehavior.locked()

Libraries

infinity_canvas