1 Commits

Author SHA1 Message Date
779f119992 WIP 2025-04-15 13:59:35 +02:00
8 changed files with 202 additions and 84 deletions

View File

@@ -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<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
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<int, String> 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<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',
// );
}
}

View File

6
lib/kanimaji/gif.dart Normal file
View File

@@ -0,0 +1,6 @@
import 'package:kanimaji/kanimaji/options.dart';
Future<void> createGif(String kanji, KanimajiOptions options) async {
}

0
lib/kanimaji/js_svg.dart Normal file
View File

24
lib/kanimaji/options.dart Normal file
View File

@@ -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';
}

3
lib/kanimaji/svg.dart Normal file
View File

@@ -0,0 +1,3 @@
// String calculate_css() {
// }

View File

@@ -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:

View File

@@ -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<dynamic, dynamic> matchState)
=> finder is Point && finder.x
bool matches(covariant Object? object, Map<dynamic, dynamic> 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
// )
});
// });