653 lines
21 KiB
Dart
653 lines
21 KiB
Dart
import 'dart:async';
|
|
import 'dart:ui' as ui;
|
|
import 'dart:math' as math;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
import 'package:kanimaji/kanjivg_parser.dart';
|
|
import 'package:kanimaji/primitives/point.dart';
|
|
import 'package:kanimaji/primitives/bezier.dart' as bezier;
|
|
|
|
Future<KanjiVGItem> _defaultKanjiDataProvider(String kanji) async {
|
|
final hex = kanji.runes.isEmpty
|
|
? '00000'
|
|
: kanji.runes.first.toRadixString(16).padLeft(5, '0');
|
|
final assetPath = 'assets/kanjivg/kanji/$hex.svg';
|
|
final svgString = await rootBundle.loadString(assetPath);
|
|
return KanjiVGItem.parseFromXml(svgString);
|
|
}
|
|
|
|
enum TimingFunction { linear, ease, easeIn, easeInOut, easeOut }
|
|
|
|
// ease, ease-in, etc:
|
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/timing-function#ease
|
|
const pt1 = Point(0, 0);
|
|
const easeCt1 = Point(0.25, 0.1);
|
|
const easeCt2 = Point(0.25, 1.0);
|
|
const easeInCt1 = Point(0.42, 0.0);
|
|
const easeInCt2 = Point(1.0, 1.0);
|
|
const easeInOutCt1 = Point(0.42, 0.0);
|
|
const easeInOutCt2 = Point(0.58, 1.0);
|
|
const easeOutCt1 = Point(0.0, 0.0);
|
|
const easeOutCt2 = Point(0.58, 1.0);
|
|
const pt2 = Point(1, 1);
|
|
|
|
extension Funcs on TimingFunction {
|
|
double Function(double) get func => {
|
|
TimingFunction.linear: (double x) => x,
|
|
TimingFunction.ease: (double x) =>
|
|
bezier.value(pt1, easeCt1, easeCt2, pt2, x),
|
|
TimingFunction.easeIn: (double x) =>
|
|
bezier.value(pt1, easeInCt1, easeInCt2, pt2, x),
|
|
TimingFunction.easeInOut: (double x) =>
|
|
bezier.value(pt1, easeInOutCt1, easeInOutCt2, pt2, x),
|
|
TimingFunction.easeOut: (double x) =>
|
|
bezier.value(pt1, easeOutCt1, easeOutCt2, pt2, x),
|
|
}[this]!;
|
|
|
|
String get name => {
|
|
TimingFunction.linear: 'linear',
|
|
TimingFunction.ease: 'ease',
|
|
TimingFunction.easeIn: 'ease-in',
|
|
TimingFunction.easeInOut: 'ease-in-out',
|
|
TimingFunction.easeOut: 'ease-out',
|
|
}[this]!;
|
|
}
|
|
|
|
// TODO: fall back to just drawing the character as text if it does not exist in the kanjivg dataset
|
|
|
|
/// A widget that animates the stroke order of a given kanji character using KanjiVG data.
|
|
class Kanimaji extends StatefulWidget {
|
|
final String kanji;
|
|
final FutureOr<KanjiVGItem> Function(String kanji) kanjiDataProvider;
|
|
|
|
// Animation parameters
|
|
final bool loop;
|
|
final Duration delayBetweenStrokes;
|
|
final Duration delayBetweenLoops;
|
|
// TODO: add support for specifying animation bezier curve
|
|
final TimingFunction timingFunction;
|
|
// final Cubic animationCurve;
|
|
// final double speed;
|
|
|
|
// Brush parameters
|
|
final bool showBrush;
|
|
final Color brushColor;
|
|
final double brushRadius;
|
|
|
|
// Stroke parameters
|
|
final Color strokeColor;
|
|
final Color currentStrokeColor;
|
|
final Color strokeUnfilledColor;
|
|
final double strokeWidth;
|
|
final Color backgroundColor;
|
|
|
|
// Stroke number parameters
|
|
final bool showStrokeNumbers;
|
|
final Color strokeNumberColor;
|
|
final Color currentStrokeNumberColor;
|
|
final double strokeNumberFontSize;
|
|
final String? strokeNumberFontFamily;
|
|
|
|
// Cross parameters
|
|
final bool showCross;
|
|
final Color crossColor;
|
|
final double crossStrokeWidth;
|
|
final double crossStipleLength;
|
|
final double crossStipleGap;
|
|
|
|
const Kanimaji({
|
|
super.key,
|
|
required this.kanji,
|
|
this.kanjiDataProvider = _defaultKanjiDataProvider,
|
|
|
|
this.loop = true,
|
|
this.timingFunction = TimingFunction.ease,
|
|
this.delayBetweenLoops = const Duration(seconds: 1),
|
|
this.delayBetweenStrokes = const Duration(milliseconds: 100),
|
|
|
|
this.strokeColor = Colors.black,
|
|
this.currentStrokeColor = Colors.red,
|
|
this.strokeUnfilledColor = const Color(0xA0DDDDDD),
|
|
this.strokeWidth = 3.0,
|
|
this.backgroundColor = Colors.transparent,
|
|
|
|
this.showBrush = true,
|
|
this.brushColor = Colors.red,
|
|
this.brushRadius = 4.0,
|
|
|
|
this.showStrokeNumbers = false,
|
|
this.strokeNumberColor = Colors.grey,
|
|
this.currentStrokeNumberColor = Colors.red,
|
|
this.strokeNumberFontSize = 4.0,
|
|
this.strokeNumberFontFamily,
|
|
|
|
this.showCross = true,
|
|
this.crossColor = const Color(0x40AAAAAA),
|
|
this.crossStrokeWidth = 0.8,
|
|
this.crossStipleLength = 5.0,
|
|
this.crossStipleGap = 3.0,
|
|
});
|
|
|
|
@override
|
|
State<Kanimaji> createState() => _KanimajiState();
|
|
}
|
|
|
|
class _KanimajiState extends State<Kanimaji>
|
|
with SingleTickerProviderStateMixin {
|
|
KanjiVGItem? _kanjiData;
|
|
String? _error;
|
|
late AnimationController _controller;
|
|
|
|
List<double> get _pathLengths =>
|
|
_kanjiData?.paths
|
|
.map((p) => p.svgPath.size(error: 1e-8).toDouble())
|
|
.toList() ??
|
|
[];
|
|
|
|
/// sqrt, ie a stroke 4 times the length is drawn
|
|
/// at twice the speed, in twice the time.
|
|
static double _strokeLengthToDuration(double length) => math.sqrt(length) / 8;
|
|
|
|
List<double> get _pathDurations =>
|
|
_pathLengths.map((len) => _strokeLengthToDuration(len)).toList();
|
|
|
|
static const double _viewBoxWidth = 109.0;
|
|
static const double _viewBoxHeight = 109.0;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(vsync: this);
|
|
_controller.addListener(_onTick);
|
|
_loadAndParseSvg().then((_) => _configureController());
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant Kanimaji oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.kanji != widget.kanji) {
|
|
_loadAndParseSvg().then((_) => _configureController());
|
|
} else if (oldWidget.loop != widget.loop ||
|
|
oldWidget.timingFunction != widget.timingFunction ||
|
|
oldWidget.delayBetweenLoops != widget.delayBetweenLoops ||
|
|
oldWidget.delayBetweenStrokes != widget.delayBetweenStrokes ||
|
|
oldWidget.strokeColor != widget.strokeColor ||
|
|
oldWidget.currentStrokeColor != widget.currentStrokeColor ||
|
|
oldWidget.strokeUnfilledColor != widget.strokeUnfilledColor ||
|
|
oldWidget.strokeWidth != widget.strokeWidth ||
|
|
oldWidget.backgroundColor != widget.backgroundColor ||
|
|
oldWidget.showBrush != widget.showBrush ||
|
|
oldWidget.brushColor != widget.brushColor ||
|
|
oldWidget.brushRadius != widget.brushRadius ||
|
|
oldWidget.showStrokeNumbers != widget.showStrokeNumbers ||
|
|
oldWidget.strokeNumberColor != widget.strokeNumberColor ||
|
|
oldWidget.currentStrokeNumberColor != widget.currentStrokeNumberColor ||
|
|
oldWidget.strokeNumberFontSize != widget.strokeNumberFontSize ||
|
|
oldWidget.strokeNumberFontFamily != widget.strokeNumberFontFamily ||
|
|
oldWidget.showCross != widget.showCross ||
|
|
oldWidget.crossColor != widget.crossColor ||
|
|
oldWidget.crossStrokeWidth != widget.crossStrokeWidth ||
|
|
oldWidget.crossStipleLength != widget.crossStipleLength ||
|
|
oldWidget.crossStipleGap != widget.crossStipleGap) {
|
|
setState(() {});
|
|
}
|
|
}
|
|
|
|
void _onTick() {
|
|
if (mounted) setState(() {});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.removeListener(_onTick);
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _loadAndParseSvg() async {
|
|
try {
|
|
final data = await widget.kanjiDataProvider(widget.kanji);
|
|
setState(() {
|
|
_kanjiData = data;
|
|
_error = null;
|
|
});
|
|
} catch (e) {
|
|
setState(() {
|
|
_kanjiData = null;
|
|
_error = 'Error loading/parsing kanji data: $e';
|
|
});
|
|
}
|
|
}
|
|
|
|
void _configureController() {
|
|
if (_kanjiData == null) return;
|
|
final int totalSec =
|
|
(_pathDurations.fold(0.0, (a, b) => a + b) * 1000).round() +
|
|
widget.delayBetweenStrokes.inMilliseconds *
|
|
(_pathDurations.length - 1) +
|
|
widget.delayBetweenLoops.inMilliseconds;
|
|
|
|
_controller.stop();
|
|
_controller.duration = Duration(milliseconds: totalSec);
|
|
if (widget.loop) {
|
|
_controller.repeat();
|
|
} else {
|
|
_controller.forward(from: 0);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_kanjiData == null && _error == null) {
|
|
return SizedBox.expand(child: Center(child: CircularProgressIndicator()));
|
|
}
|
|
|
|
if (_error != null) {
|
|
return SizedBox.expand(child: ErrorWidget(_error!));
|
|
}
|
|
|
|
return FittedBox(
|
|
child: SizedBox(
|
|
width: _viewBoxWidth,
|
|
height: _viewBoxHeight,
|
|
child: CustomPaint(
|
|
painter: _KanimajiPainter(
|
|
progress: _controller.value,
|
|
|
|
paths: _kanjiData!.paths.map((p) => p.svgPath.toUiPath()).toList(),
|
|
pathLengths: _pathLengths,
|
|
pathDurations: _pathDurations,
|
|
strokeNumbers: _kanjiData!.strokeNumbers,
|
|
|
|
viewBoxWidth: _viewBoxWidth,
|
|
viewBoxHeight: _viewBoxHeight,
|
|
|
|
timingFunction: widget.timingFunction,
|
|
delayBetweenStrokes: widget.delayBetweenStrokes,
|
|
delayBetweenLoops: widget.delayBetweenLoops,
|
|
|
|
strokeColor: widget.strokeColor,
|
|
currentStrokeColor: widget.currentStrokeColor,
|
|
strokeUnfilledColor: widget.strokeUnfilledColor,
|
|
strokeWidth: widget.strokeWidth,
|
|
backgroundColor: widget.backgroundColor,
|
|
|
|
showBrush: widget.showBrush,
|
|
brushColor: widget.brushColor,
|
|
brushRadius: widget.brushRadius,
|
|
|
|
showStrokeNumbers: widget.showStrokeNumbers,
|
|
strokeNumberColor: widget.strokeNumberColor,
|
|
currentStrokeNumberColor: widget.currentStrokeNumberColor,
|
|
strokeNumberFontSize: widget.strokeNumberFontSize,
|
|
strokeNumberFontFamily: widget.strokeNumberFontFamily,
|
|
|
|
showCross: widget.showCross,
|
|
crossColor: widget.crossColor,
|
|
crossStrokeWidth: widget.crossStrokeWidth,
|
|
crossStipleLength: widget.crossStipleLength,
|
|
crossStipleGap: widget.crossStipleGap,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _KanimajiPainter extends CustomPainter {
|
|
final double progress; // 0..1
|
|
|
|
final List<ui.Path> paths;
|
|
final List<double> pathLengths;
|
|
final List<double> pathDurations;
|
|
|
|
final List<KanjiStrokeNumber> strokeNumbers;
|
|
|
|
final double viewBoxWidth;
|
|
final double viewBoxHeight;
|
|
|
|
final TimingFunction timingFunction;
|
|
final Duration delayBetweenStrokes;
|
|
final Duration delayBetweenLoops;
|
|
|
|
final Color strokeColor;
|
|
final Color currentStrokeColor;
|
|
final Color strokeUnfilledColor;
|
|
final double strokeWidth;
|
|
final Color backgroundColor;
|
|
|
|
final bool showBrush;
|
|
final Color brushColor;
|
|
final double brushRadius;
|
|
|
|
final bool showStrokeNumbers;
|
|
final Color strokeNumberColor;
|
|
final Color currentStrokeNumberColor;
|
|
final double strokeNumberFontSize;
|
|
final String? strokeNumberFontFamily;
|
|
|
|
final bool showCross;
|
|
final Color crossColor;
|
|
final double crossStrokeWidth;
|
|
final double crossStipleLength;
|
|
final double crossStipleGap;
|
|
|
|
const _KanimajiPainter({
|
|
required this.progress,
|
|
|
|
required this.paths,
|
|
required this.pathLengths,
|
|
required this.pathDurations,
|
|
required this.strokeNumbers,
|
|
|
|
required this.viewBoxWidth,
|
|
required this.viewBoxHeight,
|
|
|
|
required this.timingFunction,
|
|
required this.delayBetweenStrokes,
|
|
required this.delayBetweenLoops,
|
|
|
|
required this.strokeColor,
|
|
required this.currentStrokeColor,
|
|
required this.strokeUnfilledColor,
|
|
required this.strokeWidth,
|
|
required this.backgroundColor,
|
|
|
|
required this.showBrush,
|
|
required this.brushColor,
|
|
required this.brushRadius,
|
|
|
|
required this.showStrokeNumbers,
|
|
required this.strokeNumberColor,
|
|
required this.currentStrokeNumberColor,
|
|
required this.strokeNumberFontSize,
|
|
required this.strokeNumberFontFamily,
|
|
|
|
required this.showCross,
|
|
required this.crossColor,
|
|
required this.crossStrokeWidth,
|
|
required this.crossStipleLength,
|
|
required this.crossStipleGap,
|
|
});
|
|
|
|
int get elapsedTimeMilliseconds {
|
|
// TODO: only calculate the total time once
|
|
final int totalTimeMilliseconds =
|
|
(pathDurations.fold(0.0, (a, b) => a + b) * 1000).round() +
|
|
delayBetweenStrokes.inMilliseconds * (pathDurations.length - 1) +
|
|
delayBetweenLoops.inMilliseconds;
|
|
|
|
final double p = progress.clamp(0.0, 1.0);
|
|
return (p * totalTimeMilliseconds).round();
|
|
}
|
|
|
|
// TODO: cache the value of the previous paint iteration, to avoid having to recalculate the entire stroke index and progress on every frame
|
|
// fall back to recalculating if it does not step forward.
|
|
|
|
// The index of the currently drawing stroke if it is being drawn.
|
|
// Returns null if any of the following is true:
|
|
// - We are in the delay after a stroke
|
|
// - We are in the delay between loops
|
|
int? get _currentStrokeIndex {
|
|
int currentTime = 0;
|
|
for (int i = 0; i < pathDurations.length; i++) {
|
|
final int strokeTime = (pathDurations[i] * 1000).round();
|
|
|
|
if (elapsedTimeMilliseconds < currentTime + strokeTime) {
|
|
return i;
|
|
} else if (elapsedTimeMilliseconds <
|
|
currentTime + strokeTime + delayBetweenStrokes.inMilliseconds) {
|
|
return null;
|
|
}
|
|
|
|
currentTime += strokeTime;
|
|
currentTime += delayBetweenStrokes.inMilliseconds;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// TODO: optimize by caching the last stroke index and progress, and only recalculating if the elapsed time has moved past the next expected threshold (either the end of the current stroke or the end of the current delay)
|
|
|
|
/// The index of the last fully drawn stroke. Returns -1 if no stroke has been fully drawn yet.
|
|
int get _lastStrokeIndex {
|
|
int currentTime = 0;
|
|
for (int i = 0; i < pathDurations.length; i++) {
|
|
final int strokeTime = (pathDurations[i] * 1000).round();
|
|
if (elapsedTimeMilliseconds < currentTime + strokeTime) {
|
|
return i - 1;
|
|
}
|
|
currentTime += strokeTime;
|
|
currentTime += delayBetweenStrokes.inMilliseconds;
|
|
}
|
|
|
|
return pathDurations.length - 1;
|
|
}
|
|
|
|
/// The progress of the currently drawing stroke (0.0..1.0). Returns null if we are in a delay.
|
|
double? get _currentStrokeProgress {
|
|
final int? currentStrokeIndex = _currentStrokeIndex;
|
|
if (currentStrokeIndex == null) return null;
|
|
|
|
int currentTime = 0;
|
|
for (int i = 0; i < currentStrokeIndex; i++) {
|
|
final int strokeTime = (pathDurations[i] * 1000).round();
|
|
currentTime += strokeTime;
|
|
currentTime += delayBetweenStrokes.inMilliseconds;
|
|
}
|
|
|
|
final int strokeTime = (pathDurations[currentStrokeIndex] * 1000).round();
|
|
final int elapsedInCurrentStroke = elapsedTimeMilliseconds - currentTime;
|
|
return (elapsedInCurrentStroke / strokeTime).clamp(0.0, 1.0);
|
|
}
|
|
|
|
/// Draw the static parts of the canvas that do not change with each frame, such as the background, cross, and unfilled paths.
|
|
void _drawBaseCanvas(Canvas canvas, Size size) {
|
|
final bgPaint = Paint()..color = backgroundColor;
|
|
canvas.drawRect(Offset.zero & size, bgPaint);
|
|
|
|
if (paths.isEmpty) return;
|
|
|
|
final double sx = size.width / viewBoxWidth;
|
|
final double sy = size.height / viewBoxHeight;
|
|
final double scale = math.min(sx, sy);
|
|
|
|
final double dx = (size.width - viewBoxWidth * scale) / 2.0;
|
|
final double dy = (size.height - viewBoxHeight * scale) / 2.0;
|
|
|
|
final Paint crossPaint = Paint()
|
|
..style = PaintingStyle.stroke
|
|
..color = crossColor
|
|
..strokeWidth = crossStrokeWidth;
|
|
|
|
final Paint unfilledPaint = Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeCap = StrokeCap.round
|
|
..strokeJoin = StrokeJoin.round
|
|
..strokeWidth = strokeWidth / scale
|
|
..color = strokeUnfilledColor;
|
|
|
|
// Draw cross if enabled
|
|
if (showCross) {
|
|
// Draw vertical stipled line
|
|
for (
|
|
double y = 0;
|
|
y < size.height;
|
|
y += crossStipleLength + crossStipleGap
|
|
) {
|
|
canvas.drawLine(
|
|
Offset(size.width / 2, y),
|
|
Offset(size.width / 2, math.min(y + crossStipleLength, size.height)),
|
|
crossPaint,
|
|
);
|
|
}
|
|
// Draw horizontal stipled line
|
|
for (
|
|
double x = 0;
|
|
x < size.width;
|
|
x += crossStipleLength + crossStipleGap
|
|
) {
|
|
canvas.drawLine(
|
|
Offset(x, size.height / 2),
|
|
Offset(math.min(x + crossStipleLength, size.width), size.height / 2),
|
|
crossPaint,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Draw all unfilled paths
|
|
for (final path in paths) {
|
|
canvas.drawPath(path, unfilledPaint);
|
|
}
|
|
|
|
// Draw stroke numbers if enabled
|
|
if (showStrokeNumbers) {
|
|
final textPainter = TextPainter(
|
|
textAlign: TextAlign.center,
|
|
textDirection: TextDirection.ltr,
|
|
);
|
|
|
|
for (final sn in strokeNumbers) {
|
|
final textSpan = TextSpan(
|
|
text: sn.num.toString(),
|
|
style: TextStyle(
|
|
color: strokeNumberColor,
|
|
fontSize: strokeNumberFontSize / scale,
|
|
fontFamily: strokeNumberFontFamily,
|
|
),
|
|
);
|
|
textPainter.text = textSpan;
|
|
textPainter.layout();
|
|
|
|
final Offset pos = Offset(
|
|
sn.position.x.toDouble(),
|
|
sn.position.y.toDouble(),
|
|
);
|
|
final Offset centeredPos =
|
|
pos - Offset(textPainter.width / 2, textPainter.height / 2);
|
|
textPainter.paint(canvas, centeredPos);
|
|
}
|
|
}
|
|
|
|
// canvas.save();
|
|
canvas.translate(dx, dy);
|
|
canvas.scale(scale, scale);
|
|
}
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
// TODO: see if we can optimize by storing the base canvas once and restoring it on each frame instead of redrawing it every time
|
|
_drawBaseCanvas(canvas, size);
|
|
|
|
final double sx = size.width / viewBoxWidth;
|
|
final double sy = size.height / viewBoxHeight;
|
|
final double scale = math.min(sx, sy);
|
|
|
|
final Paint filledPaint = Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeCap = StrokeCap.round
|
|
..strokeJoin = StrokeJoin.round
|
|
..strokeWidth = strokeWidth / scale
|
|
..color = strokeColor;
|
|
|
|
final Paint currentStrokePaint = Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeCap = StrokeCap.round
|
|
..strokeJoin = StrokeJoin.round
|
|
..strokeWidth = strokeWidth / scale
|
|
..color = currentStrokeColor;
|
|
|
|
final Paint brushPaint = Paint()
|
|
..style = PaintingStyle.fill
|
|
..color = brushColor;
|
|
|
|
final int currentlyDrawingIndex = _currentStrokeIndex ?? -1;
|
|
final int lastStrokeIndex = _lastStrokeIndex;
|
|
|
|
// Draw all completed strokes fully filled
|
|
for (int i = 0; i < lastStrokeIndex + 1; i++) {
|
|
canvas.drawPath(paths[i], filledPaint);
|
|
}
|
|
|
|
// Draw the currently drawing stroke with partial coverage
|
|
if (currentlyDrawingIndex >= 0) {
|
|
final ui.Path path = paths[currentlyDrawingIndex];
|
|
final double len = pathLengths[currentlyDrawingIndex];
|
|
final double strokeProgress = timingFunction.func(
|
|
_currentStrokeProgress!,
|
|
);
|
|
|
|
final ui.PathMetrics metrics = path.computeMetrics();
|
|
final ui.PathMetric metric = metrics.first;
|
|
final double drawLength = len * strokeProgress;
|
|
final ui.Path partialPath = metric.extractPath(0, drawLength);
|
|
canvas.drawPath(partialPath, currentStrokePaint);
|
|
|
|
if (showBrush) {
|
|
final ui.Tangent? tangent = metric.getTangentForOffset(drawLength);
|
|
if (tangent != null) {
|
|
canvas.drawCircle(tangent.position, brushRadius / scale, brushPaint);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Color over the stroke number of the currently drawing stroke if enabled
|
|
if (showStrokeNumbers && currentlyDrawingIndex != -1) {
|
|
final textPainter = TextPainter(
|
|
textAlign: TextAlign.center,
|
|
textDirection: TextDirection.ltr,
|
|
);
|
|
|
|
final sn = strokeNumbers[currentlyDrawingIndex];
|
|
final textSpan = TextSpan(
|
|
text: sn.num.toString(),
|
|
style: TextStyle(
|
|
color: currentStrokeNumberColor,
|
|
fontSize: strokeNumberFontSize / scale,
|
|
fontFamily: strokeNumberFontFamily,
|
|
),
|
|
);
|
|
textPainter.text = textSpan;
|
|
textPainter.layout();
|
|
final Offset pos = Offset(
|
|
sn.position.x.toDouble(),
|
|
sn.position.y.toDouble(),
|
|
);
|
|
final Offset centeredPos =
|
|
pos - Offset(textPainter.width / 2, textPainter.height / 2);
|
|
textPainter.paint(canvas, centeredPos);
|
|
}
|
|
|
|
// canvas.restore();
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant _KanimajiPainter oldDelegate) {
|
|
return oldDelegate.progress != progress ||
|
|
oldDelegate.paths != paths ||
|
|
oldDelegate.strokeNumbers != strokeNumbers ||
|
|
oldDelegate.timingFunction != timingFunction ||
|
|
oldDelegate.delayBetweenStrokes != delayBetweenStrokes ||
|
|
oldDelegate.delayBetweenLoops != delayBetweenLoops ||
|
|
oldDelegate.strokeColor != strokeColor ||
|
|
oldDelegate.strokeUnfilledColor != strokeUnfilledColor ||
|
|
oldDelegate.strokeWidth != strokeWidth ||
|
|
oldDelegate.backgroundColor != backgroundColor ||
|
|
oldDelegate.showBrush != showBrush ||
|
|
oldDelegate.brushColor != brushColor ||
|
|
oldDelegate.brushRadius != brushRadius ||
|
|
oldDelegate.showStrokeNumbers != showStrokeNumbers ||
|
|
oldDelegate.strokeNumberColor != strokeNumberColor ||
|
|
oldDelegate.currentStrokeNumberColor != currentStrokeNumberColor ||
|
|
oldDelegate.strokeNumberFontSize != strokeNumberFontSize ||
|
|
oldDelegate.strokeNumberFontFamily != strokeNumberFontFamily ||
|
|
oldDelegate.showCross != showCross ||
|
|
oldDelegate.crossColor != crossColor ||
|
|
oldDelegate.crossStrokeWidth != crossStrokeWidth ||
|
|
oldDelegate.crossStipleLength != crossStipleLength ||
|
|
oldDelegate.crossStipleGap != crossStipleGap;
|
|
}
|
|
}
|