Compare commits
20 Commits
wip
..
restructure
| Author | SHA1 | Date | |
|---|---|---|---|
|
87e23e794c
|
|||
|
aba99a8996
|
|||
|
8ae8d4ebd1
|
|||
|
039c7e2693
|
|||
|
417db6ab0c
|
|||
|
00ec3c43fc
|
|||
|
1130ae2974
|
|||
|
0623bbc50a
|
|||
|
c83821fa38
|
|||
|
9f9757d710
|
|||
|
5cdd383e86
|
|||
|
e7f1e3563b
|
|||
|
c02009f601
|
|||
|
2338e34a19
|
|||
|
ee92a46b11
|
|||
|
f983a3adf1
|
|||
|
b1732b71f1
|
|||
|
4c98d488f8
|
|||
|
e5ed44c24b
|
|||
|
1c692a6889
|
@@ -0,0 +1,33 @@
|
|||||||
|
name: "Test"
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: debian-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: apt-get install dependencies
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y git jq
|
||||||
|
|
||||||
|
|
||||||
|
- run: git config --global --add safe.directory '*'
|
||||||
|
|
||||||
|
- run: git clone --depth=1 --single-branch --branch=master https://git.pvv.ntnu.no/mugiten/kanjivg.git kanjivg
|
||||||
|
|
||||||
|
- name: Setup Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: 'stable'
|
||||||
|
|
||||||
|
- run: flutter pub get
|
||||||
|
|
||||||
|
- run: flutter test
|
||||||
|
env:
|
||||||
|
KANJIVG_PATH: ${{ github.workspace }}/kanjivg
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[submodule "assets/kanjivg"]
|
|
||||||
path = assets/kanjivg
|
|
||||||
url = git@github.com:KanjiVG/kanjivg.git
|
|
||||||
@@ -1 +1,21 @@
|
|||||||
TODO: Add your license here.
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 h7x4 <h7x4@nani.wtf>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|||||||
@@ -1,34 +1,17 @@
|
|||||||
|
|
||||||
# Kanimaji
|
# Kanimaji
|
||||||
|
|
||||||
Add animated kanji strokes to your app!
|
Kanji stroke order animations for flutter.
|
||||||
|
|
||||||
## Features
|
> [!WARNING]
|
||||||
|
> This library is still not finished, take the contents and promises of this README with a grain of salt.
|
||||||
|
|
||||||
This library is a port of [Kanimaji][kanimaji], a library for animating kanji.
|
This library is a port of [Kanimaji][kanimaji], a library for animating kanji strokes.
|
||||||
It provides a way to convert stroke data from [KanjiVG][kanjivg] into kanji animations.
|
It uses the stroke data from the [KanjiVG][kanjivg] project, and renders them as either SVG animations or flutter 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.
|
You can configure the animation's speed, curve, colors, and more.
|
||||||
|
|
||||||
## Getting started
|
|
||||||
|
|
||||||
Start by adding the project to your pubspec.yaml.
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
TODO: Include short and useful examples for package users. Add longer examples
|
TODO: Include short and useful examples for package users. Add longer examples
|
||||||
to `/example` folder.
|
to `/example` folder.
|
||||||
|
|
||||||
```dart
|
|
||||||
const like = 'sample';
|
|
||||||
```
|
|
||||||
|
|
||||||
## Additional information
|
|
||||||
|
|
||||||
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/
|
|
||||||
|
|||||||
Submodule assets/kanjivg deleted from e1d99250c5
Generated
+43
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"kanjivg": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1770109946,
|
||||||
|
"narHash": "sha256-zgkyLJwEJe6YABUNrL27BBrDSWVNcAUk7K6P1mcVHxQ=",
|
||||||
|
"owner": "KanjiVG",
|
||||||
|
"repo": "kanjivg",
|
||||||
|
"rev": "2fe6daaba502ee6735f888e5fb4a7d30639a18ee",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "KanjiVG",
|
||||||
|
"repo": "kanjivg",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1771369470,
|
||||||
|
"narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "0182a361324364ae3f436a63005877674cf45efb",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "nixpkgs",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"kanjivg": "kanjivg",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
description = "Flutter package for rendering kanji stroke animations";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
|
kanjivg = {
|
||||||
|
url = "github:KanjiVG/kanjivg";
|
||||||
|
flake = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = {
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
kanjivg
|
||||||
|
}: let
|
||||||
|
inherit (nixpkgs) lib;
|
||||||
|
systems = [
|
||||||
|
"x86_64-linux"
|
||||||
|
"aarch64-linux"
|
||||||
|
"x86_64-darwin"
|
||||||
|
"aarch64-darwin"
|
||||||
|
];
|
||||||
|
|
||||||
|
forAllSystems = f: lib.genAttrs systems (system: f system nixpkgs.legacyPackages.${system});
|
||||||
|
in {
|
||||||
|
devShells = forAllSystems (system: pkgs: {
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
flutter
|
||||||
|
dart
|
||||||
|
];
|
||||||
|
|
||||||
|
env.KANJIVG_PATH = "${kanjivg}/kanji";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
packages = let
|
||||||
|
src = builtins.filterSource (path: type: let
|
||||||
|
baseName = baseNameOf (toString path);
|
||||||
|
in !(lib.any (b: b) [
|
||||||
|
(!(lib.cleanSourceFilter path type))
|
||||||
|
(baseName == "nix" && type == "directory")
|
||||||
|
(baseName == ".envrc" && type == "regular")
|
||||||
|
(baseName == "flake.lock" && type == "regular")
|
||||||
|
(baseName == "flake.nix" && type == "regular")
|
||||||
|
])) ./.;
|
||||||
|
in forAllSystems (system: pkgs: {
|
||||||
|
# default = self.packages.${system}.kanjimaji;
|
||||||
|
|
||||||
|
filteredSource = pkgs.runCommandLocal "filtered-source" { } ''
|
||||||
|
ln -s ${src} $out
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,31 +1,24 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math' show min, sqrt, pow;
|
import 'dart:math' show min, sqrt, pow;
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:flutter_svg/flutter_svg.dart' as svg;
|
|
||||||
import 'package:image/image.dart' as image;
|
|
||||||
|
|
||||||
import '../svg/parser.dart';
|
|
||||||
import '../common/point.dart';
|
|
||||||
|
|
||||||
import 'bezier_cubic.dart' as bezier_cubic;
|
|
||||||
import 'package:xml/xml.dart';
|
import 'package:xml/xml.dart';
|
||||||
import 'package:path/path.dart';
|
|
||||||
|
import 'svg_parser.dart';
|
||||||
|
import 'primitives/point.dart';
|
||||||
|
import 'primitives/bezier.dart' as bezier;
|
||||||
|
|
||||||
double _computePathLength(String path) =>
|
double _computePathLength(String path) =>
|
||||||
parsePath(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"(?=['\\\\])"), "\\\\")}'";
|
||||||
|
|
||||||
extension _Dedent on String {
|
extension _Dedent on String {
|
||||||
String get dedented {
|
String get dedented {
|
||||||
final withoutEmptyLines =
|
final withoutEmptyLines = split('\n').where((l) => l.isNotEmpty).toList();
|
||||||
this.split('\n').where((l) => l.isNotEmpty).toList();
|
|
||||||
final whitespaceAmounts = [
|
final whitespaceAmounts = [
|
||||||
for (final line in withoutEmptyLines)
|
for (final line in withoutEmptyLines)
|
||||||
line.split('').takeWhile((c) => c == ' ').length
|
line.split('').takeWhile((c) => c == ' ').length,
|
||||||
];
|
];
|
||||||
final whitespaceToRemove = whitespaceAmounts.reduce(min);
|
final whitespaceToRemove = whitespaceAmounts.reduce(min);
|
||||||
return withoutEmptyLines
|
return withoutEmptyLines
|
||||||
@@ -68,40 +61,34 @@ const pt2 = Point(1, 1);
|
|||||||
// class {
|
// class {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
enum TimingFunction {
|
enum TimingFunction { linear, ease, easeIn, easeInOut, easeOut }
|
||||||
linear,
|
|
||||||
ease,
|
|
||||||
easeIn,
|
|
||||||
easeInOut,
|
|
||||||
easeOut,
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Funcs on TimingFunction {
|
extension Funcs on TimingFunction {
|
||||||
double Function(double) get func => {
|
double Function(double) get func => {
|
||||||
TimingFunction.linear: (double x) => x,
|
TimingFunction.linear: (double x) => x,
|
||||||
TimingFunction.ease: (double x) =>
|
TimingFunction.ease: (double x) =>
|
||||||
bezier_cubic.value(pt1, easeCt1, easeCt2, pt2, x),
|
bezier.value(pt1, easeCt1, easeCt2, pt2, x),
|
||||||
TimingFunction.easeIn: (double x) =>
|
TimingFunction.easeIn: (double x) =>
|
||||||
bezier_cubic.value(pt1, easeInCt1, easeInCt2, pt2, x),
|
bezier.value(pt1, easeInCt1, easeInCt2, pt2, x),
|
||||||
TimingFunction.easeInOut: (double x) =>
|
TimingFunction.easeInOut: (double x) =>
|
||||||
bezier_cubic.value(pt1, easeInOutCt1, easeInOutCt2, pt2, x),
|
bezier.value(pt1, easeInOutCt1, easeInOutCt2, pt2, x),
|
||||||
TimingFunction.easeOut: (double x) =>
|
TimingFunction.easeOut: (double x) =>
|
||||||
bezier_cubic.value(pt1, easeOutCt1, easeOutCt2, pt2, x),
|
bezier.value(pt1, easeOutCt1, easeOutCt2, pt2, x),
|
||||||
}[this]!;
|
}[this]!;
|
||||||
|
|
||||||
String get name => {
|
String get name => {
|
||||||
TimingFunction.linear: 'linear',
|
TimingFunction.linear: 'linear',
|
||||||
TimingFunction.ease: 'ease',
|
TimingFunction.ease: 'ease',
|
||||||
TimingFunction.easeIn: 'ease-in',
|
TimingFunction.easeIn: 'ease-in',
|
||||||
TimingFunction.easeInOut: 'ease-in-out',
|
TimingFunction.easeInOut: 'ease-in-out',
|
||||||
TimingFunction.easeOut: 'ease-out',
|
TimingFunction.easeOut: 'ease-out',
|
||||||
}[this]!;
|
}[this]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
// we will need this to deal with svg
|
// we will need this to deal with svg
|
||||||
const namespaces = {
|
const namespaces = {
|
||||||
'n': 'http://www.w3.org/2000/svg',
|
'n': 'http://www.w3.org/2000/svg',
|
||||||
'xlink': 'http://www.w3.org/1999/xlink'
|
'xlink': 'http://www.w3.org/1999/xlink',
|
||||||
};
|
};
|
||||||
// etree.register_namespace("xlink","http://www.w3.org/1999/xlink")
|
// etree.register_namespace("xlink","http://www.w3.org/1999/xlink")
|
||||||
// final parser = etree.XMLParser(remove_blank_text=true);
|
// final parser = etree.XMLParser(remove_blank_text=true);
|
||||||
@@ -129,33 +116,61 @@ double timeRescale(interval) => pow(2 * interval, 2.0 / 3).toDouble();
|
|||||||
|
|
||||||
/// clear all extra elements this program may have previously added
|
/// clear all extra elements this program may have previously added
|
||||||
void clearPreviousElements(XmlDocument doc) {
|
void clearPreviousElements(XmlDocument doc) {
|
||||||
for (final XmlNode el in doc
|
for (final XmlNode el
|
||||||
.getElement('svg', namespace: namespaces['n'])
|
in doc
|
||||||
?.getElement('style', namespace: namespaces['n'])
|
.getElement('svg', namespace: namespaces['n'])
|
||||||
?.children ??
|
?.getElement('style', namespace: namespaces['n'])
|
||||||
[]) {
|
?.children ??
|
||||||
|
[]) {
|
||||||
if (RegExp(r'-Kanimaji$').hasMatch(el.getAttribute('id') ?? '')) {
|
if (RegExp(r'-Kanimaji$').hasMatch(el.getAttribute('id') ?? '')) {
|
||||||
el.parent!.children.remove(el);
|
el.parent!.children.remove(el);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (final XmlNode g in doc
|
for (final XmlNode g
|
||||||
.getElement('svg', namespace: namespaces['n'])
|
in doc
|
||||||
?.getElement('g', namespace: namespaces['n'])
|
.getElement('svg', namespace: namespaces['n'])
|
||||||
?.children ??
|
?.getElement('g', namespace: namespaces['n'])
|
||||||
[]) {
|
?.children ??
|
||||||
|
[]) {
|
||||||
if (RegExp(r'-Kanimaji$').hasMatch(g.getAttribute('id') ?? '')) {
|
if (RegExp(r'-Kanimaji$').hasMatch(g.getAttribute('id') ?? '')) {
|
||||||
g.parent!.children.remove(g);
|
g.parent!.children.remove(g);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
XmlDocument fetchXML(String inputFile) {
|
String basename(String path) {
|
||||||
|
final int lastSlash = path.lastIndexOf(Platform.pathSeparator);
|
||||||
//------------------------------
|
if (lastSlash == -1) {
|
||||||
// FETCH DATA FILE
|
return path;
|
||||||
//------------------------------
|
}
|
||||||
|
return path.substring(lastSlash + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Note: setting any color to transparent will result in a much bigger
|
||||||
|
/// filesize for GIFs.
|
||||||
|
void createAnimation({
|
||||||
|
required String inputFile,
|
||||||
|
String? outputFile,
|
||||||
|
TimingFunction timingFunction = TimingFunction.easeInOut,
|
||||||
|
double strokeBorderWidth = 4.5,
|
||||||
|
double strokeUnfilledWidth = 3,
|
||||||
|
double strokeFilledWidth = 3.1,
|
||||||
|
bool showBrush = true,
|
||||||
|
bool showBrushFrontBorder = true,
|
||||||
|
double brushWidth = 5.5,
|
||||||
|
double brushBorderWidth = 7,
|
||||||
|
double waitAfter = 1.5,
|
||||||
|
String strokeBorderColor = '#666',
|
||||||
|
String strokeUnfilledColor = '#EEE',
|
||||||
|
String strokeFillingColor = '#F00',
|
||||||
|
String strokeFilledColor = '#000',
|
||||||
|
String brushColor = '#F00',
|
||||||
|
String brushBorderColor = '#666',
|
||||||
|
}) {
|
||||||
print('processing $inputFile');
|
print('processing $inputFile');
|
||||||
|
final String filenameNoext = inputFile.replaceAll(RegExp(r'\.[^\.]+$'), '');
|
||||||
|
outputFile ??= '${filenameNoext}_anim.svg';
|
||||||
|
final String baseid = basename(filenameNoext);
|
||||||
|
|
||||||
// load xml
|
// load xml
|
||||||
final XmlDocument doc = XmlDocument.parse(File(inputFile).readAsStringSync());
|
final XmlDocument doc = XmlDocument.parse(File(inputFile).readAsStringSync());
|
||||||
@@ -165,44 +180,6 @@ XmlDocument fetchXML(String inputFile) {
|
|||||||
doc.rootElement.setAttribute('xlink:used', '');
|
doc.rootElement.setAttribute('xlink:used', '');
|
||||||
|
|
||||||
clearPreviousElements(doc);
|
clearPreviousElements(doc);
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
// void generateCssForPath(XmlNode p, void Function<String> addCss) {
|
|
||||||
// }
|
|
||||||
|
|
||||||
/// Note: setting any color to transparent will result in a much bigger
|
|
||||||
/// filesize for GIFs.
|
|
||||||
FutureOr<void> createAnimation({
|
|
||||||
required String kanji,
|
|
||||||
String? outputFile,
|
|
||||||
TimingFunction timingFunction = TimingFunction.easeInOut,
|
|
||||||
double strokeBorderWidth = 4.5,
|
|
||||||
double strokeUnfilledWidth = 3,
|
|
||||||
double strokeFilledWidth = 3.1,
|
|
||||||
bool showBrush = true,
|
|
||||||
// bool showNumbers = false,
|
|
||||||
bool showBrushFrontBorder = true,
|
|
||||||
double brushWidth = 5.5,
|
|
||||||
double brushBorderWidth = 7,
|
|
||||||
double waitAfter = 1.5,
|
|
||||||
int gifSize = 500,
|
|
||||||
String strokeBorderColor = '#666',
|
|
||||||
String strokeUnfilledColor = '#EEE',
|
|
||||||
String strokeFillingColor = '#F00',
|
|
||||||
String strokeFilledColor = '#000',
|
|
||||||
String brushColor = '#F00',
|
|
||||||
String brushBorderColor = '#666',
|
|
||||||
}) async {
|
|
||||||
final inputFile = 'assets/kanjivg/kanji/${kanji.codeUnitAt(0).toRadixString(16).padLeft(5, '0')}.svg';
|
|
||||||
final String filenameNoext = inputFile.replaceAll(RegExp(r'\.[^\.]+$'), '');
|
|
||||||
outputFile ??= '${filenameNoext}_anim.svg';
|
|
||||||
final String baseid = basename(filenameNoext);
|
|
||||||
final doc = fetchXML(inputFile);
|
|
||||||
|
|
||||||
//------------------------------
|
|
||||||
// CREATE SVG PATH GROUPS
|
|
||||||
//------------------------------
|
|
||||||
|
|
||||||
/// create groups with a copies (references actually) of the paths
|
/// create groups with a copies (references actually) of the paths
|
||||||
XmlDocumentFragment pathCopyGroup({
|
XmlDocumentFragment pathCopyGroup({
|
||||||
@@ -215,7 +192,8 @@ FutureOr<void> createAnimation({
|
|||||||
'g',
|
'g',
|
||||||
attributes: {
|
attributes: {
|
||||||
'id': 'kvg:$baseid-$id-Kanimaji',
|
'id': 'kvg:$baseid-$id-Kanimaji',
|
||||||
'style': 'fill:none;'
|
'style':
|
||||||
|
'fill:none;'
|
||||||
'stroke:$color;'
|
'stroke:$color;'
|
||||||
'stroke-width:$width;'
|
'stroke-width:$width;'
|
||||||
'stroke-linecap:round;'
|
'stroke-linecap:round;'
|
||||||
@@ -252,20 +230,17 @@ FutureOr<void> createAnimation({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------
|
|
||||||
// CALCULATE STROKE TIMES
|
|
||||||
//------------------------------
|
|
||||||
|
|
||||||
// compute total length and time, at first
|
// compute total length and time, at first
|
||||||
double totlen = 0;
|
double totlen = 0;
|
||||||
double tottime = 0;
|
double tottime = 0;
|
||||||
|
|
||||||
// for (final g in doc.xpath("/n:svg/n:g", namespaces=namespaces) {
|
// for (final g in doc.xpath("/n:svg/n:g", namespaces=namespaces) {
|
||||||
for (final XmlNode g in doc
|
for (final XmlNode g
|
||||||
.getElement('svg', namespace: namespaces['n'])
|
in doc
|
||||||
?.getElement('g', namespace: namespaces['n'])
|
.getElement('svg', namespace: namespaces['n'])
|
||||||
?.children ??
|
?.getElement('g', namespace: namespaces['n'])
|
||||||
[]) {
|
?.children ??
|
||||||
|
[]) {
|
||||||
if (RegExp(r'^kvg:StrokeNumbers_').hasMatch(g.getAttribute('id') ?? '')) {
|
if (RegExp(r'^kvg:StrokeNumbers_').hasMatch(g.getAttribute('id') ?? '')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -283,10 +258,6 @@ FutureOr<void> createAnimation({
|
|||||||
final double actualAnimationTime = animationTime;
|
final double actualAnimationTime = animationTime;
|
||||||
animationTime += waitAfter;
|
animationTime += waitAfter;
|
||||||
|
|
||||||
//------------------------------
|
|
||||||
// START ADDING CSS
|
|
||||||
//------------------------------
|
|
||||||
|
|
||||||
final Map<int, String> staticCss = {};
|
final Map<int, String> staticCss = {};
|
||||||
late String animatedCss;
|
late String animatedCss;
|
||||||
|
|
||||||
@@ -299,9 +270,7 @@ FutureOr<void> createAnimation({
|
|||||||
'\n/* CSS automatically generated by kanimaji.py, do not edit! */\n';
|
'\n/* CSS automatically generated by kanimaji.py, do not edit! */\n';
|
||||||
if (GENERATE_SVG) animatedCss = cssHeader;
|
if (GENERATE_SVG) animatedCss = cssHeader;
|
||||||
if (GENERATE_JS_SVG) {
|
if (GENERATE_JS_SVG) {
|
||||||
jsAnimatedCss += cssHeader +
|
jsAnimatedCss += '''$cssHeader .backward {\n
|
||||||
'''
|
|
||||||
.backward {\n
|
|
||||||
animation-direction: reverse !important;\n
|
animation-direction: reverse !important;\n
|
||||||
}
|
}
|
||||||
''';
|
''';
|
||||||
@@ -319,23 +288,21 @@ FutureOr<void> createAnimation({
|
|||||||
double elapsedlen = 0;
|
double elapsedlen = 0;
|
||||||
double elapsedtime = 0;
|
double elapsedtime = 0;
|
||||||
|
|
||||||
//------------------------------
|
|
||||||
// ADD CSS FOR STROKE STYLE
|
|
||||||
//------------------------------
|
|
||||||
|
|
||||||
// add css elements for all strokes
|
// add css elements for all strokes
|
||||||
for (final XmlNode g in doc
|
for (final XmlNode g
|
||||||
.getElement('svg', namespace: namespaces['n'])!
|
in doc
|
||||||
.findElements('g', namespace: namespaces['n'])) {
|
.getElement('svg', namespace: namespaces['n'])!
|
||||||
|
.findElements('g', namespace: namespaces['n'])) {
|
||||||
// for (final g in doc.xpath("/n:svg/n:g", namespaces=namespaces)){
|
// for (final g in doc.xpath("/n:svg/n:g", namespaces=namespaces)){
|
||||||
final groupid = g.getAttribute('id') ?? '';
|
final groupid = g.getAttribute('id') ?? '';
|
||||||
if (RegExp(r'^kvg:StrokeNumbers_').hasMatch(groupid)) {
|
if (RegExp(r'^kvg:StrokeNumbers_').hasMatch(groupid)) {
|
||||||
final String rule = '''
|
final String rule =
|
||||||
|
'''
|
||||||
#${groupid.replaceAll(':', '\\3a ')} {
|
#${groupid.replaceAll(':', '\\3a ')} {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
.dedented;
|
.dedented;
|
||||||
if (GENERATE_SVG) animatedCss += rule;
|
if (GENERATE_SVG) animatedCss += rule;
|
||||||
if (GENERATE_JS_SVG) jsAnimatedCss += rule;
|
if (GENERATE_JS_SVG) jsAnimatedCss += rule;
|
||||||
if (GENERATE_GIF) {
|
if (GENERATE_GIF) {
|
||||||
@@ -347,13 +314,14 @@ FutureOr<void> createAnimation({
|
|||||||
}
|
}
|
||||||
|
|
||||||
final gidcss = groupid.replaceAll(':', '\\3a ');
|
final gidcss = groupid.replaceAll(':', '\\3a ');
|
||||||
final rule = '''
|
final rule =
|
||||||
|
'''
|
||||||
#$gidcss {
|
#$gidcss {
|
||||||
stroke-width: ${strokeBorderWidth.toStringAsFixed(1)}px !important;
|
stroke-width: ${strokeBorderWidth.toStringAsFixed(1)}px !important;
|
||||||
stroke: $strokeBorderColor !important;
|
stroke: $strokeBorderColor !important;
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
.dedented;
|
.dedented;
|
||||||
|
|
||||||
if (GENERATE_SVG) animatedCss += rule;
|
if (GENERATE_SVG) animatedCss += rule;
|
||||||
if (GENERATE_JS_SVG) jsAnimatedCss += rule;
|
if (GENERATE_JS_SVG) jsAnimatedCss += rule;
|
||||||
@@ -363,12 +331,7 @@ FutureOr<void> createAnimation({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------
|
|
||||||
// ADD CSS FOR HREFS
|
|
||||||
//------------------------------
|
|
||||||
|
|
||||||
for (final p in g.findAllElements("path", namespace: namespaces['n'])) {
|
for (final p in g.findAllElements("path", namespace: namespaces['n'])) {
|
||||||
|
|
||||||
final pathid = p.getAttribute('id') as String;
|
final pathid = p.getAttribute('id') as String;
|
||||||
final pathidcss = pathid.replaceAll(':', '\\3a ');
|
final pathidcss = pathid.replaceAll(':', '\\3a ');
|
||||||
|
|
||||||
@@ -397,10 +360,6 @@ FutureOr<void> createAnimation({
|
|||||||
brushBorderGroupElement = addHref('brush-brd', brushBrdGroup);
|
brushBorderGroupElement = addHref('brush-brd', brushBrdGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------
|
|
||||||
// CALCULATE RELATIVE TIMING
|
|
||||||
//------------------------------
|
|
||||||
|
|
||||||
final pathname = pathid.replaceAll(RegExp(r'^kvg:'), '');
|
final pathname = pathid.replaceAll(RegExp(r'^kvg:'), '');
|
||||||
final pathlen = _computePathLength(p.getAttribute('d') as String);
|
final pathlen = _computePathLength(p.getAttribute('d') as String);
|
||||||
final duration = strokeLengthToDuration(pathlen);
|
final duration = strokeLengthToDuration(pathlen);
|
||||||
@@ -424,13 +383,10 @@ FutureOr<void> createAnimation({
|
|||||||
final animStart = elapsedtime / tottime * 100;
|
final animStart = elapsedtime / tottime * 100;
|
||||||
final animEnd = newelapsedtime / tottime * 100;
|
final animEnd = newelapsedtime / tottime * 100;
|
||||||
|
|
||||||
//------------------------------
|
|
||||||
// GENERATE SVG SPECIFIC ANIMATION CSS
|
|
||||||
//------------------------------
|
|
||||||
|
|
||||||
if (GENERATE_SVG) {
|
if (GENERATE_SVG) {
|
||||||
// animation stroke progression
|
// animation stroke progression
|
||||||
animatedCss += '''
|
animatedCss +=
|
||||||
|
'''
|
||||||
@keyframes strike-$pathname {
|
@keyframes strike-$pathname {
|
||||||
0% { stroke-dashoffset: ${pathlen.toStringAsFixed(3)}; }
|
0% { stroke-dashoffset: ${pathlen.toStringAsFixed(3)}; }
|
||||||
${animStart.toStringAsFixed(3)}% { stroke-dashoffset: ${pathlen.toStringAsFixed(3)}; }
|
${animStart.toStringAsFixed(3)}% { stroke-dashoffset: ${pathlen.toStringAsFixed(3)}; }
|
||||||
@@ -448,11 +404,12 @@ FutureOr<void> createAnimation({
|
|||||||
showhide-$pathname ${animationTime.toStringAsFixed(3)}s step-start infinite;
|
showhide-$pathname ${animationTime.toStringAsFixed(3)}s step-start infinite;
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
.dedented;
|
.dedented;
|
||||||
|
|
||||||
if (showBrush) {
|
if (showBrush) {
|
||||||
// brush element visibility
|
// brush element visibility
|
||||||
animatedCss += '''
|
animatedCss +=
|
||||||
|
'''
|
||||||
@keyframes showhide-brush-$pathname {
|
@keyframes showhide-brush-$pathname {
|
||||||
${animStart.toStringAsFixed(3)}% { visibility: hidden; }
|
${animStart.toStringAsFixed(3)}% { visibility: hidden; }
|
||||||
${animEnd.toStringAsFixed(3)}% { visibility: visible; }
|
${animEnd.toStringAsFixed(3)}% { visibility: visible; }
|
||||||
@@ -464,30 +421,28 @@ FutureOr<void> createAnimation({
|
|||||||
showhide-brush-$pathname ${animationTime.toStringAsFixed(3)}s step-start infinite;
|
showhide-brush-$pathname ${animationTime.toStringAsFixed(3)}s step-start infinite;
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
.dedented;
|
.dedented;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------
|
|
||||||
// GENERATE JS SVG SPECIFIC ANIMATION CSS
|
|
||||||
//------------------------------
|
|
||||||
|
|
||||||
if (GENERATE_JS_SVG) {
|
if (GENERATE_JS_SVG) {
|
||||||
jsAnimatedCss += '\n/* stroke $pathid */\n';
|
jsAnimatedCss += '\n/* stroke $pathid */\n';
|
||||||
|
|
||||||
// brush and background hidden by default
|
// brush and background hidden by default
|
||||||
if (showBrush) {
|
if (showBrush) {
|
||||||
jsAnimatedCss += '''
|
jsAnimatedCss +=
|
||||||
|
'''
|
||||||
#$brushPathidcss, #$brushBorderPathidcss, #$bgPathidcss {
|
#$brushPathidcss, #$brushBorderPathidcss, #$bgPathidcss {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
.dedented;
|
.dedented;
|
||||||
}
|
}
|
||||||
|
|
||||||
// hide stroke after current element
|
// hide stroke after current element
|
||||||
const afterCurrent = '[class *= "current"]';
|
const afterCurrent = '[class *= "current"]';
|
||||||
jsAnimatedCss += '''
|
jsAnimatedCss +=
|
||||||
|
'''
|
||||||
$afterCurrent ~ #$animPathidcss {
|
$afterCurrent ~ #$animPathidcss {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
@@ -505,9 +460,10 @@ FutureOr<void> createAnimation({
|
|||||||
animation: strike-$pathname ${relativeDuration.toStringAsFixed(3)}s ${timingFunction.name} forwards 1;
|
animation: strike-$pathname ${relativeDuration.toStringAsFixed(3)}s ${timingFunction.name} forwards 1;
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
.dedented;
|
.dedented;
|
||||||
if (showBrush) {
|
if (showBrush) {
|
||||||
jsAnimatedCss += '''
|
jsAnimatedCss +=
|
||||||
|
'''
|
||||||
@keyframes strike-brush-$pathname {
|
@keyframes strike-brush-$pathname {
|
||||||
0% { stroke-dashoffset: ${pathlen.toStringAsFixed(3)}; }
|
0% { stroke-dashoffset: ${pathlen.toStringAsFixed(3)}; }
|
||||||
100% { stroke-dashoffset: 0.4; }
|
100% { stroke-dashoffset: 0.4; }
|
||||||
@@ -518,20 +474,16 @@ FutureOr<void> createAnimation({
|
|||||||
animation: strike-brush-$pathname ${relativeDuration.toStringAsFixed(3)}s ${timingFunction.name} forwards 1;
|
animation: strike-brush-$pathname ${relativeDuration.toStringAsFixed(3)}s ${timingFunction.name} forwards 1;
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
.dedented;
|
.dedented;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------
|
|
||||||
// GENERATE GIF SPECIFIC STATIC FRAME CSS
|
|
||||||
//------------------------------
|
|
||||||
|
|
||||||
if (GENERATE_GIF) {
|
if (GENERATE_GIF) {
|
||||||
for (final k in staticCss.keys) {
|
for (final k in staticCss.keys) {
|
||||||
final time = k * GIF_FRAME_DURATION;
|
final time = k * GIF_FRAME_DURATION;
|
||||||
final reltime = time * tottime / animationTime; // unscaled time
|
final reltime = time * tottime / animationTime; // unscaled time
|
||||||
|
|
||||||
staticCss[k] = staticCss[k]! + '\n/* stroke $pathid */\n';
|
staticCss[k] = '${staticCss[k]!}\n/* stroke $pathid */\n';
|
||||||
|
|
||||||
String rule = '';
|
String rule = '';
|
||||||
|
|
||||||
@@ -544,7 +496,8 @@ FutureOr<void> createAnimation({
|
|||||||
rule += ", #$brushPathidcss, #$brushBorderPathidcss";
|
rule += ", #$brushPathidcss, #$brushBorderPathidcss";
|
||||||
}
|
}
|
||||||
|
|
||||||
staticCss[k] = staticCss[k]! +
|
staticCss[k] =
|
||||||
|
staticCss[k]! +
|
||||||
'''
|
'''
|
||||||
%$rule {
|
%$rule {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
@@ -559,7 +512,8 @@ FutureOr<void> createAnimation({
|
|||||||
rule += ", #$brushPathidcss, #$brushBorderPathidcss";
|
rule += ", #$brushPathidcss, #$brushBorderPathidcss";
|
||||||
}
|
}
|
||||||
|
|
||||||
staticCss[k] = staticCss[k]! +
|
staticCss[k] =
|
||||||
|
staticCss[k]! +
|
||||||
'''
|
'''
|
||||||
$rule {
|
$rule {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
@@ -571,7 +525,8 @@ FutureOr<void> createAnimation({
|
|||||||
((reltime - elapsedtime) / (newelapsedtime - elapsedtime));
|
((reltime - elapsedtime) / (newelapsedtime - elapsedtime));
|
||||||
final progression = timingFunction.func(intervalprop);
|
final progression = timingFunction.func(intervalprop);
|
||||||
|
|
||||||
staticCss[k] = staticCss[k]! +
|
staticCss[k] =
|
||||||
|
staticCss[k]! +
|
||||||
'''
|
'''
|
||||||
#$animPathidcss {
|
#$animPathidcss {
|
||||||
stroke-dasharray: ${pathlen.toStringAsFixed(3)} ${(pathlen + 0.002).toStringAsFixed(3)};
|
stroke-dasharray: ${pathlen.toStringAsFixed(3)} ${(pathlen + 0.002).toStringAsFixed(3)};
|
||||||
@@ -581,7 +536,8 @@ FutureOr<void> createAnimation({
|
|||||||
'''
|
'''
|
||||||
.dedented;
|
.dedented;
|
||||||
if (showBrush) {
|
if (showBrush) {
|
||||||
staticCss[k] = staticCss[k]! +
|
staticCss[k] =
|
||||||
|
staticCss[k]! +
|
||||||
'''
|
'''
|
||||||
#$brushPathidcss, #$brushBorderPathidcss {
|
#$brushPathidcss, #$brushBorderPathidcss {
|
||||||
stroke-dasharray: 0.001 ${(pathlen + 0.002).toStringAsFixed(3)};
|
stroke-dasharray: 0.001 ${(pathlen + 0.002).toStringAsFixed(3)};
|
||||||
@@ -597,94 +553,52 @@ FutureOr<void> createAnimation({
|
|||||||
elapsedlen = newelapsedlen;
|
elapsedlen = newelapsedlen;
|
||||||
elapsedtime = newelapsedtime;
|
elapsedtime = newelapsedtime;
|
||||||
}
|
}
|
||||||
|
|
||||||
// for (final p in g.findAllElements("path", namespace: namespaces['n'])) {
|
|
||||||
// generateCssForPath(p);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------
|
|
||||||
// ADD CSS TO XML-OBJECT INSTANCE
|
|
||||||
//------------------------------
|
|
||||||
|
|
||||||
void addGroup(XmlDocumentFragment g) =>
|
void addGroup(XmlDocumentFragment g) =>
|
||||||
doc.root.firstElementChild?.children.add(g);
|
doc.root.firstElementChild?.children.add(g);
|
||||||
|
|
||||||
// insert groups
|
// insert groups
|
||||||
if (showBrush && !showBrushFrontBorder)
|
if (showBrush && !showBrushFrontBorder) addGroup(brushBrdGroup);
|
||||||
addGroup(brushBrdGroup);
|
|
||||||
addGroup(bgGroup);
|
addGroup(bgGroup);
|
||||||
if (showBrush && showBrushFrontBorder)
|
if (showBrush && showBrushFrontBorder) addGroup(brushBrdGroup);
|
||||||
addGroup(brushBrdGroup);
|
|
||||||
addGroup(animGroup);
|
addGroup(animGroup);
|
||||||
if (showBrush)
|
if (showBrush) addGroup(brushGroup);
|
||||||
addGroup(brushGroup);
|
|
||||||
|
|
||||||
//------------------------------
|
|
||||||
// PRODUCE SVG
|
|
||||||
//------------------------------
|
|
||||||
|
|
||||||
if (GENERATE_SVG) {
|
if (GENERATE_SVG) {
|
||||||
print(animatedCss);
|
print(animatedCss);
|
||||||
final builder = XmlBuilder();
|
final builder = XmlBuilder();
|
||||||
final style = (builder
|
final style =
|
||||||
..element(
|
(builder..element(
|
||||||
'style',
|
'style',
|
||||||
attributes: {'id': "style-Kanimaji", 'type': 'text/css'},
|
attributes: {'id': "style-Kanimaji", 'type': 'text/css'},
|
||||||
nest: animatedCss,
|
nest: animatedCss,
|
||||||
))
|
))
|
||||||
.buildFragment();
|
.buildFragment();
|
||||||
doc.root.firstElementChild!.children.insert(0, style);
|
doc.root.firstElementChild!.children.insert(0, style);
|
||||||
File(outputFile).writeAsStringSync(doc.toXmlString(pretty: true));
|
File(outputFile).writeAsStringSync(doc.toXmlString(pretty: true));
|
||||||
doc.root.children.removeAt(0);
|
doc.root.children.removeAt(0);
|
||||||
print('written $outputFile');
|
print('written $outputFile');
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------
|
|
||||||
// PRODUCE GIF
|
|
||||||
//------------------------------
|
|
||||||
|
|
||||||
if (GENERATE_GIF) {
|
if (GENERATE_GIF) {
|
||||||
// var svgframefiles = [];
|
// svgframefiles = []
|
||||||
// var pngframefiles = [];
|
// pngframefiles = []
|
||||||
// var svgexport_data = [];
|
// svgexport_data = []
|
||||||
|
// for k in static_css:
|
||||||
|
// svgframefile = filename_noext_ascii + ("_frame%04d.svg"%k)
|
||||||
|
// pngframefile = filename_noext_ascii + ("_frame%04d.png"%k)
|
||||||
|
// svgframefiles.append(svgframefile)
|
||||||
|
// pngframefiles.append(pngframefile)
|
||||||
|
// svgexport_data.append({"input": [abspath(svgframefile)],
|
||||||
|
// "output": [[abspath(pngframefile),
|
||||||
|
// "%d:%d"% (GIF_SIZE, GIF_SIZE)]]})
|
||||||
|
|
||||||
final encoder = image.GifEncoder(dither: 0 as DitherKernel);
|
// style = E.style(static_css[k], id="style-Kanimaji")
|
||||||
|
// doc.getroot().insert(0, style)
|
||||||
for (var k in staticCss.keys) {
|
// doc.write(svgframefile, pretty_print=True)
|
||||||
final style = XmlBuilder()..element(
|
// doc.getroot().remove(style)
|
||||||
'style',
|
// print 'written %s' % svgframefile
|
||||||
attributes: {'id': 'style-Kanimaji'},
|
|
||||||
);
|
|
||||||
doc.children.insert(0, style.buildFragment()..innerXml = staticCss[k]!);
|
|
||||||
|
|
||||||
final svg.DrawableRoot svgRoot = await svg.svg.fromSvgString(doc.outerXml, doc.outerXml);
|
|
||||||
final picture = await svgRoot.toPicture().toImage(gifSize, gifSize);
|
|
||||||
final ByteData? bytes = await picture.toByteData();
|
|
||||||
final intBytes = bytes!.buffer.asInt32List().toList();
|
|
||||||
image.Image frame = image.Image.fromBytes(gifSize, gifSize, intBytes);
|
|
||||||
encoder.addFrame(frame);
|
|
||||||
|
|
||||||
doc.children.removeAt(0);
|
|
||||||
|
|
||||||
// svgframefile = filename_noext_ascii + ("_frame%04d.svg"%k);
|
|
||||||
// pngframefile = filename_noext_ascii + ("_frame%04d.png"%k);
|
|
||||||
// svgframefiles.append(svgframefile)
|
|
||||||
// pngframefiles.append(pngframefile)
|
|
||||||
// svgexport_data.append({"input": [svgframefile.abs()],
|
|
||||||
// "output": [[abspath(pngframefile),
|
|
||||||
// "%d:%d"% (GIF_SIZE, GIF_SIZE)]]})
|
|
||||||
|
|
||||||
// style = E.style(staticCss[k], id="style-Kanimaji")
|
|
||||||
// doc.getroot().insert(0, style)
|
|
||||||
// doc.write(svgframefile, pretty_print=True)
|
|
||||||
// doc.getroot().remove(style)
|
|
||||||
// print 'written %s' % svgframefile
|
|
||||||
}
|
|
||||||
|
|
||||||
// encoder.finish();
|
|
||||||
|
|
||||||
File(outputFile).writeAsBytesSync(encoder.finish()!);
|
|
||||||
|
|
||||||
// // create json file
|
// // create json file
|
||||||
// svgexport_datafile = filename_noext_ascii+"_export_data.json"
|
// svgexport_datafile = filename_noext_ascii+"_export_data.json"
|
||||||
@@ -775,4 +689,27 @@ FutureOr<void> createAnimation({
|
|||||||
// doc.getroot().remove(style)
|
// doc.getroot().remove(style)
|
||||||
// print('written $svgfile');
|
// print('written $svgfile');
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void main(List<String> args) {
|
||||||
|
// createAnimation('assets/kanjivg/kanji/0f9b1.svg');
|
||||||
|
|
||||||
|
const kanji = '実例';
|
||||||
|
final fileList = [];
|
||||||
|
for (int k = 0; k < kanji.length; k++) {
|
||||||
|
createAnimation(
|
||||||
|
inputFile:
|
||||||
|
'assets/kanjivg/kanji/${kanji.codeUnits[k].toRadixString(16).padLeft(5, '0')}.svg',
|
||||||
|
outputFile: '${k + 1}.svg',
|
||||||
|
);
|
||||||
|
fileList.add('${k + 1}.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
File('index.html').writeAsStringSync(
|
||||||
|
'<html>${fileList.map((e) => File(e).readAsStringSync().replaceAll(']>', '')).join('\n')}</html>',
|
||||||
|
);
|
||||||
|
// createAnimation(
|
||||||
|
// inputFile: 'assets/kanjivg/kanji/060c5.svg',
|
||||||
|
// outputFile: 'test.svg',
|
||||||
|
// );
|
||||||
|
}
|
||||||
+1
-7
@@ -1,7 +1 @@
|
|||||||
library kanimaji;
|
export 'package:kanimaji/widget.dart' show Kanimaji;
|
||||||
|
|
||||||
/// A Calculator.
|
|
||||||
class Calculator {
|
|
||||||
/// Returns [value] plus 1.
|
|
||||||
int addOne(int value) => value + 1;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
|
|
||||||
import 'package:kanimaji/kanimaji/options.dart';
|
|
||||||
|
|
||||||
Future<void> createGif(String kanji, KanimajiOptions options) async {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import 'dart:core';
|
|
||||||
|
|
||||||
import 'animate_kanji.dart';
|
|
||||||
|
|
||||||
class KanimajiOptions {
|
|
||||||
// String inputFile;
|
|
||||||
// String? outputFile;
|
|
||||||
TimingFunction timingFunction = TimingFunction.easeInOut;
|
|
||||||
double strokeBorderWidth = 4.5;
|
|
||||||
double strokeUnfilledWidth = 3;
|
|
||||||
double strokeFilledWidth = 3.1;
|
|
||||||
bool showBrush = true;
|
|
||||||
bool showBrushFrontBorder = true;
|
|
||||||
double brushWidth = 5.5;
|
|
||||||
double brushBorderWidth = 7;
|
|
||||||
double waitAfter = 1.5;
|
|
||||||
int gifSize = 500;
|
|
||||||
String strokeBorderColor = '#666';
|
|
||||||
String strokeUnfilledColor = '#EEE';
|
|
||||||
String strokeFillingColor = '#F00';
|
|
||||||
String strokeFilledColor = '#000';
|
|
||||||
String brushColor = '#F00';
|
|
||||||
String brushBorderColor = '#666';
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// String calculate_css() {
|
|
||||||
|
|
||||||
// }
|
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import 'package:xml/xml.dart';
|
||||||
|
|
||||||
|
import 'svg_parser.dart';
|
||||||
|
import 'primitives/path.dart';
|
||||||
|
import 'primitives/point.dart';
|
||||||
|
|
||||||
|
/// Enum set in the kvg:position attribute, used by `<g>` elements in the KanjiVG SVG files.
|
||||||
|
enum KanjiPathGroupPosition {
|
||||||
|
bottom,
|
||||||
|
kamae,
|
||||||
|
kamaec,
|
||||||
|
left,
|
||||||
|
middle,
|
||||||
|
nyo,
|
||||||
|
nyoc,
|
||||||
|
right,
|
||||||
|
tare,
|
||||||
|
tarec,
|
||||||
|
top;
|
||||||
|
|
||||||
|
factory KanjiPathGroupPosition.fromString(String s) => switch (s) {
|
||||||
|
'bottom' => bottom,
|
||||||
|
'kamae' => kamae,
|
||||||
|
'kamaec' => kamaec,
|
||||||
|
'left' => left,
|
||||||
|
'middle' => middle,
|
||||||
|
'nyo' => nyo,
|
||||||
|
'nyoc' => nyoc,
|
||||||
|
'right' => right,
|
||||||
|
'tare' => tare,
|
||||||
|
'tarec' => tarec,
|
||||||
|
'top' => top,
|
||||||
|
_ => throw ArgumentError('Invalid position string: $s'),
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => switch (this) {
|
||||||
|
KanjiPathGroupPosition.bottom => 'bottom',
|
||||||
|
KanjiPathGroupPosition.kamae => 'kamae',
|
||||||
|
KanjiPathGroupPosition.kamaec => 'kamaec',
|
||||||
|
KanjiPathGroupPosition.left => 'left',
|
||||||
|
KanjiPathGroupPosition.middle => 'middle',
|
||||||
|
KanjiPathGroupPosition.nyo => 'nyo',
|
||||||
|
KanjiPathGroupPosition.nyoc => 'nyoc',
|
||||||
|
KanjiPathGroupPosition.right => 'right',
|
||||||
|
KanjiPathGroupPosition.tare => 'tare',
|
||||||
|
KanjiPathGroupPosition.tarec => 'tarec',
|
||||||
|
KanjiPathGroupPosition.top => 'top',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contents of a \<g> element in the KanjiVG SVG files.
|
||||||
|
class KanjiPathGroupTreeNode {
|
||||||
|
final String id;
|
||||||
|
final List<KanjiPathGroupTreeNode> children;
|
||||||
|
final String? element;
|
||||||
|
final String? original;
|
||||||
|
final KanjiPathGroupPosition? position;
|
||||||
|
final String? radical;
|
||||||
|
final int? part;
|
||||||
|
|
||||||
|
KanjiPathGroupTreeNode({
|
||||||
|
required this.id,
|
||||||
|
this.children = const [],
|
||||||
|
this.element,
|
||||||
|
this.original,
|
||||||
|
this.position,
|
||||||
|
this.radical,
|
||||||
|
this.part,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contents of a `<text>` element in the StrokeNumber's group in the KanjiVG SVG files
|
||||||
|
class KanjiStrokeNumber {
|
||||||
|
final int num;
|
||||||
|
final Point position;
|
||||||
|
|
||||||
|
KanjiStrokeNumber(this.num, this.position);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'KanjiStrokeNumber(number: $num, position: $position)';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contents of a `<path>` element in the KanjiVG SVG files
|
||||||
|
class KanjiVGPath {
|
||||||
|
final String id;
|
||||||
|
final String type;
|
||||||
|
final Path svgPath;
|
||||||
|
|
||||||
|
KanjiVGPath({required this.id, required this.type, required this.svgPath});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'KanjiVGPath(id: $id, type: $type, svgPath: $svgPath)';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Representation of the entire KanjiVG SVG file for a single kanji character
|
||||||
|
class KanjiVGKanji {
|
||||||
|
final String character;
|
||||||
|
final List<Path> paths;
|
||||||
|
final List<KanjiStrokeNumber> strokeNumbers;
|
||||||
|
final KanjiPathGroupTreeNode? pathGroups;
|
||||||
|
|
||||||
|
KanjiVGKanji({
|
||||||
|
required this.character,
|
||||||
|
required this.paths,
|
||||||
|
required this.strokeNumbers,
|
||||||
|
this.pathGroups,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Small wrapper returned by the parser. It contains the parsed paths and stroke
|
||||||
|
/// numbers and the kanji character string.
|
||||||
|
class KanjiVGItem {
|
||||||
|
final String character;
|
||||||
|
final List<KanjiVGPath> paths;
|
||||||
|
final List<KanjiStrokeNumber> strokeNumbers;
|
||||||
|
|
||||||
|
KanjiVGItem({
|
||||||
|
required this.character,
|
||||||
|
required this.paths,
|
||||||
|
required this.strokeNumbers,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Helper method to get attributes that may be in the kvg namespace or as `kvg:` prefixed attributes.
|
||||||
|
static String? _kvgAttr(XmlElement el, String name) {
|
||||||
|
final fromNs = el.getAttribute(
|
||||||
|
name,
|
||||||
|
namespace: 'http://kanjivg.tagaini.net',
|
||||||
|
);
|
||||||
|
if (fromNs != null) return fromNs;
|
||||||
|
return el.getAttribute('kvg:$name');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the path data from the provided XML document and return a list of [Path]s.
|
||||||
|
static List<KanjiVGPath> _parsePaths(XmlDocument doc) {
|
||||||
|
final List<KanjiVGPath> paths = [];
|
||||||
|
for (final p in doc.findAllElements('path')) {
|
||||||
|
final d = p.getAttribute('d');
|
||||||
|
if (d == null || d.trim().isEmpty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final svgPath = parsePath(d);
|
||||||
|
final id = p.getAttribute('id') ?? '';
|
||||||
|
final type = _kvgAttr(p, 'type') ?? p.getAttribute('kvg:type') ?? '';
|
||||||
|
paths.add(KanjiVGPath(id: id, type: type, svgPath: svgPath));
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the stroke number group from the provided XML document and return a list of [KanjiStrokeNumber]s.
|
||||||
|
static List<KanjiStrokeNumber> _parseStrokeNumbers(XmlDocument doc) {
|
||||||
|
final List<KanjiStrokeNumber> strokeNumbers = [];
|
||||||
|
final strokeNumberGroup = doc.findAllElements('g').firstWhere((g) {
|
||||||
|
final id = g.getAttribute('id') ?? '';
|
||||||
|
return RegExp(r'^kvg:StrokeNumbers_').hasMatch(id);
|
||||||
|
}, orElse: () => XmlElement(XmlName('')));
|
||||||
|
|
||||||
|
if (strokeNumberGroup.name.local != '') {
|
||||||
|
for (final t in strokeNumberGroup.findAllElements('text')) {
|
||||||
|
final rawText = t.innerText.trim();
|
||||||
|
if (rawText.isEmpty) continue;
|
||||||
|
final numVal = int.tryParse(rawText);
|
||||||
|
if (numVal == null) continue;
|
||||||
|
|
||||||
|
final transform = t.getAttribute('transform') ?? '';
|
||||||
|
final numbers = transform
|
||||||
|
.replaceAll('matrix(1 0 0 1', '')
|
||||||
|
.replaceAll(')', '')
|
||||||
|
.trim()
|
||||||
|
.split(' ')
|
||||||
|
.map(num.parse)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
assert(
|
||||||
|
numbers.length >= 2,
|
||||||
|
'Expected at least 2 numbers in transform for stroke number position',
|
||||||
|
);
|
||||||
|
|
||||||
|
Point pos = Point(
|
||||||
|
numbers[numbers.length - 2],
|
||||||
|
numbers[numbers.length - 1],
|
||||||
|
);
|
||||||
|
|
||||||
|
strokeNumbers.add(KanjiStrokeNumber(numVal, pos));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strokeNumbers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the provided KanjiVG SVG content and return a [KanjiVGItem].
|
||||||
|
factory KanjiVGItem.parseFromXml(String xmlContent) {
|
||||||
|
final XmlDocument doc = XmlDocument.parse(xmlContent);
|
||||||
|
|
||||||
|
XmlElement strokePathsGroup = doc
|
||||||
|
.findElements('svg')
|
||||||
|
.first
|
||||||
|
.findElements('g')
|
||||||
|
.first;
|
||||||
|
XmlElement kanjiGroup = strokePathsGroup.findElements('g').first;
|
||||||
|
final character = _kvgAttr(kanjiGroup, 'element') ?? '';
|
||||||
|
final paths = _parsePaths(doc);
|
||||||
|
final strokeNumbers = _parseStrokeNumbers(doc);
|
||||||
|
|
||||||
|
return KanjiVGItem(
|
||||||
|
character: character,
|
||||||
|
paths: paths,
|
||||||
|
strokeNumbers: strokeNumbers,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
|
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'point.dart';
|
||||||
import '../common/point.dart';
|
|
||||||
|
|
||||||
// class Point {
|
// class Point {
|
||||||
// final double x;
|
// final double x;
|
||||||
@@ -19,9 +17,9 @@ double thrt(double x) =>
|
|||||||
|
|
||||||
double sqrt(double x) => x > 0 ? math.sqrt(x) : 0;
|
double sqrt(double x) => x > 0 ? math.sqrt(x) : 0;
|
||||||
|
|
||||||
double sq(x) => x * x;
|
num sq(num x) => x * x;
|
||||||
|
|
||||||
double cb(x) => x * x * x;
|
num cb(num x) => x * x * x;
|
||||||
|
|
||||||
/// x(t) = t^3 T + 3t^2(1-t) U + 3t(1-t)^2 V + (1-t)^3 W
|
/// x(t) = t^3 T + 3t^2(1-t) U + 3t(1-t)^2 V + (1-t)^3 W
|
||||||
double time(Point pt1, Point ct1, Point ct2, Point pt2, double x) {
|
double time(Point pt1, Point ct1, Point ct2, Point pt2, double x) {
|
||||||
@@ -31,7 +29,8 @@ double time(Point pt1, Point ct1, Point ct2, Point pt2, double x) {
|
|||||||
final num c = 3 * ct2.x - 3 * pt2.x;
|
final num c = 3 * ct2.x - 3 * pt2.x;
|
||||||
final num d = pt2.x - x;
|
final num d = pt2.x - x;
|
||||||
|
|
||||||
if (a.abs() < 0.000000001) { // quadratic
|
if (a.abs() < 0.000000001) {
|
||||||
|
// quadratic
|
||||||
if (b.abs() < 0.000000001) return -d / c; // linear
|
if (b.abs() < 0.000000001) return -d / c; // linear
|
||||||
|
|
||||||
final qb = c / b;
|
final qb = c / b;
|
||||||
@@ -45,7 +44,8 @@ double time(Point pt1, Point ct1, Point ct2, Point pt2, double x) {
|
|||||||
final addcoef = -b / (3 * a);
|
final addcoef = -b / (3 * a);
|
||||||
|
|
||||||
final lmbd = sq(q) / 4 + cb(p) / 27;
|
final lmbd = sq(q) / 4 + cb(p) / 27;
|
||||||
if (lmbd >= 0) { // real
|
if (lmbd >= 0) {
|
||||||
|
// real
|
||||||
final sqlambda = sqrt(lmbd);
|
final sqlambda = sqrt(lmbd);
|
||||||
final tmp = thrt(-q / 2 + (q < 0 ? sqlambda : -sqlambda));
|
final tmp = thrt(-q / 2 + (q < 0 ? sqlambda : -sqlambda));
|
||||||
return tmp - p / (3 * tmp) + addcoef;
|
return tmp - p / (3 * tmp) + addcoef;
|
||||||
@@ -84,4 +84,4 @@ double value(Point pt1, Point ct1, Point ct2, Point pt2, double x) {
|
|||||||
// for i in range(0,part+1,1):
|
// for i in range(0,part+1,1):
|
||||||
// x = float(i) / part
|
// x = float(i) / part
|
||||||
// y = value(pt1, ct1, ct2, pt2, x)
|
// y = value(pt1, ct1, ct2, pt2, x)
|
||||||
// f.write("%f %f\n" % (x,y))
|
// f.write("%f %f\n" % (x,y))
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
/// This file contains classes for the different types of SVG path segments as
|
/// 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.
|
/// well as a Path object that contains a sequence of path segments.
|
||||||
|
library;
|
||||||
|
|
||||||
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;
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:bisection/extension.dart';
|
import 'package:bisection/extension.dart';
|
||||||
|
|
||||||
import '../common/point.dart';
|
import 'point.dart';
|
||||||
|
|
||||||
num radians(num n) => n * pi / 180;
|
num radians(num n) => n * pi / 180;
|
||||||
num degrees(num n) => n * 180 / pi;
|
num degrees(num n) => n * 180 / pi;
|
||||||
@@ -69,10 +71,7 @@ abstract class SvgPath {
|
|||||||
final Point start;
|
final Point start;
|
||||||
final Point end;
|
final Point end;
|
||||||
|
|
||||||
const SvgPath({
|
const SvgPath({required this.start, required this.end});
|
||||||
required this.start,
|
|
||||||
required this.end,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
@@ -89,10 +88,7 @@ abstract class SvgPath {
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract class Bezier extends SvgPath {
|
abstract class Bezier extends SvgPath {
|
||||||
const Bezier({
|
const Bezier({required super.start, required super.end});
|
||||||
required Point start,
|
|
||||||
required Point end,
|
|
||||||
}) : super(start: start, end: end);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => other is Bezier && super == other;
|
bool operator ==(Object other) => other is Bezier && super == other;
|
||||||
@@ -107,10 +103,7 @@ abstract class Bezier extends SvgPath {
|
|||||||
/// A straight line
|
/// A straight line
|
||||||
/// The base for Line() and Close().
|
/// The base for Line() and Close().
|
||||||
class Linear extends SvgPath {
|
class Linear extends SvgPath {
|
||||||
const Linear({
|
const Linear({required super.start, required super.end});
|
||||||
required Point start,
|
|
||||||
required Point end,
|
|
||||||
}) : super(start: start, end: end);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => other is Linear && super == other;
|
bool operator ==(Object other) => other is Linear && super == other;
|
||||||
@@ -129,10 +122,7 @@ class Linear extends SvgPath {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Line extends Linear {
|
class Line extends Linear {
|
||||||
const Line({
|
const Line({required super.start, required super.end});
|
||||||
required Point start,
|
|
||||||
required Point end,
|
|
||||||
}) : super(start: start, end: end);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => other is Line && super == other;
|
bool operator ==(Object other) => other is Line && super == other;
|
||||||
@@ -151,11 +141,11 @@ class CubicBezier extends Bezier {
|
|||||||
final Point control2;
|
final Point control2;
|
||||||
|
|
||||||
const CubicBezier({
|
const CubicBezier({
|
||||||
required Point start,
|
required super.start,
|
||||||
required this.control1,
|
required this.control1,
|
||||||
required this.control2,
|
required this.control2,
|
||||||
required Point end,
|
required super.end,
|
||||||
}) : super(start: start, end: end);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
@@ -168,13 +158,14 @@ class CubicBezier extends Bezier {
|
|||||||
int get hashCode => super.hashCode ^ control1.hashCode ^ control2.hashCode;
|
int get hashCode => super.hashCode ^ control1.hashCode ^ control2.hashCode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => "CubicBezier(start=$start, control1=$control1, "
|
String toString() =>
|
||||||
|
"CubicBezier(start=$start, control1=$control1, "
|
||||||
"control2=$control2, end=$end)";
|
"control2=$control2, end=$end)";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool isSmoothFrom(Object? previous) => previous is CubicBezier
|
bool isSmoothFrom(Object? previous) => previous is CubicBezier
|
||||||
? start == previous.end &&
|
? start == previous.end &&
|
||||||
control1 - start == previous.end - previous.control2
|
control1 - start == previous.end - previous.control2
|
||||||
: control1 == start;
|
: control1 == start;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -205,10 +196,10 @@ class QuadraticBezier extends Bezier {
|
|||||||
final Point control;
|
final Point control;
|
||||||
|
|
||||||
const QuadraticBezier({
|
const QuadraticBezier({
|
||||||
required Point start,
|
required super.start,
|
||||||
required Point end,
|
required super.end,
|
||||||
required this.control,
|
required this.control,
|
||||||
}) : super(start: start, end: end);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
@@ -224,7 +215,7 @@ class QuadraticBezier extends Bezier {
|
|||||||
@override
|
@override
|
||||||
bool isSmoothFrom(Object? previous) => previous is QuadraticBezier
|
bool isSmoothFrom(Object? previous) => previous is QuadraticBezier
|
||||||
? start == previous.end &&
|
? start == previous.end &&
|
||||||
(control - start) == (previous.end - previous.control)
|
(control - start) == (previous.end - previous.control)
|
||||||
: control == start;
|
: control == start;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -258,7 +249,8 @@ class QuadraticBezier extends Bezier {
|
|||||||
final num c2 = 2 * sqrt(C);
|
final num c2 = 2 * sqrt(C);
|
||||||
final num bA = B / a2;
|
final num bA = B / a2;
|
||||||
|
|
||||||
s = (a32 * sabc +
|
s =
|
||||||
|
(a32 * sabc +
|
||||||
a2 * B * (sabc - c2) +
|
a2 * B * (sabc - c2) +
|
||||||
(4 * C * A - (B * B)) * log((2 * a2 + bA + sabc) / (bA + c2))) /
|
(4 * C * A - (B * B)) * log((2 * a2 + bA + sabc) / (bA + c2))) /
|
||||||
(4 * a32);
|
(4 * a32);
|
||||||
@@ -280,13 +272,13 @@ class Arc extends SvgPath {
|
|||||||
// late num delta;
|
// late num delta;
|
||||||
|
|
||||||
const Arc({
|
const Arc({
|
||||||
required Point start,
|
required super.start,
|
||||||
required Point end,
|
required super.end,
|
||||||
required this.radius,
|
required this.radius,
|
||||||
required this.rotation,
|
required this.rotation,
|
||||||
required this.arc,
|
required this.arc,
|
||||||
required this.sweep,
|
required this.sweep,
|
||||||
}) : super(start: start, end: end);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
@@ -306,10 +298,10 @@ class Arc extends SvgPath {
|
|||||||
sweep.hashCode;
|
sweep.hashCode;
|
||||||
|
|
||||||
@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)';
|
||||||
|
|
||||||
|
|
||||||
// Conversion from endpoint to center parameterization
|
// Conversion from endpoint to center parameterization
|
||||||
// http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
|
// http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
|
||||||
num get _cosr => cos(radians(rotation));
|
num get _cosr => cos(radians(rotation));
|
||||||
@@ -337,16 +329,17 @@ class Arc extends SvgPath {
|
|||||||
num get _cyprim => -c * _ry * _x1prim / _rx;
|
num get _cyprim => -c * _ry * _x1prim / _rx;
|
||||||
|
|
||||||
num get radiusScale {
|
num get radiusScale {
|
||||||
final rs = (_x1primSq / (radius.x * radius.x)) +
|
final rs =
|
||||||
|
(_x1primSq / (radius.x * radius.x)) +
|
||||||
(_y1primSq / (radius.y * radius.y));
|
(_y1primSq / (radius.y * radius.y));
|
||||||
return rs > 1 ? sqrt(rs) : 1;
|
return rs > 1 ? sqrt(rs) : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Point get center => Point(
|
Point get center => Point(
|
||||||
(_cosr * _cxprim - _sinr * _cyprim) + ((start.x + end.x) / 2),
|
(_cosr * _cxprim - _sinr * _cyprim) + ((start.x + end.x) / 2),
|
||||||
(_sinr * _cxprim + _cosr * _cyprim) + ((start.y + end.y) / 2),
|
(_sinr * _cxprim + _cosr * _cyprim) + ((start.y + end.y) / 2),
|
||||||
);
|
);
|
||||||
|
|
||||||
num get theta {
|
num get theta {
|
||||||
final num n = sqrt(_ux * _ux + _uy * _uy);
|
final num n = sqrt(_ux * _ux + _uy * _uy);
|
||||||
final num p = _ux;
|
final num p = _ux;
|
||||||
@@ -365,7 +358,8 @@ class Arc extends SvgPath {
|
|||||||
d = -1.0;
|
d = -1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ((((_ux * _vy - _uy * _vx) < 0) ? -1 : 1) * degrees(acos(d))) % 360 - (!sweep ? 360 : 0);
|
return ((((_ux * _vy - _uy * _vx) < 0) ? -1 : 1) * degrees(acos(d))) % 360 -
|
||||||
|
(!sweep ? 360 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -375,7 +369,7 @@ class Arc extends SvgPath {
|
|||||||
|
|
||||||
// This should be treated as a straight line
|
// This should be treated as a straight line
|
||||||
if (this.radius.x == 0 || this.radius.y == 0) {
|
if (this.radius.x == 0 || this.radius.y == 0) {
|
||||||
return start + (end - start) * pos;
|
return start + (end - start).times(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
final angle = radians(theta + pos * delta);
|
final angle = radians(theta + pos * delta);
|
||||||
@@ -415,14 +409,15 @@ class Arc extends SvgPath {
|
|||||||
final startPoint = point(0);
|
final startPoint = point(0);
|
||||||
final endPoint = point(1);
|
final endPoint = point(1);
|
||||||
return segmentLength(
|
return segmentLength(
|
||||||
curve: this,
|
curve: this,
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 1,
|
end: 1,
|
||||||
startPoint: startPoint,
|
startPoint: startPoint,
|
||||||
endPoint: endPoint,
|
endPoint: endPoint,
|
||||||
error: error,
|
error: error,
|
||||||
minDepth: minDepth,
|
minDepth: minDepth,
|
||||||
depth: 0);
|
depth: 0,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,10 +444,7 @@ class Move extends SvgPath {
|
|||||||
|
|
||||||
/// Represents the closepath command
|
/// Represents the closepath command
|
||||||
class Close extends Linear {
|
class Close extends Linear {
|
||||||
const Close({
|
const Close({required super.start, required super.end});
|
||||||
required Point start,
|
|
||||||
required Point end,
|
|
||||||
}) : super(start: start, end: end);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => other is Close && super == other;
|
bool operator ==(Object other) => other is Close && super == other;
|
||||||
@@ -502,12 +494,14 @@ class Path extends ListBase<SvgPath> {
|
|||||||
String toString() =>
|
String toString() =>
|
||||||
'Path(${[for (final s in segments) s.toString()].join(", ")})';
|
'Path(${[for (final s in segments) s.toString()].join(", ")})';
|
||||||
|
|
||||||
void _calcLengths(
|
void _calcLengths({
|
||||||
{num error = defaultError, int minDepth = defaultMinDepth}) {
|
num error = defaultError,
|
||||||
|
int minDepth = defaultMinDepth,
|
||||||
|
}) {
|
||||||
if (_memoizedLength != null) return;
|
if (_memoizedLength != null) return;
|
||||||
|
|
||||||
final lengths = [
|
final lengths = [
|
||||||
for (final s in segments) s!.size(error: error, minDepth: minDepth)
|
for (final s in segments) s!.size(error: error, minDepth: minDepth),
|
||||||
];
|
];
|
||||||
_memoizedLength = lengths.reduce((a, b) => a + b);
|
_memoizedLength = lengths.reduce((a, b) => a + b);
|
||||||
if (_memoizedLength == 0) {
|
if (_memoizedLength == 0) {
|
||||||
@@ -552,7 +546,7 @@ class Path extends ListBase<SvgPath> {
|
|||||||
return segments[i]!.point(segmentPos);
|
return segments[i]!.point(segmentPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
num size({error = defaultError, minDepth = defaultMinDepth}) {
|
num size({double error = defaultError, int minDepth = defaultMinDepth}) {
|
||||||
_calcLengths(error: error, minDepth: minDepth);
|
_calcLengths(error: error, minDepth: minDepth);
|
||||||
return _memoizedLength!;
|
return _memoizedLength!;
|
||||||
}
|
}
|
||||||
@@ -579,27 +573,35 @@ class Path extends ListBase<SvgPath> {
|
|||||||
parts.add("M ${coord(segment.start)}");
|
parts.add("M ${coord(segment.start)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (segment is Line) {
|
switch (segment) {
|
||||||
parts.add("L ${coord(segment.end)}");
|
case Line _:
|
||||||
} else if (segment is CubicBezier) {
|
parts.add("L ${coord(segment.end)}");
|
||||||
if (segment.isSmoothFrom(previousSegment)) {
|
break;
|
||||||
parts.add("S ${coord(segment.control2)} ${coord(segment.end)}");
|
|
||||||
} else {
|
case CubicBezier _:
|
||||||
|
if (segment.isSmoothFrom(previousSegment)) {
|
||||||
|
parts.add("S ${coord(segment.control2)} ${coord(segment.end)}");
|
||||||
|
} else {
|
||||||
|
parts.add(
|
||||||
|
"C ${coord(segment.control1)} ${coord(segment.control2)} ${coord(segment.end)}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case QuadraticBezier _:
|
||||||
|
if (segment.isSmoothFrom(previousSegment)) {
|
||||||
|
parts.add("T ${coord(segment.end)}");
|
||||||
|
} else {
|
||||||
|
parts.add("Q ${coord(segment.control)} ${coord(segment.end)}");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Arc _:
|
||||||
parts.add(
|
parts.add(
|
||||||
"C ${coord(segment.control1)} ${coord(segment.control2)} ${coord(segment.end)}",
|
"A ${coord(segment.radius)} ${formatNumber(segment.rotation)} "
|
||||||
|
"${segment.arc ? 1 : 0},${segment.sweep ? 1 : 0} ${coord(segment.end)}",
|
||||||
);
|
);
|
||||||
}
|
break;
|
||||||
} else if (segment is QuadraticBezier) {
|
|
||||||
if (segment.isSmoothFrom(previousSegment)) {
|
|
||||||
parts.add("T ${coord(segment.end)}");
|
|
||||||
} else {
|
|
||||||
parts.add("Q ${coord(segment.control)} ${coord(segment.end)}");
|
|
||||||
}
|
|
||||||
} else if (segment is Arc) {
|
|
||||||
parts.add(
|
|
||||||
"A ${coord(segment.radius)} ${formatNumber(segment.rotation)} "
|
|
||||||
"${segment.arc ? 1 : 0},${segment.sweep ? 1 : 0} ${coord(segment.end)}",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPos = segment.end;
|
currentPos = segment.end;
|
||||||
@@ -608,4 +610,94 @@ class Path extends ListBase<SvgPath> {
|
|||||||
|
|
||||||
return parts.join(" ").toUpperCase();
|
return parts.join(" ").toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert the path into a dart:ui.Path.
|
||||||
|
ui.Path toUiPath() {
|
||||||
|
final ui.Path p = ui.Path();
|
||||||
|
bool started = false;
|
||||||
|
|
||||||
|
for (final seg in this) {
|
||||||
|
switch (seg) {
|
||||||
|
case Move(:final start):
|
||||||
|
p.moveTo(start.x.toDouble(), start.y.toDouble());
|
||||||
|
started = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Line(:final start, :final end):
|
||||||
|
if (!started) {
|
||||||
|
p.moveTo(start.x.toDouble(), start.y.toDouble());
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
p.lineTo(end.x.toDouble(), end.y.toDouble());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Close():
|
||||||
|
p.close();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CubicBezier(
|
||||||
|
:final start,
|
||||||
|
:final control1,
|
||||||
|
:final control2,
|
||||||
|
:final end,
|
||||||
|
):
|
||||||
|
if (!started) {
|
||||||
|
p.moveTo(start.x.toDouble(), start.y.toDouble());
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
p.cubicTo(
|
||||||
|
control1.x.toDouble(),
|
||||||
|
control1.y.toDouble(),
|
||||||
|
control2.x.toDouble(),
|
||||||
|
control2.y.toDouble(),
|
||||||
|
end.x.toDouble(),
|
||||||
|
end.y.toDouble(),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case QuadraticBezier(:final start, :final control, :final end):
|
||||||
|
if (!started) {
|
||||||
|
p.moveTo(start.x.toDouble(), start.y.toDouble());
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
p.quadraticBezierTo(
|
||||||
|
control.x.toDouble(),
|
||||||
|
control.y.toDouble(),
|
||||||
|
end.x.toDouble(),
|
||||||
|
end.y.toDouble(),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Arc(
|
||||||
|
:final start,
|
||||||
|
:final radius,
|
||||||
|
:final rotation,
|
||||||
|
:final arc,
|
||||||
|
:final sweep,
|
||||||
|
:final end,
|
||||||
|
):
|
||||||
|
if (!started) {
|
||||||
|
p.moveTo(start.x.toDouble(), start.y.toDouble());
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
// rotation is in degrees in svg; arcToPoint expects radians.
|
||||||
|
final r = ui.Radius.elliptical(
|
||||||
|
radius.x.toDouble(),
|
||||||
|
radius.y.toDouble(),
|
||||||
|
);
|
||||||
|
p.arcToPoint(
|
||||||
|
ui.Offset(end.x.toDouble(), end.y.toDouble()),
|
||||||
|
radius: r,
|
||||||
|
rotation: rotation.toDouble() * (math.pi / 180.0),
|
||||||
|
largeArc: arc,
|
||||||
|
clockwise: sweep,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case _:
|
||||||
|
throw Exception('Unknown segment type: ${seg.runtimeType}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,10 +8,11 @@ class Point {
|
|||||||
const Point.from({this.x = 0, this.y = 0});
|
const Point.from({this.x = 0, this.y = 0});
|
||||||
static const zero = Point(0, 0);
|
static const zero = Point(0, 0);
|
||||||
|
|
||||||
operator +(covariant Point p) => Point(x + p.x, y + p.y);
|
Point operator +(covariant Point p) => Point(x + p.x, y + p.y);
|
||||||
operator -(covariant Point p) => Point(x - p.x, y - p.y);
|
Point operator -(covariant Point p) => Point(x - p.x, y - p.y);
|
||||||
operator *(covariant Point p) => Point(x * p.x, y * p.y);
|
Point operator *(covariant Point p) => Point(x * p.x, y * p.y);
|
||||||
operator /(covariant Point p) => Point(x / p.x, y / p.y);
|
Point operator /(covariant Point p) => Point(x / p.x, y / p.y);
|
||||||
|
Point operator -() => Point(-x, -y);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
/// SVG Path specification parser
|
/// SVG Path specification parser
|
||||||
|
///
|
||||||
|
/// See https://pypi.org/project/svg.path/ for the original implementation.
|
||||||
|
library;
|
||||||
|
|
||||||
import '../common/point.dart';
|
import 'primitives/point.dart';
|
||||||
import 'path.dart';
|
import 'primitives/path.dart';
|
||||||
|
|
||||||
const _commands = {
|
const _commands = {
|
||||||
'M',
|
'M',
|
||||||
@@ -23,13 +26,13 @@ const _commands = {
|
|||||||
'T',
|
'T',
|
||||||
't',
|
't',
|
||||||
'A',
|
'A',
|
||||||
'a'
|
'a',
|
||||||
};
|
};
|
||||||
|
|
||||||
// const _uppercaseCommands = {'M', 'Z', 'L', 'H', 'V', 'C', 'S', 'Q', 'T', 'A'};
|
// const _uppercaseCommands = {'M', 'Z', 'L', 'H', 'V', 'C', 'S', 'Q', 'T', 'A'};
|
||||||
|
|
||||||
final _commandPattern = RegExp("(?=[${_commands.join('')}])");
|
final _commandPattern = RegExp("(?=[${_commands.join('')}])");
|
||||||
final _floatPattern = RegExp(r"^[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?");
|
final _floatPattern = RegExp(r"^[-+]?(?:[0-9]+\.?[0-9]*|[0-9]*\.?[0-9]+)(?:[eE][-+]?[0-9]+)?");
|
||||||
|
|
||||||
class ParserResult<T> {
|
class ParserResult<T> {
|
||||||
final T value;
|
final T value;
|
||||||
@@ -179,8 +182,9 @@ class Token {
|
|||||||
other is Token &&
|
other is Token &&
|
||||||
command == other.command &&
|
command == other.command &&
|
||||||
args.length == other.args.length &&
|
args.length == other.args.length &&
|
||||||
![for (int i = 0; i < args.length; i++) args[i] == other.args[i]]
|
![
|
||||||
.any((b) => !b);
|
for (int i = 0; i < args.length; i++) args[i] == other.args[i],
|
||||||
|
].any((b) => !b);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => command.hashCode ^ args.hashCode;
|
int get hashCode => command.hashCode ^ args.hashCode;
|
||||||
@@ -356,7 +360,8 @@ Path parsePath(String pathdef) {
|
|||||||
// 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 = currentPos +
|
control =
|
||||||
|
currentPos +
|
||||||
currentPos -
|
currentPos -
|
||||||
(segments.last as QuadraticBezier).control;
|
(segments.last as QuadraticBezier).control;
|
||||||
} else {
|
} else {
|
||||||
+484
@@ -0,0 +1,484 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import 'package:kanimaji/animator.dart';
|
||||||
|
import 'package:kanimaji/kanjivg_parser.dart';
|
||||||
|
|
||||||
|
Future<KanjiVGItem> _defaultKanjiDataProvider(String kanji) async {
|
||||||
|
final hex = kanji.runes.isEmpty
|
||||||
|
? '00000'
|
||||||
|
: kanji.runes.first.toRadixString(16).padLeft(5, '0');
|
||||||
|
final assetPath = 'assets/kanjivg/kanji/$hex.svg';
|
||||||
|
final svgString = await rootBundle.loadString(assetPath);
|
||||||
|
return KanjiVGItem.parseFromXml(svgString);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that animates the stroke order of a given kanji character using KanjiVG data.
|
||||||
|
class Kanimaji extends StatefulWidget {
|
||||||
|
final String kanji;
|
||||||
|
final FutureOr<KanjiVGItem> Function(String kanji) kanjiDataProvider;
|
||||||
|
|
||||||
|
// Animation parameters
|
||||||
|
final bool loop;
|
||||||
|
// final Duration delayBetweenStrokes;
|
||||||
|
// final Duration delayBetweenLoops;
|
||||||
|
// TODO: add support for specifying animation bezier curve
|
||||||
|
// final Cubic animationCurve;
|
||||||
|
// final double speed;
|
||||||
|
|
||||||
|
// Brush parameters
|
||||||
|
final bool showBrush;
|
||||||
|
final Color brushColor;
|
||||||
|
final double brushRadius;
|
||||||
|
|
||||||
|
// Stroke parameters
|
||||||
|
final Color strokeColor;
|
||||||
|
final Color strokeUnfilledColor;
|
||||||
|
final double strokeWidth;
|
||||||
|
final Color backgroundColor;
|
||||||
|
|
||||||
|
// Stroke number parameters
|
||||||
|
final bool showStrokeNumbers;
|
||||||
|
final Color strokeNumberColor;
|
||||||
|
final Color currentStrokeNumberColor;
|
||||||
|
final double strokeNumberFontSize;
|
||||||
|
final String? strokeNumberFontFamily;
|
||||||
|
|
||||||
|
// Cross parameters
|
||||||
|
final bool showCross;
|
||||||
|
final Color crossColor;
|
||||||
|
final double crossStrokeWidth;
|
||||||
|
final double crossStipleLength;
|
||||||
|
final double crossStipleGap;
|
||||||
|
|
||||||
|
const Kanimaji({
|
||||||
|
super.key,
|
||||||
|
required this.kanji,
|
||||||
|
this.kanjiDataProvider = _defaultKanjiDataProvider,
|
||||||
|
this.loop = true,
|
||||||
|
this.strokeColor = Colors.black,
|
||||||
|
this.strokeUnfilledColor = const Color(0xA0DDDDDD),
|
||||||
|
this.strokeWidth = 3.0,
|
||||||
|
this.backgroundColor = Colors.transparent,
|
||||||
|
this.showBrush = true,
|
||||||
|
this.brushColor = Colors.red,
|
||||||
|
this.brushRadius = 4.0,
|
||||||
|
this.showStrokeNumbers = true,
|
||||||
|
this.strokeNumberColor = Colors.grey,
|
||||||
|
this.currentStrokeNumberColor = Colors.red,
|
||||||
|
this.strokeNumberFontSize = 4.0,
|
||||||
|
this.strokeNumberFontFamily,
|
||||||
|
this.showCross = true,
|
||||||
|
this.crossColor = const Color(0x40AAAAAA),
|
||||||
|
this.crossStrokeWidth = 0.8,
|
||||||
|
this.crossStipleLength = 5.0,
|
||||||
|
this.crossStipleGap = 3.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<Kanimaji> createState() => _KanimajiState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _KanimajiState extends State<Kanimaji>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
KanjiVGItem? _kanjiData;
|
||||||
|
String? _error;
|
||||||
|
late AnimationController _controller;
|
||||||
|
|
||||||
|
List<double> get _pathLengths =>
|
||||||
|
_kanjiData?.paths
|
||||||
|
.map((p) => p.svgPath.size(error: 1e-8).toDouble())
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
List<double> get _pathDurations =>
|
||||||
|
_pathLengths.map((len) => strokeLengthToDuration(len)).toList();
|
||||||
|
|
||||||
|
static const double _viewBoxWidth = 109.0;
|
||||||
|
static const double _viewBoxHeight = 109.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(vsync: this);
|
||||||
|
_controller.addListener(_onTick);
|
||||||
|
_loadAndParseSvg().then((_) => _configureController());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant Kanimaji oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.kanji != widget.kanji) {
|
||||||
|
_loadAndParseSvg().then((_) => _configureController());
|
||||||
|
} else if (oldWidget.loop != widget.loop ||
|
||||||
|
oldWidget.strokeColor != widget.strokeColor ||
|
||||||
|
oldWidget.strokeUnfilledColor != widget.strokeUnfilledColor ||
|
||||||
|
oldWidget.strokeWidth != widget.strokeWidth ||
|
||||||
|
oldWidget.backgroundColor != widget.backgroundColor ||
|
||||||
|
oldWidget.showBrush != widget.showBrush ||
|
||||||
|
oldWidget.brushColor != widget.brushColor ||
|
||||||
|
oldWidget.brushRadius != widget.brushRadius ||
|
||||||
|
oldWidget.showStrokeNumbers != widget.showStrokeNumbers ||
|
||||||
|
oldWidget.strokeNumberColor != widget.strokeNumberColor ||
|
||||||
|
oldWidget.currentStrokeNumberColor != widget.currentStrokeNumberColor ||
|
||||||
|
oldWidget.strokeNumberFontSize != widget.strokeNumberFontSize ||
|
||||||
|
oldWidget.strokeNumberFontFamily != widget.strokeNumberFontFamily ||
|
||||||
|
oldWidget.showCross != widget.showCross ||
|
||||||
|
oldWidget.crossColor != widget.crossColor ||
|
||||||
|
oldWidget.crossStrokeWidth != widget.crossStrokeWidth ||
|
||||||
|
oldWidget.crossStipleLength != widget.crossStipleLength ||
|
||||||
|
oldWidget.crossStipleGap != widget.crossStipleGap) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTick() {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.removeListener(_onTick);
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadAndParseSvg() async {
|
||||||
|
try {
|
||||||
|
final data = await widget.kanjiDataProvider(widget.kanji);
|
||||||
|
setState(() {
|
||||||
|
_kanjiData = data;
|
||||||
|
_error = null;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_kanjiData = null;
|
||||||
|
_error = 'Error loading/parsing kanji data: $e';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _configureController() {
|
||||||
|
if (_kanjiData == null) return;
|
||||||
|
final double totalSec = _pathDurations.fold(0.0, (a, b) => a + b);
|
||||||
|
|
||||||
|
_controller.stop();
|
||||||
|
_controller.duration = Duration(milliseconds: (totalSec * 1000.0).round());
|
||||||
|
if (widget.loop) {
|
||||||
|
_controller.repeat();
|
||||||
|
} else {
|
||||||
|
_controller.forward(from: 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_kanjiData == null && _error == null) {
|
||||||
|
return SizedBox.expand(child: Center(child: CircularProgressIndicator()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_error != null) {
|
||||||
|
return SizedBox.expand(child: ErrorWidget(_error!));
|
||||||
|
}
|
||||||
|
|
||||||
|
return FittedBox(
|
||||||
|
child: SizedBox(
|
||||||
|
width: _viewBoxWidth,
|
||||||
|
height: _viewBoxHeight,
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: _KanimajiPainter(
|
||||||
|
paths: _kanjiData!.paths.map((p) => p.svgPath.toUiPath()).toList(),
|
||||||
|
pathLengths: _pathLengths,
|
||||||
|
pathDurations: _pathDurations,
|
||||||
|
strokeNumbers: _kanjiData!.strokeNumbers,
|
||||||
|
progress: _controller.value,
|
||||||
|
strokeColor: widget.strokeColor,
|
||||||
|
strokeUnfilledColor: widget.strokeUnfilledColor,
|
||||||
|
strokeWidth: widget.strokeWidth,
|
||||||
|
backgroundColor: widget.backgroundColor,
|
||||||
|
viewBoxWidth: _viewBoxWidth,
|
||||||
|
viewBoxHeight: _viewBoxHeight,
|
||||||
|
showBrush: widget.showBrush,
|
||||||
|
brushColor: widget.brushColor,
|
||||||
|
brushRadius: widget.brushRadius,
|
||||||
|
showStrokeNumbers: widget.showStrokeNumbers,
|
||||||
|
strokeNumberColor: widget.strokeNumberColor,
|
||||||
|
currentStrokeNumberColor: widget.currentStrokeNumberColor,
|
||||||
|
strokeNumberFontSize: widget.strokeNumberFontSize,
|
||||||
|
strokeNumberFontFamily: widget.strokeNumberFontFamily,
|
||||||
|
showCross: widget.showCross,
|
||||||
|
crossColor: widget.crossColor,
|
||||||
|
crossStrokeWidth: widget.crossStrokeWidth,
|
||||||
|
crossStipleLength: widget.crossStipleLength,
|
||||||
|
crossStipleGap: widget.crossStipleGap,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _KanimajiPainter extends CustomPainter {
|
||||||
|
final double progress; // 0..1
|
||||||
|
|
||||||
|
final List<ui.Path> paths;
|
||||||
|
final List<double> pathLengths;
|
||||||
|
final List<double> pathDurations;
|
||||||
|
|
||||||
|
// TODO: don't recalculate these all the time, compute once and cache
|
||||||
|
List<double> get absolutePathDurations {
|
||||||
|
final List<double> absolute = [];
|
||||||
|
double sum = 0.0;
|
||||||
|
for (final dur in pathDurations) {
|
||||||
|
absolute.add(sum);
|
||||||
|
sum += dur;
|
||||||
|
}
|
||||||
|
return absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<KanjiStrokeNumber> strokeNumbers;
|
||||||
|
|
||||||
|
final double viewBoxWidth;
|
||||||
|
final double viewBoxHeight;
|
||||||
|
|
||||||
|
final Color strokeColor;
|
||||||
|
final Color strokeUnfilledColor;
|
||||||
|
final double strokeWidth;
|
||||||
|
final Color backgroundColor;
|
||||||
|
|
||||||
|
final bool showBrush;
|
||||||
|
final Color brushColor;
|
||||||
|
final double brushRadius;
|
||||||
|
|
||||||
|
final bool showStrokeNumbers;
|
||||||
|
final Color strokeNumberColor;
|
||||||
|
final Color currentStrokeNumberColor;
|
||||||
|
final double strokeNumberFontSize;
|
||||||
|
final String? strokeNumberFontFamily;
|
||||||
|
|
||||||
|
final bool showCross;
|
||||||
|
final Color crossColor;
|
||||||
|
final double crossStrokeWidth;
|
||||||
|
final double crossStipleLength;
|
||||||
|
final double crossStipleGap;
|
||||||
|
|
||||||
|
_KanimajiPainter({
|
||||||
|
required this.progress,
|
||||||
|
|
||||||
|
required this.paths,
|
||||||
|
required this.pathLengths,
|
||||||
|
required this.pathDurations,
|
||||||
|
required this.strokeNumbers,
|
||||||
|
|
||||||
|
required this.viewBoxWidth,
|
||||||
|
required this.viewBoxHeight,
|
||||||
|
|
||||||
|
required this.strokeColor,
|
||||||
|
required this.strokeUnfilledColor,
|
||||||
|
required this.strokeWidth,
|
||||||
|
required this.backgroundColor,
|
||||||
|
|
||||||
|
required this.showBrush,
|
||||||
|
required this.brushColor,
|
||||||
|
required this.brushRadius,
|
||||||
|
|
||||||
|
required this.showStrokeNumbers,
|
||||||
|
required this.strokeNumberColor,
|
||||||
|
required this.currentStrokeNumberColor,
|
||||||
|
required this.strokeNumberFontSize,
|
||||||
|
required this.strokeNumberFontFamily,
|
||||||
|
|
||||||
|
required this.showCross,
|
||||||
|
required this.crossColor,
|
||||||
|
required this.crossStrokeWidth,
|
||||||
|
required this.crossStipleLength,
|
||||||
|
required this.crossStipleGap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final bgPaint = Paint()..color = backgroundColor;
|
||||||
|
canvas.drawRect(Offset.zero & size, bgPaint);
|
||||||
|
|
||||||
|
if (paths.isEmpty) return;
|
||||||
|
|
||||||
|
final double sx = size.width / viewBoxWidth;
|
||||||
|
final double sy = size.height / viewBoxHeight;
|
||||||
|
final double scale = math.min(sx, sy);
|
||||||
|
|
||||||
|
final double dx = (size.width - viewBoxWidth * scale) / 2.0;
|
||||||
|
final double dy = (size.height - viewBoxHeight * scale) / 2.0;
|
||||||
|
|
||||||
|
final Paint unfilledPaint = Paint()
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeCap = StrokeCap.round
|
||||||
|
..strokeJoin = StrokeJoin.round
|
||||||
|
..strokeWidth = strokeWidth / scale
|
||||||
|
..color = strokeUnfilledColor;
|
||||||
|
|
||||||
|
final Paint filledPaint = Paint()
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeCap = StrokeCap.round
|
||||||
|
..strokeJoin = StrokeJoin.round
|
||||||
|
..strokeWidth = strokeWidth / scale
|
||||||
|
..color = strokeColor;
|
||||||
|
|
||||||
|
final Paint brushPaint = Paint()
|
||||||
|
..style = PaintingStyle.fill
|
||||||
|
..color = brushColor;
|
||||||
|
|
||||||
|
final Paint crossPaint = Paint()
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..color = crossColor
|
||||||
|
..strokeWidth = crossStrokeWidth;
|
||||||
|
|
||||||
|
// Draw cross if enabled
|
||||||
|
if (showCross) {
|
||||||
|
// Draw vertical stipled line
|
||||||
|
for (
|
||||||
|
double y = 0;
|
||||||
|
y < size.height;
|
||||||
|
y += crossStipleLength + crossStipleGap
|
||||||
|
) {
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(size.width / 2, y),
|
||||||
|
Offset(size.width / 2, math.min(y + crossStipleLength, size.height)),
|
||||||
|
crossPaint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Draw horizontal stipled line
|
||||||
|
for (
|
||||||
|
double x = 0;
|
||||||
|
x < size.width;
|
||||||
|
x += crossStipleLength + crossStipleGap
|
||||||
|
) {
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(x, size.height / 2),
|
||||||
|
Offset(math.min(x + crossStipleLength, size.width), size.height / 2),
|
||||||
|
crossPaint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw all unfilled paths
|
||||||
|
for (final path in paths) {
|
||||||
|
canvas.drawPath(path, unfilledPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.save();
|
||||||
|
canvas.translate(dx, dy);
|
||||||
|
canvas.scale(scale, scale);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
final int currentlyDrawingIndex = absolutePathDurations.lastIndexWhere(
|
||||||
|
(t) => t <= p * totalTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentlyDrawingIndex == -1) {
|
||||||
|
for (final path in paths) {
|
||||||
|
canvas.drawPath(path, filledPaint);
|
||||||
|
}
|
||||||
|
canvas.restore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw all completed strokes fully filled
|
||||||
|
for (int i = 0; i < currentlyDrawingIndex; i++) {
|
||||||
|
canvas.drawPath(paths[i], filledPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the currently drawing stroke with partial coverage
|
||||||
|
if (currentlyDrawingIndex >= 0 && currentlyDrawingIndex < paths.length) {
|
||||||
|
final ui.Path path = paths[currentlyDrawingIndex];
|
||||||
|
final double len = pathLengths[currentlyDrawingIndex];
|
||||||
|
final double dur = pathDurations[currentlyDrawingIndex];
|
||||||
|
|
||||||
|
final relativeElapsedTime =
|
||||||
|
p * totalTime -
|
||||||
|
(currentlyDrawingIndex > 0
|
||||||
|
? absolutePathDurations[currentlyDrawingIndex]
|
||||||
|
: 0.0);
|
||||||
|
|
||||||
|
final double strokeProgress = (relativeElapsedTime / dur).clamp(0.0, 1.0);
|
||||||
|
final ui.PathMetrics metrics = path.computeMetrics();
|
||||||
|
final ui.PathMetric metric = metrics.first;
|
||||||
|
final double drawLength = len * strokeProgress;
|
||||||
|
final ui.Path partialPath = metric.extractPath(0, drawLength);
|
||||||
|
canvas.drawPath(partialPath, filledPaint);
|
||||||
|
|
||||||
|
if (showBrush) {
|
||||||
|
final ui.Tangent? tangent = metric.getTangentForOffset(drawLength);
|
||||||
|
if (tangent != null) {
|
||||||
|
canvas.drawCircle(tangent.position, brushRadius / scale, brushPaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw stroke numbers
|
||||||
|
if (showStrokeNumbers) {
|
||||||
|
final textPainter = TextPainter(
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final sn in strokeNumbers) {
|
||||||
|
final bool isCurrent =
|
||||||
|
sn.num ==
|
||||||
|
(currentlyDrawingIndex + 1).clamp(1, strokeNumbers.length);
|
||||||
|
final textSpan = TextSpan(
|
||||||
|
text: sn.num.toString(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: isCurrent ? currentStrokeNumberColor : strokeNumberColor,
|
||||||
|
fontSize: strokeNumberFontSize / scale,
|
||||||
|
fontFamily: strokeNumberFontFamily,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
textPainter.text = textSpan;
|
||||||
|
textPainter.layout();
|
||||||
|
|
||||||
|
final Offset pos = Offset(
|
||||||
|
sn.position.x.toDouble(),
|
||||||
|
sn.position.y.toDouble(),
|
||||||
|
);
|
||||||
|
final Offset centeredPos =
|
||||||
|
pos - Offset(textPainter.width / 2, textPainter.height / 2);
|
||||||
|
textPainter.paint(canvas, centeredPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant _KanimajiPainter oldDelegate) {
|
||||||
|
return oldDelegate.progress != progress ||
|
||||||
|
oldDelegate.paths != paths ||
|
||||||
|
oldDelegate.strokeNumbers != strokeNumbers ||
|
||||||
|
oldDelegate.strokeColor != strokeColor ||
|
||||||
|
oldDelegate.strokeUnfilledColor != strokeUnfilledColor ||
|
||||||
|
oldDelegate.strokeWidth != strokeWidth ||
|
||||||
|
oldDelegate.backgroundColor != backgroundColor ||
|
||||||
|
oldDelegate.showBrush != showBrush ||
|
||||||
|
oldDelegate.brushColor != brushColor ||
|
||||||
|
oldDelegate.brushRadius != brushRadius ||
|
||||||
|
oldDelegate.showStrokeNumbers != showStrokeNumbers ||
|
||||||
|
oldDelegate.strokeNumberColor != strokeNumberColor ||
|
||||||
|
oldDelegate.currentStrokeNumberColor != currentStrokeNumberColor ||
|
||||||
|
oldDelegate.strokeNumberFontSize != strokeNumberFontSize ||
|
||||||
|
oldDelegate.strokeNumberFontFamily != strokeNumberFontFamily ||
|
||||||
|
oldDelegate.showCross != showCross ||
|
||||||
|
oldDelegate.crossColor != crossColor ||
|
||||||
|
oldDelegate.crossStrokeWidth != crossStrokeWidth ||
|
||||||
|
oldDelegate.crossStipleLength != crossStipleLength ||
|
||||||
|
oldDelegate.crossStipleGap != crossStipleGap;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
extension _Hexcode on Color {
|
|
||||||
String get hexcode => '#${value.toRadixString(16).padLeft(8, '0')}';
|
|
||||||
}
|
|
||||||
|
|
||||||
class Kanimaji extends StatelessWidget {
|
|
||||||
final String kanji;
|
|
||||||
const Kanimaji({
|
|
||||||
Key? key,
|
|
||||||
required this.kanji,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+5
-49
@@ -1,62 +1,18 @@
|
|||||||
name: kanimaji
|
name: kanimaji
|
||||||
description: A new Flutter package project.
|
description: Kanji stroke animations.
|
||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
homepage:
|
|
||||||
|
|
||||||
executables:
|
|
||||||
kanimaji: lib/kanimaji/animate_kanji
|
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.12.0 <3.0.0"
|
sdk: "^3.8.0"
|
||||||
flutter: ">=1.17.0"
|
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
bisection: ^0.4.3+1
|
bisection: ^0.4.3+1
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_svg: ^1.0.3
|
xml: ^6.5.0
|
||||||
gifencoder: ^1.0.0
|
|
||||||
image: ^3.1.3
|
|
||||||
xml: ^5.3.1
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^1.0.0
|
flutter_lints: ^6.0.0
|
||||||
|
path: ^1.9.1
|
||||||
# For information on the generic Dart part of this file, see the
|
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
|
||||||
|
|
||||||
# The following section is specific to Flutter.
|
|
||||||
flutter:
|
|
||||||
|
|
||||||
# To add assets to your package, add an assets section, like this:
|
|
||||||
# assets:
|
|
||||||
# - images/a_dot_burr.jpeg
|
|
||||||
# - images/a_dot_ham.jpeg
|
|
||||||
#
|
|
||||||
# For details regarding assets in packages, see
|
|
||||||
# https://flutter.dev/assets-and-images/#from-packages
|
|
||||||
#
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
|
||||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
|
||||||
|
|
||||||
# To add custom fonts to your package, add a fonts section here,
|
|
||||||
# in this "flutter" section. Each entry in this list should have a
|
|
||||||
# "family" key with the font family name, and a "fonts" key with a
|
|
||||||
# list giving the asset and other descriptors for the font. For
|
|
||||||
# example:
|
|
||||||
# fonts:
|
|
||||||
# - family: Schyler
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/Schyler-Regular.ttf
|
|
||||||
# - asset: fonts/Schyler-Italic.ttf
|
|
||||||
# style: italic
|
|
||||||
# - family: Trajan Pro
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/TrajanPro.ttf
|
|
||||||
# - asset: fonts/TrajanPro_Bold.ttf
|
|
||||||
# weight: 700
|
|
||||||
#
|
|
||||||
# For details regarding fonts in packages, see
|
|
||||||
# https://flutter.dev/custom-fonts/#from-packages
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:kanimaji/kanjivg_parser.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
final kanjivgDirStr = Platform.environment['KANJIVG_PATH'];
|
||||||
|
if (kanjivgDirStr == null) {
|
||||||
|
throw Exception('KANJIVG_PATH environment variable not set');
|
||||||
|
}
|
||||||
|
final kanjivgDir = Directory(kanjivgDirStr);
|
||||||
|
final kanjivgFiles = kanjivgDir
|
||||||
|
.listSync(recursive: true)
|
||||||
|
.whereType<File>()
|
||||||
|
.where((f) => f.path.endsWith('.svg'))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
for (final file in kanjivgFiles) {
|
||||||
|
test('Test parsing KanjiVG file ${path.basename(file.path)}', () async {
|
||||||
|
final content = await file.readAsString();
|
||||||
|
try {
|
||||||
|
final item = KanjiVGItem.parseFromXml(content);
|
||||||
|
|
||||||
|
expect(item, isNotNull, reason: 'Failed to parse ${file.path}');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
item.strokeNumbers,
|
||||||
|
isNotEmpty,
|
||||||
|
reason: 'No stroke numbers found in ${file.path}',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
item.strokeNumbers.length,
|
||||||
|
item.paths.length,
|
||||||
|
reason: 'Mismatch between stroke numbers and paths in ${file.path}',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
fail('Error parsing ${file.path}: $e');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:kanimaji/svg/parser.dart';
|
import 'package:kanimaji/svg_parser.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('Test generating SVG path strings', () {
|
test('Test generating SVG path strings', () {
|
||||||
|
|||||||
+44
-38
@@ -1,21 +1,22 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:kanimaji/common/point.dart';
|
import 'package:kanimaji/primitives/path.dart';
|
||||||
import 'package:kanimaji/svg/parser.dart';
|
import 'package:kanimaji/primitives/point.dart';
|
||||||
import 'package:kanimaji/svg/path.dart';
|
import 'package:kanimaji/svg_parser.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group("Examples from the SVG spec", () {
|
group("Examples from the SVG spec", () {
|
||||||
test(
|
test(
|
||||||
"[Path 1]: MLLz",
|
"[Path 1]: MLLz",
|
||||||
() => expect(
|
() => expect(
|
||||||
parsePath("M 100 100 L 300 100 L 200 300 z"),
|
parsePath("M 100 100 L 300 100 L 200 300 z"),
|
||||||
Path.fromSegments(const [
|
Path.fromSegments(const [
|
||||||
Move(to: Point(100, 100)),
|
Move(to: Point(100, 100)),
|
||||||
Line(start: Point(100, 100), end: Point(300, 100)),
|
Line(start: Point(100, 100), end: Point(300, 100)),
|
||||||
Line(start: Point(300, 100), end: Point(200, 300)),
|
Line(start: Point(300, 100), end: Point(200, 300)),
|
||||||
Close(start: Point(200, 300), end: Point(100, 100)),
|
Close(start: Point(200, 300), end: Point(100, 100)),
|
||||||
]),
|
]),
|
||||||
));
|
),
|
||||||
|
);
|
||||||
|
|
||||||
//
|
//
|
||||||
test(
|
test(
|
||||||
@@ -160,10 +161,11 @@ void main() {
|
|||||||
Path.fromSegments(const [
|
Path.fromSegments(const [
|
||||||
Move(to: Point(600, 800)),
|
Move(to: Point(600, 800)),
|
||||||
CubicBezier(
|
CubicBezier(
|
||||||
start: Point(600, 800),
|
start: Point(600, 800),
|
||||||
control1: Point(625, 700),
|
control1: Point(625, 700),
|
||||||
control2: Point(725, 700),
|
control2: Point(725, 700),
|
||||||
end: Point(750, 800)),
|
end: Point(750, 800),
|
||||||
|
),
|
||||||
CubicBezier(
|
CubicBezier(
|
||||||
start: Point(750, 800),
|
start: Point(750, 800),
|
||||||
control1: Point(775, 900),
|
control1: Point(775, 900),
|
||||||
@@ -347,9 +349,10 @@ void main() {
|
|||||||
Path.fromSegments(const [
|
Path.fromSegments(const [
|
||||||
Move(to: Point(100, 200)),
|
Move(to: Point(100, 200)),
|
||||||
QuadraticBezier(
|
QuadraticBezier(
|
||||||
start: Point(100, 200),
|
start: Point(100, 200),
|
||||||
control: Point(100, 200),
|
control: Point(100, 200),
|
||||||
end: Point(250, 200)),
|
end: Point(250, 200),
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -361,9 +364,10 @@ void main() {
|
|||||||
Path.fromSegments(const [
|
Path.fromSegments(const [
|
||||||
Move(to: Point(100, 200)),
|
Move(to: Point(100, 200)),
|
||||||
QuadraticBezier(
|
QuadraticBezier(
|
||||||
start: Point(100, 200),
|
start: Point(100, 200),
|
||||||
control: Point(100, 200),
|
control: Point(100, 200),
|
||||||
end: Point(250, 200)),
|
end: Point(250, 200),
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -382,12 +386,12 @@ void main() {
|
|||||||
() =>
|
() =>
|
||||||
// It can be e or E, the plus is optional, and a minimum of +/-3.4e38 must be supported.
|
// It can be e or E, the plus is optional, and a minimum of +/-3.4e38 must be supported.
|
||||||
expect(
|
expect(
|
||||||
parsePath("M-3.4e38 3.4E+38L-3.4E-38,3.4e-38"),
|
parsePath("M-3.4e38 3.4E+38L-3.4E-38,3.4e-38"),
|
||||||
Path.fromSegments(const [
|
Path.fromSegments(const [
|
||||||
Move(to: Point(-3.4e38, 3.4e38)),
|
Move(to: Point(-3.4e38, 3.4e38)),
|
||||||
Line(start: Point(-3.4e38, 3.4e38), end: Point(-3.4e-38, 3.4e-38))
|
Line(start: Point(-3.4e38, 3.4e38), end: Point(-3.4e-38, 3.4e-38)),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
@@ -405,15 +409,17 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
test('svg.path library, issue 45', () {
|
test('svg.path library, issue 45', () {
|
||||||
final path = parsePath("m 1672.2372,-54.8161 "
|
final path = parsePath(
|
||||||
"a 14.5445,14.5445 0 0 0 -11.3152,23.6652 "
|
"m 1672.2372,-54.8161 "
|
||||||
"l 27.2573,27.2572 27.2572,-27.2572 "
|
"a 14.5445,14.5445 0 0 0 -11.3152,23.6652 "
|
||||||
"a 14.5445,14.5445 0 0 0 -11.3012,-23.634 "
|
"l 27.2573,27.2572 27.2572,-27.2572 "
|
||||||
"a 14.5445,14.5445 0 0 0 -11.414,5.4625 "
|
"a 14.5445,14.5445 0 0 0 -11.3012,-23.634 "
|
||||||
"l -4.542,4.5420 "
|
"a 14.5445,14.5445 0 0 0 -11.414,5.4625 "
|
||||||
"l -4.5437,-4.5420 "
|
"l -4.542,4.5420 "
|
||||||
"a 14.5445,14.5445 0 0 0 -11.3984,-5.4937 "
|
"l -4.5437,-4.5420 "
|
||||||
"z");
|
"a 14.5445,14.5445 0 0 0 -11.3984,-5.4937 "
|
||||||
|
"z",
|
||||||
|
);
|
||||||
expect(path.d(), contains("A 14.5445,14.5445 0 0,0 1672.2372,-54.8161 Z"));
|
expect(path.d(), contains("A 14.5445,14.5445 0 0,0 1672.2372,-54.8161 Z"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+620
-629
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,9 @@
|
|||||||
// from svg.path import parser
|
// from svg.path import parser
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:kanimaji/common/point.dart';
|
import 'package:kanimaji/primitives/point.dart';
|
||||||
import 'package:kanimaji/svg/parser.dart' show Command, Token, commandifyPath, parsePath, tokenizePath;
|
import 'package:kanimaji/svg_parser.dart'
|
||||||
|
show Command, Token, commandifyPath, parsePath, tokenizePath;
|
||||||
|
|
||||||
class TokenizerTest {
|
class TokenizerTest {
|
||||||
final String pathdef;
|
final String pathdef;
|
||||||
@@ -30,7 +31,7 @@ final List<TokenizerTest> tokenizerTests = [
|
|||||||
Token(command: "M", args: [Point(100, 100)]),
|
Token(command: "M", args: [Point(100, 100)]),
|
||||||
Token(command: "L", args: [Point(300, 100)]),
|
Token(command: "L", args: [Point(300, 100)]),
|
||||||
Token(command: "L", args: [Point(200, 300)]),
|
Token(command: "L", args: [Point(200, 300)]),
|
||||||
Token(command: "z", args: [])
|
Token(command: "z", args: []),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const TokenizerTest(
|
const TokenizerTest(
|
||||||
@@ -42,17 +43,22 @@ final List<TokenizerTest> tokenizerTests = [
|
|||||||
Command(command: "M", args: "5 1"),
|
Command(command: "M", args: "5 1"),
|
||||||
Command(command: "v", args: "7.344"),
|
Command(command: "v", args: "7.344"),
|
||||||
Command(
|
Command(
|
||||||
command: "A", args: "3.574 3.574 0 003.5 8 3.515 3.515 0 000 11.5"),
|
command: "A",
|
||||||
|
args: "3.574 3.574 0 003.5 8 3.515 3.515 0 000 11.5",
|
||||||
|
),
|
||||||
Command(command: "C", args: "0 13.421 1.579 15 3.5 15"),
|
Command(command: "C", args: "0 13.421 1.579 15 3.5 15"),
|
||||||
Command(command: "A", args: "3.517 3.517 0 007 11.531"),
|
Command(command: "A", args: "3.517 3.517 0 007 11.531"),
|
||||||
Command(command: "v", args: "-7.53"),
|
Command(command: "v", args: "-7.53"),
|
||||||
Command(command: "h", args: "6"),
|
Command(command: "h", args: "6"),
|
||||||
Command(command: "v", args: "4.343"),
|
Command(command: "v", args: "4.343"),
|
||||||
Command(
|
Command(
|
||||||
command: "A", args: "3.574 3.574 0 0011.5 8 3.515 3.515 0 008 11.5"),
|
command: "A",
|
||||||
|
args: "3.574 3.574 0 0011.5 8 3.515 3.515 0 008 11.5",
|
||||||
|
),
|
||||||
Command(
|
Command(
|
||||||
command: "c",
|
command: "c",
|
||||||
args: "0 1.921 1.579 3.5 3.5 3.5 1.9 0 3.465 -1.546 3.5 -3.437"),
|
args: "0 1.921 1.579 3.5 3.5 3.5 1.9 0 3.465 -1.546 3.5 -3.437",
|
||||||
|
),
|
||||||
Command(command: "V", args: "1"),
|
Command(command: "V", args: "1"),
|
||||||
Command(command: "z", args: ""),
|
Command(command: "z", args: ""),
|
||||||
],
|
],
|
||||||
@@ -61,26 +67,36 @@ final List<TokenizerTest> tokenizerTests = [
|
|||||||
Token(command: "v", args: [7.344]),
|
Token(command: "v", args: [7.344]),
|
||||||
Token(command: "A", args: [3.574, 3.574, 0, false, false, Point(3.5, 8)]),
|
Token(command: "A", args: [3.574, 3.574, 0, false, false, Point(3.5, 8)]),
|
||||||
Token(
|
Token(
|
||||||
command: "A", args: [3.515, 3.515, 0, false, false, Point(0, 11.5)]),
|
command: "A",
|
||||||
|
args: [3.515, 3.515, 0, false, false, Point(0, 11.5)],
|
||||||
|
),
|
||||||
Token(
|
Token(
|
||||||
command: "C",
|
command: "C",
|
||||||
args: [Point(0, 13.421), Point(1.579, 15), Point(3.5, 15)]),
|
args: [Point(0, 13.421), Point(1.579, 15), Point(3.5, 15)],
|
||||||
|
),
|
||||||
Token(
|
Token(
|
||||||
command: "A",
|
command: "A",
|
||||||
args: [3.517, 3.517, 0, false, false, Point(7, 11.531)]),
|
args: [3.517, 3.517, 0, false, false, Point(7, 11.531)],
|
||||||
|
),
|
||||||
Token(command: "v", args: [-7.53]),
|
Token(command: "v", args: [-7.53]),
|
||||||
Token(command: "h", args: [6]),
|
Token(command: "h", args: [6]),
|
||||||
Token(command: "v", args: [4.343]),
|
Token(command: "v", args: [4.343]),
|
||||||
Token(
|
Token(
|
||||||
command: "A", args: [3.574, 3.574, 0, false, false, Point(11.5, 8)]),
|
command: "A",
|
||||||
|
args: [3.574, 3.574, 0, false, false, Point(11.5, 8)],
|
||||||
|
),
|
||||||
Token(
|
Token(
|
||||||
command: "A", args: [3.515, 3.515, 0, false, false, Point(8, 11.5)]),
|
command: "A",
|
||||||
|
args: [3.515, 3.515, 0, false, false, Point(8, 11.5)],
|
||||||
|
),
|
||||||
Token(
|
Token(
|
||||||
command: "c",
|
command: "c",
|
||||||
args: [Point(0, 1.921), Point(1.579, 3.5), Point(3.5, 3.5)]),
|
args: [Point(0, 1.921), Point(1.579, 3.5), Point(3.5, 3.5)],
|
||||||
|
),
|
||||||
Token(
|
Token(
|
||||||
command: "c",
|
command: "c",
|
||||||
args: [Point(1.9, 0), Point(3.465, -1.546), Point(3.5, -3.437)]),
|
args: [Point(1.9, 0), Point(3.465, -1.546), Point(3.5, -3.437)],
|
||||||
|
),
|
||||||
Token(command: "V", args: [1]),
|
Token(command: "V", args: [1]),
|
||||||
Token(command: "z", args: []),
|
Token(command: "z", args: []),
|
||||||
],
|
],
|
||||||
@@ -120,4 +136,4 @@ void main() {
|
|||||||
parsePath(tokenizerTest.pathdef);
|
parsePath(tokenizerTest.pathdef);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user