This commit is contained in:
2026-02-21 17:24:59 +09:00
parent aba99a8996
commit 819eecd811
18 changed files with 1307 additions and 31 deletions
+119
View File
File diff suppressed because one or more lines are too long
+133
View File
File diff suppressed because one or more lines are too long
Generated
+6 -6
View File
@@ -3,11 +3,11 @@
"kanjivg": {
"flake": false,
"locked": {
"lastModified": 1754131552,
"narHash": "sha256-1h3nT1f1QqXme4rU3HMmEREn74sAASyZ8qzjZ0tPi4I=",
"lastModified": 1770109946,
"narHash": "sha256-zgkyLJwEJe6YABUNrL27BBrDSWVNcAUk7K6P1mcVHxQ=",
"owner": "KanjiVG",
"repo": "kanjivg",
"rev": "0d08020e69611552a0fbe13d49365d6196431b96",
"rev": "2fe6daaba502ee6735f888e5fb4a7d30639a18ee",
"type": "github"
},
"original": {
@@ -18,11 +18,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1754214453,
"narHash": "sha256-Q/I2xJn/j1wpkGhWkQnm20nShYnG7TI99foDBpXm1SY=",
"lastModified": 1771369470,
"narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5b09dc45f24cf32316283e62aec81ffee3c3e376",
"rev": "0182a361324364ae3f436a63005877674cf45efb",
"type": "github"
},
"original": {
+252
View File
File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -1,12 +1,12 @@
import 'dart:io';
import 'dart:math' show min, sqrt, pow;
import 'svg/parser.dart';
import 'point.dart';
import 'bezier.dart' as bezier;
import 'package:xml/xml.dart';
import 'svg_parser.dart';
import 'primitives/point.dart';
import 'primitives/bezier.dart' as bezier;
double _computePathLength(String path) =>
parsePath(path).size(error: 1e-8).toDouble();
+212
View File
@@ -0,0 +1,212 @@
import 'package:xml/xml.dart';
import 'svg_parser.dart';
import 'primitives/path.dart';
import 'primitives/point.dart';
/// Enum set in the kvg:position attribute, used by <g> elements in the KanjiVG SVG files.
enum KanjiPathGroupPosition {
bottom,
kamae,
kamaec,
left,
middle,
nyo,
nyoc,
right,
tare,
tarec,
top;
factory KanjiPathGroupPosition.fromString(String s) => switch (s) {
'bottom' => bottom,
'kamae' => kamae,
'kamaec' => kamaec,
'left' => left,
'middle' => middle,
'nyo' => nyo,
'nyoc' => nyoc,
'right' => right,
'tare' => tare,
'tarec' => tarec,
'top' => top,
_ => throw ArgumentError('Invalid position string: $s'),
};
@override
String toString() => switch (this) {
KanjiPathGroupPosition.bottom => 'bottom',
KanjiPathGroupPosition.kamae => 'kamae',
KanjiPathGroupPosition.kamaec => 'kamaec',
KanjiPathGroupPosition.left => 'left',
KanjiPathGroupPosition.middle => 'middle',
KanjiPathGroupPosition.nyo => 'nyo',
KanjiPathGroupPosition.nyoc => 'nyoc',
KanjiPathGroupPosition.right => 'right',
KanjiPathGroupPosition.tare => 'tare',
KanjiPathGroupPosition.tarec => 'tarec',
KanjiPathGroupPosition.top => 'top',
};
}
/// Contents of a \<g> element in the KanjiVG SVG files.
class KanjiPathGroupTreeNode {
final String id;
final List<KanjiPathGroupTreeNode> children;
final String? element;
final String? original;
final KanjiPathGroupPosition? position;
final String? radical;
final int? part;
KanjiPathGroupTreeNode({
required this.id,
this.children = const [],
this.element,
this.original,
this.position,
this.radical,
this.part,
});
}
/// Contents of a <text> element in the StrokeNumber's group in the KanjiVG SVG files
class KanjiStrokeNumber {
final int number;
final Point position;
KanjiStrokeNumber(this.number, this.position);
@override
String toString() =>
'KanjiStrokeNumber(number: $number, position: $position)';
}
/// Contents of a <path> element in the KanjiVG SVG files
class KanjiVGPath {
final String id;
final String type;
final Path svgPath;
KanjiVGPath({required this.id, required this.type, required this.svgPath});
@override
String toString() => 'KanjiVGPath(id: $id, type: $type, svgPath: $svgPath)';
}
/// Representation of the entire KanjiVG SVG file for a single kanji character
class KanjiVGKanji {
final String character;
final List<Path> paths;
final List<KanjiStrokeNumber> strokeNumbers;
final KanjiPathGroupTreeNode? pathGroups;
KanjiVGKanji({
required this.character,
required this.paths,
required this.strokeNumbers,
this.pathGroups,
});
}
/// Small wrapper returned by the parser. It contains the parsed paths and stroke
/// numbers and the kanji character string.
class KanjiVGItem {
final String character;
final List<KanjiVGPath> paths;
final List<KanjiStrokeNumber> strokeNumbers;
KanjiVGItem({
required this.character,
required this.paths,
required this.strokeNumbers,
});
/// Helper method to get attributes that may be in the kvg namespace or as kvg: prefixed attributes.
static String? _kvgAttr(XmlElement el, String name) {
final fromNs = el.getAttribute(
name,
namespace: 'http://kanjivg.tagaini.net',
);
if (fromNs != null) return fromNs;
return el.getAttribute('kvg:$name');
}
/// Parse the path data from the provided XML document and return a list of [Path]s.
static List<KanjiVGPath> _parsePaths(XmlDocument doc) {
final List<KanjiVGPath> paths = [];
for (final p in doc.findAllElements('path')) {
final d = p.getAttribute('d');
if (d == null || d.trim().isEmpty) {
continue;
}
final svgPath = parsePath(d);
final id = p.getAttribute('id') ?? '';
final type = _kvgAttr(p, 'type') ?? p.getAttribute('kvg:type') ?? '';
paths.add(KanjiVGPath(id: id, type: type, svgPath: svgPath));
}
return paths;
}
/// Parse the stroke number group from the provided XML document and return a list of [KanjiStrokeNumber]s.
static List<KanjiStrokeNumber> _parseStrokeNumbers(XmlDocument doc) {
final List<KanjiStrokeNumber> strokeNumbers = [];
final strokeNumberGroup = doc.findAllElements('g').firstWhere((g) {
final id = g.getAttribute('id') ?? '';
return RegExp(r'^kvg:StrokeNumbers_').hasMatch(id);
}, orElse: () => XmlElement(XmlName('')));
if (strokeNumberGroup.name.local != '') {
for (final t in strokeNumberGroup.findAllElements('text')) {
final rawText = t.innerText.trim();
if (rawText.isEmpty) continue;
final numVal = int.tryParse(rawText);
if (numVal == null) continue;
final transform = t.getAttribute('transform') ?? '';
final numbers = transform
.replaceAll('matrix(1 0 0 1', '')
.replaceAll(')', '')
.trim()
.split(' ')
.map(num.parse)
.toList();
assert(
numbers.length >= 2,
'Expected at least 2 numbers in transform for stroke number position',
);
Point pos = Point(
numbers[numbers.length - 2],
numbers[numbers.length - 1],
);
strokeNumbers.add(KanjiStrokeNumber(numVal, pos));
}
}
return strokeNumbers;
}
/// Parse the provided KanjiVG SVG content and return a [KanjiVGItem].
factory KanjiVGItem.parseFromXml(String xmlContent) {
final XmlDocument doc = XmlDocument.parse(xmlContent);
XmlElement strokePathsGroup = doc
.findElements('svg')
.first
.findElements('g')
.first;
XmlElement kanjiGroup = strokePathsGroup.findElements('g').first;
final character = _kvgAttr(kanjiGroup, 'element') ?? '';
final paths = _parsePaths(doc);
final strokeNumbers = _parseStrokeNumbers(doc);
return KanjiVGItem(
character: character,
paths: paths,
strokeNumbers: strokeNumbers,
);
}
}
@@ -8,7 +8,7 @@ import 'dart:math' show sqrt, sin, cos, acos, log, pi;
import 'package:bisection/extension.dart';
import '../point.dart';
import 'point.dart';
num radians(num n) => n * pi / 180;
num degrees(num n) => n * 180 / pi;
View File
+3 -3
View File
@@ -3,8 +3,8 @@
/// See https://pypi.org/project/svg.path/ for the original implementation.
library;
import '../point.dart';
import 'path.dart';
import 'primitives/point.dart';
import 'primitives/path.dart';
const _commands = {
'M',
@@ -32,7 +32,7 @@ const _commands = {
// const _uppercaseCommands = {'M', 'Z', 'L', 'H', 'V', 'C', 'S', 'Q', 'T', 'A'};
final _commandPattern = RegExp("(?=[${_commands.join('')}])");
final _floatPattern = RegExp(r"^[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?");
final _floatPattern = RegExp(r"^[-+]?(?:[0-9]+\.?[0-9]*|[0-9]*\.?[0-9]+)(?:[eE][-+]?[0-9]+)?");
class ParserResult<T> {
final T value;
+525 -9
View File
@@ -1,11 +1,527 @@
import 'package:flutter/material.dart';
// // A Flutter widget that renders an animated single-kanji stroke drawing using
// // the project's SVG path parser. This widget loads the Kanji SVG from
// // `assets/kanjivg/kanji/<hex>.svg` (the same assets used by the exporter),
// // parses the path `d` attributes, converts them into `ui.Path` objects and
// // animates the stroke draw using an AnimationController.
// //
// // Notes:
// // - This implementation uses the repository's SVG path parser (`parsePath`)
// // and the custom `Path` representation in `lib/svg/path.dart`. We convert
// // those into Flutter `ui.Path` by sampling points along the sampled SVG path.
// // - The SVGs in KanjiVG typically use a 109x109 viewBox. We detect the
// // `viewBox` attribute (if present) and scale paths to fit the widget size.
// //
// // Public API (Kanimaji widget parameters):
// // - `kanji` (required): the single character Kanji to display.
// // - `size`: target width/height of the widget (square).
// // - `strokeColor`, `strokeUnfilledColor`, `strokeWidth`.
// // - `backgroundColor`.
// // - `speed`: multiplier to speed up (>1) or slow down (<1) the animation.
// // - `loop`: whether to repeat the animation.
// // - `showBrush`: whether to show a small circular "brush" tip following the stroke.
// // - `durationOverride`: optionally override the computed animation duration.
// //
// // Example:
// // Kanimaji(
// // kanji: '水',
// // size: 240,
// // strokeColor: Colors.black,
// // strokeUnfilledColor: Colors.grey.shade300,
// // strokeWidth: 3.0,
// // backgroundColor: Colors.white,
// // speed: 1.0,
// // loop: true,
// // );
class Kanimaji extends StatelessWidget {
final String kanji;
const Kanimaji({super.key, required this.kanji});
@override
Widget build(BuildContext context) {
return Container();
}
}
// import 'dart:math' as math;
// import 'dart:ui' as ui;
// import 'package:flutter/material.dart';
// import 'package:flutter/services.dart' show rootBundle;
// import 'package:xml/xml.dart';
// import 'svg/parser.dart' show parsePath;
// import 'svg/path.dart' as svgpath;
// import 'point.dart' as kpt;
// import 'animator.dart' show strokeLengthToDuration;
// class Kanimaji extends StatefulWidget {
// final String kanji;
// final double size;
// final Color strokeColor;
// final Color strokeUnfilledColor;
// final double strokeWidth;
// final Color backgroundColor;
// final double speed;
// final bool loop;
// final bool showBrush;
// final Duration? durationOverride;
// const Kanimaji({
// super.key,
// required this.kanji,
// this.size = 150,
// this.strokeColor = Colors.black,
// this.strokeUnfilledColor = const Color(0xFFEEEEEE),
// this.strokeWidth = 3.0,
// this.backgroundColor = Colors.transparent,
// this.speed = 1.0,
// this.loop = true,
// this.showBrush = true,
// this.durationOverride,
// });
// @override
// State<Kanimaji> createState() => _KanimajiState();
// }
// class _KanimajiState extends State<Kanimaji> with SingleTickerProviderStateMixin {
// bool _loaded = false;
// String? _error;
// late AnimationController _controller;
// // Converted UI paths and per-path lengths
// List<ui.Path> _paths = [];
// List<double> _pathLengths = [];
// // per-stroke durations (in seconds) computed using animator.strokeLengthToDuration
// List<double> _pathDurations = [];
// // viewBox width/height from svg for scaling
// double _viewBoxWidth = 109.0;
// double _viewBoxHeight = 109.0;
// // total length summation (for drawing metrics)
// double _totalLength = 0.0;
// @override
// void initState() {
// super.initState();
// // Dummy controller; real duration will be set after loading
// _controller = AnimationController(vsync: this);
// // Add a single listener once to avoid duplicates when reconfiguring the controller.
// _controller.addListener(_onTick);
// _loadAndParseSvg();
// }
// @override
// void didUpdateWidget(covariant Kanimaji oldWidget) {
// super.didUpdateWidget(oldWidget);
// // If kanji changed, re-load
// if (oldWidget.kanji != widget.kanji) {
// _loadAndParseSvg();
// } else if (oldWidget.speed != widget.speed ||
// oldWidget.durationOverride != widget.durationOverride ||
// oldWidget.loop != widget.loop) {
// // Update controller if animation parameters changed
// _configureController();
// }
// }
// void _onTick() {
// if (mounted) setState(() {});
// }
// @override
// void dispose() {
// _controller.removeListener(_onTick);
// _controller.dispose();
// super.dispose();
// }
// String _codepointHex(String s) {
// // Support multi-code-unit characters by taking the first Unicode rune
// final runes = s.runes.toList();
// if (runes.isEmpty) return '00000';
// return runes[0].toRadixString(16).padLeft(5, '0');
// }
// Future<void> _loadAndParseSvg() async {
// setState(() {
// _loaded = false;
// _error = null;
// _paths = [];
// _pathLengths = [];
// _pathDurations = [];
// _totalLength = 0.0;
// });
// final hex = _codepointHex(widget.kanji);
// final assetPath = 'assets/kanjivg/kanji/$hex.svg';
// try {
// final svgString = await rootBundle.loadString(assetPath);
// final doc = XmlDocument.parse(svgString);
// // parse viewBox: "minX minY width height"
// final viewBoxAttr = doc.rootElement.getAttribute('viewBox');
// if (viewBoxAttr != null) {
// final parts = viewBoxAttr.split(RegExp(r'[\s,]+')).where((p) => p.isNotEmpty).toList();
// if (parts.length >= 4) {
// final w = double.tryParse(parts[2]);
// final h = double.tryParse(parts[3]);
// if (w != null && h != null && w > 0 && h > 0) {
// _viewBoxWidth = w;
// _viewBoxHeight = h;
// }
// }
// } else {
// // fallback to width/height attributes if present (may include "px")
// final widthAttr = doc.rootElement.getAttribute('width');
// final heightAttr = doc.rootElement.getAttribute('height');
// if (widthAttr != null && heightAttr != null) {
// final w = double.tryParse(widthAttr.replaceAll('px', '').trim());
// final h = double.tryParse(heightAttr.replaceAll('px', '').trim());
// if (w != null && h != null && w > 0 && h > 0) {
// _viewBoxWidth = w;
// _viewBoxHeight = h;
// }
// }
// }
// // Find all <path ... d="..."> elements inside the SVG. KanjiVG groups strokes in <g>.
// final pathElements = doc.findAllElements('path', namespace: doc.rootElement.name.namespaceUri).toList();
// if (pathElements.isEmpty) {
// throw Exception('No <path> elements found in $assetPath');
// }
// final uiPaths = <ui.Path>[];
// final lengths = <double>[];
// for (final p in pathElements) {
// final d = p.getAttribute('d');
// if (d == null || d.trim().isEmpty) continue;
// // Parse the SVG path using existing parser
// final svgParsed = parsePath(d); // returns svgpath.Path (project type)
// // Convert to ui.Path using direct segment conversion (no sampling)
// final uiPath = _svgPathToUiPath(svgParsed);
// if (uiPath == null) continue;
// // Compute its drawing length using PathMetrics (for extractPath)
// final pm = uiPath.computeMetrics();
// double pathLen = 0.0;
// for (final metric in pm) {
// pathLen += metric.length;
// }
// // If the path metric yields zero (rare), skip
// if (pathLen <= 0) continue;
// // Compute logical stroke length using the parser (precise geometry) and convert to duration
// final double logicalLen = svgParsed.size(error: 1e-8).toDouble();
// final double durationSec = strokeLengthToDuration(logicalLen);
// uiPaths.add(uiPath);
// lengths.add(pathLen);
// _pathDurations.add(durationSec);
// _totalLength += pathLen;
// }
// if (uiPaths.isEmpty) {
// throw Exception('No drawable paths found in $assetPath');
// }
// setState(() {
// _paths = uiPaths;
// _pathLengths = lengths;
// });
// _configureController();
// setState(() {
// _loaded = true;
// });
// } catch (e) {
// setState(() {
// _error = 'Error loading/ parsing $assetPath: $e';
// _loaded = true; // mark loaded so we can show error
// });
// }
// }
// // Convert a parsed SVG path (project Path) into a Flutter ui.Path without sampling.
// // Uses the concrete segment types (Move, Line, CubicBezier, QuadraticBezier, Arc, Close)
// // and maps them to the corresponding ui.Path methods for native, accurate drawing.
// ui.Path? _svgPathToUiPath(svgpath.Path svgP) {
// try {
// final ui.Path p = ui.Path();
// bool started = false;
// for (final seg in svgP) {
// if (seg is svgpath.Move) {
// p.moveTo(seg.start.x.toDouble(), seg.start.y.toDouble());
// started = true;
// } else if (seg is svgpath.Line) {
// if (!started) {
// p.moveTo(seg.start.x.toDouble(), seg.start.y.toDouble());
// started = true;
// }
// p.lineTo(seg.end.x.toDouble(), seg.end.y.toDouble());
// } else if (seg is svgpath.Close) {
// p.close();
// } else if (seg is svgpath.CubicBezier) {
// if (!started) {
// p.moveTo(seg.start.x.toDouble(), seg.start.y.toDouble());
// started = true;
// }
// p.cubicTo(
// seg.control1.x.toDouble(),
// seg.control1.y.toDouble(),
// seg.control2.x.toDouble(),
// seg.control2.y.toDouble(),
// seg.end.x.toDouble(),
// seg.end.y.toDouble(),
// );
// } else if (seg is svgpath.QuadraticBezier) {
// if (!started) {
// p.moveTo(seg.start.x.toDouble(), seg.start.y.toDouble());
// started = true;
// }
// p.quadraticBezierTo(
// seg.control.x.toDouble(),
// seg.control.y.toDouble(),
// seg.end.x.toDouble(),
// seg.end.y.toDouble(),
// );
// } else if (seg is svgpath.Arc) {
// if (!started) {
// p.moveTo(seg.start.x.toDouble(), seg.start.y.toDouble());
// started = true;
// }
// // Map SVG Arc to Flutter arcToPoint. rotation is in degrees in svg; arcToPoint expects radians.
// final radius = Radius.elliptical(seg.radius.x.toDouble(), seg.radius.y.toDouble());
// p.arcToPoint(
// Offset(seg.end.x.toDouble(), seg.end.y.toDouble()),
// radius: radius,
// rotation: seg.rotation.toDouble() * (math.pi / 180.0),
// largeArc: seg.arc,
// clockwise: seg.sweep,
// );
// } else {
// // Unknown segment fallback: try to move to end
// try {
// p.moveTo(seg.end.x.toDouble(), seg.end.y.toDouble());
// started = true;
// } catch (_) {}
// }
// }
// return p;
// } catch (e) {
// return null;
// }
// }
// void _configureController() {
// // Use per-stroke durations computed from the parsed geometry (strokeLengthToDuration)
// // Sum durations and scale by widget.speed. If durationOverride is provided use it.
// final double totalSec = _pathDurations.fold(0.0, (a, b) => a + b);
// final double scaledTotalSec = (widget.speed <= 0) ? totalSec : (totalSec / widget.speed);
// final Duration duration = widget.durationOverride ??
// Duration(milliseconds: (math.max(0.001, scaledTotalSec) * 1000.0).round());
// final bool shouldRepeat = widget.loop;
// _controller.stop();
// _controller.duration = duration;
// if (shouldRepeat) {
// _controller.repeat();
// } else {
// _controller.forward(from: 0);
// }
// // Repaint handled by the single listener added in initState.
// }
// @override
// Widget build(BuildContext context) {
// if (!_loaded) {
// return SizedBox(
// width: widget.size,
// height: widget.size,
// child: Center(child: CircularProgressIndicator()),
// );
// }
// if (_error != null) {
// return SizedBox(
// width: widget.size,
// height: widget.size,
// child: Container(
// color: widget.backgroundColor,
// alignment: Alignment.center,
// child: Text(
// 'Error',
// style: TextStyle(color: Colors.red, fontSize: math.max(10, widget.size * 0.08)),
// ),
// ),
// );
// }
// return SizedBox(
// width: widget.size,
// height: widget.size,
// child: CustomPaint(
// painter: _KanimajiPainter(
// paths: _paths,
// pathLengths: _pathLengths,
// pathDurations: _pathDurations,
// progress: _controller.value,
// strokeColor: widget.strokeColor,
// strokeUnfilledColor: widget.strokeUnfilledColor,
// strokeWidth: widget.strokeWidth,
// backgroundColor: widget.backgroundColor,
// viewBoxWidth: _viewBoxWidth,
// viewBoxHeight: _viewBoxHeight,
// showBrush: widget.showBrush,
// ),
// ),
// );
// }
// }
// class _KanimajiPainter extends CustomPainter {
// final List<ui.Path> paths;
// final List<double> pathLengths;
// final List<double> pathDurations; // per-stroke durations in seconds
// final double progress; // 0..1
// final Color strokeColor;
// final Color strokeUnfilledColor;
// final double strokeWidth;
// final Color backgroundColor;
// final double viewBoxWidth;
// final double viewBoxHeight;
// final bool showBrush;
// _KanimajiPainter({
// required this.paths,
// required this.pathLengths,
// required this.pathDurations,
// required this.progress,
// required this.strokeColor,
// required this.strokeUnfilledColor,
// required this.strokeWidth,
// required this.backgroundColor,
// required this.viewBoxWidth,
// required this.viewBoxHeight,
// required this.showBrush,
// });
// @override
// void paint(Canvas canvas, Size size) {
// // Paint background
// final bgPaint = Paint()..color = backgroundColor;
// canvas.drawRect(Offset.zero & size, bgPaint);
// if (paths.isEmpty) return;
// // compute scale to fit viewBox into size, preserving aspect ratio
// final double sx = size.width / viewBoxWidth;
// final double sy = size.height / viewBoxHeight;
// final double scale = math.min(sx, sy);
// // compute translation to center the drawing
// final double dx = (size.width - viewBoxWidth * scale) / 2.0;
// final double dy = (size.height - viewBoxHeight * scale) / 2.0;
// canvas.save();
// canvas.translate(dx, dy);
// canvas.scale(scale, scale);
// // paints
// final Paint unfilledPaint = Paint()
// ..style = PaintingStyle.stroke
// ..strokeCap = StrokeCap.round
// ..strokeJoin = StrokeJoin.round
// ..strokeWidth = strokeWidth / scale // scale stroke width so it looks consistent
// ..color = strokeUnfilledColor;
// final Paint filledPaint = Paint()
// ..style = PaintingStyle.stroke
// ..strokeCap = StrokeCap.round
// ..strokeJoin = StrokeJoin.round
// ..strokeWidth = strokeWidth / scale
// ..color = strokeColor;
// // total animation time in seconds computed from durations
// final double totalTime = pathDurations.isEmpty ? 1.0 : pathDurations.fold(0.0, (a, b) => a + b);
// final double p = progress.clamp(0.0, 1.0);
// double elapsedTime = 0.0; // seconds accumulated so far
// for (int i = 0; i < paths.length; i++) {
// final ui.Path path = paths[i];
// final double len = pathLengths[i];
// final double dur = (i < pathDurations.length) ? pathDurations[i] : 0.0;
// final double startFraction = totalTime == 0 ? 0.0 : (elapsedTime / totalTime);
// final double endFraction = totalTime == 0 ? 1.0 : ((elapsedTime + dur) / totalTime);
// // Draw background (unfilled) fully for this stroke
// canvas.drawPath(path, unfilledPaint);
// // Determine coverage for this stroke based on global progress (time-based)
// double coverage = 0.0;
// if (p >= endFraction) {
// coverage = 1.0;
// } else if (p <= startFraction || dur <= 0.0) {
// coverage = 0.0;
// } else {
// coverage = (p - startFraction) / (endFraction - startFraction);
// }
// if (coverage > 0.0) {
// // extract subpath up to coverage * len (length-based extraction for drawing)
// final metrics = path.computeMetrics().toList();
// final ui.Path drawn = ui.Path();
// double remain = coverage * len;
// for (final metric in metrics) {
// if (remain <= 0) break;
// final take = math.min(metric.length, remain);
// final sub = metric.extractPath(0.0, take);
// drawn.addPath(sub, Offset.zero);
// remain -= take;
// }
// // draw filled portion
// canvas.drawPath(drawn, filledPaint);
// // brush tip
// if (showBrush && coverage > 0.0 && coverage < 1.0) {
// // find the tangent at the very end of the drawn part
// final metrics2 = path.computeMetrics().toList();
// double rem = coverage * len;
// for (final metric in metrics2) {
// if (rem <= 0) break;
// if (rem <= metric.length + 1e-9) {
// final tangent = metric.getTangentForOffset(rem.clamp(0.0, metric.length));
// if (tangent != null) {
// final Offset pos = tangent.position;
// final double brushRadius = (strokeWidth * 0.9) / scale;
// final Paint brushPaint = Paint()..color = strokeColor;
// canvas.drawCircle(pos, brushRadius, brushPaint);
// }
// break;
// } else {
// rem -= metric.length;
// }
// }
// }
// }
// elapsedTime += dur;
// }
// canvas.restore();
// }
// @override
// bool shouldRepaint(covariant _KanimajiPainter oldDelegate) {
// return oldDelegate.progress != progress ||
// oldDelegate.paths != paths ||
// oldDelegate.strokeColor != strokeColor ||
// oldDelegate.strokeUnfilledColor != strokeUnfilledColor ||
// oldDelegate.strokeWidth != strokeWidth ||
// oldDelegate.backgroundColor != backgroundColor;
// }
// }
+1
View File
@@ -15,3 +15,4 @@ dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
path: ^1.9.1
+43
View File
@@ -0,0 +1,43 @@
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:flutter_test/flutter_test.dart';
import 'package:kanimaji/kanjivg_parser.dart';
void main() {
final kanjivgDirStr = Platform.environment['KANJIVG_PATH'];
if (kanjivgDirStr == null) {
throw Exception('KANJIVG_PATH environment variable not set');
}
final kanjivgDir = Directory(kanjivgDirStr);
final kanjivgFiles = kanjivgDir
.listSync(recursive: true)
.whereType<File>()
.where((f) => f.path.endsWith('.svg'))
.toList();
for (final file in kanjivgFiles) {
test('Test parsing KanjiVG file ${path.basename(file.path)}', () async {
final content = await file.readAsString();
try {
final item = KanjiVGItem.parseFromXml(content);
expect(item, isNotNull, reason: 'Failed to parse ${file.path}');
expect(
item.strokeNumbers,
isNotEmpty,
reason: 'No stroke numbers found in ${file.path}',
);
expect(
item.strokeNumbers.length,
item.paths.length,
reason: 'Mismatch between stroke numbers and paths in ${file.path}',
);
} catch (e) {
fail('Error parsing ${file.path}: $e');
}
});
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:kanimaji/svg/parser.dart';
import 'package:kanimaji/svg_parser.dart';
void main() {
test('Test generating SVG path strings', () {
+3 -3
View File
@@ -1,7 +1,7 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:kanimaji/point.dart';
import 'package:kanimaji/svg/parser.dart';
import 'package:kanimaji/svg/path.dart';
import 'package:kanimaji/primitives/path.dart';
import 'package:kanimaji/primitives/point.dart';
import 'package:kanimaji/svg_parser.dart';
void main() {
group("Examples from the SVG spec", () {
+2 -2
View File
@@ -2,8 +2,8 @@
import 'dart:math' show sqrt, pi;
import 'package:flutter_test/flutter_test.dart';
import 'package:kanimaji/point.dart';
import 'package:kanimaji/svg/path.dart';
import 'package:kanimaji/primitives/point.dart';
import 'package:kanimaji/primitives/path.dart';
// from ..path import CubicBezier, QuadraticBezier, Line, Arc, Move, Close, Path
+2 -2
View File
@@ -2,8 +2,8 @@
// from svg.path import parser
import 'package:flutter_test/flutter_test.dart';
import 'package:kanimaji/point.dart';
import 'package:kanimaji/svg/parser.dart'
import 'package:kanimaji/primitives/point.dart';
import 'package:kanimaji/svg_parser.dart'
show Command, Token, commandifyPath, parsePath, tokenizePath;
class TokenizerTest {