Created
September 2, 2025 15:41
-
-
Save mohamedrashad102/149f6b8b3941c92ad045af3191a06cfe to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'dart:ui'; | |
| import 'package:flutter/material.dart'; | |
| class LetterTracing extends StatefulWidget { | |
| final String letter; | |
| final TextStyle textStyle; | |
| final double coloredStrokeWidth; | |
| final double strokeWidth; | |
| final Color color = Colors.black; | |
| final BlendMode blendMode; | |
| final StrokeCap strokeCap; | |
| final VoidCallback? onColored; | |
| final double strokeLengthThreshold; | |
| // final double coverageThreshold; | |
| final TextDirection textDirection; | |
| final List<Offset> points; | |
| const LetterTracing({ | |
| super.key, | |
| required this.points, | |
| required this.letter, | |
| this.textStyle = const TextStyle( | |
| fontSize: 20, | |
| fontWeight: FontWeight.bold, | |
| color: Colors.grey, | |
| ), | |
| this.coloredStrokeWidth = 25, | |
| this.strokeWidth = 10, | |
| this.blendMode = BlendMode.overlay, | |
| this.strokeCap = StrokeCap.round, | |
| this.onColored, | |
| this.strokeLengthThreshold = 2.0, | |
| // this.coverageThreshold = 0.7, | |
| this.textDirection = TextDirection.ltr, | |
| }); | |
| @override | |
| State<LetterTracing> createState() => _LetterTracingState(); | |
| } | |
| class _LetterTracingState extends State<LetterTracing> { | |
| late Size _textSize; | |
| late GlobalKey _containerKey; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _containerKey = GlobalKey(); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| _textSize = _getTextSize(widget.letter, widget.textStyle); | |
| final containerWidth = _textSize.width + widget.coloredStrokeWidth; | |
| final containerHeight = _textSize.height + widget.coloredStrokeWidth; | |
| return Container( | |
| key: _containerKey, | |
| color: Colors.white, | |
| alignment: Alignment.center, | |
| width: containerWidth, | |
| height: containerHeight, | |
| child: Stack( | |
| alignment: Alignment.center, | |
| children: [ | |
| Text( | |
| widget.letter, | |
| style: widget.textStyle.copyWith( | |
| foreground: Paint() | |
| ..color = widget.color | |
| ..style = PaintingStyle.stroke | |
| ..strokeWidth = widget.strokeWidth, | |
| ), | |
| ), | |
| GestureDetector( | |
| onPanStart: (details) => _addPoint(details.localPosition), | |
| onPanUpdate: (details) => _addPoint(details.localPosition), | |
| onPanEnd: (details) => _onPanEnd(), | |
| child: CustomPaint( | |
| size: _textSize, | |
| painter: _LetterPainter( | |
| textStyle: widget.textStyle, | |
| letter: widget.letter, | |
| points: widget.points, | |
| color: widget.color, | |
| strokeWidth: widget.coloredStrokeWidth, | |
| blendMode: widget.blendMode, | |
| strokeCap: widget.strokeCap, | |
| textDirection: widget.textDirection, | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| void _addPoint(Offset localPosition) { | |
| setState(() { | |
| widget.points.add(Offset( | |
| localPosition.dx.clamp(0, _textSize.width), | |
| localPosition.dy.clamp(0, _textSize.height), | |
| )); | |
| }); | |
| } | |
| void _onPanEnd() { | |
| // Check for completion when the user finishes drawing | |
| _checkPixelsForCompletion(); | |
| } | |
| Size _getTextSize(String letter, TextStyle style) { | |
| final textPainter = TextPainter( | |
| text: TextSpan(text: letter, style: style), | |
| textDirection: TextDirection.ltr, | |
| )..layout(); | |
| return textPainter.size; | |
| } | |
| void _checkPixelsForCompletion() async { | |
| final greyColor = widget.textStyle.color; // The grey color of the text | |
| final RenderBox renderBox = | |
| _containerKey.currentContext!.findRenderObject() as RenderBox; | |
| final size = renderBox.size; | |
| final width = size.width.toInt(); | |
| final height = size.height.toInt(); | |
| bool isAllColored = true; | |
| // Create a picture recorder and draw the canvas content to it | |
| final pictureRecorder = PictureRecorder(); | |
| final tempCanvas = Canvas( | |
| pictureRecorder, | |
| Rect.fromPoints( | |
| const Offset(0, 0), Offset(width.toDouble(), height.toDouble()))); | |
| // Paint the letter and the strokes over it | |
| _LetterPainter( | |
| textStyle: widget.textStyle, | |
| letter: widget.letter, | |
| points: widget.points, | |
| color: widget.color, | |
| strokeWidth: widget.coloredStrokeWidth, | |
| blendMode: widget.blendMode, | |
| strokeCap: widget.strokeCap, | |
| textDirection: widget.textDirection, | |
| ).paint(tempCanvas, size); | |
| final picture = pictureRecorder.endRecording(); | |
| final img = await picture.toImage(width, height); | |
| img.toByteData(format: ImageByteFormat.rawRgba).then((byteData) { | |
| final pixels = byteData!.buffer.asUint8List(); | |
| for (int i = 0; i < pixels.length; i += 4) { | |
| // Each pixel has 4 bytes (RGBA) | |
| int r = pixels[i]; // Red component | |
| int g = pixels[i + 1]; // Green component | |
| int b = pixels[i + 2]; // Blue component | |
| int a = pixels[i + 3]; // Alpha component | |
| // Compare the pixel with the grey color (for example, grey is around (200, 200, 200)) | |
| if (r == _floatToInt8(greyColor!.r) && | |
| g == _floatToInt8(greyColor.g) && | |
| b == _floatToInt8(greyColor.b) && | |
| a > 0) { | |
| isAllColored = false; | |
| break; // No need to check further if we find grey | |
| } | |
| } | |
| if (isAllColored && widget.onColored != null) { | |
| widget.onColored!(); | |
| } | |
| }); | |
| } | |
| int _floatToInt8(double x) { | |
| return (x * 255.0).round() & 0xff; | |
| } | |
| } | |
| class _LetterPainter extends CustomPainter { | |
| final String letter; | |
| final TextStyle textStyle; | |
| final List<Offset> points; | |
| final Color color; | |
| final double strokeWidth; | |
| final BlendMode blendMode; | |
| final StrokeCap strokeCap; | |
| final TextDirection textDirection; | |
| _LetterPainter({ | |
| required this.letter, | |
| required this.textStyle, | |
| required this.points, | |
| required this.color, | |
| required this.strokeWidth, | |
| required this.blendMode, | |
| required this.strokeCap, | |
| required this.textDirection, | |
| }); | |
| @override | |
| void paint(Canvas canvas, Size size) { | |
| _drawLetter(canvas); | |
| _drawPoints(canvas); | |
| } | |
| void _drawLetter(Canvas canvas) { | |
| final textPainter = TextPainter( | |
| text: TextSpan(text: letter, style: textStyle), | |
| textDirection: textDirection, | |
| )..layout(); | |
| textPainter.paint(canvas, Offset.zero); | |
| } | |
| void _drawPoints(Canvas canvas) { | |
| final paint = Paint() | |
| ..color = color | |
| ..blendMode = blendMode | |
| ..strokeWidth = strokeWidth | |
| ..strokeCap = strokeCap; | |
| canvas.drawPoints(PointMode.points, points, paint); | |
| } | |
| @override | |
| bool shouldRepaint(covariant CustomPainter oldDelegate) => true; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment