1

Can Flutter's inbuilt Canvas drawing methods be directly used to render variable-width strokes, for example to reflect pressure applied throughout each stroke in a handwriting app?

Ideally, in a manner compatible with saving in the XML-esque format of SVG (example at bottom).


What I think I've noticed / troubles I'm having / current attempts:

canvas.drawPath, canvas.drawPoints, canvas.drawPolygon, canvas.drawLines etc all take only a single Paint object, which can in turn have a single strokeWidth (as opposed to taking lists of Paint objects or strokeWidths, such that parameters of the path besides position could change point to point and be interpolated between).

Drawing lines, polygons or points of varying strokeWidths or radii by iterating over lists of position and pressure data and using the respective Canvas method results in no interpolation / paths not looking continuously stroked.

Screenshot from OneNote showing the behaviour I'd like:

variable width lines reflecting input pressure from OneNote

Screenshot from the app the minimal working example below produces: variable width, furry lines from my app

(Unoptimized) minimal working example:

import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() {
  runApp(Container(
    color: Colors.white,
    child: Writeable(),
  ));
}

class Writeable extends StatefulWidget {
  @override
  _WriteableState createState() => _WriteableState();
}

class _WriteableState extends State<Writeable> {
  List<List<double>> pressures = List<List<double>>();
  List<Offset> currentLine = List<Offset>();
  List<List<Offset>> lines = List<List<Offset>>();
  List<double> currentLinePressures = List<double>();
  double pressure;
  Offset position;
  Color color = Colors.black;
  Painter painter;
  CustomPaint paintCanvas;

  @override
  Widget build(BuildContext context) {
    painter = Painter(
        lines: lines,
        currentLine: currentLine,
        pressures: pressures,
        currentLinePressures: currentLinePressures,
        color: color);

    paintCanvas = CustomPaint(
      painter: painter,
    );

    return Listener(
      onPointerMove: (details) {
        setState(() {
          currentLinePressures.add(details.pressure);
          currentLine.add(details.localPosition);
        });
      },
      onPointerUp: (details) {
        setState(() {
          lines.add(currentLine.toList());
          pressures.add(currentLinePressures.toList());
          currentLine.clear();
          currentLinePressures.clear();
        });
      },
      child: paintCanvas,
    );
  }
}

class Painter extends CustomPainter {
  Painter(
      {@required this.lines,
      @required this.currentLine,
      @required this.color,
      @required this.pressures,
      @required this.currentLinePressures});

  final List<List<Offset>> lines;
  final List<Offset> currentLine;
  final Color color;
  final List<List<double>> pressures;
  final List<double> currentLinePressures;

  double scalePressures = 10;
  Paint paintStyle = Paint();

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }

  // Paints here using drawPoints and PointMode.lines, but have also tried
  // PointMode.points, PointMode.polygon and drawPath with a path variable and
  // moveTo, lineTo methods.
  @override
  void paint(Canvas canvas, Size size) {
    // Paint line currently being drawn (points added since pointer was
    // last lifted)
    for (int i = 0; i < currentLine.length - 1; i++) {
      paintStyle.strokeWidth = currentLinePressures[i] * scalePressures;
      canvas.drawPoints(
          PointMode.lines, [currentLine[i], currentLine[i + 1]], paintStyle);
    }
    // Paint all completed lines drawn since app start
    for (int i = 0; i < lines.length; i++) {
      for (int j = 0; j < lines[i].length - 1; j++) {
        paintStyle.strokeWidth = pressures[i][j] * scalePressures;
        canvas.drawPoints(
            PointMode.lines, [lines[i][j], lines[i][j + 1]], paintStyle);
      }
    }
  }
}

I'm about to try writing my own implementation for rendering aesthetic SVG-friendly data from PointerEvents, but so many of the existing classes feel SVG/pretty-vectors-compatible (e.g. all the lineTos, moveTos, stroke types and endings and other parameters) that I thought it worth checking if there's something I've missed, and these methods can already do this?


Example of a few lines in an SVG file saved by Xournal++, with the stroke-width parameter changing for each line segment, and all other listed parameters presumably also having potential to change. Each line contains a moveTo command (M) and a lineTo command (L), where the latter draws a line from the current position (the last moveTo-ed or lineTo-ed), reminiscent of Flutter's segments/sub-paths and current point, to the specified offset:

<path style="fill:none;stroke-width:0.288794;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(0%,100%,0%);stroke-opacity:1;comp-op:src;clip-to-self:true;stroke-miterlimit:10;" d="M 242.683594 45.519531 L 242.980469 45.476562 "/>
<path style="fill:none;stroke-width:0.295785;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(0%,100%,0%);stroke-opacity:1;comp-op:src;clip-to-self:true;stroke-miterlimit:10;" d="M 242.980469 45.476562 L 243.28125 45.308594 "/>
<path style="fill:none;stroke-width:0.309105;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(0%,100%,0%);stroke-opacity:1;comp-op:src;clip-to-self:true;stroke-miterlimit:10;" d="M 243.28125 45.308594 L 243.601562 45.15625 "/>

The approach seems to be 'draw a very short line, change stroke-width, draw the next very short line starting from the previous position', which I've tried to emulate with the paint method above.

userManyNumbers
  • 904
  • 2
  • 10
  • 26
  • don't use strokeWidth at all, create the outline of each shape and fill it in whatever colour you need. If you want it to appear bolder, create a wider outline. – Robert Longson Aug 29 '20 at 11:43
  • @RobertLongson This is what I'm considering doing, but it feels like it would be re-inventing drawPath, so I thought it worth checking this functionality doesn't already exist somewhere. Thanks for the suggestion and validation that it mightn't. – userManyNumbers Aug 29 '20 at 11:51
  • Nonetheless two things come to my mind that might inspire you: pomax has written an [example function](https://pomax.github.io/bezierjs/#outline) of how to graduate a bezier curve - the very first step towards drawing calligrafic paths. Inkscape builds outlines from this principle with its freehand drawing tool, which accepts pressure values from tablet devices as input. It's [open source](https://gitlab.com/inkscape/inkscape/-/blob/master/src/live_effects/lpe-powerstroke.cpp), so you can study how that has been done. – ccprog Aug 29 '20 at 18:27
  • @RobertLongson My intention was to check if this could be implemented with the libraries I've mentioned, drawPath, drawPoints etc, and my issues were due to misusing them, in the way [this question](https://stackoverflow.com/questions/20560322/how-to-draw-path-with-variable-width-in-canvas?noredirect=1&lq=1) and several of the related were posed for Android/Java. I see how my phrasing sounded too broad, and I'll try to fix it now. – userManyNumbers Aug 29 '20 at 21:19
  • @ccprog This looks very useful; thank you! – userManyNumbers Aug 29 '20 at 21:20
  • You need to set the strokeCap field in the Paint object. You also need to setup multiple buffers if you are accepting drawing inputs in real-time because the CustomPaint widget will repaint ALL the strokes everytime it's updated which probably isn't what you want. – user2272296 Dec 09 '20 at 01:23

0 Answers0