2

I'm trying to rotate text painted on a Canvas about it's center. Instead, in the below code, the text rotates about the top left corner of the text when I press the floating button.

Pressing the button increments the angle, which is passed to CanvasPainter to draw the text.

The rectangle's top left corner should be initially positioned at offset.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  double _angle = 0;
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Container(
          child: CustomPaint(
            painter: CanvasPainter(_angle),
            child: Container(),
          )
         ),
        appBar: AppBar(title: Text('Test')),
        floatingActionButton: FloatingActionButton(
          onPressed: () => setState(() => _angle += .1),
          child: const Icon(Icons.add),
      )
      ),
    );
  }
}

class CanvasPainter extends CustomPainter {
  final double angle;
  final Offset offset = Offset(50, 50);
  
  CanvasPainter(this.angle);
  
  @override
  void paint(Canvas canvas, Size size) {
    final fill = TextPainter(
      text: TextSpan(text: 'This is a test', style: TextStyle(fontSize: 80)),
      textDirection: TextDirection.rtl);
    
    fill.layout();
    
    canvas.save();
    //canvas.translate(-fill.width/2, -fill.height/2);
    canvas.rotate(angle);
    canvas.translate(offset.dx, offset.dy);
      
    fill.paint(canvas, Offset.zero);
    
    canvas.restore();
  }
  
  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}
user2233706
  • 6,148
  • 5
  • 44
  • 86
  • first `translate`, then `rotate` – pskink Nov 10 '20 at 03:21
  • What amount do I translate? I've passed different values to translate(), different transformation orders, but I'm still not able to rotate about center. – user2233706 Nov 10 '20 at 04:03
  • btw you could skip the second `translate` and change `tp.paint(canvas, offset - pivot);` but i think it is a worse solution as you have to modify "painting" code – pskink Nov 10 '20 at 11:31
  • Yes. To summarize, you translate to the point in the object you want to rotate about in the final rendered object. Then rotate. Then translate back to point (0, 0). Since translate takes deltas, this is equivalent to the negative of the point you translated to. Draw text at desired offset. – user2233706 Nov 10 '20 at 15:57
  • exactly, but as I wrote you can 'combine' the second `translate` with `TextPainter.paint` – pskink Nov 10 '20 at 16:01

2 Answers2

11

this is what you have to do:

