Skip to content

Instantly share code, notes, and snippets.

@mohamedrashad102
Created September 2, 2025 15:41
Show Gist options
  • Select an option

  • Save mohamedrashad102/149f6b8b3941c92ad045af3191a06cfe to your computer and use it in GitHub Desktop.

Select an option

Save mohamedrashad102/149f6b8b3941c92ad045af3191a06cfe to your computer and use it in GitHub Desktop.
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