Overview

CustomPaint is a widget that allows you to draw custom graphics using Canvas API or SVG. Inspired by Flutter’s CustomPaint, it’s used when you need to draw complex graphics, charts, diagrams, or any custom visual elements.

A unique feature of Flitter’s CustomPaint is its support for both SVG and Canvas rendering. While the interface differs slightly from Flutter due to SVG support, the layout rules remain the same.

See: https://api.flutter.dev/flutter/widgets/CustomPaint-class.html

When to Use?

  • When you need to draw complex graphics or shapes
  • When implementing custom charts or graphs
  • When creating custom UI with animations
  • When developing games or visualization tools
  • When existing widgets can’t express your specific UI requirements

Basic Usage

import { CustomPaint, Size } from 'flitter';

// Canvas rendering example
const MyCanvasPaint = CustomPaint({
  size: new Size(200, 200),
  painter: {
    canvas: {
      paint: (context, size) => {
        const ctx = context.canvas;
        ctx.fillStyle = 'blue';
        ctx.fillRect(0, 0, size.width, size.height);
      }
    }
  }
});

// SVG rendering example
const MySvgPaint = CustomPaint({
  size: new Size(200, 200),
  painter: {
    svg: {
      createDefaultSvgEl: ({ createSvgEl }) => {
        return {
          rect: createSvgEl('rect')
        };
      },
      paint: ({ rect }, size) => {
        rect.setAttribute('fill', 'red');
        rect.setAttribute('width', `${size.width}`);
        rect.setAttribute('height', `${size.height}`);
      }
    }
  }
});

Props

size (optional)

Value: Size (default: Size.zero)

Specifies the size of the CustomPaint area. When Size.zero, it follows the child widget’s size. Use Size.infinite to expand to the maximum available size.

// Fixed size
size: new Size(300, 200)

// Expand to maximum size
size: Size.infinite

// Follow child size (default)
size: Size.zero

painter (required)

Value: Painter

The Painter object containing the graphics drawing logic. You can implement SVG, Canvas, or both.

Painter Type Definition:

type Painter<SVGEls, D> = {
  dependencies?: D;  // Dependency data
  shouldRepaint?: (oldPainter: Painter<SVGEls, D>) => boolean;  // Determine repaint
  svg?: CustomSvgPainter<SVGEls>;    // SVG rendering implementation
  canvas?: CustomCanvasPainter;       // Canvas rendering implementation
};

Canvas Painter:

type CustomCanvasPainter = {
  paint: (context: CanvasPaintingContext, size: Size) => void;
};

SVG Painter:

type CustomSvgPainter<T> = {
  createDefaultSvgEl: (context: SvgPaintContext) => T;  // Create SVG elements
  paint: (els: T, size: Size) => void;                  // Update SVG elements
};

child (optional)

Value: Widget

The child widget to be drawn on top of the CustomPaint. When a child is present, CustomPaint is drawn behind it.

Real-World Examples

Example 1: Drawing Simple Shapes

import { CustomPaint, Size } from 'flitter';

const SimpleShapes = CustomPaint({
  size: new Size(300, 300),
  painter: {
    canvas: {
      paint: (context, size) => {
        const ctx = context.canvas;
        
        // Background
        ctx.fillStyle = '#f0f0f0';
        ctx.fillRect(0, 0, size.width, size.height);
        
        // Draw circle
        ctx.fillStyle = '#3498db';
        ctx.beginPath();
        ctx.arc(150, 100, 50, 0, Math.PI * 2);
        ctx.fill();
        
        // Draw rectangle
        ctx.fillStyle = '#e74c3c';
        ctx.fillRect(100, 160, 100, 80);
        
        // Draw triangle
        ctx.fillStyle = '#2ecc71';
        ctx.beginPath();
        ctx.moveTo(150, 250);
        ctx.lineTo(100, 320);
        ctx.lineTo(200, 320);
        ctx.closePath();
        ctx.fill();
      }
    }
  }
});

Example 2: Animated Chart

import { CustomPaint, Size, StatefulWidget, State } from 'flitter';

class AnimatedChart extends StatefulWidget {
  createState() {
    return new AnimatedChartState();
  }
}

