Misc changes

This commit is contained in:
Oystein Kristoffer Tveit 2022-02-01 01:57:45 +01:00
parent 4151df40b5
commit aaad8b1db6
5 changed files with 125 additions and 253 deletions

@ -1,27 +1,18 @@
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
# Kanimaji
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/developing-packages).
-->
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/

@ -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<String> args) {
// createAnimation('assets/kanjivg/kanji/0f9b1.svg');
const kanji = '情報科学';
const kanji = '実例';
final fileList = [];
for (int k = 0; k < kanji.length; k++) {
createAnimation(

@ -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<T> {
final _commandPattern = RegExp("(?=[${_commands.join('')}])");
final _floatPattern = RegExp(r"^[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?");
class _ParserResult<T> {
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<double> pop_number(String arg_array) {
final res = FLOAT_RE.firstMatch(arg_array);
_ParserResult<double> _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<double> pop_unsigned_number(arg_array) {
final number = pop_number(arg_array);
_ParserResult<double> _parseUnsignedNumber(String stringToParse) {
final number = _parseNumber(stringToParse);
if (number.value < 0) {
throw InvalidPathError("Expected a non-negative number, got '$number'.");
}
return number;
}
ParserResult<Point> 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<Point> _parseCoordinatePair(String stringToParse) {
final x = _parseNumber(stringToParse);
final y = _parseNumber(x.remaining);
return _ParserResult(value: Point(x.value, y.value), remaining: y.remaining);
}
ParserResult<bool> 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<bool> _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<Command> _commandify_path(String pathdef) {
List<Command> tokens = [];
List<_Command> _commandifyPath(String pathdef) {
List<_Command> tokens = [];
List<String> 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<Command> _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<Token> _tokenize_path(String pathdef) {
List<Token> _tokenizePath(String pathdef) {
List<Token> tokens = [];
for (final token in _commandify_path(pathdef)) {
// _commandify_path(pathdef).forEach((List<String> token) {
for (final token in _commandifyPath(pathdef)) {
String command = token.command;
String args = token.args;
@ -184,22 +185,21 @@ List<Token> _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<Object> command_arguments = [];
for (final arg in arg_sequence.split('')) {
final List<Object> 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<Token> _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<String> 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
"""));
}

@ -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<SvgPath> {
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
}