diff --git a/1.svg b/1.svg
new file mode 100644
index 0000000..a1cb0a1
--- /dev/null
+++ b/1.svg
@@ -0,0 +1,119 @@
+
+
+
+
+]>
+
\ No newline at end of file
diff --git a/2.svg b/2.svg
new file mode 100644
index 0000000..1ea4042
--- /dev/null
+++ b/2.svg
@@ -0,0 +1,133 @@
+
+
+
+
+]>
+
\ No newline at end of file
diff --git a/flake.lock b/flake.lock
index ab901b6..e5e7f24 100644
--- a/flake.lock
+++ b/flake.lock
@@ -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": {
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..2c0af1f
--- /dev/null
+++ b/index.html
@@ -0,0 +1,252 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/lib/animator.dart b/lib/animator.dart
index 59da897..42592d8 100644
--- a/lib/animator.dart
+++ b/lib/animator.dart
@@ -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();
diff --git a/lib/kanjivg_parser.dart b/lib/kanjivg_parser.dart
new file mode 100644
index 0000000..aca59a1
--- /dev/null
+++ b/lib/kanjivg_parser.dart
@@ -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 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 \ element in the KanjiVG SVG files.
+class KanjiPathGroupTreeNode {
+ final String id;
+ final List 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 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 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 paths;
+ final List 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 paths;
+ final List 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 _parsePaths(XmlDocument doc) {
+ final List 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 _parseStrokeNumbers(XmlDocument doc) {
+ final List 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,
+ );
+ }
+}
diff --git a/lib/bezier.dart b/lib/primitives/bezier.dart
similarity index 100%
rename from lib/bezier.dart
rename to lib/primitives/bezier.dart
diff --git a/lib/svg/path.dart b/lib/primitives/path.dart
similarity index 99%
rename from lib/svg/path.dart
rename to lib/primitives/path.dart
index 8aea693..d20df74 100644
--- a/lib/svg/path.dart
+++ b/lib/primitives/path.dart
@@ -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;
diff --git a/lib/point.dart b/lib/primitives/point.dart
similarity index 100%
rename from lib/point.dart
rename to lib/primitives/point.dart
diff --git a/lib/svg_exporter.dart b/lib/svg_exporter.dart
new file mode 100644
index 0000000..e69de29
diff --git a/lib/svg/parser.dart b/lib/svg_parser.dart
similarity index 98%
rename from lib/svg/parser.dart
rename to lib/svg_parser.dart
index c0609a9..743f6f3 100644
--- a/lib/svg/parser.dart
+++ b/lib/svg_parser.dart
@@ -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 {
final T value;
diff --git a/lib/widget.dart b/lib/widget.dart
index 88e1c3d..e4b7761 100644
--- a/lib/widget.dart
+++ b/lib/widget.dart
@@ -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/.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 createState() => _KanimajiState();
+// }
+
+// class _KanimajiState extends State with SingleTickerProviderStateMixin {
+// bool _loaded = false;
+// String? _error;
+// late AnimationController _controller;
+
+// // Converted UI paths and per-path lengths
+// List _paths = [];
+// List _pathLengths = [];
+// // per-stroke durations (in seconds) computed using animator.strokeLengthToDuration
+// List _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 _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 elements inside the SVG. KanjiVG groups strokes in .
+// final pathElements = doc.findAllElements('path', namespace: doc.rootElement.name.namespaceUri).toList();
+
+// if (pathElements.isEmpty) {
+// throw Exception('No elements found in $assetPath');
+// }
+
+// final uiPaths = [];
+// final lengths = [];
+
+// 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 paths;
+// final List pathLengths;
+// final List 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;
+// }
+// }
diff --git a/pubspec.yaml b/pubspec.yaml
index 6d81533..c9116ed 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -15,3 +15,4 @@ dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
+ path: ^1.9.1
diff --git a/test/kanjivg_parser_test.dart b/test/kanjivg_parser_test.dart
new file mode 100644
index 0000000..567e194
--- /dev/null
+++ b/test/kanjivg_parser_test.dart
@@ -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()
+ .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');
+ }
+ });
+ }
+}
diff --git a/test/svg/generation_test.dart b/test/svg/generation_test.dart
index 525d6e0..5762e1e 100644
--- a/test/svg/generation_test.dart
+++ b/test/svg/generation_test.dart
@@ -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', () {
diff --git a/test/svg/parser_test.dart b/test/svg/parser_test.dart
index 7411ff3..3faacce 100644
--- a/test/svg/parser_test.dart
+++ b/test/svg/parser_test.dart
@@ -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", () {
diff --git a/test/svg/path_test.dart b/test/svg/path_test.dart
index 42603c6..d0ff037 100644
--- a/test/svg/path_test.dart
+++ b/test/svg/path_test.dart
@@ -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
diff --git a/test/svg/tokenizer_test.dart b/test/svg/tokenizer_test.dart
index 24f20f2..a0f632b 100644
--- a/test/svg/tokenizer_test.dart
+++ b/test/svg/tokenizer_test.dart
@@ -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 {