From 779f1199922a683a0ed60f50bc0c0e6138ff9942 Mon Sep 17 00:00:00 2001 From: h7x4 Date: Tue, 15 Apr 2025 13:59:35 +0200 Subject: [PATCH] WIP --- lib/kanimaji/animate_kanji.dart | 214 ++++++++++++++++++++++---------- lib/kanimaji/commandline.dart | 0 lib/kanimaji/gif.dart | 6 + lib/kanimaji/js_svg.dart | 0 lib/kanimaji/options.dart | 24 ++++ lib/kanimaji/svg.dart | 3 + pubspec.yaml | 6 + test/svg/path_test.dart | 33 ++--- 8 files changed, 202 insertions(+), 84 deletions(-) create mode 100644 lib/kanimaji/commandline.dart create mode 100644 lib/kanimaji/gif.dart create mode 100644 lib/kanimaji/js_svg.dart create mode 100644 lib/kanimaji/options.dart create mode 100644 lib/kanimaji/svg.dart diff --git a/lib/kanimaji/animate_kanji.dart b/lib/kanimaji/animate_kanji.dart index 8e3ece9..7384d44 100644 --- a/lib/kanimaji/animate_kanji.dart +++ b/lib/kanimaji/animate_kanji.dart @@ -1,5 +1,10 @@ +import 'dart:async'; import 'dart:io'; 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'; @@ -144,31 +149,13 @@ void clearPreviousElements(XmlDocument doc) { } } -/// 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', -}) { +XmlDocument fetchXML(String inputFile) { + + //------------------------------ + // FETCH DATA FILE + //------------------------------ + 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()); @@ -178,6 +165,44 @@ void createAnimation({ doc.rootElement.setAttribute('xlink:used', ''); clearPreviousElements(doc); + return doc; +} + +// void generateCssForPath(XmlNode p, void Function addCss) { +// } + +/// Note: setting any color to transparent will result in a much bigger +/// filesize for GIFs. +FutureOr 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 XmlDocumentFragment pathCopyGroup({ @@ -227,6 +252,10 @@ void createAnimation({ ); } + //------------------------------ + // CALCULATE STROKE TIMES + //------------------------------ + // compute total length and time, at first double totlen = 0; double tottime = 0; @@ -254,6 +283,10 @@ void createAnimation({ final double actualAnimationTime = animationTime; animationTime += waitAfter; + //------------------------------ + // START ADDING CSS + //------------------------------ + final Map staticCss = {}; late String animatedCss; @@ -286,6 +319,10 @@ void createAnimation({ double elapsedlen = 0; double elapsedtime = 0; + //------------------------------ + // ADD CSS FOR STROKE STYLE + //------------------------------ + // add css elements for all strokes for (final XmlNode g in doc .getElement('svg', namespace: namespaces['n'])! @@ -326,7 +363,12 @@ void createAnimation({ } } + //------------------------------ + // ADD CSS FOR HREFS + //------------------------------ + for (final p in g.findAllElements("path", namespace: namespaces['n'])) { + final pathid = p.getAttribute('id') as String; final pathidcss = pathid.replaceAll(':', '\\3a '); @@ -355,6 +397,10 @@ void createAnimation({ brushBorderGroupElement = addHref('brush-brd', brushBrdGroup); } + //------------------------------ + // CALCULATE RELATIVE TIMING + //------------------------------ + final pathname = pathid.replaceAll(RegExp(r'^kvg:'), ''); final pathlen = _computePathLength(p.getAttribute('d') as String); final duration = strokeLengthToDuration(pathlen); @@ -378,6 +424,10 @@ void createAnimation({ final animStart = elapsedtime / tottime * 100; final animEnd = newelapsedtime / tottime * 100; + //------------------------------ + // GENERATE SVG SPECIFIC ANIMATION CSS + //------------------------------ + if (GENERATE_SVG) { // animation stroke progression animatedCss += ''' @@ -418,6 +468,10 @@ void createAnimation({ } } + //------------------------------ + // GENERATE JS SVG SPECIFIC ANIMATION CSS + //------------------------------ + if (GENERATE_JS_SVG) { jsAnimatedCss += '\n/* stroke $pathid */\n'; @@ -468,6 +522,10 @@ void createAnimation({ } } + //------------------------------ + // GENERATE GIF SPECIFIC STATIC FRAME CSS + //------------------------------ + if (GENERATE_GIF) { for (final k in staticCss.keys) { final time = k * GIF_FRAME_DURATION; @@ -539,17 +597,32 @@ void createAnimation({ elapsedlen = newelapsedlen; elapsedtime = newelapsedtime; } + + // for (final p in g.findAllElements("path", namespace: namespaces['n'])) { + // generateCssForPath(p); + // } } + //------------------------------ + // ADD CSS TO XML-OBJECT INSTANCE + //------------------------------ + void addGroup(XmlDocumentFragment g) => doc.root.firstElementChild?.children.add(g); // insert groups - if (showBrush && !showBrushFrontBorder) addGroup(brushBrdGroup); + if (showBrush && !showBrushFrontBorder) + addGroup(brushBrdGroup); addGroup(bgGroup); - if (showBrush && showBrushFrontBorder) addGroup(brushBrdGroup); + if (showBrush && showBrushFrontBorder) + addGroup(brushBrdGroup); addGroup(animGroup); - if (showBrush) addGroup(brushGroup); + if (showBrush) + addGroup(brushGroup); + + //------------------------------ + // PRODUCE SVG + //------------------------------ if (GENERATE_SVG) { print(animatedCss); @@ -567,24 +640,51 @@ void createAnimation({ 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)]]}) + //------------------------------ + // PRODUCE GIF + //------------------------------ - // 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 + if (GENERATE_GIF) { + // var svgframefiles = []; + // var pngframefiles = []; + // var svgexport_data = []; + + final encoder = image.GifEncoder(dither: 0 as DitherKernel); + + for (var k in staticCss.keys) { + final style = XmlBuilder()..element( + 'style', + 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 // svgexport_datafile = filename_noext_ascii+"_export_data.json" @@ -675,28 +775,4 @@ void createAnimation({ // doc.getroot().remove(style) // print('written $svgfile'); // } -} - -void main(List 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( - '' + - fileList.map((e) => File(e).readAsStringSync().replaceAll(']>', '')).join('\n') + - '' - ); - // createAnimation( - // inputFile: 'assets/kanjivg/kanji/060c5.svg', - // outputFile: 'test.svg', - // ); -} +} \ No newline at end of file diff --git a/lib/kanimaji/commandline.dart b/lib/kanimaji/commandline.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/kanimaji/gif.dart b/lib/kanimaji/gif.dart new file mode 100644 index 0000000..d0d4628 --- /dev/null +++ b/lib/kanimaji/gif.dart @@ -0,0 +1,6 @@ + +import 'package:kanimaji/kanimaji/options.dart'; + +Future createGif(String kanji, KanimajiOptions options) async { + +} \ No newline at end of file diff --git a/lib/kanimaji/js_svg.dart b/lib/kanimaji/js_svg.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/kanimaji/options.dart b/lib/kanimaji/options.dart new file mode 100644 index 0000000..3e0267e --- /dev/null +++ b/lib/kanimaji/options.dart @@ -0,0 +1,24 @@ +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'; +} \ No newline at end of file diff --git a/lib/kanimaji/svg.dart b/lib/kanimaji/svg.dart new file mode 100644 index 0000000..f9df242 --- /dev/null +++ b/lib/kanimaji/svg.dart @@ -0,0 +1,3 @@ +// String calculate_css() { + +// } \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index b1b5932..db7d8cb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,6 +3,9 @@ description: A new Flutter package project. version: 0.0.1 homepage: +executables: + kanimaji: lib/kanimaji/animate_kanji + environment: sdk: ">=2.12.0 <3.0.0" flutter: ">=1.17.0" @@ -11,6 +14,9 @@ dependencies: bisection: ^0.4.3+1 flutter: sdk: flutter + flutter_svg: ^1.0.3 + gifencoder: ^1.0.0 + image: ^3.1.3 xml: ^5.3.1 dev_dependencies: diff --git a/test/svg/path_test.dart b/test/svg/path_test.dart index cb56a23..f5b4d15 100644 --- a/test/svg/path_test.dart +++ b/test/svg/path_test.dart @@ -42,11 +42,14 @@ import 'package:kanimaji/svg/path.dart'; // } class MoreOrLessEqualsToPoint extends Matcher { - const MoreOrLessEqualsToPoint(); + static const double threshold = 0.000001; + final Point _expected; + + const MoreOrLessEqualsToPoint(this._expected); @override - bool matches(covariant Finder finder, Map matchState) - => finder is Point && finder.x + bool matches(covariant Object? object, Map matchState) + => object is Point && (_expected.x - object.x).abs() >=threshold && (_expected.y - object.y).abs() >=threshold; @override Description describe(Description description) => description.add('in card'); @@ -83,19 +86,19 @@ test('Test lines', () { expect(line3.size(), moreOrLessEquals(500)); }); - def test_equality(self): - // This is to test the __eq__ and __ne__ methods, so we can't use - // assertEqual and assertNotEqual - line = Line(0j, 400 + 0j) - self.assertTrue(line == Line(0, 400)) - self.assertTrue(line != Line(100, 400)) - self.assertFalse(line == str(line)) - self.assertTrue(line != str(line)) - self.assertFalse( - CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j) == line - ) + // def test_equality(self): + // // This is to test the __eq__ and __ne__ methods, so we can't use + // // assertEqual and assertNotEqual + // line = Line(0j, 400 + 0j) + // self.assertTrue(line == Line(0, 400)) + // self.assertTrue(line != Line(100, 400)) + // self.assertFalse(line == str(line)) + // self.assertTrue(line != str(line)) + // self.assertFalse( + // CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j) == line + // ) - }); + // });