Misc changes
This commit is contained in:
26
README.md
26
README.md
@@ -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
|
# Kanimaji
|
||||||
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
|
|
||||||
|
|
||||||
For general information about developing packages, see the Dart guide for
|
Add animated kanji strokes to your app!
|
||||||
[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.
|
|
||||||
|
|
||||||
## Features
|
## 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
|
## Getting started
|
||||||
|
|
||||||
TODO: List prerequisites and provide or point to information on how to
|
Start by adding the project to your pubspec.yaml.
|
||||||
start using the package.
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -37,4 +28,7 @@ const like = 'sample';
|
|||||||
The [svg library used](lib/svg) is mostly a rewrite of pythons [svg.path][svg.path].
|
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!
|
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/
|
[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:io';
|
||||||
import 'dart:math' show min, sqrt, pow;
|
import 'dart:math' show min, sqrt, pow;
|
||||||
|
|
||||||
import '../svg/parser.dart';
|
import '../svg/parser.dart';
|
||||||
import '../common/Point.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:xml/xml.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
double _computePathLength(String path) =>
|
double _computePathLength(String path) =>
|
||||||
parse_path(path).size(error: 1e-8).toDouble();
|
parsePath(path).size(error: 1e-8).toDouble();
|
||||||
|
|
||||||
String _shescape(String path) =>
|
String _shescape(String path) =>
|
||||||
"'${path.replaceAll(RegExp(r"(?=['\\\\])"), "\\\\")}'";
|
"'${path.replaceAll(RegExp(r"(?=['\\\\])"), "\\\\")}'";
|
||||||
@@ -682,7 +680,7 @@ void createAnimation({
|
|||||||
void main(List<String> args) {
|
void main(List<String> args) {
|
||||||
// createAnimation('assets/kanjivg/kanji/0f9b1.svg');
|
// createAnimation('assets/kanjivg/kanji/0f9b1.svg');
|
||||||
|
|
||||||
const kanji = '情報科学';
|
const kanji = '実例';
|
||||||
final fileList = [];
|
final fileList = [];
|
||||||
for (int k = 0; k < kanji.length; k++) {
|
for (int k = 0; k < kanji.length; k++) {
|
||||||
createAnimation(
|
createAnimation(
|
@@ -1,9 +1,9 @@
|
|||||||
/// SVG Path specification parser
|
/// SVG Path specification parser
|
||||||
///
|
|
||||||
import '../common/Point.dart';
|
import '../common/Point.dart';
|
||||||
import 'path.dart';
|
import 'path.dart';
|
||||||
|
|
||||||
const COMMANDS = {
|
const _commands = {
|
||||||
'M',
|
'M',
|
||||||
'm',
|
'm',
|
||||||
'Z',
|
'Z',
|
||||||
@@ -25,16 +25,17 @@ const COMMANDS = {
|
|||||||
'A',
|
'A',
|
||||||
'a'
|
'a'
|
||||||
};
|
};
|
||||||
const UPPERCASE = {'M', 'Z', 'L', 'H', 'V', 'C', 'S', 'Q', 'T', 'A'};
|
|
||||||
|
|
||||||
final COMMAND_RE = RegExp("(?=[${COMMANDS.join('')}])");
|
// const _uppercaseCommands = {'M', 'Z', 'L', 'H', 'V', 'C', 'S', 'Q', 'T', 'A'};
|
||||||
final FLOAT_RE = RegExp(r"^[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?");
|
|
||||||
|
|
||||||
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 T value;
|
||||||
final String remaining;
|
final String remaining;
|
||||||
|
|
||||||
const ParserResult({required this.value, required this.remaining});
|
const _ParserResult({required this.value, required this.remaining});
|
||||||
}
|
}
|
||||||
|
|
||||||
class InvalidPathError implements Exception {
|
class InvalidPathError implements Exception {
|
||||||
@@ -50,7 +51,7 @@ class InvalidPathError implements Exception {
|
|||||||
// s: Signed number or coordinate
|
// s: Signed number or coordinate
|
||||||
// c: coordinate-pair, which is two coordinates/numbers, separated by whitespace
|
// c: coordinate-pair, which is two coordinates/numbers, separated by whitespace
|
||||||
// f: A one character flag, doesn't need whitespace, 1 or 0
|
// f: A one character flag, doesn't need whitespace, 1 or 0
|
||||||
const ARGUMENT_SEQUENCE = {
|
const _argumentSequence = {
|
||||||
"M": "c",
|
"M": "c",
|
||||||
"Z": "",
|
"Z": "",
|
||||||
"L": "c",
|
"L": "c",
|
||||||
@@ -64,82 +65,83 @@ const ARGUMENT_SEQUENCE = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Strips whitespace and commas
|
/// Strips whitespace and commas
|
||||||
String strip_array(String arg_array) {
|
String _stripArray(String stringToParse) {
|
||||||
// EBNF wsp:(#x20 | #x9 | #xD | #xA) + comma: 0x2C
|
// EBNF wsp:(#x20 | #x9 | #xD | #xA) + comma: 0x2C
|
||||||
while (arg_array.isNotEmpty && ' \t\n\r,'.contains(arg_array[0])) {
|
while (stringToParse.isNotEmpty && ' \t\n\r,'.contains(stringToParse[0])) {
|
||||||
arg_array = arg_array.substring(1);
|
stringToParse = stringToParse.substring(1);
|
||||||
}
|
}
|
||||||
return arg_array;
|
return stringToParse;
|
||||||
}
|
}
|
||||||
|
|
||||||
ParserResult<double> pop_number(String arg_array) {
|
_ParserResult<double> _parseNumber(String stringToParse) {
|
||||||
final res = FLOAT_RE.firstMatch(arg_array);
|
final res = _floatPattern.firstMatch(stringToParse);
|
||||||
if (res == null) {
|
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 number = double.parse(res.group(0)!);
|
||||||
final start = res.start;
|
final start = res.start;
|
||||||
final end = res.end;
|
final end = res.end;
|
||||||
arg_array = arg_array.substring(0, start) + arg_array.substring(end);
|
stringToParse =
|
||||||
arg_array = strip_array(arg_array);
|
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) {
|
_ParserResult<double> _parseUnsignedNumber(String stringToParse) {
|
||||||
final number = pop_number(arg_array);
|
final number = _parseNumber(stringToParse);
|
||||||
if (number.value < 0) {
|
if (number.value < 0) {
|
||||||
throw InvalidPathError("Expected a non-negative number, got '$number'.");
|
throw InvalidPathError("Expected a non-negative number, got '$number'.");
|
||||||
}
|
}
|
||||||
return number;
|
return number;
|
||||||
}
|
}
|
||||||
|
|
||||||
ParserResult<Point> pop_coordinate_pair(arg_array) {
|
_ParserResult<Point> _parseCoordinatePair(String stringToParse) {
|
||||||
final x = pop_number(arg_array);
|
final x = _parseNumber(stringToParse);
|
||||||
final y = pop_number(x.remaining);
|
final y = _parseNumber(x.remaining);
|
||||||
return ParserResult(value: Point(x.value, y.value), remaining: y.remaining);
|
return _ParserResult(value: Point(x.value, y.value), remaining: y.remaining);
|
||||||
}
|
}
|
||||||
|
|
||||||
ParserResult<bool> pop_flag(String arg_array) {
|
_ParserResult<bool> _parseflag(String stringToParse) {
|
||||||
final flag = arg_array[0];
|
final flag = stringToParse[0];
|
||||||
arg_array = arg_array.substring(1);
|
stringToParse = stringToParse.substring(1);
|
||||||
arg_array = strip_array(arg_array);
|
stringToParse = _stripArray(stringToParse);
|
||||||
if (flag == '0') return ParserResult(value: false, remaining: arg_array);
|
if (flag == '0') return _ParserResult(value: false, remaining: stringToParse);
|
||||||
if (flag == '1') return ParserResult(value: true, remaining: arg_array);
|
if (flag == '1') return _ParserResult(value: true, remaining: stringToParse);
|
||||||
|
|
||||||
throw InvalidPathError("Expected either 1 or 0, got '$flag'");
|
throw InvalidPathError("Expected either 1 or 0, got '$flag'");
|
||||||
}
|
}
|
||||||
|
|
||||||
const FIELD_POPPERS = {
|
const fieldParsers = {
|
||||||
"u": pop_unsigned_number,
|
"u": _parseUnsignedNumber,
|
||||||
"s": pop_number,
|
"s": _parseNumber,
|
||||||
"c": pop_coordinate_pair,
|
"c": _parseCoordinatePair,
|
||||||
"f": pop_flag,
|
"f": _parseflag,
|
||||||
};
|
};
|
||||||
|
|
||||||
class Command {
|
class _Command {
|
||||||
final String command;
|
final String command;
|
||||||
final String args;
|
final String args;
|
||||||
|
|
||||||
const Command({required this.command, required this.args});
|
const _Command({required this.command, required this.args});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'Command: $command $args';
|
String toString() => 'Command: $command $args';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Splits path into commands and arguments
|
// Splits path into commands and arguments
|
||||||
List<Command> _commandify_path(String pathdef) {
|
List<_Command> _commandifyPath(String pathdef) {
|
||||||
List<Command> tokens = [];
|
List<_Command> tokens = [];
|
||||||
List<String> token = [];
|
List<String> token = [];
|
||||||
for (String c in pathdef.split(COMMAND_RE)) {
|
for (String c in pathdef.split(_commandPattern)) {
|
||||||
String x = c[0];
|
String x = c[0];
|
||||||
String? y = (c.length > 1) ? c.substring(1).trim() : null;
|
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");
|
throw InvalidPathError("Path does not start with a command: $pathdef");
|
||||||
}
|
}
|
||||||
if (token.isNotEmpty) {
|
if (token.isNotEmpty) {
|
||||||
tokens.add(Command(command: token[0], args: token[1]));
|
tokens.add(_Command(command: token[0], args: token[1]));
|
||||||
// yield token;
|
// yield token;
|
||||||
}
|
}
|
||||||
if (x == "z" || x == "Z") {
|
if (x == "z" || x == "Z") {
|
||||||
@@ -154,7 +156,7 @@ List<Command> _commandify_path(String pathdef) {
|
|||||||
token.add(y);
|
token.add(y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tokens.add(Command(command: token[0], args: token[1]));
|
tokens.add(_Command(command: token[0], args: token[1]));
|
||||||
// yield token;
|
// yield token;
|
||||||
return tokens;
|
return tokens;
|
||||||
}
|
}
|
||||||
@@ -169,10 +171,9 @@ class Token {
|
|||||||
String toString() => 'Token: $command ($args)';
|
String toString() => 'Token: $command ($args)';
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Token> _tokenize_path(String pathdef) {
|
List<Token> _tokenizePath(String pathdef) {
|
||||||
List<Token> tokens = [];
|
List<Token> tokens = [];
|
||||||
for (final token in _commandify_path(pathdef)) {
|
for (final token in _commandifyPath(pathdef)) {
|
||||||
// _commandify_path(pathdef).forEach((List<String> token) {
|
|
||||||
String command = token.command;
|
String command = token.command;
|
||||||
String args = token.args;
|
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
|
// For the rest of the commands, we parse the arguments and
|
||||||
// yield one command per full set of arguments
|
// 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;
|
String arguments = args;
|
||||||
while (arguments.isNotEmpty) {
|
while (arguments.isNotEmpty) {
|
||||||
final List<Object> command_arguments = [];
|
final List<Object> commandArguments = [];
|
||||||
for (final arg in arg_sequence.split('')) {
|
for (final arg in stringToParse.split('')) {
|
||||||
try {
|
try {
|
||||||
final result = FIELD_POPPERS[arg]!.call(arguments);
|
final result = fieldParsers[arg]!.call(arguments);
|
||||||
arguments = result.remaining;
|
arguments = result.remaining;
|
||||||
command_arguments.add(result.value);
|
commandArguments.add(result.value);
|
||||||
} on InvalidPathError {
|
} on InvalidPathError {
|
||||||
throw InvalidPathError("Invalid path element $command $args");
|
throw InvalidPathError("Invalid path element $command $args");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens.add(Token(command: command, args: command_arguments));
|
tokens.add(Token(command: command, args: commandArguments));
|
||||||
// yield (command,) + tuple(command_arguments)
|
|
||||||
|
|
||||||
// Implicit Moveto commands should be treated as Lineto commands.
|
// Implicit Moveto commands should be treated as Lineto commands.
|
||||||
if (command == "m") {
|
if (command == "m") {
|
||||||
@@ -212,71 +212,71 @@ List<Token> _tokenize_path(String pathdef) {
|
|||||||
return tokens;
|
return tokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
Path parse_path(String pathdef) {
|
Path parsePath(String pathdef) {
|
||||||
final segments = Path();
|
final segments = Path();
|
||||||
Point? start_pos;
|
Point? startPos;
|
||||||
String? last_command;
|
String? lastCommand;
|
||||||
Point current_pos = Point.zero;
|
Point currentPos = Point.zero;
|
||||||
|
|
||||||
for (final token in _tokenize_path(pathdef)) {
|
for (final token in _tokenizePath(pathdef)) {
|
||||||
final command = token.command.toUpperCase();
|
final command = token.command.toUpperCase();
|
||||||
final absolute = token.command.toUpperCase() == token.command;
|
final absolute = token.command.toUpperCase() == token.command;
|
||||||
if (command == "M") {
|
if (command == "M") {
|
||||||
final pos = token.args[0] as Point;
|
final pos = token.args[0] as Point;
|
||||||
if (absolute) {
|
if (absolute) {
|
||||||
current_pos = pos;
|
currentPos = pos;
|
||||||
} else {
|
} else {
|
||||||
current_pos += pos;
|
currentPos += pos;
|
||||||
}
|
}
|
||||||
segments.add(Move(to: current_pos));
|
segments.add(Move(to: currentPos));
|
||||||
start_pos = current_pos;
|
startPos = currentPos;
|
||||||
} else if (command == "Z") {
|
} else if (command == "Z") {
|
||||||
// TODO Throw error if not available:
|
// TODO Throw error if not available:
|
||||||
segments.add(Close(start: current_pos, end: start_pos!));
|
segments.add(Close(start: currentPos, end: startPos!));
|
||||||
current_pos = start_pos;
|
currentPos = startPos;
|
||||||
} else if (command == "L") {
|
} else if (command == "L") {
|
||||||
Point pos = token.args[0] as Point;
|
Point pos = token.args[0] as Point;
|
||||||
if (!absolute) {
|
if (!absolute) {
|
||||||
pos += current_pos;
|
pos += currentPos;
|
||||||
}
|
}
|
||||||
segments.add(Line(start: current_pos, end: pos));
|
segments.add(Line(start: currentPos, end: pos));
|
||||||
current_pos = pos;
|
currentPos = pos;
|
||||||
} else if (command == "H") {
|
} else if (command == "H") {
|
||||||
double hpos = token.args[0] as double;
|
double hpos = token.args[0] as double;
|
||||||
if (!absolute) {
|
if (!absolute) {
|
||||||
hpos += current_pos.x;
|
hpos += currentPos.x;
|
||||||
}
|
}
|
||||||
final pos = Point(hpos, current_pos.y);
|
final pos = Point(hpos, currentPos.y);
|
||||||
segments.add(Line(start: current_pos, end: pos));
|
segments.add(Line(start: currentPos, end: pos));
|
||||||
current_pos = pos;
|
currentPos = pos;
|
||||||
} else if (command == "V") {
|
} else if (command == "V") {
|
||||||
double vpos = token.args[0] as double;
|
double vpos = token.args[0] as double;
|
||||||
if (!absolute) {
|
if (!absolute) {
|
||||||
vpos += current_pos.y;
|
vpos += currentPos.y;
|
||||||
}
|
}
|
||||||
final pos = Point(current_pos.x, vpos);
|
final pos = Point(currentPos.x, vpos);
|
||||||
segments.add(Line(start: current_pos, end: pos));
|
segments.add(Line(start: currentPos, end: pos));
|
||||||
current_pos = pos;
|
currentPos = pos;
|
||||||
} else if (command == "C") {
|
} else if (command == "C") {
|
||||||
Point control1 = token.args[0] as Point;
|
Point control1 = token.args[0] as Point;
|
||||||
Point control2 = token.args[1] as Point;
|
Point control2 = token.args[1] as Point;
|
||||||
Point end = token.args[2] as Point;
|
Point end = token.args[2] as Point;
|
||||||
|
|
||||||
if (!absolute) {
|
if (!absolute) {
|
||||||
control1 += current_pos;
|
control1 += currentPos;
|
||||||
control2 += current_pos;
|
control2 += currentPos;
|
||||||
end += current_pos;
|
end += currentPos;
|
||||||
}
|
}
|
||||||
|
|
||||||
segments.add(
|
segments.add(
|
||||||
CubicBezier(
|
CubicBezier(
|
||||||
start: current_pos,
|
start: currentPos,
|
||||||
control1: control1,
|
control1: control1,
|
||||||
control2: control2,
|
control2: control2,
|
||||||
end: end,
|
end: end,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
current_pos = end;
|
currentPos = end;
|
||||||
} else if (command == "S") {
|
} else if (command == "S") {
|
||||||
// Smooth curve. First control point is the "reflection" of
|
// Smooth curve. First control point is the "reflection" of
|
||||||
// the second control point in the previous path.
|
// the second control point in the previous path.
|
||||||
@@ -284,73 +284,73 @@ Path parse_path(String pathdef) {
|
|||||||
Point end = token.args[1] as Point;
|
Point end = token.args[1] as Point;
|
||||||
|
|
||||||
if (!absolute) {
|
if (!absolute) {
|
||||||
control2 += current_pos;
|
control2 += currentPos;
|
||||||
end += current_pos;
|
end += currentPos;
|
||||||
}
|
}
|
||||||
|
|
||||||
late final Point control1;
|
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 first control point is assumed to be the reflection of
|
||||||
// the second control point on the previous command relative
|
// the second control point on the previous command relative
|
||||||
// to the current point.
|
// to the current point.
|
||||||
control1 =
|
control1 =
|
||||||
current_pos + current_pos - (segments.last as CubicBezier).control2;
|
currentPos + currentPos - (segments.last as CubicBezier).control2;
|
||||||
} else {
|
} else {
|
||||||
// If there is no previous command or if the previous command
|
// 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
|
// was not an C, c, S or s, assume the first control point is
|
||||||
// coincident with the current point.
|
// coincident with the current point.
|
||||||
control1 = current_pos;
|
control1 = currentPos;
|
||||||
}
|
}
|
||||||
segments.add(
|
segments.add(
|
||||||
CubicBezier(
|
CubicBezier(
|
||||||
start: current_pos,
|
start: currentPos,
|
||||||
control1: control1,
|
control1: control1,
|
||||||
control2: control2,
|
control2: control2,
|
||||||
end: end),
|
end: end),
|
||||||
);
|
);
|
||||||
current_pos = end;
|
currentPos = end;
|
||||||
} else if (command == "Q") {
|
} else if (command == "Q") {
|
||||||
Point control = token.args[0] as Point;
|
Point control = token.args[0] as Point;
|
||||||
Point end = token.args[1] as Point;
|
Point end = token.args[1] as Point;
|
||||||
|
|
||||||
if (!absolute) {
|
if (!absolute) {
|
||||||
control += current_pos;
|
control += currentPos;
|
||||||
end += current_pos;
|
end += currentPos;
|
||||||
}
|
}
|
||||||
|
|
||||||
segments.add(
|
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") {
|
} else if (command == "T") {
|
||||||
// Smooth curve. Control point is the "reflection" of
|
// Smooth curve. Control point is the "reflection" of
|
||||||
// the second control point in the previous path.
|
// the second control point in the previous path.
|
||||||
Point end = token.args[0] as Point;
|
Point end = token.args[0] as Point;
|
||||||
|
|
||||||
if (!absolute) {
|
if (!absolute) {
|
||||||
end += current_pos;
|
end += currentPos;
|
||||||
}
|
}
|
||||||
|
|
||||||
late final Point control;
|
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 is assumed to be the reflection of
|
||||||
// the control point on the previous command relative
|
// the control point on the previous command relative
|
||||||
// to the current point.
|
// to the current point.
|
||||||
control = current_pos +
|
control = currentPos +
|
||||||
current_pos -
|
currentPos -
|
||||||
(segments.last as QuadraticBezier).control;
|
(segments.last as QuadraticBezier).control;
|
||||||
} else {
|
} else {
|
||||||
// If there is no previous command or if the previous command
|
// 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
|
// was not an Q, q, T or t, assume the first control point is
|
||||||
// coincident with the current point.
|
// coincident with the current point.
|
||||||
control = current_pos;
|
control = currentPos;
|
||||||
}
|
}
|
||||||
|
|
||||||
segments.add(
|
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") {
|
} else if (command == "A") {
|
||||||
// For some reason I implemented the Arc with a complex radius.
|
// For some reason I implemented the Arc with a complex radius.
|
||||||
// That doesn't really make much sense, but... *shrugs*
|
// 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;
|
Point end = token.args[5] as Point;
|
||||||
|
|
||||||
if (!absolute) {
|
if (!absolute) {
|
||||||
end += current_pos;
|
end += currentPos;
|
||||||
}
|
}
|
||||||
|
|
||||||
segments.add(
|
segments.add(
|
||||||
Arc(
|
Arc(
|
||||||
start: current_pos,
|
start: currentPos,
|
||||||
radius: radius,
|
radius: radius,
|
||||||
rotation: rotation,
|
rotation: rotation,
|
||||||
arc: arc,
|
arc: arc,
|
||||||
@@ -374,37 +374,12 @@ Path parse_path(String pathdef) {
|
|||||||
end: end,
|
end: end,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
current_pos = end;
|
currentPos = end;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finish up the loop in preparation for next command
|
// Finish up the loop in preparation for next command
|
||||||
last_command = command;
|
lastCommand = command;
|
||||||
}
|
}
|
||||||
|
|
||||||
return segments;
|
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:collection';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:math' show sqrt, sin, cos, acos, log, pi;
|
import 'dart:math' show sqrt, sin, cos, acos, log, pi;
|
||||||
@@ -6,18 +9,9 @@ import 'package:bisection/extension.dart';
|
|||||||
|
|
||||||
import '../common/Point.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 radians(num n) => n * pi / 180;
|
||||||
double degrees(num n) => n * 180 / pi;
|
double degrees(num n) => n * 180 / pi;
|
||||||
|
|
||||||
|
|
||||||
const defaultMinDepth = 5;
|
const defaultMinDepth = 5;
|
||||||
const defaultError = 1e-12;
|
const defaultError = 1e-12;
|
||||||
|
|
||||||
@@ -101,11 +95,6 @@ class Linear extends SvgPath {
|
|||||||
required Point end,
|
required Point end,
|
||||||
}) : super(start: start, end: end);
|
}) : super(start: start, end: end);
|
||||||
|
|
||||||
// def __ne__(self, other):
|
|
||||||
// if not isinstance(other, Line):
|
|
||||||
// return NotImplemented
|
|
||||||
// return not self == other
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Point point(num pos) => start + (end - start).times(pos);
|
Point point(num pos) => start + (end - start).times(pos);
|
||||||
|
|
||||||
@@ -126,8 +115,6 @@ class Line extends Linear {
|
|||||||
String toString() {
|
String toString() {
|
||||||
return "Line(start=$start, end=$end)";
|
return "Line(start=$start, end=$end)";
|
||||||
}
|
}
|
||||||
// @override
|
|
||||||
// operator ==(covariant Line other) => start == other.start && end == other.end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CubicBezier extends Bezier {
|
class CubicBezier extends Bezier {
|
||||||
@@ -145,18 +132,6 @@ class CubicBezier extends Bezier {
|
|||||||
String toString() => "CubicBezier(start=$start, control1=$control1, "
|
String toString() => "CubicBezier(start=$start, control1=$control1, "
|
||||||
"control2=$control2, end=$end)";
|
"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
|
@override
|
||||||
bool isSmoothFrom(Object? previous) => previous is CubicBezier
|
bool isSmoothFrom(Object? previous) => previous is CubicBezier
|
||||||
? start == previous.end &&
|
? start == previous.end &&
|
||||||
@@ -202,20 +177,6 @@ class QuadraticBezier extends Bezier {
|
|||||||
String toString() =>
|
String toString() =>
|
||||||
"QuadraticBezier(start=$start, control=$control, end=$end)";
|
"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
|
@override
|
||||||
bool isSmoothFrom(Object? previous) => previous is QuadraticBezier
|
bool isSmoothFrom(Object? previous) => previous is QuadraticBezier
|
||||||
? start == previous.end &&
|
? start == previous.end &&
|
||||||
@@ -286,30 +247,12 @@ class Arc extends SvgPath {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => "Arc(start=$start, radius=$radius, rotation=$rotation, "
|
String toString() => 'Arc(start=$start, radius=$radius, rotation=$rotation, '
|
||||||
"arc=$arc, sweep=$sweep, end=$end)";
|
'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
|
|
||||||
|
|
||||||
|
/// Conversion from endpoint to center parameterization
|
||||||
|
/// http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
|
||||||
void _parameterize() {
|
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
|
// This is equivalent of omitting the segment, so do nothing
|
||||||
if (start == end) return;
|
if (start == end) return;
|
||||||
|
|
||||||
@@ -439,22 +382,13 @@ class Arc extends SvgPath {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Represents move commands. Does nothing, but is there to handle
|
/// Represents move commands. Does nothing, but is there to handle
|
||||||
// paths that consist of only move commands, which is valid, but pointless.
|
/// paths that consist of only move commands, which is valid, but pointless.
|
||||||
class Move extends SvgPath {
|
class Move extends SvgPath {
|
||||||
const Move({required Point to}) : super(start: to, end: to);
|
const Move({required Point to}) : super(start: to, end: to);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => "Move(to=$start)";
|
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
|
@override
|
||||||
Point point(num pos) => start;
|
Point point(num pos) => start;
|
||||||
@@ -464,7 +398,7 @@ class Move extends SvgPath {
|
|||||||
0;
|
0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Represents the closepath command
|
/// Represents the closepath command
|
||||||
class Close extends Linear {
|
class Close extends Linear {
|
||||||
const Close({
|
const Close({
|
||||||
required Point start,
|
required Point start,
|
||||||
@@ -616,33 +550,4 @@ class Path extends ListBase<SvgPath> {
|
|||||||
|
|
||||||
return parts.join(" ");
|
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
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user