Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
87e23e794c
|
@@ -1,6 +1,8 @@
|
|||||||
name: "Test"
|
name: "Test"
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -14,7 +16,9 @@ jobs:
|
|||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y git jq
|
apt-get install -y git jq
|
||||||
|
|
||||||
|
|
||||||
- run: git config --global --add safe.directory '*'
|
- 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
|
- run: git clone --depth=1 --single-branch --branch=master https://git.pvv.ntnu.no/mugiten/kanjivg.git kanjivg
|
||||||
|
|
||||||
- name: Setup Flutter
|
- name: Setup Flutter
|
||||||
Generated
+3
-3
@@ -18,11 +18,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1771848320,
|
"lastModified": 1771369470,
|
||||||
"narHash": "sha256-0MAd+0mun3K/Ns8JATeHT1sX28faLII5hVLq0L3BdZU=",
|
"narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "2fc6539b481e1d2569f25f8799236694180c0993",
|
"rev": "0182a361324364ae3f436a63005877674cf45efb",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -0,0 +1,715 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math' show min, sqrt, pow;
|
||||||
|
|
||||||
|
import 'package:xml/xml.dart';
|
||||||
|
|
||||||
|
import 'svg_parser.dart';
|
||||||
|
import 'primitives/point.dart';
|
||||||
|
import 'primitives/bezier.dart' as bezier;
|
||||||
|
|
||||||
|
double _computePathLength(String path) =>
|
||||||
|
parsePath(path).size(error: 1e-8).toDouble();
|
||||||
|
|
||||||
|
// String _shescape(String path) =>
|
||||||
|
// "'${path.replaceAll(RegExp(r"(?=['\\\\])"), "\\\\")}'";
|
||||||
|
|
||||||
|
extension _Dedent on String {
|
||||||
|
String get dedented {
|
||||||
|
final withoutEmptyLines = split('\n').where((l) => l.isNotEmpty).toList();
|
||||||
|
final whitespaceAmounts = [
|
||||||
|
for (final line in withoutEmptyLines)
|
||||||
|
line.split('').takeWhile((c) => c == ' ').length,
|
||||||
|
];
|
||||||
|
final whitespaceToRemove = whitespaceAmounts.reduce(min);
|
||||||
|
return withoutEmptyLines
|
||||||
|
.map((l) => l.replaceRange(0, whitespaceToRemove, ''))
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JsAnimationElement {
|
||||||
|
final XmlDocumentFragment bg;
|
||||||
|
final XmlDocumentFragment anim;
|
||||||
|
final XmlDocumentFragment? brush;
|
||||||
|
final XmlDocumentFragment? brushBorder;
|
||||||
|
|
||||||
|
/// the time set (as default) for each animation
|
||||||
|
final num time;
|
||||||
|
|
||||||
|
const JsAnimationElement({
|
||||||
|
required this.bg,
|
||||||
|
required this.anim,
|
||||||
|
required this.time,
|
||||||
|
this.brush,
|
||||||
|
this.brushBorder,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ease, ease-in, etc:
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/timing-function#ease
|
||||||
|
const pt1 = Point(0, 0);
|
||||||
|
const easeCt1 = Point(0.25, 0.1);
|
||||||
|
const easeCt2 = Point(0.25, 1.0);
|
||||||
|
const easeInCt1 = Point(0.42, 0.0);
|
||||||
|
const easeInCt2 = Point(1.0, 1.0);
|
||||||
|
const easeInOutCt1 = Point(0.42, 0.0);
|
||||||
|
const easeInOutCt2 = Point(0.58, 1.0);
|
||||||
|
const easeOutCt1 = Point(0.0, 0.0);
|
||||||
|
const easeOutCt2 = Point(0.58, 1.0);
|
||||||
|
const pt2 = Point(1, 1);
|
||||||
|
|
||||||
|
// class {
|
||||||
|
// }
|
||||||
|
|
||||||
|
enum TimingFunction { linear, ease, easeIn, easeInOut, easeOut }
|
||||||
|
|
||||||
|
extension Funcs on TimingFunction {
|
||||||
|
double Function(double) get func => {
|
||||||
|
TimingFunction.linear: (double x) => x,
|
||||||
|
TimingFunction.ease: (double x) =>
|
||||||
|
bezier.value(pt1, easeCt1, easeCt2, pt2, x),
|
||||||
|
TimingFunction.easeIn: (double x) =>
|
||||||
|
bezier.value(pt1, easeInCt1, easeInCt2, pt2, x),
|
||||||
|
TimingFunction.easeInOut: (double x) =>
|
||||||
|
bezier.value(pt1, easeInOutCt1, easeInOutCt2, pt2, x),
|
||||||
|
TimingFunction.easeOut: (double x) =>
|
||||||
|
bezier.value(pt1, easeOutCt1, easeOutCt2, pt2, x),
|
||||||
|
}[this]!;
|
||||||
|
|
||||||
|
String get name => {
|
||||||
|
TimingFunction.linear: 'linear',
|
||||||
|
TimingFunction.ease: 'ease',
|
||||||
|
TimingFunction.easeIn: 'ease-in',
|
||||||
|
TimingFunction.easeInOut: 'ease-in-out',
|
||||||
|
TimingFunction.easeOut: 'ease-out',
|
||||||
|
}[this]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we will need this to deal with svg
|
||||||
|
const namespaces = {
|
||||||
|
'n': 'http://www.w3.org/2000/svg',
|
||||||
|
'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);
|
||||||
|
|
||||||
|
// gif settings
|
||||||
|
// const DELETE_TEMPORARY_FILES = false;
|
||||||
|
const GIF_SIZE = 150;
|
||||||
|
const GIF_FRAME_DURATION = 0.04;
|
||||||
|
const GIF_BACKGROUND_COLOR = '#ddf';
|
||||||
|
// set to true to allow transparent background, much bigger file!
|
||||||
|
// const GIF_ALLOW_TRANSPARENT = false;
|
||||||
|
|
||||||
|
// edit here to decide what will be generated
|
||||||
|
const GENERATE_SVG = true;
|
||||||
|
const GENERATE_JS_SVG = true;
|
||||||
|
const GENERATE_GIF = true;
|
||||||
|
|
||||||
|
/// sqrt, ie a stroke 4 times the length is drawn
|
||||||
|
/// at twice the speed, in twice the time.
|
||||||
|
double strokeLengthToDuration(double length) => sqrt(length) / 8;
|
||||||
|
|
||||||
|
/// global time rescale, let's make animation a bit
|
||||||
|
/// faster when there are many strokes.
|
||||||
|
double timeRescale(interval) => pow(2 * interval, 2.0 / 3).toDouble();
|
||||||
|
|
||||||
|
/// clear all extra elements this program may have previously added
|
||||||
|
void clearPreviousElements(XmlDocument doc) {
|
||||||
|
for (final XmlNode el
|
||||||
|
in doc
|
||||||
|
.getElement('svg', namespace: namespaces['n'])
|
||||||
|
?.getElement('style', namespace: namespaces['n'])
|
||||||
|
?.children ??
|
||||||
|
[]) {
|
||||||
|
if (RegExp(r'-Kanimaji$').hasMatch(el.getAttribute('id') ?? '')) {
|
||||||
|
el.parent!.children.remove(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final XmlNode g
|
||||||
|
in doc
|
||||||
|
.getElement('svg', namespace: namespaces['n'])
|
||||||
|
?.getElement('g', namespace: namespaces['n'])
|
||||||
|
?.children ??
|
||||||
|
[]) {
|
||||||
|
if (RegExp(r'-Kanimaji$').hasMatch(g.getAttribute('id') ?? '')) {
|
||||||
|
g.parent!.children.remove(g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String basename(String path) {
|
||||||
|
final int lastSlash = path.lastIndexOf(Platform.pathSeparator);
|
||||||
|
if (lastSlash == -1) {
|
||||||
|
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');
|
||||||
|
final String filenameNoext = inputFile.replaceAll(RegExp(r'\.[^\.]+$'), '');
|
||||||
|
outputFile ??= '${filenameNoext}_anim.svg';
|
||||||
|
final String baseid = basename(filenameNoext);
|
||||||
|
|
||||||
|
// load xml
|
||||||
|
final XmlDocument doc = XmlDocument.parse(File(inputFile).readAsStringSync());
|
||||||
|
|
||||||
|
// for xlink namespace introduction
|
||||||
|
doc.rootElement.setAttribute('xmlns:xlink', namespaces['xlink']);
|
||||||
|
doc.rootElement.setAttribute('xlink:used', '');
|
||||||
|
|
||||||
|
clearPreviousElements(doc);
|
||||||
|
|
||||||
|
/// create groups with a copies (references actually) of the paths
|
||||||
|
XmlDocumentFragment pathCopyGroup({
|
||||||
|
required String id,
|
||||||
|
required String color,
|
||||||
|
required double width,
|
||||||
|
}) {
|
||||||
|
final builder = XmlBuilder();
|
||||||
|
builder.element(
|
||||||
|
'g',
|
||||||
|
attributes: {
|
||||||
|
'id': 'kvg:$baseid-$id-Kanimaji',
|
||||||
|
'style':
|
||||||
|
'fill:none;'
|
||||||
|
'stroke:$color;'
|
||||||
|
'stroke-width:$width;'
|
||||||
|
'stroke-linecap:round;'
|
||||||
|
'stroke-linejoin:round;',
|
||||||
|
},
|
||||||
|
isSelfClosing: false,
|
||||||
|
);
|
||||||
|
return builder.buildFragment();
|
||||||
|
}
|
||||||
|
|
||||||
|
final bgGroup = pathCopyGroup(
|
||||||
|
id: 'bg',
|
||||||
|
color: strokeUnfilledColor,
|
||||||
|
width: strokeUnfilledWidth,
|
||||||
|
);
|
||||||
|
final animGroup = pathCopyGroup(
|
||||||
|
id: 'anim',
|
||||||
|
color: strokeFilledColor,
|
||||||
|
width: strokeFilledWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
late final XmlDocumentFragment brushGroup;
|
||||||
|
late final XmlDocumentFragment brushBrdGroup;
|
||||||
|
if (showBrush) {
|
||||||
|
brushGroup = pathCopyGroup(
|
||||||
|
id: 'brush',
|
||||||
|
color: brushColor,
|
||||||
|
width: brushWidth,
|
||||||
|
);
|
||||||
|
brushBrdGroup = pathCopyGroup(
|
||||||
|
id: 'brush-brd',
|
||||||
|
color: brushBorderColor,
|
||||||
|
width: brushBorderWidth,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// compute total length and time, at first
|
||||||
|
double totlen = 0;
|
||||||
|
double tottime = 0;
|
||||||
|
|
||||||
|
// for (final g in doc.xpath("/n:svg/n:g", namespaces=namespaces) {
|
||||||
|
for (final XmlNode g
|
||||||
|
in doc
|
||||||
|
.getElement('svg', namespace: namespaces['n'])
|
||||||
|
?.getElement('g', namespace: namespaces['n'])
|
||||||
|
?.children ??
|
||||||
|
[]) {
|
||||||
|
if (RegExp(r'^kvg:StrokeNumbers_').hasMatch(g.getAttribute('id') ?? '')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final p in g.findAllElements('path', namespace: namespaces['n'])) {
|
||||||
|
final pathlen = _computePathLength(p.getAttribute('d')!);
|
||||||
|
final duration = strokeLengthToDuration(pathlen);
|
||||||
|
totlen += pathlen;
|
||||||
|
tottime += duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double animationTime = timeRescale(tottime); // math.pow(3 * tottime, 2.0/3)
|
||||||
|
tottime += waitAfter * tottime / animationTime;
|
||||||
|
final double actualAnimationTime = animationTime;
|
||||||
|
animationTime += waitAfter;
|
||||||
|
|
||||||
|
final Map<int, String> staticCss = {};
|
||||||
|
late String animatedCss;
|
||||||
|
|
||||||
|
/// collect the ids of animating elements
|
||||||
|
final List<JsAnimationElement> jsAnimationElements = [];
|
||||||
|
|
||||||
|
String jsAnimatedCss = '';
|
||||||
|
|
||||||
|
const String cssHeader =
|
||||||
|
'\n/* CSS automatically generated by kanimaji.py, do not edit! */\n';
|
||||||
|
if (GENERATE_SVG) animatedCss = cssHeader;
|
||||||
|
if (GENERATE_JS_SVG) {
|
||||||
|
jsAnimatedCss += '''$cssHeader .backward {\n
|
||||||
|
animation-direction: reverse !important;\n
|
||||||
|
}
|
||||||
|
''';
|
||||||
|
}
|
||||||
|
late final int lastFrameIndex;
|
||||||
|
late final double lastFrameDelay;
|
||||||
|
if (GENERATE_GIF) {
|
||||||
|
// final static_css = {};
|
||||||
|
lastFrameIndex = actualAnimationTime ~/ GIF_FRAME_DURATION + 1;
|
||||||
|
for (int i = 0; i < lastFrameIndex + 1; i++) {
|
||||||
|
staticCss[i] = cssHeader;
|
||||||
|
}
|
||||||
|
lastFrameDelay = animationTime - lastFrameIndex * GIF_FRAME_DURATION;
|
||||||
|
}
|
||||||
|
double elapsedlen = 0;
|
||||||
|
double elapsedtime = 0;
|
||||||
|
|
||||||
|
// add css elements for all strokes
|
||||||
|
for (final XmlNode g
|
||||||
|
in doc
|
||||||
|
.getElement('svg', namespace: namespaces['n'])!
|
||||||
|
.findElements('g', namespace: namespaces['n'])) {
|
||||||
|
// for (final g in doc.xpath("/n:svg/n:g", namespaces=namespaces)){
|
||||||
|
final groupid = g.getAttribute('id') ?? '';
|
||||||
|
if (RegExp(r'^kvg:StrokeNumbers_').hasMatch(groupid)) {
|
||||||
|
final String rule =
|
||||||
|
'''
|
||||||
|
#${groupid.replaceAll(':', '\\3a ')} {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
.dedented;
|
||||||
|
if (GENERATE_SVG) animatedCss += rule;
|
||||||
|
if (GENERATE_JS_SVG) jsAnimatedCss += rule;
|
||||||
|
if (GENERATE_GIF) {
|
||||||
|
for (final k in staticCss.keys) {
|
||||||
|
staticCss[k] = staticCss[k]! + rule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final gidcss = groupid.replaceAll(':', '\\3a ');
|
||||||
|
final rule =
|
||||||
|
'''
|
||||||
|
#$gidcss {
|
||||||
|
stroke-width: ${strokeBorderWidth.toStringAsFixed(1)}px !important;
|
||||||
|
stroke: $strokeBorderColor !important;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
.dedented;
|
||||||
|
|
||||||
|
if (GENERATE_SVG) animatedCss += rule;
|
||||||
|
if (GENERATE_JS_SVG) jsAnimatedCss += rule;
|
||||||
|
if (GENERATE_GIF) {
|
||||||
|
for (final k in staticCss.keys) {
|
||||||
|
staticCss[k] = staticCss[k]! + rule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final p in g.findAllElements("path", namespace: namespaces['n'])) {
|
||||||
|
final pathid = p.getAttribute('id') as String;
|
||||||
|
final pathidcss = pathid.replaceAll(':', '\\3a ');
|
||||||
|
|
||||||
|
XmlDocumentFragment addHref(String suffix, XmlDocumentFragment parent) {
|
||||||
|
final builder = XmlBuilder();
|
||||||
|
builder.element(
|
||||||
|
'use',
|
||||||
|
attributes: {'id': '$pathid-$suffix', 'xlink:href': '#$pathid'},
|
||||||
|
);
|
||||||
|
final ref = builder.buildFragment();
|
||||||
|
parent.firstElementChild!.children.add(ref);
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String bgPathidcss = '$pathidcss-bg';
|
||||||
|
final String animPathidcss = '$pathidcss-anim';
|
||||||
|
final String brushPathidcss = '$pathidcss-brush';
|
||||||
|
final String brushBorderPathidcss = '$pathidcss-brush-brd';
|
||||||
|
|
||||||
|
final bgGroupElement = addHref('bg', bgGroup);
|
||||||
|
final animGroupElement = addHref('anim', animGroup);
|
||||||
|
XmlDocumentFragment? brushGroupElement;
|
||||||
|
XmlDocumentFragment? brushBorderGroupElement;
|
||||||
|
if (showBrush) {
|
||||||
|
brushGroupElement = addHref('brush', brushGroup);
|
||||||
|
brushBorderGroupElement = addHref('brush-brd', brushBrdGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
final pathname = pathid.replaceAll(RegExp(r'^kvg:'), '');
|
||||||
|
final pathlen = _computePathLength(p.getAttribute('d') as String);
|
||||||
|
final duration = strokeLengthToDuration(pathlen);
|
||||||
|
final relativeDuration =
|
||||||
|
duration * tottime / animationTime; // unscaled time
|
||||||
|
|
||||||
|
if (GENERATE_JS_SVG) {
|
||||||
|
jsAnimationElements.add(
|
||||||
|
JsAnimationElement(
|
||||||
|
bg: bgGroupElement,
|
||||||
|
anim: animGroupElement,
|
||||||
|
brush: brushGroupElement,
|
||||||
|
brushBorder: brushBorderGroupElement,
|
||||||
|
time: relativeDuration,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final newelapsedlen = elapsedlen + pathlen;
|
||||||
|
final newelapsedtime = elapsedtime + duration;
|
||||||
|
final animStart = elapsedtime / tottime * 100;
|
||||||
|
final animEnd = newelapsedtime / tottime * 100;
|
||||||
|
|
||||||
|
if (GENERATE_SVG) {
|
||||||
|
// animation stroke progression
|
||||||
|
animatedCss +=
|
||||||
|
'''
|
||||||
|
@keyframes strike-$pathname {
|
||||||
|
0% { stroke-dashoffset: ${pathlen.toStringAsFixed(3)}; }
|
||||||
|
${animStart.toStringAsFixed(3)}% { stroke-dashoffset: ${pathlen.toStringAsFixed(3)}; }
|
||||||
|
${animEnd.toStringAsFixed(3)}% { stroke-dashoffset: 0; }
|
||||||
|
100% { stroke-dashoffset: 0; }
|
||||||
|
}
|
||||||
|
@keyframes showhide-$pathname {
|
||||||
|
${animStart.toStringAsFixed(3)}% { visibility: hidden; }
|
||||||
|
${animEnd.toStringAsFixed(3)}% { stroke: $strokeFillingColor; }
|
||||||
|
}
|
||||||
|
#$animPathidcss {
|
||||||
|
stroke-dasharray: ${pathlen.toStringAsFixed(3)} ${pathlen.toStringAsFixed(3)};
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
animation: strike-$pathname ${animationTime.toStringAsFixed(3)}s ${timingFunction.name} infinite,
|
||||||
|
showhide-$pathname ${animationTime.toStringAsFixed(3)}s step-start infinite;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
.dedented;
|
||||||
|
|
||||||
|
if (showBrush) {
|
||||||
|
// brush element visibility
|
||||||
|
animatedCss +=
|
||||||
|
'''
|
||||||
|
@keyframes showhide-brush-$pathname {
|
||||||
|
${animStart.toStringAsFixed(3)}% { visibility: hidden; }
|
||||||
|
${animEnd.toStringAsFixed(3)}% { visibility: visible; }
|
||||||
|
100% { visibility: hidden; }
|
||||||
|
}
|
||||||
|
#$brushPathidcss, #$brushBorderPathidcss {
|
||||||
|
stroke-dasharray: 0 ${pathlen.toStringAsFixed(3)};
|
||||||
|
animation: strike-$pathname ${animationTime.toStringAsFixed(3)}s ${timingFunction.name} infinite,
|
||||||
|
showhide-brush-$pathname ${animationTime.toStringAsFixed(3)}s step-start infinite;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
.dedented;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GENERATE_JS_SVG) {
|
||||||
|
jsAnimatedCss += '\n/* stroke $pathid */\n';
|
||||||
|
|
||||||
|
// brush and background hidden by default
|
||||||
|
if (showBrush) {
|
||||||
|
jsAnimatedCss +=
|
||||||
|
'''
|
||||||
|
#$brushPathidcss, #$brushBorderPathidcss, #$bgPathidcss {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
.dedented;
|
||||||
|
}
|
||||||
|
|
||||||
|
// hide stroke after current element
|
||||||
|
const afterCurrent = '[class *= "current"]';
|
||||||
|
jsAnimatedCss +=
|
||||||
|
'''
|
||||||
|
$afterCurrent ~ #$animPathidcss {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
$afterCurrent ~ #$bgPathidcss, #$bgPathidcss.animate {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
@keyframes strike-$pathname {
|
||||||
|
0% { stroke-dashoffset: ${pathlen.toStringAsFixed(3)}; }
|
||||||
|
100% { stroke-dashoffset: 0; }
|
||||||
|
}
|
||||||
|
#$animPathidcss.animate {
|
||||||
|
stroke: $strokeFillingColor;
|
||||||
|
stroke-dasharray: ${pathlen.toStringAsFixed(3)} ${pathlen.toStringAsFixed(3)};
|
||||||
|
visibility: visible;
|
||||||
|
animation: strike-$pathname ${relativeDuration.toStringAsFixed(3)}s ${timingFunction.name} forwards 1;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
.dedented;
|
||||||
|
if (showBrush) {
|
||||||
|
jsAnimatedCss +=
|
||||||
|
'''
|
||||||
|
@keyframes strike-brush-$pathname {
|
||||||
|
0% { stroke-dashoffset: ${pathlen.toStringAsFixed(3)}; }
|
||||||
|
100% { stroke-dashoffset: 0.4; }
|
||||||
|
}
|
||||||
|
#$brushPathidcss.animate.brush, #$brushBorderPathidcss.animate.brush {
|
||||||
|
stroke-dasharray: 0 ${pathlen.toStringAsFixed(3)};
|
||||||
|
visibility: visible;
|
||||||
|
animation: strike-brush-$pathname ${relativeDuration.toStringAsFixed(3)}s ${timingFunction.name} forwards 1;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
.dedented;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GENERATE_GIF) {
|
||||||
|
for (final k in staticCss.keys) {
|
||||||
|
final time = k * GIF_FRAME_DURATION;
|
||||||
|
final reltime = time * tottime / animationTime; // unscaled time
|
||||||
|
|
||||||
|
staticCss[k] = '${staticCss[k]!}\n/* stroke $pathid */\n';
|
||||||
|
|
||||||
|
String rule = '';
|
||||||
|
|
||||||
|
// animation
|
||||||
|
if (reltime < elapsedtime) {
|
||||||
|
// just hide everything
|
||||||
|
rule += "#$animPathidcss";
|
||||||
|
|
||||||
|
if (showBrush) {
|
||||||
|
rule += ", #$brushPathidcss, #$brushBorderPathidcss";
|
||||||
|
}
|
||||||
|
|
||||||
|
staticCss[k] =
|
||||||
|
staticCss[k]! +
|
||||||
|
'''
|
||||||
|
%$rule {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
.dedented;
|
||||||
|
} else if (reltime > newelapsedtime) {
|
||||||
|
// just hide the brush, and bg
|
||||||
|
rule += "#$bgPathidcss";
|
||||||
|
|
||||||
|
if (showBrush) {
|
||||||
|
rule += ", #$brushPathidcss, #$brushBorderPathidcss";
|
||||||
|
}
|
||||||
|
|
||||||
|
staticCss[k] =
|
||||||
|
staticCss[k]! +
|
||||||
|
'''
|
||||||
|
$rule {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
.dedented;
|
||||||
|
} else {
|
||||||
|
final intervalprop =
|
||||||
|
((reltime - elapsedtime) / (newelapsedtime - elapsedtime));
|
||||||
|
final progression = timingFunction.func(intervalprop);
|
||||||
|
|
||||||
|
staticCss[k] =
|
||||||
|
staticCss[k]! +
|
||||||
|
'''
|
||||||
|
#$animPathidcss {
|
||||||
|
stroke-dasharray: ${pathlen.toStringAsFixed(3)} ${(pathlen + 0.002).toStringAsFixed(3)};
|
||||||
|
stroke-dashoffset: ${(pathlen * (1 - progression) + 0.0015).toStringAsFixed(4)};
|
||||||
|
stroke: $strokeFillingColor;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
.dedented;
|
||||||
|
if (showBrush) {
|
||||||
|
staticCss[k] =
|
||||||
|
staticCss[k]! +
|
||||||
|
'''
|
||||||
|
#$brushPathidcss, #$brushBorderPathidcss {
|
||||||
|
stroke-dasharray: 0.001 ${(pathlen + 0.002).toStringAsFixed(3)};
|
||||||
|
stroke-dashoffset: ${(pathlen * (1 - progression) + 0.0015).toStringAsFixed(4)};
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
.dedented;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsedlen = newelapsedlen;
|
||||||
|
elapsedtime = newelapsedtime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void addGroup(XmlDocumentFragment g) =>
|
||||||
|
doc.root.firstElementChild?.children.add(g);
|
||||||
|
|
||||||
|
// insert groups
|
||||||
|
if (showBrush && !showBrushFrontBorder) addGroup(brushBrdGroup);
|
||||||
|
addGroup(bgGroup);
|
||||||
|
if (showBrush && showBrushFrontBorder) addGroup(brushBrdGroup);
|
||||||
|
addGroup(animGroup);
|
||||||
|
if (showBrush) addGroup(brushGroup);
|
||||||
|
|
||||||
|
if (GENERATE_SVG) {
|
||||||
|
print(animatedCss);
|
||||||
|
final builder = XmlBuilder();
|
||||||
|
final style =
|
||||||
|
(builder..element(
|
||||||
|
'style',
|
||||||
|
attributes: {'id': "style-Kanimaji", 'type': 'text/css'},
|
||||||
|
nest: animatedCss,
|
||||||
|
))
|
||||||
|
.buildFragment();
|
||||||
|
doc.root.firstElementChild!.children.insert(0, style);
|
||||||
|
File(outputFile).writeAsStringSync(doc.toXmlString(pretty: true));
|
||||||
|
doc.root.children.removeAt(0);
|
||||||
|
print('written $outputFile');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GENERATE_GIF) {
|
||||||
|
// svgframefiles = []
|
||||||
|
// pngframefiles = []
|
||||||
|
// 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)]]})
|
||||||
|
|
||||||
|
// style = E.style(static_css[k], id="style-Kanimaji")
|
||||||
|
// doc.getroot().insert(0, style)
|
||||||
|
// doc.write(svgframefile, pretty_print=True)
|
||||||
|
// doc.getroot().remove(style)
|
||||||
|
// print 'written %s' % svgframefile
|
||||||
|
|
||||||
|
// // create json file
|
||||||
|
// svgexport_datafile = filename_noext_ascii+"_export_data.json"
|
||||||
|
// with open(svgexport_datafile,'w') as f:
|
||||||
|
// f.write(json.dumps(svgexport_data))
|
||||||
|
// print 'created instructions %s' % svgexport_datafile
|
||||||
|
|
||||||
|
// // run svgexport
|
||||||
|
// cmdline = 'svgexport %s' % shescape(svgexport_datafile)
|
||||||
|
// print cmdline
|
||||||
|
// if os.system(cmdline) != 0:
|
||||||
|
// exit('Error running external command')
|
||||||
|
|
||||||
|
// if DELETE_TEMPORARY_FILES:
|
||||||
|
// os.remove(svgexport_datafile)
|
||||||
|
// for f in svgframefiles:
|
||||||
|
// os.remove(f)
|
||||||
|
|
||||||
|
// // generate GIF
|
||||||
|
// giffile_tmp1 = filename_noext + '_anim_tmp1.gif'
|
||||||
|
// giffile_tmp2 = filename_noext + '_anim_tmp2.gif'
|
||||||
|
// giffile = filename_noext + '_anim.gif'
|
||||||
|
// escpngframefiles = ' '.join(shescape(f) for f in pngframefiles[0:-1])
|
||||||
|
|
||||||
|
// if GIF_BACKGROUND_COLOR == 'transparent':
|
||||||
|
// bgopts = '-dispose previous'
|
||||||
|
// else:
|
||||||
|
// bgopts = "-background '%s' -alpha remove" % GIF_BACKGROUND_COLOR
|
||||||
|
// cmdline = ("convert -delay %d %s -delay %d %s "+
|
||||||
|
// "%s -layers OptimizePlus %s") % (
|
||||||
|
// int(GIF_FRAME_DURATION*100),
|
||||||
|
// escpngframefiles,
|
||||||
|
// int(last_frame_delay*100),
|
||||||
|
// shescape(pngframefiles[-1]),
|
||||||
|
// bgopts,
|
||||||
|
// shescape(giffile_tmp1))
|
||||||
|
// print(cmdline);
|
||||||
|
// if os.system(cmdline) != 0:
|
||||||
|
// exit('Error running external command')
|
||||||
|
|
||||||
|
// if DELETE_TEMPORARY_FILES:
|
||||||
|
// for f in pngframefiles:
|
||||||
|
// os.remove(f)
|
||||||
|
// print 'cleaned up.'
|
||||||
|
|
||||||
|
// cmdline = ("convert %s \\( -clone 0--1 -background none "+
|
||||||
|
// "+append -quantize transparent -colors 63 "+
|
||||||
|
// "-unique-colors -write mpr:cmap +delete \\) "+
|
||||||
|
// "-map mpr:cmap %s") % (
|
||||||
|
// shescape(giffile_tmp1),
|
||||||
|
// shescape(giffile_tmp2))
|
||||||
|
// print cmdline
|
||||||
|
// if os.system(cmdline) != 0:
|
||||||
|
// exit('Error running external command')
|
||||||
|
// if DELETE_TEMPORARY_FILES:
|
||||||
|
// os.remove(giffile_tmp1)
|
||||||
|
|
||||||
|
// cmdline = ("gifsicle -O3 %s -o %s") % (
|
||||||
|
// shescape(giffile_tmp2),
|
||||||
|
// shescape(giffile))
|
||||||
|
// print cmdline
|
||||||
|
// if os.system(cmdline) != 0:
|
||||||
|
// exit('Error running external command')
|
||||||
|
// if DELETE_TEMPORARY_FILES:
|
||||||
|
// os.remove(giffile_tmp2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (GENERATE_JS_SVG) {
|
||||||
|
// final f0insert = [bg_g, anim_g];
|
||||||
|
// if (SHOW_BRUSH) f0insert += [brush_g, brush_brd_g];
|
||||||
|
// for g in f0insert:
|
||||||
|
// el = E.a()
|
||||||
|
// el.set("data-stroke","0")
|
||||||
|
// g.insert(0, el)
|
||||||
|
|
||||||
|
// for i in range(0, len(js_anim_els)):
|
||||||
|
// els = js_anim_els[i]
|
||||||
|
// for k in els:
|
||||||
|
// els[k].set("data-stroke",str(i+1))
|
||||||
|
// els["anim"].set("data-duration", str(js_anim_time[i]))
|
||||||
|
|
||||||
|
// doc.getroot().set('data-num-strokes', str(len(js_anim_els)))
|
||||||
|
|
||||||
|
// style = E.style(js_animated_css, id="style-Kanimaji")
|
||||||
|
// doc.getroot().insert(0, style)
|
||||||
|
// svgfile = filename_noext + '_js_anim.svg'
|
||||||
|
// doc.write(svgfile, pretty_print=True)
|
||||||
|
// doc.getroot().remove(style)
|
||||||
|
// 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
-2
@@ -1,2 +1 @@
|
|||||||
export 'package:kanimaji/widget.dart' show Kanimaji, TimingFunction;
|
export 'package:kanimaji/widget.dart' show Kanimaji;
|
||||||
export 'package:kanimaji/kanjivg_parser.dart';
|
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ class KanjiStrokeNumber {
|
|||||||
KanjiStrokeNumber(this.num, this.position);
|
KanjiStrokeNumber(this.num, this.position);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'KanjiStrokeNumber(number: $num, position: $position)';
|
String toString() =>
|
||||||
|
'KanjiStrokeNumber(number: $num, position: $position)';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contents of a `<path>` element in the KanjiVG SVG files
|
/// Contents of a `<path>` element in the KanjiVG SVG files
|
||||||
|
|||||||
+3
-7
@@ -32,9 +32,7 @@ const _commands = {
|
|||||||
// 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(
|
final _floatPattern = RegExp(r"^[-+]?(?:[0-9]+\.?[0-9]*|[0-9]*\.?[0-9]+)(?:[eE][-+]?[0-9]+)?");
|
||||||
r"^[-+]?(?:[0-9]+\.?[0-9]*|[0-9]*\.?[0-9]+)(?:[eE][-+]?[0-9]+)?",
|
|
||||||
);
|
|
||||||
|
|
||||||
class ParserResult<T> {
|
class ParserResult<T> {
|
||||||
final T value;
|
final T value;
|
||||||
@@ -255,10 +253,8 @@ Path parsePath(String pathdef) {
|
|||||||
segments.add(Move(to: currentPos));
|
segments.add(Move(to: currentPos));
|
||||||
startPos = currentPos;
|
startPos = currentPos;
|
||||||
} else if (command == "Z") {
|
} else if (command == "Z") {
|
||||||
if (startPos == null) {
|
// TODO Throw error if not available:
|
||||||
throw InvalidPathError("Path closed without a starting position.");
|
segments.add(Close(start: currentPos, end: startPos!));
|
||||||
}
|
|
||||||
segments.add(Close(start: currentPos, end: startPos));
|
|
||||||
currentPos = startPos;
|
currentPos = startPos;
|
||||||
} else if (command == "L") {
|
} else if (command == "L") {
|
||||||
Point pos = token.args[0] as Point;
|
Point pos = token.args[0] as Point;
|
||||||
|
|||||||
+8
-86
@@ -5,9 +5,8 @@ import 'dart:math' as math;
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import 'package:kanimaji/animator.dart';
|
||||||
import 'package:kanimaji/kanjivg_parser.dart';
|
import 'package:kanimaji/kanjivg_parser.dart';
|
||||||
import 'package:kanimaji/primitives/point.dart';
|
|
||||||
import 'package:kanimaji/primitives/bezier.dart' as bezier;
|
|
||||||
|
|
||||||
Future<KanjiVGItem> _defaultKanjiDataProvider(String kanji) async {
|
Future<KanjiVGItem> _defaultKanjiDataProvider(String kanji) async {
|
||||||
final hex = kanji.runes.isEmpty
|
final hex = kanji.runes.isEmpty
|
||||||
@@ -18,45 +17,6 @@ Future<KanjiVGItem> _defaultKanjiDataProvider(String kanji) async {
|
|||||||
return KanjiVGItem.parseFromXml(svgString);
|
return KanjiVGItem.parseFromXml(svgString);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TimingFunction { linear, ease, easeIn, easeInOut, easeOut }
|
|
||||||
|
|
||||||
// ease, ease-in, etc:
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/timing-function#ease
|
|
||||||
const pt1 = Point(0, 0);
|
|
||||||
const easeCt1 = Point(0.25, 0.1);
|
|
||||||
const easeCt2 = Point(0.25, 1.0);
|
|
||||||
const easeInCt1 = Point(0.42, 0.0);
|
|
||||||
const easeInCt2 = Point(1.0, 1.0);
|
|
||||||
const easeInOutCt1 = Point(0.42, 0.0);
|
|
||||||
const easeInOutCt2 = Point(0.58, 1.0);
|
|
||||||
const easeOutCt1 = Point(0.0, 0.0);
|
|
||||||
const easeOutCt2 = Point(0.58, 1.0);
|
|
||||||
const pt2 = Point(1, 1);
|
|
||||||
|
|
||||||
extension Funcs on TimingFunction {
|
|
||||||
double Function(double) get func => {
|
|
||||||
TimingFunction.linear: (double x) => x,
|
|
||||||
TimingFunction.ease: (double x) =>
|
|
||||||
bezier.value(pt1, easeCt1, easeCt2, pt2, x),
|
|
||||||
TimingFunction.easeIn: (double x) =>
|
|
||||||
bezier.value(pt1, easeInCt1, easeInCt2, pt2, x),
|
|
||||||
TimingFunction.easeInOut: (double x) =>
|
|
||||||
bezier.value(pt1, easeInOutCt1, easeInOutCt2, pt2, x),
|
|
||||||
TimingFunction.easeOut: (double x) =>
|
|
||||||
bezier.value(pt1, easeOutCt1, easeOutCt2, pt2, x),
|
|
||||||
}[this]!;
|
|
||||||
|
|
||||||
String get name => {
|
|
||||||
TimingFunction.linear: 'linear',
|
|
||||||
TimingFunction.ease: 'ease',
|
|
||||||
TimingFunction.easeIn: 'ease-in',
|
|
||||||
TimingFunction.easeInOut: 'ease-in-out',
|
|
||||||
TimingFunction.easeOut: 'ease-out',
|
|
||||||
}[this]!;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: fall back to just drawing the character as text if it does not exist in the kanjivg dataset
|
|
||||||
|
|
||||||
/// A widget that animates the stroke order of a given kanji character using KanjiVG data.
|
/// A widget that animates the stroke order of a given kanji character using KanjiVG data.
|
||||||
class Kanimaji extends StatefulWidget {
|
class Kanimaji extends StatefulWidget {
|
||||||
final String kanji;
|
final String kanji;
|
||||||
@@ -67,7 +27,6 @@ class Kanimaji extends StatefulWidget {
|
|||||||
// final Duration delayBetweenStrokes;
|
// final Duration delayBetweenStrokes;
|
||||||
// final Duration delayBetweenLoops;
|
// final Duration delayBetweenLoops;
|
||||||
// TODO: add support for specifying animation bezier curve
|
// TODO: add support for specifying animation bezier curve
|
||||||
final TimingFunction timingFunction;
|
|
||||||
// final Cubic animationCurve;
|
// final Cubic animationCurve;
|
||||||
// final double speed;
|
// final double speed;
|
||||||
|
|
||||||
@@ -78,7 +37,6 @@ class Kanimaji extends StatefulWidget {
|
|||||||
|
|
||||||
// Stroke parameters
|
// Stroke parameters
|
||||||
final Color strokeColor;
|
final Color strokeColor;
|
||||||
final Color currentStrokeColor;
|
|
||||||
final Color strokeUnfilledColor;
|
final Color strokeUnfilledColor;
|
||||||
final double strokeWidth;
|
final double strokeWidth;
|
||||||
final Color backgroundColor;
|
final Color backgroundColor;
|
||||||
@@ -101,26 +59,19 @@ class Kanimaji extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.kanji,
|
required this.kanji,
|
||||||
this.kanjiDataProvider = _defaultKanjiDataProvider,
|
this.kanjiDataProvider = _defaultKanjiDataProvider,
|
||||||
|
|
||||||
this.loop = true,
|
this.loop = true,
|
||||||
this.timingFunction = TimingFunction.ease,
|
|
||||||
|
|
||||||
this.strokeColor = Colors.black,
|
this.strokeColor = Colors.black,
|
||||||
this.currentStrokeColor = Colors.red,
|
|
||||||
this.strokeUnfilledColor = const Color(0xA0DDDDDD),
|
this.strokeUnfilledColor = const Color(0xA0DDDDDD),
|
||||||
this.strokeWidth = 3.0,
|
this.strokeWidth = 3.0,
|
||||||
this.backgroundColor = Colors.transparent,
|
this.backgroundColor = Colors.transparent,
|
||||||
|
|
||||||
this.showBrush = true,
|
this.showBrush = true,
|
||||||
this.brushColor = Colors.red,
|
this.brushColor = Colors.red,
|
||||||
this.brushRadius = 4.0,
|
this.brushRadius = 4.0,
|
||||||
|
this.showStrokeNumbers = true,
|
||||||
this.showStrokeNumbers = false,
|
|
||||||
this.strokeNumberColor = Colors.grey,
|
this.strokeNumberColor = Colors.grey,
|
||||||
this.currentStrokeNumberColor = Colors.red,
|
this.currentStrokeNumberColor = Colors.red,
|
||||||
this.strokeNumberFontSize = 4.0,
|
this.strokeNumberFontSize = 4.0,
|
||||||
this.strokeNumberFontFamily,
|
this.strokeNumberFontFamily,
|
||||||
|
|
||||||
this.showCross = true,
|
this.showCross = true,
|
||||||
this.crossColor = const Color(0x40AAAAAA),
|
this.crossColor = const Color(0x40AAAAAA),
|
||||||
this.crossStrokeWidth = 0.8,
|
this.crossStrokeWidth = 0.8,
|
||||||
@@ -144,12 +95,8 @@ class _KanimajiState extends State<Kanimaji>
|
|||||||
.toList() ??
|
.toList() ??
|
||||||
[];
|
[];
|
||||||
|
|
||||||
/// sqrt, ie a stroke 4 times the length is drawn
|
|
||||||
/// at twice the speed, in twice the time.
|
|
||||||
static double _strokeLengthToDuration(double length) => math.sqrt(length) / 8;
|
|
||||||
|
|
||||||
List<double> get _pathDurations =>
|
List<double> get _pathDurations =>
|
||||||
_pathLengths.map((len) => _strokeLengthToDuration(len)).toList();
|
_pathLengths.map((len) => strokeLengthToDuration(len)).toList();
|
||||||
|
|
||||||
static const double _viewBoxWidth = 109.0;
|
static const double _viewBoxWidth = 109.0;
|
||||||
static const double _viewBoxHeight = 109.0;
|
static const double _viewBoxHeight = 109.0;
|
||||||
@@ -169,7 +116,6 @@ class _KanimajiState extends State<Kanimaji>
|
|||||||
_loadAndParseSvg().then((_) => _configureController());
|
_loadAndParseSvg().then((_) => _configureController());
|
||||||
} else if (oldWidget.loop != widget.loop ||
|
} else if (oldWidget.loop != widget.loop ||
|
||||||
oldWidget.strokeColor != widget.strokeColor ||
|
oldWidget.strokeColor != widget.strokeColor ||
|
||||||
oldWidget.currentStrokeColor != widget.currentStrokeColor ||
|
|
||||||
oldWidget.strokeUnfilledColor != widget.strokeUnfilledColor ||
|
oldWidget.strokeUnfilledColor != widget.strokeUnfilledColor ||
|
||||||
oldWidget.strokeWidth != widget.strokeWidth ||
|
oldWidget.strokeWidth != widget.strokeWidth ||
|
||||||
oldWidget.backgroundColor != widget.backgroundColor ||
|
oldWidget.backgroundColor != widget.backgroundColor ||
|
||||||
@@ -245,34 +191,25 @@ class _KanimajiState extends State<Kanimaji>
|
|||||||
height: _viewBoxHeight,
|
height: _viewBoxHeight,
|
||||||
child: CustomPaint(
|
child: CustomPaint(
|
||||||
painter: _KanimajiPainter(
|
painter: _KanimajiPainter(
|
||||||
progress: _controller.value,
|
|
||||||
|
|
||||||
paths: _kanjiData!.paths.map((p) => p.svgPath.toUiPath()).toList(),
|
paths: _kanjiData!.paths.map((p) => p.svgPath.toUiPath()).toList(),
|
||||||
pathLengths: _pathLengths,
|
pathLengths: _pathLengths,
|
||||||
pathDurations: _pathDurations,
|
pathDurations: _pathDurations,
|
||||||
strokeNumbers: _kanjiData!.strokeNumbers,
|
strokeNumbers: _kanjiData!.strokeNumbers,
|
||||||
|
progress: _controller.value,
|
||||||
viewBoxWidth: _viewBoxWidth,
|
|
||||||
viewBoxHeight: _viewBoxHeight,
|
|
||||||
|
|
||||||
timingFunction: widget.timingFunction,
|
|
||||||
|
|
||||||
strokeColor: widget.strokeColor,
|
strokeColor: widget.strokeColor,
|
||||||
currentStrokeColor: widget.currentStrokeColor,
|
|
||||||
strokeUnfilledColor: widget.strokeUnfilledColor,
|
strokeUnfilledColor: widget.strokeUnfilledColor,
|
||||||
strokeWidth: widget.strokeWidth,
|
strokeWidth: widget.strokeWidth,
|
||||||
backgroundColor: widget.backgroundColor,
|
backgroundColor: widget.backgroundColor,
|
||||||
|
viewBoxWidth: _viewBoxWidth,
|
||||||
|
viewBoxHeight: _viewBoxHeight,
|
||||||
showBrush: widget.showBrush,
|
showBrush: widget.showBrush,
|
||||||
brushColor: widget.brushColor,
|
brushColor: widget.brushColor,
|
||||||
brushRadius: widget.brushRadius,
|
brushRadius: widget.brushRadius,
|
||||||
|
|
||||||
showStrokeNumbers: widget.showStrokeNumbers,
|
showStrokeNumbers: widget.showStrokeNumbers,
|
||||||
strokeNumberColor: widget.strokeNumberColor,
|
strokeNumberColor: widget.strokeNumberColor,
|
||||||
currentStrokeNumberColor: widget.currentStrokeNumberColor,
|
currentStrokeNumberColor: widget.currentStrokeNumberColor,
|
||||||
strokeNumberFontSize: widget.strokeNumberFontSize,
|
strokeNumberFontSize: widget.strokeNumberFontSize,
|
||||||
strokeNumberFontFamily: widget.strokeNumberFontFamily,
|
strokeNumberFontFamily: widget.strokeNumberFontFamily,
|
||||||
|
|
||||||
showCross: widget.showCross,
|
showCross: widget.showCross,
|
||||||
crossColor: widget.crossColor,
|
crossColor: widget.crossColor,
|
||||||
crossStrokeWidth: widget.crossStrokeWidth,
|
crossStrokeWidth: widget.crossStrokeWidth,
|
||||||
@@ -308,10 +245,7 @@ class _KanimajiPainter extends CustomPainter {
|
|||||||
final double viewBoxWidth;
|
final double viewBoxWidth;
|
||||||
final double viewBoxHeight;
|
final double viewBoxHeight;
|
||||||
|
|
||||||
final TimingFunction timingFunction;
|
|
||||||
|
|
||||||
final Color strokeColor;
|
final Color strokeColor;
|
||||||
final Color currentStrokeColor;
|
|
||||||
final Color strokeUnfilledColor;
|
final Color strokeUnfilledColor;
|
||||||
final double strokeWidth;
|
final double strokeWidth;
|
||||||
final Color backgroundColor;
|
final Color backgroundColor;
|
||||||
@@ -343,10 +277,7 @@ class _KanimajiPainter extends CustomPainter {
|
|||||||
required this.viewBoxWidth,
|
required this.viewBoxWidth,
|
||||||
required this.viewBoxHeight,
|
required this.viewBoxHeight,
|
||||||
|
|
||||||
required this.timingFunction,
|
|
||||||
|
|
||||||
required this.strokeColor,
|
required this.strokeColor,
|
||||||
required this.currentStrokeColor,
|
|
||||||
required this.strokeUnfilledColor,
|
required this.strokeUnfilledColor,
|
||||||
required this.strokeWidth,
|
required this.strokeWidth,
|
||||||
required this.backgroundColor,
|
required this.backgroundColor,
|
||||||
@@ -396,13 +327,6 @@ class _KanimajiPainter extends CustomPainter {
|
|||||||
..strokeWidth = strokeWidth / scale
|
..strokeWidth = strokeWidth / scale
|
||||||
..color = strokeColor;
|
..color = strokeColor;
|
||||||
|
|
||||||
final Paint currentStrokePaint = Paint()
|
|
||||||
..style = PaintingStyle.stroke
|
|
||||||
..strokeCap = StrokeCap.round
|
|
||||||
..strokeJoin = StrokeJoin.round
|
|
||||||
..strokeWidth = strokeWidth / scale
|
|
||||||
..color = currentStrokeColor;
|
|
||||||
|
|
||||||
final Paint brushPaint = Paint()
|
final Paint brushPaint = Paint()
|
||||||
..style = PaintingStyle.fill
|
..style = PaintingStyle.fill
|
||||||
..color = brushColor;
|
..color = brushColor;
|
||||||
@@ -484,14 +408,12 @@ class _KanimajiPainter extends CustomPainter {
|
|||||||
? absolutePathDurations[currentlyDrawingIndex]
|
? absolutePathDurations[currentlyDrawingIndex]
|
||||||
: 0.0);
|
: 0.0);
|
||||||
|
|
||||||
final double strokeProgress = timingFunction.func(
|
final double strokeProgress = (relativeElapsedTime / dur).clamp(0.0, 1.0);
|
||||||
(relativeElapsedTime / dur).clamp(0.0, 1.0),
|
|
||||||
);
|
|
||||||
final ui.PathMetrics metrics = path.computeMetrics();
|
final ui.PathMetrics metrics = path.computeMetrics();
|
||||||
final ui.PathMetric metric = metrics.first;
|
final ui.PathMetric metric = metrics.first;
|
||||||
final double drawLength = len * strokeProgress;
|
final double drawLength = len * strokeProgress;
|
||||||
final ui.Path partialPath = metric.extractPath(0, drawLength);
|
final ui.Path partialPath = metric.extractPath(0, drawLength);
|
||||||
canvas.drawPath(partialPath, currentStrokePaint);
|
canvas.drawPath(partialPath, filledPaint);
|
||||||
|
|
||||||
if (showBrush) {
|
if (showBrush) {
|
||||||
final ui.Tangent? tangent = metric.getTangentForOffset(drawLength);
|
final ui.Tangent? tangent = metric.getTangentForOffset(drawLength);
|
||||||
|
|||||||
@@ -394,15 +394,13 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// NOTE: The parser seems to just ignore the arguments to Z, so this doesn't throw an error.
|
test(
|
||||||
// This is probably not completely spec compliant, but it's good enough for now.
|
'Errors',
|
||||||
// test(
|
() => expect(
|
||||||
// 'Errors',
|
parsePath("M 100 100 L 200 200 Z 100 200"),
|
||||||
// () => expect(
|
throwsA(const TypeMatcher<InvalidPathError>()),
|
||||||
// parsePath("M 100 100 L 200 200 Z 100 200"),
|
),
|
||||||
// throwsA(const TypeMatcher<InvalidPathError>()),
|
);
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
|
|
||||||
test(
|
test(
|
||||||
"Nonpath: It's possible in SVG to create paths that has zero length, "
|
"Nonpath: It's possible in SVG to create paths that has zero length, "
|
||||||
|
|||||||
Reference in New Issue
Block a user