[...]
canvas.save();
final pivot = fill.size.center(offset); 
canvas.translate(pivot.dx, pivot.dy); 
canvas.rotate(angle);
canvas.translate(-pivot.dx, -pivot.dy);
fill.paint(canvas, offset);
canvas.restore();
pskink
  • 23,874
  • 6
  • 66
  • 77
  • This solution is not completely correct. Check tarak-parabs [answer](https://stackoverflow.com/a/73202118/3516001). "Issue with the above code is that the text shifts in global X direction if the number of characters are changed.". I have tested it and he's totally right. tarak-parabs answer should be the accepted one. – Daniel Oct 20 '22 at 12:11
  • 1
    @Daniel it all depends what `offset` is used in `fill.paint(canvas, offset` - i assume that OP knows where he wants to draw his text, btw if you need a general solution with translation, rotation and scaling check https://gist.github.com/pskink/aa0b0c80af9a986619845625c0e87a67#file-transform_entry-dart-L12 – pskink Oct 20 '22 at 14:05
2

Update: Extension on the Canvas, compacted code, alignment

DartPad example

gist: rotated_text.dart

Example:

canvas.drawRotatedText(
    pivot: pivot,
    textPainter: mainTextPainter,
    superTextPainter: superTextPainter,
    subTextPainter: subTextPainter,
    angle: angle, //radians
    isInDegrees: false,
    alignment: alignment);

The alignment of the main text can be adjusted with respect to the pivot point.

Obsolete: Rotate text, subscript and superscript

Image: Text with subscript and superscript

import 'package:flutter/material.dart';
import 'dart:math' as math;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  double _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter += 0.1;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            CustomPaint(
              painter: CustomText(multiplier: _counter),
            )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

class CustomText extends CustomPainter {
  CustomText({required this.multiplier});

  double multiplier;

  // Text centre about which the text should rotate
  final textCentre = const Offset(0, 0);

  // Angle of rotation
  double get theta => -math.pi * multiplier;

  // Text styles
  double textFontSize = 24;
  double get subTextFontSize => textFontSize / 2;
  double get supTextFontSize => textFontSize / 2;

  TextStyle get textStyle => TextStyle(
      color: Colors.red, fontSize: textFontSize, fontStyle: FontStyle.italic);

  TextStyle get subTextStyle => TextStyle(
      color: Colors.green,
      fontSize: subTextFontSize,
      fontStyle: FontStyle.italic);

  TextStyle get supTextStyle => TextStyle(
      color: Colors.cyan,
      fontSize: subTextFontSize,
      fontStyle: FontStyle.italic);

  @override
  void paint(Canvas canvas, Size size) {
    //
    // MAIN TEXT //
    final textPainter = TextPainter(
      text: TextSpan(text: 'Example text', style: textStyle),
      textDirection: TextDirection.ltr,
    );

    textPainter.layout();

    // Calculate delta offset with reference to which any text should
    // paint, such that the centre of the text will always be
    // at the given textCentre
    final delta = Offset(textCentre.dx - textPainter.size.width / 2,
        textCentre.dy - textPainter.size.height / 2);

    // Rotate the text about textCentrePoint
    canvas.save();
    canvas.translate(textCentre.dx, textCentre.dy);
    canvas.rotate(theta);
    canvas.translate(-textCentre.dx, -textCentre.dy);
    textPainter.paint(canvas, delta);
    canvas.restore();
    //

    // SUBSCRIPT TEXT //
    final subTextPainter = TextPainter(
      text: TextSpan(
        text: 'subscript',
        style: subTextStyle,
      ),
      textDirection: TextDirection.ltr,
    );

    subTextPainter.layout();

    // Position of top left point of the subscript text
    final deltaSubtextDx = textCentre.dx +
        // Cos
        math.cos(theta) * (textPainter.size.width / 2) +
        // Sine
        math.sin(theta) *
            (-textPainter.size.height / 2 + subTextPainter.size.height / 2);

    final deltaSubtextDy = textCentre.dy +
        // Cos
        math.cos(theta) *
            (textPainter.size.height / 2 - subTextPainter.size.height / 2) +
        // Sine
        math.sin(theta) * (textPainter.size.width / 2);

    final deltaSubText = Offset(deltaSubtextDx, deltaSubtextDy);

    // Rotate the text about textCentrePoint
    canvas.save();
    canvas.translate(deltaSubText.dx, deltaSubText.dy);
    canvas.rotate(theta);
    canvas.translate(-deltaSubText.dx, -deltaSubText.dy);
    subTextPainter.paint(canvas, deltaSubText);
    canvas.restore();
    //

    // SUPERSCRIPT TEXT //
    final supTextPainter = TextPainter(
      text: TextSpan(
        text: 'superscript',
        style: supTextStyle,
      ),
      textDirection: TextDirection.ltr,
    );

    supTextPainter.layout();

    // Position of top left point of the superscript text
    final deltaSuptextDx = textCentre.dx +
        // Cos
        math.cos(theta) * (textPainter.size.width / 2) +
        // Sine
        math.sin(theta) *
            (textPainter.size.height / 2 + supTextPainter.size.height / 2);

    final deltaSuptextDy = textCentre.dy +
        // Cos
        math.cos(theta) *
            (-textPainter.size.height / 2 - supTextPainter.size.height / 2) +
        // Sine
        math.sin(theta) * (textPainter.size.width / 2);

    final deltaSupText = Offset(deltaSuptextDx, deltaSuptextDy);

    // Rotate the text about textCentrePoint
    canvas.save();
    canvas.translate(deltaSupText.dx, deltaSupText.dy);
    canvas.rotate(theta);
    canvas.translate(-deltaSupText.dx, -deltaSupText.dy);
    supTextPainter.paint(canvas, deltaSupText);
    canvas.restore();
    //

    // Centre point marker
    final pointPaint = Paint()..color = Colors.blue;
    canvas.drawCircle(textCentre, 4, pointPaint);
    // Subscript point marker
    final pointPaint2 = Paint()..color = Colors.orange;
    canvas.drawCircle(deltaSubText, 4, pointPaint2);
    // Superscript point marker
    final pointPaint3 = Paint()..color = Colors.brown;
    canvas.drawCircle(deltaSupText, 4, pointPaint3);
  }

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

Original answer

pskink's answer is modified to solve an issue that the text shifts in global X direction if the number of characters are changed.

Image: Long text

Image: Short text

So to position and then rotate a dynamic text I have slightly modified the code such that the text is rotated about a given center point. Red dot is at the center point.

Image: Text rotated about given offset/center point

If you want to rotate text about it's bottom center point then just add new line character at the end in the text String.

Image: Rotated about bottom center of the text

    class NewPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    var text = 'You have pushed the button this many times:\n'; //Remove \n if you don't want to rotate about bottom centre

    // I want my text centred at this point
    const textCentrePoint = Offset(200, 300);

    const textStyle1 = TextStyle(color: Colors.black, fontSize: 20);

    final pointPaint = Paint()..color = const Color.fromARGB(255, 255, 0, 0);

    // Rotated text
    final textPainter1 = TextPainter(
      text: TextSpan(text: text, style: textStyle1),
      textDirection: TextDirection.ltr,
    );
    textPainter1.layout();

    // Calculate delta offset with reference to which any text should
    // paint, such that the centre of the text will be
    // at the given textCentrePoint
    final delta = Offset(textCentrePoint.dx - textPainter1.size.width / 2,
        textCentrePoint.dy - textPainter1.size.height / 2);

    // Rotate the text about textCentrePoint

    canvas.save();
    canvas.translate(textCentrePoint.dx, textCentrePoint.dy);
    canvas.rotate(-pi / 2);
    canvas.translate(-textCentrePoint.dx, -textCentrePoint.dy);
    textPainter1.paint(canvas, delta);
    canvas.restore();

    canvas.drawCircle(textCentrePoint, 4, pointPaint);

    // Normal horizontal text
    const textStyle2 =
        TextStyle(color: Color.fromARGB(255, 139, 0, 0), fontSize: 20);

    final textPainter2 = TextPainter(
      text: TextSpan(text: text, style: textStyle2),
      textDirection: TextDirection.ltr,
    );
    textPainter2.layout();
    textPainter2.paint(canvas, delta);
  }