class AnimatedChartState extends State<AnimatedChart> {
  values = [30, 50, 80, 40, 60];
  
  build() {
    return CustomPaint({
      size: new Size(400, 300),
      painter: {
        dependencies: this.values,  // Add dependencies
        shouldRepaint: (oldPainter) => {
          // Repaint only when values change
          return oldPainter.dependencies !== this.values;
        },
        canvas: {
          paint: (context, size) => {
            const ctx = context.canvas;
            const barWidth = size.width / this.values.length;
            const maxValue = Math.max(...this.values);
            
            // Background
            ctx.fillStyle = '#f8f9fa';
            ctx.fillRect(0, 0, size.width, size.height);
            
            // Bar chart
            this.values.forEach((value, index) => {
              const barHeight = (value / maxValue) * size.height * 0.8;
              const x = index * barWidth + barWidth * 0.1;
              const y = size.height - barHeight;
              const width = barWidth * 0.8;
              
              // Bar
              ctx.fillStyle = '#3498db';
              ctx.fillRect(x, y, width, barHeight);
              
              // Value label
              ctx.fillStyle = '#2c3e50';
              ctx.font = '14px Arial';
              ctx.textAlign = 'center';
              ctx.fillText(value.toString(), x + width / 2, y - 10);
            });
          }
        }
      }
    });
  }
}

Example 3: Complex Path with SVG

import { CustomPaint, Size } from 'flitter';

const SvgPathExample = CustomPaint({
  size: new Size(300, 300),
  painter: {
    svg: {
      createDefaultSvgEl: ({ createSvgEl }) => {
        return {
          background: createSvgEl('rect'),
          path: createSvgEl('path'),
          circle: createSvgEl('circle')
        };
      },
      paint: (els, size) => {
        // Background
        els.background.setAttribute('fill', '#f0f0f0');
        els.background.setAttribute('width', `${size.width}`);
        els.background.setAttribute('height', `${size.height}`);
        
        // Complex path
        const pathData = `
          M ${size.width * 0.2} ${size.height * 0.5}
          Q ${size.width * 0.5} ${size.height * 0.2}
            ${size.width * 0.8} ${size.height * 0.5}
          T ${size.width * 0.5} ${size.height * 0.8}
          Z
        `;
        els.path.setAttribute('d', pathData);
        els.path.setAttribute('fill', 'none');
        els.path.setAttribute('stroke', '#3498db');
        els.path.setAttribute('stroke-width', '3');
        
        // Center point
        els.circle.setAttribute('cx', `${size.width * 0.5}`);
        els.circle.setAttribute('cy', `${size.height * 0.5}`);
        els.circle.setAttribute('r', '5');
        els.circle.setAttribute('fill', '#e74c3c');
      }
    }
  }
});

Example 4: Using with Child Widget

import { CustomPaint, Size, Container, Text } from 'flitter';

const BackgroundPattern = CustomPaint({
  painter: {
    canvas: {
      paint: (context, size) => {
        const ctx = context.canvas;
        
        // Draw pattern
        for (let x = 0; x < size.width; x += 20) {
          for (let y = 0; y < size.height; y += 20) {
            ctx.fillStyle = (x + y) % 40 === 0 ? '#e0e0e0' : '#f5f5f5';
            ctx.fillRect(x, y, 20, 20);
          }
        }
      }
    }
  },
  child: Container({
    alignment: Alignment.center,
    padding: EdgeInsets.all(20),
    child: Text('Text on CustomPaint background', {
      style: {
        fontSize: 18,
        fontWeight: 'bold',
        color: '#2c3e50'
      }
    })
  })
});

Best Practices

  • Implement shouldRepaint method properly to avoid unnecessary repainting
  • When using Canvas API, pre-calculate complex computations for better performance
  • Choose the appropriate renderer for your use case (SVG maintains quality when scaled, Canvas is better for complex animations)
  • Don’t forget to clean up animations to prevent memory leaks
  • Use dependencies to efficiently manage painter state changes
  • Container: When you only need simple background color or border
  • DecoratedBox: When you need complex decoration but not custom drawing
  • Transform: When you can achieve the desired effect by transforming existing widgets
  • ClipPath: When you only need to clip with a custom shape