From 4c98d488f88d4418bf05f60a203b375cbc70434d Mon Sep 17 00:00:00 2001 From: h7x4 Date: Tue, 1 Feb 2022 01:57:45 +0100 Subject: [PATCH] Misc changes --- README.md | 26 +- .../{animate_kanji.dart => animateKanji.dart} | 8 +- .../{bezier_cubic.dart => bezierCubic.dart} | 0 lib/svg/parser.dart | 229 ++++++++---------- lib/svg/path.dart | 115 +-------- 5 files changed, 125 insertions(+), 253 deletions(-) rename lib/kanimaji/{animate_kanji.dart => animateKanji.dart} (98%) rename lib/kanimaji/{bezier_cubic.dart => bezierCubic.dart} (100%) diff --git a/README.md b/README.md index b44ac25..d51b111 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,18 @@ - - -TODO: Put a short description of the package here that helps potential users -know whether this package might be useful for them. +Add animated kanji strokes to your app! ## Features -TODO: List what your package can do. Maybe include images, gifs, or videos. +This library is a port of [Kanimaji][kanimaji], a library for animating kanji. +It provides a way to convert stroke data from [KanjiVG][kanjivg] into kanji animations. + +This library ports this ability into flutter, and lets you choose speed, colors, and formats, in the form of a `Kanimaji` widget and a SVG/GIF generating function. ## Getting started -TODO: List prerequisites and provide or point to information on how to -start using the package. +Start by adding the project to your pubspec.yaml. ## Usage @@ -37,4 +28,7 @@ const like = 'sample'; The [svg library used](lib/svg) is mostly a rewrite of pythons [svg.path][svg.path]. This is what kanimaji originally used for animation, and even thought there's a lot of svg path parsers in dart, I found none that was able to calculate the length of the path. If you do find one, please let me know! +Also, do note that most of the comments in the project is brought over from the python projects. +I've tried to adjust and remove some of them to make them more useful, but they shouldn't be trusted if there's doubt. + [svg.path]: https://pypi.org/project/svg.path/ \ No newline at end of file diff --git a/lib/kanimaji/animate_kanji.dart b/lib/kanimaji/animateKanji.dart similarity index 98% rename from lib/kanimaji/animate_kanji.dart rename to lib/kanimaji/animateKanji.dart index 398cc3e..b0d9046 100644 --- a/lib/kanimaji/animate_kanji.dart +++ b/lib/kanimaji/animateKanji.dart @@ -1,17 +1,15 @@ -/// ignore_for_file: non_constant_identifier_names, avoid_print, unused_local_variable, dead_code, constant_identifier_names - import 'dart:io'; import 'dart:math' show min, sqrt, pow; import '../svg/parser.dart'; import '../common/Point.dart'; -import 'bezier_cubic.dart' as bezier_cubic; +import 'bezierCubic.dart' as bezier_cubic; import 'package:xml/xml.dart'; import 'package:path/path.dart'; double _computePathLength(String path) => - parse_path(path).size(error: 1e-8).toDouble(); + parsePath(path).size(error: 1e-8).toDouble(); String _shescape(String path) => "'${path.replaceAll(RegExp(r"(?=['\\\\])"), "\\\\")}'"; @@ -682,7 +680,7 @@ void createAnimation({ void main(List args) { // createAnimation('assets/kanjivg/kanji/0f9b1.svg'); - const kanji = '情報科学'; + const kanji = '実例'; final fileList = []; for (int k = 0; k < kanji.length; k++) { createAnimation( diff --git a/lib/kanimaji/bezier_cubic.dart b/lib/kanimaji/bezierCubic.dart similarity index 100% rename from lib/kanimaji/bezier_cubic.dart rename to lib/kanimaji/bezierCubic.dart diff --git a/lib/svg/parser.dart b/lib/svg/parser.dart index 261a466..0120a13 100644 --- a/lib/svg/parser.dart +++ b/lib/svg/parser.dart @@ -1,9 +1,9 @@ /// SVG Path specification parser -/// + import '../common/Point.dart'; import 'path.dart'; -const COMMANDS = { +const _commands = { 'M', 'm', 'Z', @@ -25,16 +25,17 @@ const COMMANDS = { 'A', 'a' }; -const UPPERCASE = {'M', 'Z', 'L', 'H', 'V', 'C', 'S', 'Q', 'T', 'A'}; -final COMMAND_RE = RegExp("(?=[${COMMANDS.join('')}])"); -final FLOAT_RE = RegExp(r"^[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?"); +// const _uppercaseCommands = {'M', 'Z', 'L', 'H', 'V', 'C', 'S', 'Q', 'T', 'A'}; -class ParserResult { +final _commandPattern = RegExp("(?=[${_commands.join('')}])"); +final _floatPattern = RegExp(r"^[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?"); + +class _ParserResult { final T value; final String remaining; - const ParserResult({required this.value, required this.remaining}); + const _ParserResult({required this.value, required this.remaining}); } class InvalidPathError implements Exception { @@ -50,7 +51,7 @@ class InvalidPathError implements Exception { // s: Signed number or coordinate // c: coordinate-pair, which is two coordinates/numbers, separated by whitespace // f: A one character flag, doesn't need whitespace, 1 or 0 -const ARGUMENT_SEQUENCE = { +const _argumentSequence = { "M": "c", "Z": "", "L": "c", @@ -64,82 +65,83 @@ const ARGUMENT_SEQUENCE = { }; /// Strips whitespace and commas -String strip_array(String arg_array) { +String _stripArray(String stringToParse) { // EBNF wsp:(#x20 | #x9 | #xD | #xA) + comma: 0x2C - while (arg_array.isNotEmpty && ' \t\n\r,'.contains(arg_array[0])) { - arg_array = arg_array.substring(1); + while (stringToParse.isNotEmpty && ' \t\n\r,'.contains(stringToParse[0])) { + stringToParse = stringToParse.substring(1); } - return arg_array; + return stringToParse; } -ParserResult pop_number(String arg_array) { - final res = FLOAT_RE.firstMatch(arg_array); +_ParserResult _parseNumber(String stringToParse) { + final res = _floatPattern.firstMatch(stringToParse); if (res == null) { - throw InvalidPathError("Expected a number, got '$arg_array'."); + throw InvalidPathError("Expected a number, got '$stringToParse'."); } final number = double.parse(res.group(0)!); final start = res.start; final end = res.end; - arg_array = arg_array.substring(0, start) + arg_array.substring(end); - arg_array = strip_array(arg_array); + stringToParse = + stringToParse.substring(0, start) + stringToParse.substring(end); + stringToParse = _stripArray(stringToParse); - return ParserResult(value: number, remaining: arg_array); + return _ParserResult(value: number, remaining: stringToParse); } -ParserResult pop_unsigned_number(arg_array) { - final number = pop_number(arg_array); +_ParserResult _parseUnsignedNumber(String stringToParse) { + final number = _parseNumber(stringToParse); if (number.value < 0) { throw InvalidPathError("Expected a non-negative number, got '$number'."); } return number; } -ParserResult pop_coordinate_pair(arg_array) { - final x = pop_number(arg_array); - final y = pop_number(x.remaining); - return ParserResult(value: Point(x.value, y.value), remaining: y.remaining); +_ParserResult _parseCoordinatePair(String stringToParse) { + final x = _parseNumber(stringToParse); + final y = _parseNumber(x.remaining); + return _ParserResult(value: Point(x.value, y.value), remaining: y.remaining); } -ParserResult pop_flag(String arg_array) { - final flag = arg_array[0]; - arg_array = arg_array.substring(1); - arg_array = strip_array(arg_array); - if (flag == '0') return ParserResult(value: false, remaining: arg_array); - if (flag == '1') return ParserResult(value: true, remaining: arg_array); +_ParserResult _parseflag(String stringToParse) { + final flag = stringToParse[0]; + stringToParse = stringToParse.substring(1); + stringToParse = _stripArray(stringToParse); + if (flag == '0') return _ParserResult(value: false, remaining: stringToParse); + if (flag == '1') return _ParserResult(value: true, remaining: stringToParse); throw InvalidPathError("Expected either 1 or 0, got '$flag'"); } -const FIELD_POPPERS = { - "u": pop_unsigned_number, - "s": pop_number, - "c": pop_coordinate_pair, - "f": pop_flag, +const fieldParsers = { + "u": _parseUnsignedNumber, + "s": _parseNumber, + "c": _parseCoordinatePair, + "f": _parseflag, }; -class Command { +class _Command { final String command; final String args; - const Command({required this.command, required this.args}); + const _Command({required this.command, required this.args}); @override String toString() => 'Command: $command $args'; } // Splits path into commands and arguments -List _commandify_path(String pathdef) { - List tokens = []; +List<_Command> _commandifyPath(String pathdef) { + List<_Command> tokens = []; List token = []; - for (String c in pathdef.split(COMMAND_RE)) { + for (String c in pathdef.split(_commandPattern)) { String x = c[0]; String? y = (c.length > 1) ? c.substring(1).trim() : null; - if (!COMMANDS.contains(x)) { + if (!_commands.contains(x)) { throw InvalidPathError("Path does not start with a command: $pathdef"); } if (token.isNotEmpty) { - tokens.add(Command(command: token[0], args: token[1])); + tokens.add(_Command(command: token[0], args: token[1])); // yield token; } if (x == "z" || x == "Z") { @@ -154,7 +156,7 @@ List _commandify_path(String pathdef) { token.add(y); } } - tokens.add(Command(command: token[0], args: token[1])); + tokens.add(_Command(command: token[0], args: token[1])); // yield token; return tokens; } @@ -169,10 +171,9 @@ class Token { String toString() => 'Token: $command ($args)'; } -List _tokenize_path(String pathdef) { +List _tokenizePath(String pathdef) { List tokens = []; - for (final token in _commandify_path(pathdef)) { - // _commandify_path(pathdef).forEach((List token) { + for (final token in _commandifyPath(pathdef)) { String command = token.command; String args = token.args; @@ -184,22 +185,21 @@ List _tokenize_path(String pathdef) { // For the rest of the commands, we parse the arguments and // yield one command per full set of arguments - final String arg_sequence = ARGUMENT_SEQUENCE[command.toUpperCase()]!; + final String stringToParse = _argumentSequence[command.toUpperCase()]!; String arguments = args; while (arguments.isNotEmpty) { - final List command_arguments = []; - for (final arg in arg_sequence.split('')) { + final List commandArguments = []; + for (final arg in stringToParse.split('')) { try { - final result = FIELD_POPPERS[arg]!.call(arguments); + final result = fieldParsers[arg]!.call(arguments); arguments = result.remaining; - command_arguments.add(result.value); + commandArguments.add(result.value); } on InvalidPathError { throw InvalidPathError("Invalid path element $command $args"); } } - tokens.add(Token(command: command, args: command_arguments)); - // yield (command,) + tuple(command_arguments) + tokens.add(Token(command: command, args: commandArguments)); // Implicit Moveto commands should be treated as Lineto commands. if (command == "m") { @@ -212,71 +212,71 @@ List _tokenize_path(String pathdef) { return tokens; } -Path parse_path(String pathdef) { +Path parsePath(String pathdef) { final segments = Path(); - Point? start_pos; - String? last_command; - Point current_pos = Point.zero; + Point? startPos; + String? lastCommand; + Point currentPos = Point.zero; - for (final token in _tokenize_path(pathdef)) { + for (final token in _tokenizePath(pathdef)) { final command = token.command.toUpperCase(); final absolute = token.command.toUpperCase() == token.command; if (command == "M") { final pos = token.args[0] as Point; if (absolute) { - current_pos = pos; + currentPos = pos; } else { - current_pos += pos; + currentPos += pos; } - segments.add(Move(to: current_pos)); - start_pos = current_pos; + segments.add(Move(to: currentPos)); + startPos = currentPos; } else if (command == "Z") { // TODO Throw error if not available: - segments.add(Close(start: current_pos, end: start_pos!)); - current_pos = start_pos; + segments.add(Close(start: currentPos, end: startPos!)); + currentPos = startPos; } else if (command == "L") { Point pos = token.args[0] as Point; if (!absolute) { - pos += current_pos; + pos += currentPos; } - segments.add(Line(start: current_pos, end: pos)); - current_pos = pos; + segments.add(Line(start: currentPos, end: pos)); + currentPos = pos; } else if (command == "H") { double hpos = token.args[0] as double; if (!absolute) { - hpos += current_pos.x; + hpos += currentPos.x; } - final pos = Point(hpos, current_pos.y); - segments.add(Line(start: current_pos, end: pos)); - current_pos = pos; + final pos = Point(hpos, currentPos.y); + segments.add(Line(start: currentPos, end: pos)); + currentPos = pos; } else if (command == "V") { double vpos = token.args[0] as double; if (!absolute) { - vpos += current_pos.y; + vpos += currentPos.y; } - final pos = Point(current_pos.x, vpos); - segments.add(Line(start: current_pos, end: pos)); - current_pos = pos; + final pos = Point(currentPos.x, vpos); + segments.add(Line(start: currentPos, end: pos)); + currentPos = pos; } else if (command == "C") { Point control1 = token.args[0] as Point; Point control2 = token.args[1] as Point; Point end = token.args[2] as Point; if (!absolute) { - control1 += current_pos; - control2 += current_pos; - end += current_pos; + control1 += currentPos; + control2 += currentPos; + end += currentPos; } segments.add( CubicBezier( - start: current_pos, + start: currentPos, control1: control1, control2: control2, end: end, ), ); - current_pos = end; + currentPos = end; } else if (command == "S") { // Smooth curve. First control point is the "reflection" of // the second control point in the previous path. @@ -284,73 +284,73 @@ Path parse_path(String pathdef) { Point end = token.args[1] as Point; if (!absolute) { - control2 += current_pos; - end += current_pos; + control2 += currentPos; + end += currentPos; } late final Point control1; - if (last_command == 'C' || last_command == 'S') { + if (lastCommand == 'C' || lastCommand == 'S') { // The first control point is assumed to be the reflection of // the second control point on the previous command relative // to the current point. control1 = - current_pos + current_pos - (segments.last as CubicBezier).control2; + currentPos + currentPos - (segments.last as CubicBezier).control2; } else { // If there is no previous command or if the previous command // was not an C, c, S or s, assume the first control point is // coincident with the current point. - control1 = current_pos; + control1 = currentPos; } segments.add( CubicBezier( - start: current_pos, + start: currentPos, control1: control1, control2: control2, end: end), ); - current_pos = end; + currentPos = end; } else if (command == "Q") { Point control = token.args[0] as Point; Point end = token.args[1] as Point; if (!absolute) { - control += current_pos; - end += current_pos; + control += currentPos; + end += currentPos; } segments.add( - QuadraticBezier(start: current_pos, control: control, end: end), + QuadraticBezier(start: currentPos, control: control, end: end), ); - current_pos = end; + currentPos = end; } else if (command == "T") { // Smooth curve. Control point is the "reflection" of // the second control point in the previous path. Point end = token.args[0] as Point; if (!absolute) { - end += current_pos; + end += currentPos; } late final Point control; - if (last_command == "Q" || last_command == 'T') { + if (lastCommand == "Q" || lastCommand == 'T') { // The control point is assumed to be the reflection of // the control point on the previous command relative // to the current point. - control = current_pos + - current_pos - + control = currentPos + + currentPos - (segments.last as QuadraticBezier).control; } else { // If there is no previous command or if the previous command // was not an Q, q, T or t, assume the first control point is // coincident with the current point. - control = current_pos; + control = currentPos; } segments.add( - QuadraticBezier(start: current_pos, control: control, end: end), + QuadraticBezier(start: currentPos, control: control, end: end), ); - current_pos = end; + currentPos = end; } else if (command == "A") { // For some reason I implemented the Arc with a complex radius. // That doesn't really make much sense, but... *shrugs* @@ -361,12 +361,12 @@ Path parse_path(String pathdef) { Point end = token.args[5] as Point; if (!absolute) { - end += current_pos; + end += currentPos; } segments.add( Arc( - start: current_pos, + start: currentPos, radius: radius, rotation: rotation, arc: arc, @@ -374,37 +374,12 @@ Path parse_path(String pathdef) { end: end, ), ); - current_pos = end; + currentPos = end; } // Finish up the loop in preparation for next command - last_command = command; + lastCommand = command; } return segments; } - -void main(List args) { - // print(_commandify_path('M 10 10 C 20 20, 40 20, 50 10')); -// print(_tokenize_path('M 10 10 C 20 20, 40 20, 50 10')); -// print(_tokenize_path('M 10 80 Q 52.5 10, 95 80 T 180 80')); -// print(_tokenize_path(""" -// M 10 315 -// L 110 215 -// A 30 50 0 0 1 162.55 162.45 -// L 172.55 152.45 -// A 30 50 -45 0 1 215.1 109.9 -// L 315 10 -// """)); - - print(parse_path('M 10 10 C 20 20, 40 20, 50 10')); - print(parse_path('M 10 80 Q 52.5 10, 95 80 T 180 80')); - print(parse_path(""" -M 10 315 - L 110 215 - A 30 50 0 0 1 162.55 162.45 - L 172.55 152.45 - A 30 50 -45 0 1 215.1 109.9 - L 315 10 - """)); -} \ No newline at end of file diff --git a/lib/svg/path.dart b/lib/svg/path.dart index 9024d6a..86b7a08 100644 --- a/lib/svg/path.dart +++ b/lib/svg/path.dart @@ -1,3 +1,6 @@ +/// This file contains classes for the different types of SVG path segments as +/// well as a Path object that contains a sequence of path segments. + import 'dart:collection'; import 'dart:math' as math; import 'dart:math' show sqrt, sin, cos, acos, log, pi; @@ -6,18 +9,9 @@ import 'package:bisection/extension.dart'; import '../common/Point.dart'; -// try: -// from collections.abc import MutableSequence -// except ImportError: -// from collections import MutableSequence - -// This file contains classes for the different types of SVG path segments as -// well as a Path object that contains a sequence of path segments. - double radians(num n) => n * pi / 180; double degrees(num n) => n * 180 / pi; - const defaultMinDepth = 5; const defaultError = 1e-12; @@ -101,11 +95,6 @@ class Linear extends SvgPath { required Point end, }) : super(start: start, end: end); - // def __ne__(self, other): - // if not isinstance(other, Line): - // return NotImplemented - // return not self == other - @override Point point(num pos) => start + (end - start).times(pos); @@ -126,8 +115,6 @@ class Line extends Linear { String toString() { return "Line(start=$start, end=$end)"; } - // @override - // operator ==(covariant Line other) => start == other.start && end == other.end; } class CubicBezier extends Bezier { @@ -145,18 +132,6 @@ class CubicBezier extends Bezier { String toString() => "CubicBezier(start=$start, control1=$control1, " "control2=$control2, end=$end)"; - // @override - // operator ==(covariant CubicBezier other) => - // start == other.start && - // and end == other.end && - // and control1 == other.control1 && - // and control2 == other.control2; - - // def __ne__(self, other): - // if not isinstance(other, CubicBezier): - // return NotImplemented - // return not self == other - @override bool isSmoothFrom(Object? previous) => previous is CubicBezier ? start == previous.end && @@ -202,20 +177,6 @@ class QuadraticBezier extends Bezier { String toString() => "QuadraticBezier(start=$start, control=$control, end=$end)"; - // def __eq__(self, other): - // if not isinstance(other, QuadraticBezier): - // return NotImplemented - // return ( - // self.start == other.start - // and self.end == other.end - // and self.control == other.control - // ) - - // def __ne__(self, other): - // if not isinstance(other, QuadraticBezier): - // return NotImplemented - // return not self == other - @override bool isSmoothFrom(Object? previous) => previous is QuadraticBezier ? start == previous.end && @@ -286,30 +247,12 @@ class Arc extends SvgPath { } @override - String toString() => "Arc(start=$start, radius=$radius, rotation=$rotation, " - "arc=$arc, sweep=$sweep, end=$end)"; - - // def __eq__(self, other): - // if not isinstance(other, Arc): - // return NotImplemented - // return ( - // self.start == other.start - // and self.end == other.end - // and self.radius == other.radius - // and self.rotation == other.rotation - // and self.arc == other.arc - // and self.sweep == other.sweep - // ) - - // def __ne__(self, other): - // if not isinstance(other, Arc): - // return NotImplemented - // return not self == other + String toString() => 'Arc(start=$start, radius=$radius, rotation=$rotation, ' + 'arc=$arc, sweep=$sweep, end=$end)'; + /// Conversion from endpoint to center parameterization + /// http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes void _parameterize() { - // Conversion from endpoint to center parameterization - // http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes - // This is equivalent of omitting the segment, so do nothing if (start == end) return; @@ -439,22 +382,13 @@ class Arc extends SvgPath { } } -// Represents move commands. Does nothing, but is there to handle -// paths that consist of only move commands, which is valid, but pointless. +/// Represents move commands. Does nothing, but is there to handle +/// paths that consist of only move commands, which is valid, but pointless. class Move extends SvgPath { const Move({required Point to}) : super(start: to, end: to); @override String toString() => "Move(to=$start)"; -// def __eq__(self, other): -// if not isinstance(other, Move): -// return NotImplemented -// return self.start == other.start - -// def __ne__(self, other): -// if not isinstance(other, Move): -// return NotImplemented -// return not self == other @override Point point(num pos) => start; @@ -464,7 +398,7 @@ class Move extends SvgPath { 0; } -// Represents the closepath command +/// Represents the closepath command class Close extends Linear { const Close({ required Point start, @@ -616,33 +550,4 @@ class Path extends ListBase { return parts.join(" "); } - -// def __delitem__(self, index): -// del self._segments[index] -// self._length = None - -// def reverse(self): -// # Reversing the order of a path would require reversing each element -// # as well. That's not implemented. -// raise NotImplementedError - -// def __len__(self): -// return len(self._segments) - -// def __eq__(self, other): - -// if not isinstance(other, Path): -// return NotImplemented -// if len(self) != len(other): -// return False -// for s, o in zip(self._segments, other._segments): -// if not s == o: -// return False -// return True - -// def __ne__(self, other): -// if not isinstance(other, Path): -// return NotImplemented -// return not self == other - }