Several changes

This commit is contained in:
2022-02-01 00:13:50 +01:00
parent 1ab1f067b5
commit 4151df40b5
8 changed files with 361 additions and 266 deletions

41
lib/common/Point.dart Normal file
View File

@@ -0,0 +1,41 @@
import 'dart:math' as math;
class Point {
final num x;
final num y;
const Point(this.x, this.y);
const Point.from({this.x = 0, this.y = 0});
static const zero = Point(0, 0);
operator +(covariant Point p) => Point(x + p.x, y + p.y);
operator -(covariant Point p) => Point(x - p.x, y - p.y);
operator *(covariant Point p) => Point(x * p.x, y * p.y);
operator /(covariant Point p) => Point(x / p.x, y / p.y);
Point addX(num n) => Point(x + n, y);
Point addY(num n) => Point(x, y + n);
Point add(num n) => Point(x + n, y + n);
Point subtractX(num n) => Point(x - n, y);
Point subtractY(num n) => Point(x, y - n);
Point subtractXY(num n) => Point(x - n, y - n);
Point xSubtract(num n) => Point(n - x, y);
Point ySubtract(num n) => Point(x, n - y);
Point xySubtract(num n) => Point(n - x, n - y);
Point timesX(num n) => Point(x * n, y);
Point timesY(num n) => Point(x, y * n);
Point times(num n) => Point(x * n, y * n);
Point dividesX(num n) => Point(x / n, y);
Point dividesY(num n) => Point(x, y / n);
Point divides(num n) => Point(x / n, y / n);
Point pow(int n) => Point(math.pow(x, n), math.pow(y, n));
double abs() => math.sqrt(x * x + y * y);
@override
String toString() => '($x,$y)';
}

View File

@@ -1,60 +1,99 @@
/// ignore_for_file: non_constant_identifier_names, avoid_print, unused_local_variable, dead_code, constant_identifier_names /// ignore_for_file: non_constant_identifier_names, avoid_print, unused_local_variable, dead_code, constant_identifier_names
import 'dart:io'; import 'dart:io';
import 'dart:math' show min; import 'dart:math' show min, sqrt, pow;
import '../svg/parser.dart'; import '../svg/parser.dart';
import '../common/Point.dart';
import 'bezier_cubic.dart' as bezier_cubic; import 'bezier_cubic.dart' as bezier_cubic;
import 'settings.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
double computePathLength(String path) => double _computePathLength(String path) =>
parse_path(path).size(error: 1e-8).toDouble(); parse_path(path).size(error: 1e-8).toDouble();
String shescape(path) => "'${path.replace(RegExp(r"(?=['\\\\])"), "\\\\")}'"; String _shescape(String path) =>
"'${path.replaceAll(RegExp(r"(?=['\\\\])"), "\\\\")}'";
String dedent(String s) { extension _Dedent on String {
String get dedented {
final withoutEmptyLines = final withoutEmptyLines =
s.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.map((l) => l.replaceRange(0, whitespaceToRemove, '')).join('\n'); 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: // ease, ease-in, etc:
// https://developer.mozilla.org/en-US/docs/Web/CSS/timing-function#ease // https://developer.mozilla.org/en-US/docs/Web/CSS/timing-function#ease
const pt1 = bezier_cubic.Point(0, 0); const pt1 = Point(0, 0);
const easeCt1 = bezier_cubic.Point(0.25, 0.1); const easeCt1 = Point(0.25, 0.1);
const easeCt2 = bezier_cubic.Point(0.25, 1.0); const easeCt2 = Point(0.25, 1.0);
const easeInCt1 = bezier_cubic.Point(0.42, 0.0); const easeInCt1 = Point(0.42, 0.0);
const easeInCt2 = bezier_cubic.Point(1.0, 1.0); const easeInCt2 = Point(1.0, 1.0);
const easeInOutCt1 = bezier_cubic.Point(0.42, 0.0); const easeInOutCt1 = Point(0.42, 0.0);
const easeInOutCt2 = bezier_cubic.Point(0.58, 1.0); const easeInOutCt2 = Point(0.58, 1.0);
const easeOutCt1 = bezier_cubic.Point(0.0, 0.0); const easeOutCt1 = Point(0.0, 0.0);
const easeOutCt2 = bezier_cubic.Point(0.58, 1.0); const easeOutCt2 = Point(0.58, 1.0);
const pt2 = bezier_cubic.Point(1, 1); const pt2 = Point(1, 1);
double linear(x) => x; // class {
double ease(x) => bezier_cubic.value(pt1, easeCt1, easeCt2, pt2, x); // }
double easeIn(x) => bezier_cubic.value(pt1, easeInCt1, easeInCt2, pt2, x);
double easeInOut(x) =>
bezier_cubic.value(pt1, easeInOutCt1, easeInOutCt2, pt2, x);
double easeOut(x) => bezier_cubic.value(pt1, easeOutCt1, easeOutCt2, pt2, x);
const Map<String, double Function(double)> timingFunctions = { enum TimingFunction {
'linear': linear, linear,
'ease': ease, ease,
'ease-in': easeIn, easeIn,
'ease-in-out': easeInOut, easeInOut,
'ease-out': easeOut easeOut,
}; }
final myTimingFunction = timingFunctions[TIMING_FUNCTION]!; extension Funcs on TimingFunction {
double Function(double) get func => {
TimingFunction.linear: (double x) => x,
TimingFunction.ease: (double x) =>
bezier_cubic.value(pt1, easeCt1, easeCt2, pt2, x),
TimingFunction.easeIn: (double x) =>
bezier_cubic.value(pt1, easeInCt1, easeInCt2, pt2, x),
TimingFunction.easeInOut: (double x) =>
bezier_cubic.value(pt1, easeInOutCt1, easeInOutCt2, pt2, x),
TimingFunction.easeOut: (double x) =>
bezier_cubic.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 // we will need this to deal with svg
const namespaces = { const namespaces = {
@@ -64,19 +103,29 @@ const namespaces = {
// 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);
void createAnimation(String filename) { // gif settings
print('processing $filename'); // const DELETE_TEMPORARY_FILES = false;
final String filenameNoext = filename.replaceAll(RegExp(r'\.[^\.]+$'), ''); const GIF_SIZE = 150;
final String baseid = basename(filenameNoext); 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;
// load xml // edit here to decide what will be generated
final XmlDocument doc = XmlDocument.parse(File(filename).readAsStringSync()); const GENERATE_SVG = true;
const GENERATE_JS_SVG = true;
const GENERATE_GIF = true;
// for xlink namespace introduction /// sqrt, ie a stroke 4 times the length is drawn
doc.rootElement.setAttribute('xmlns:xlink', namespaces['xlink']); /// at twice the speed, in twice the time.
doc.rootElement.setAttribute('xlink:used', ''); double strokeLengthToDuration(double length) => sqrt(length) / 8;
// clear all extra elements this program may have previously added /// 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 for (final XmlNode el in doc
.getElement('svg', namespace: namespaces['n']) .getElement('svg', namespace: namespaces['n'])
?.getElement('style', namespace: namespaces['n']) ?.getElement('style', namespace: namespaces['n'])
@@ -95,10 +144,49 @@ void createAnimation(String filename) {
g.parent!.children.remove(g); g.parent!.children.remove(g);
} }
} }
}
// create groups with a copies (references actually) of the paths /// Note: setting any color to transparent will result in a much bigger
XmlDocumentFragment pathCopyGroup( /// filesize for GIFs.
{required String id, required String color, required double width}) { 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(); final builder = XmlBuilder();
builder.element( builder.element(
'g', 'g',
@@ -117,27 +205,27 @@ void createAnimation(String filename) {
final bgGroup = pathCopyGroup( final bgGroup = pathCopyGroup(
id: 'bg', id: 'bg',
color: STOKE_UNFILLED_COLOR, color: strokeUnfilledColor,
width: STOKE_UNFILLED_WIDTH, width: strokeUnfilledWidth,
); );
final animGroup = pathCopyGroup( final animGroup = pathCopyGroup(
id: 'anim', id: 'anim',
color: STOKE_FILLED_COLOR, color: strokeFilledColor,
width: STOKE_FILLED_WIDTH, width: strokeFilledWidth,
); );
late final XmlDocumentFragment brushGroup; late final XmlDocumentFragment brushGroup;
late final XmlDocumentFragment brushBrdGroup; late final XmlDocumentFragment brushBrdGroup;
if (SHOW_BRUSH) { if (showBrush) {
brushGroup = pathCopyGroup( brushGroup = pathCopyGroup(
id: 'brush', id: 'brush',
color: BRUSH_COLOR, color: brushColor,
width: BRUSH_WIDTH, width: brushWidth,
); );
brushBrdGroup = pathCopyGroup( brushBrdGroup = pathCopyGroup(
id: 'brush-brd', id: 'brush-brd',
color: BRUSH_BORDER_COLOR, color: brushBorderColor,
width: BRUSH_BORDER_WIDTH, width: brushBorderWidth,
); );
} }
@@ -156,22 +244,23 @@ void createAnimation(String filename) {
} }
for (final p in g.findAllElements('path', namespace: namespaces['n'])) { for (final p in g.findAllElements('path', namespace: namespaces['n'])) {
final pathlen = computePathLength(p.getAttribute('d')!); final pathlen = _computePathLength(p.getAttribute('d')!);
final duration = stroke_length_to_duration(pathlen); final duration = strokeLengthToDuration(pathlen);
totlen += pathlen; totlen += pathlen;
tottime += duration; tottime += duration;
} }
} }
double animationTime = time_rescale(tottime); // math.pow(3 * tottime, 2.0/3) double animationTime = timeRescale(tottime); // math.pow(3 * tottime, 2.0/3)
tottime += WAIT_AFTER * tottime / animationTime; tottime += waitAfter * tottime / animationTime;
final double actualAnimationTime = animationTime; final double actualAnimationTime = animationTime;
animationTime += WAIT_AFTER; animationTime += waitAfter;
final Map<int, String> staticCss = {}; final Map<int, String> staticCss = {};
late String animatedCss; late String animatedCss;
final jsAnimationElements = []; // collect the ids of animating elements
final jsAnimationTimes = []; // the time set (as default) for each animation /// collect the ids of animating elements
final List<JsAnimationElement> jsAnimationElements = [];
String jsAnimatedCss = ''; String jsAnimatedCss = '';
@@ -186,19 +275,19 @@ void createAnimation(String filename) {
} }
'''; ''';
} }
late final int lastFrameIndex;
late final double lastFrameDelay;
if (GENERATE_GIF) { if (GENERATE_GIF) {
// final static_css = {}; // final static_css = {};
final last_frame_index = actualAnimationTime ~/ GIF_FRAME_DURATION + 1; lastFrameIndex = actualAnimationTime ~/ GIF_FRAME_DURATION + 1;
for (int i = 0; i < last_frame_index + 1; i++) { for (int i = 0; i < lastFrameIndex + 1; i++) {
staticCss[i] = cssHeader; staticCss[i] = cssHeader;
} }
final last_frame_delay = lastFrameDelay = animationTime - lastFrameIndex * GIF_FRAME_DURATION;
animationTime - last_frame_index * GIF_FRAME_DURATION;
} }
double elapsedlen = 0; double elapsedlen = 0;
double elapsedtime = 0; double elapsedtime = 0;
// add css elements for all strokes // add css elements for all strokes
for (final XmlNode g in doc for (final XmlNode g in doc
.getElement('svg', namespace: namespaces['n'])! .getElement('svg', namespace: namespaces['n'])!
@@ -206,11 +295,12 @@ void createAnimation(String filename) {
// 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 = dedent(''' final String rule = '''
#${groupid.replaceAll(':', '\\3a ')} { #${groupid.replaceAll(':', '\\3a ')} {
display: none; display: none;
} }
'''); '''
.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) {
@@ -222,12 +312,13 @@ void createAnimation(String filename) {
} }
final gidcss = groupid.replaceAll(':', '\\3a '); final gidcss = groupid.replaceAll(':', '\\3a ');
final rule = dedent(''' final rule = '''
#$gidcss { #$gidcss {
stroke-width: ${STOKE_BORDER_WIDTH.toStringAsFixed(1)}px !important; stroke-width: ${strokeBorderWidth.toStringAsFixed(1)}px !important;
stroke: $STOKE_BORDER_COLOR !important; stroke: $strokeBorderColor !important;
} }
'''); '''
.dedented;
if (GENERATE_SVG) animatedCss += rule; if (GENERATE_SVG) animatedCss += rule;
if (GENERATE_JS_SVG) jsAnimatedCss += rule; if (GENERATE_JS_SVG) jsAnimatedCss += rule;
@@ -241,39 +332,47 @@ void createAnimation(String filename) {
final pathid = p.getAttribute('id') as String; final pathid = p.getAttribute('id') as String;
final pathidcss = pathid.replaceAll(':', '\\3a '); final pathidcss = pathid.replaceAll(':', '\\3a ');
if (GENERATE_JS_SVG) jsAnimationElements.add({}); XmlDocumentFragment addHref(String suffix, XmlDocumentFragment parent) {
void addHref(String suffix, XmlDocumentFragment element) {
final builder = XmlBuilder(); final builder = XmlBuilder();
builder.element( builder.element(
'use', 'use',
attributes: {'id': '$pathid-$suffix', 'xlink:href': '#$pathid'}, attributes: {'id': '$pathid-$suffix', 'xlink:href': '#$pathid'},
); );
final ref = builder.buildFragment(); final ref = builder.buildFragment();
element.firstElementChild!.children.add(ref); parent.firstElementChild!.children.add(ref);
if (GENERATE_JS_SVG) jsAnimationElements.last[suffix] = ref; return ref;
} }
final String bgPathidcss = '$pathidcss-bg'; final String bgPathidcss = '$pathidcss-bg';
final String animPathidcss = '$pathidcss-anim'; final String animPathidcss = '$pathidcss-anim';
final String brushPathidcss = '$pathidcss-brush'; final String brushPathidcss = '$pathidcss-brush';
final String brushBrdPathidcss = '$pathidcss-brush-brd'; final String brushBorderPathidcss = '$pathidcss-brush-brd';
addHref('bg', bgGroup); final bgGroupElement = addHref('bg', bgGroup);
addHref('anim', animGroup); final animGroupElement = addHref('anim', animGroup);
XmlDocumentFragment? brushGroupElement;
if (SHOW_BRUSH) { XmlDocumentFragment? brushBorderGroupElement;
addHref('brush', brushGroup); if (showBrush) {
addHref('brush-brd', brushBrdGroup); brushGroupElement = addHref('brush', brushGroup);
brushBorderGroupElement = addHref('brush-brd', brushBrdGroup);
} }
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 = stroke_length_to_duration(pathlen); final duration = strokeLengthToDuration(pathlen);
final relduration = duration * tottime / animationTime; // unscaled time final relativeDuration =
duration * tottime / animationTime; // unscaled time
if (GENERATE_JS_SVG) { if (GENERATE_JS_SVG) {
jsAnimationTimes.add(relduration); jsAnimationElements.add(
JsAnimationElement(
bg: bgGroupElement,
anim: animGroupElement,
brush: brushGroupElement,
brushBorder: brushBorderGroupElement,
time: relativeDuration,
),
);
} }
final newelapsedlen = elapsedlen + pathlen; final newelapsedlen = elapsedlen + pathlen;
@@ -283,7 +382,7 @@ void createAnimation(String filename) {
if (GENERATE_SVG) { if (GENERATE_SVG) {
// animation stroke progression // animation stroke progression
animatedCss += dedent(''' 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)}; }
@@ -292,52 +391,55 @@ void createAnimation(String filename) {
} }
@keyframes showhide-$pathname { @keyframes showhide-$pathname {
${animStart.toStringAsFixed(3)}% { visibility: hidden; } ${animStart.toStringAsFixed(3)}% { visibility: hidden; }
${animEnd.toStringAsFixed(3)}% { stroke: $STOKE_FILLING_COLOR; } ${animEnd.toStringAsFixed(3)}% { stroke: $strokeFillingColor; }
} }
#$animPathidcss { #$animPathidcss {
stroke-dasharray: ${pathlen.toStringAsFixed(3)} ${pathlen.toStringAsFixed(3)}; stroke-dasharray: ${pathlen.toStringAsFixed(3)} ${pathlen.toStringAsFixed(3)};
stroke-dashoffset: 0; stroke-dashoffset: 0;
animation: strike-$pathname ${animationTime.toStringAsFixed(3)}s $TIMING_FUNCTION infinite, animation: strike-$pathname ${animationTime.toStringAsFixed(3)}s ${timingFunction.name} infinite,
showhide-$pathname ${animationTime.toStringAsFixed(3)}s step-start infinite; showhide-$pathname ${animationTime.toStringAsFixed(3)}s step-start infinite;
} }
'''); '''
.dedented;
if (SHOW_BRUSH) { if (showBrush) {
// brush element visibility // brush element visibility
animatedCss += dedent(''' 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; }
100% { visibility: hidden; } 100% { visibility: hidden; }
} }
#$brushPathidcss, #$brushBrdPathidcss { #$brushPathidcss, #$brushBorderPathidcss {
stroke-dasharray: 0 ${pathlen.toStringAsFixed(3)}; stroke-dasharray: 0 ${pathlen.toStringAsFixed(3)};
animation: strike-$pathname ${animationTime.toStringAsFixed(3)}s $TIMING_FUNCTION infinite, animation: strike-$pathname ${animationTime.toStringAsFixed(3)}s ${timingFunction.name} infinite,
showhide-brush-$pathname ${animationTime.toStringAsFixed(3)}s step-start infinite; showhide-brush-$pathname ${animationTime.toStringAsFixed(3)}s step-start infinite;
} }
'''); '''
.dedented;
} }
} }
if (GENERATE_JS_SVG) { if (GENERATE_JS_SVG) {
jsAnimatedCss += '\n/* stroke $pathid */'; jsAnimatedCss += '\n/* stroke $pathid */\n';
// brush and background hidden by default // brush and background hidden by default
if (SHOW_BRUSH) { if (showBrush) {
jsAnimatedCss += dedent(''' jsAnimatedCss += '''
#$brushPathidcss, #$brushBrdPathidcss, #$bgPathidcss { #$brushPathidcss, #$brushBorderPathidcss, #$bgPathidcss {
visibility: hidden; visibility: hidden;
} }
'''); '''
.dedented;
} }
// hide stroke after current element // hide stroke after current element
const after_curr = '[class *= "current"]'; const afterCurrent = '[class *= "current"]';
jsAnimatedCss += dedent(''' jsAnimatedCss += '''
$after_curr ~ #$animPathidcss { $afterCurrent ~ #$animPathidcss {
visibility: hidden; visibility: hidden;
} }
$after_curr ~ #$bgPathidcss, #$bgPathidcss.animate { $afterCurrent ~ #$bgPathidcss, #$bgPathidcss.animate {
visibility: visible; visibility: visible;
} }
@keyframes strike-$pathname { @keyframes strike-$pathname {
@@ -345,24 +447,26 @@ void createAnimation(String filename) {
100% { stroke-dashoffset: 0; } 100% { stroke-dashoffset: 0; }
} }
#$animPathidcss.animate { #$animPathidcss.animate {
stroke: $STOKE_FILLING_COLOR; stroke: $strokeFillingColor;
stroke-dasharray: ${pathlen.toStringAsFixed(3)} ${pathlen.toStringAsFixed(3)}; stroke-dasharray: ${pathlen.toStringAsFixed(3)} ${pathlen.toStringAsFixed(3)};
visibility: visible; visibility: visible;
animation: strike-$pathname ${relduration.toStringAsFixed(3)}s $TIMING_FUNCTION forwards 1; animation: strike-$pathname ${relativeDuration.toStringAsFixed(3)}s ${timingFunction.name} forwards 1;
} }
'''); '''
if (SHOW_BRUSH) { .dedented;
jsAnimatedCss += dedent(''' if (showBrush) {
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; }
} }
#$brushPathidcss.animate.brush, #$brushBrdPathidcss.animate.brush { #$brushPathidcss.animate.brush, #$brushBorderPathidcss.animate.brush {
stroke-dasharray: 0 ${pathlen.toStringAsFixed(3)}; stroke-dasharray: 0 ${pathlen.toStringAsFixed(3)};
visibility: visible; visibility: visible;
animation: strike-brush-$pathname ${relduration.toStringAsFixed(3)}s $TIMING_FUNCTION forwards 1; animation: strike-brush-$pathname ${relativeDuration.toStringAsFixed(3)}s ${timingFunction.name} forwards 1;
} }
'''); '''
.dedented;
} }
} }
@@ -380,51 +484,55 @@ void createAnimation(String filename) {
// just hide everything // just hide everything
rule += "#$animPathidcss"; rule += "#$animPathidcss";
if (SHOW_BRUSH) { if (showBrush) {
rule += ", #$brushPathidcss, #$brushBrdPathidcss"; rule += ", #$brushPathidcss, #$brushBorderPathidcss";
} }
staticCss[k] = staticCss[k]! + staticCss[k] = staticCss[k]! +
dedent(''' '''
%$rule { %$rule {
visibility: hidden; visibility: hidden;
} }
'''); '''
.dedented;
} else if (reltime > newelapsedtime) { } else if (reltime > newelapsedtime) {
// just hide the brush, and bg // just hide the brush, and bg
rule += "#$bgPathidcss"; rule += "#$bgPathidcss";
if (SHOW_BRUSH) { if (showBrush) {
rule += ", #$brushPathidcss, #$brushBrdPathidcss"; rule += ", #$brushPathidcss, #$brushBorderPathidcss";
} }
staticCss[k] = staticCss[k]! + staticCss[k] = staticCss[k]! +
dedent(''' '''
$rule { $rule {
visibility: hidden; visibility: hidden;
} }
'''); '''
.dedented;
} else { } else {
final intervalprop = final intervalprop =
((reltime - elapsedtime) / (newelapsedtime - elapsedtime)); ((reltime - elapsedtime) / (newelapsedtime - elapsedtime));
final progression = myTimingFunction(intervalprop); final progression = timingFunction.func(intervalprop);
staticCss[k] = staticCss[k]! + staticCss[k] = staticCss[k]! +
dedent(''' '''
#$animPathidcss { #$animPathidcss {
stroke-dasharray: ${pathlen.toStringAsFixed(3)} ${(pathlen + 0.002).toStringAsFixed(3)}; stroke-dasharray: ${pathlen.toStringAsFixed(3)} ${(pathlen + 0.002).toStringAsFixed(3)};
stroke-dashoffset: ${(pathlen * (1 - progression) + 0.0015).toStringAsFixed(4)}; stroke-dashoffset: ${(pathlen * (1 - progression) + 0.0015).toStringAsFixed(4)};
stroke: $STOKE_FILLING_COLOR; stroke: $strokeFillingColor;
} }
'''); '''
if (SHOW_BRUSH) { .dedented;
if (showBrush) {
staticCss[k] = staticCss[k]! + staticCss[k] = staticCss[k]! +
dedent(''' '''
#$brushPathidcss, #$brushBrdPathidcss { #$brushPathidcss, #$brushBorderPathidcss {
stroke-dasharray: 0.001 ${(pathlen + 0.002).toStringAsFixed(3)}; stroke-dasharray: 0.001 ${(pathlen + 0.002).toStringAsFixed(3)};
stroke-dashoffset: ${(pathlen * (1 - progression) + 0.0015).toStringAsFixed(4)}; stroke-dashoffset: ${(pathlen * (1 - progression) + 0.0015).toStringAsFixed(4)};
} }
'''); '''
.dedented;
} }
} }
} }
@@ -439,11 +547,11 @@ void createAnimation(String filename) {
doc.root.firstElementChild?.children.add(g); doc.root.firstElementChild?.children.add(g);
// insert groups // insert groups
if (SHOW_BRUSH && !SHOW_BRUSH_FRONT_BORDER) addGroup(brushBrdGroup); if (showBrush && !showBrushFrontBorder) addGroup(brushBrdGroup);
addGroup(bgGroup); addGroup(bgGroup);
if (SHOW_BRUSH && SHOW_BRUSH_FRONT_BORDER) addGroup(brushBrdGroup); if (showBrush && showBrushFrontBorder) addGroup(brushBrdGroup);
addGroup(animGroup); addGroup(animGroup);
if (SHOW_BRUSH) addGroup(brushGroup); if (showBrush) addGroup(brushGroup);
if (GENERATE_SVG) { if (GENERATE_SVG) {
print(animatedCss); print(animatedCss);
@@ -456,10 +564,9 @@ void createAnimation(String filename) {
)) ))
.buildFragment(); .buildFragment();
doc.root.firstElementChild!.children.insert(0, style); doc.root.firstElementChild!.children.insert(0, style);
final svgfile = '${filenameNoext}_anim.svg'; File(outputFile).writeAsStringSync(doc.toXmlString(pretty: true));
File(svgfile).writeAsStringSync(doc.toXmlString(pretty: true));
doc.root.children.removeAt(0); doc.root.children.removeAt(0);
print('written $svgfile'); print('written $outputFile');
} }
if (GENERATE_GIF) { if (GENERATE_GIF) {
@@ -573,9 +680,25 @@ void createAnimation(String filename) {
} }
void main(List<String> args) { void main(List<String> args) {
if (!timingFunctions.keys.contains(TIMING_FUNCTION)) {
throw 'Sorry, invalid timing function "$TIMING_FUNCTION"';
}
// createAnimation('assets/kanjivg/kanji/0f9b1.svg'); // createAnimation('assets/kanjivg/kanji/0f9b1.svg');
createAnimation('assets/kanjivg/kanji/04f5c.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

@@ -1,15 +1,18 @@
import 'dart:math' as math; import 'dart:math' as math;
class Point {
final double x;
final double y;
const Point(this.x, this.y); import '../common/Point.dart';
@override // class Point {
String toString() => '($x,$y)'; // final double x;
} // final double y;
// const Point(this.x, this.y);
// @override
// String toString() => '($x,$y)';
// }
double thrt(double x) => double thrt(double x) =>
x > 0 ? math.pow(x, 1.0 / 3).toDouble() : -math.pow(-x, 1.0 / 3).toDouble(); x > 0 ? math.pow(x, 1.0 / 3).toDouble() : -math.pow(-x, 1.0 / 3).toDouble();
@@ -23,10 +26,10 @@ double cb(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) {
// var C = Cubic, a,b,c,d,p,q,lambda,sqlambda,tmp,addcoef,t,qb,qc,norm,angle,fact; // var C = Cubic, a,b,c,d,p,q,lambda,sqlambda,tmp,addcoef,t,qb,qc,norm,angle,fact;
final double a = pt1.x - 3 * ct1.x + 3 * ct2.x - pt2.x; final num a = pt1.x - 3 * ct1.x + 3 * ct2.x - pt2.x;
final double b = 3 * ct1.x - 6 * ct2.x + 3 * pt2.x; final num b = 3 * ct1.x - 6 * ct2.x + 3 * pt2.x;
final double c = 3 * ct2.x - 3 * pt2.x; final num c = 3 * ct2.x - 3 * pt2.x;
final double 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

View File

@@ -1,56 +0,0 @@
import 'dart:math';
// *_BORDER_WIDTH is the width INCLUDING the border.
const STOKE_BORDER_WIDTH = 4.5;
const STOKE_BORDER_COLOR = "#666";
const STOKE_UNFILLED_COLOR = "#eee";
const double STOKE_UNFILLED_WIDTH = 3;
const STOKE_FILLING_COLOR = "#f00";
const STOKE_FILLED_COLOR = "#000";
const double STOKE_FILLED_WIDTH = 3.1;
// brush settings
const SHOW_BRUSH = true;
const SHOW_BRUSH_FRONT_BORDER = true;
const BRUSH_COLOR = "#f00";
const double BRUSH_WIDTH = 5.5;
const BRUSH_BORDER_COLOR = "#666";
const double BRUSH_BORDER_WIDTH = 7;
const WAIT_AFTER = 1.5;
// 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 stroke_length_to_duration(double length) => sqrt(length) / 8;
// global time rescale, let's make animation a bit
// faster when there are many strokes.
double time_rescale(interval) => pow(2 * interval, 2.0 / 3).toDouble();
// Possibilities are linear, ease, ease-in, ease-in-out, ease-out, see
// https://developer.mozilla.org/en-US/docs/Web/CSS/timing-function
// for more info.
const TIMING_FUNCTION = "ease-in-out";
//
// colorful debug settings
//
// STOKE_BORDER_COLOR = "#00f"
// STOKE_UNFILLED_COLOR = "#ff0"
// STOKE_FILLING_COLOR = "#f00"
// STOKE_FILLED_COLOR = "#000"
// BRUSH_COLOR = "#0ff"
// BRUSH_BORDER_COLOR = "#0f0"

View File

@@ -1,7 +1,7 @@
/// SVG Path specification parser /// SVG Path specification parser
/// ///
import 'path.dart' import '../common/Point.dart';
show Arc, Close, CubicBezier, Line, Move, Path, Point, QuadraticBezier; import 'path.dart';
const COMMANDS = { const COMMANDS = {
'M', 'M',

View File

@@ -1,7 +1,10 @@
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 'package:bisect/bisect.dart';
import 'package:bisection/extension.dart';
import '../common/Point.dart';
// try: // try:
// from collections.abc import MutableSequence // from collections.abc import MutableSequence
@@ -14,45 +17,6 @@ import 'package:bisect/bisect.dart';
double radians(num n) => n * pi / 180; double radians(num n) => n * pi / 180;
double degrees(num n) => n * 180 / pi; double degrees(num n) => n * 180 / pi;
class Point {
final num x;
final num y;
const Point(this.x, this.y);
const Point.from({this.x = 0, this.y = 0});
static const zero = Point(0, 0);
operator +(covariant Point p) => Point(x + p.x, y + p.y);
operator -(covariant Point p) => Point(x - p.x, y - p.y);
operator *(covariant Point p) => Point(x * p.x, y * p.y);
operator /(covariant Point p) => Point(x / p.x, y / p.y);
Point addX(num n) => Point(x + n, y);
Point addY(num n) => Point(x, y + n);
Point add(num n) => Point(x + n, y + n);
Point subtractX(num n) => Point(x - n, y);
Point subtractY(num n) => Point(x, y - n);
Point subtractXY(num n) => Point(x - n, y - n);
Point xSubtract(num n) => Point(n - x, y);
Point ySubtract(num n) => Point(x, n - y);
Point xySubtract(num n) => Point(n - x, n - y);
Point timesX(num n) => Point(x * n, y);
Point timesY(num n) => Point(x, y * n);
Point times(num n) => Point(x * n, y * n);
Point dividesX(num n) => Point(x / n, y);
Point dividesY(num n) => Point(x, y / n);
Point divides(num n) => Point(x / n, y / n);
Point pow(int n) => Point(math.pow(x, n), math.pow(y, n));
double abs() => math.sqrt(x * x + y * y);
@override
String toString() => '($x,$y)';
}
const defaultMinDepth = 5; const defaultMinDepth = 5;
const defaultError = 1e-12; const defaultError = 1e-12;
@@ -496,7 +460,8 @@ class Move extends SvgPath {
Point point(num pos) => start; Point point(num pos) => start;
@override @override
double size({double error = defaultError, int minDepth = defaultMinDepth}) => 0; double size({double error = defaultError, int minDepth = defaultMinDepth}) =>
0;
} }
// Represents the closepath command // Represents the closepath command
@@ -517,7 +482,7 @@ class Close extends Linear {
/// A Path is a sequence of path segments /// A Path is a sequence of path segments
class Path extends ListBase<SvgPath> { class Path extends ListBase<SvgPath> {
late final List<SvgPath> segments; late final List<SvgPath?> segments;
List<num>? _memoizedLengths; List<num>? _memoizedLengths;
num? _memoizedLength; num? _memoizedLength;
final List<num> _fractions = []; final List<num> _fractions = [];
@@ -527,7 +492,7 @@ class Path extends ListBase<SvgPath> {
} }
@override @override
SvgPath operator [](int index) => segments[index]; SvgPath operator [](int index) => segments[index]!;
@override @override
void operator []=(int index, SvgPath value) { void operator []=(int index, SvgPath value) {
@@ -545,11 +510,12 @@ 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({double error = defaultError, int minDepth = defaultMinDepth}) { void _calcLengths(
{double 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) {
@@ -569,29 +535,29 @@ class Path extends ListBase<SvgPath> {
Point point({required num pos, double error = defaultError}) { Point point({required num pos, double error = defaultError}) {
// Shortcuts // Shortcuts
if (pos == 0.0) { if (pos == 0.0) {
return segments[0].point(pos); return segments[0]!.point(pos);
} }
if (pos == 1.0) { if (pos == 1.0) {
return segments.last.point(pos); return segments.last!.point(pos);
} }
_calcLengths(error: error); _calcLengths(error: error);
// Fix for paths of length 0 (i.e. points) // Fix for paths of length 0 (i.e. points)
if (length == 0) { if (length == 0) {
return segments[0].point(0.0); return segments[0]!.point(0.0);
} }
// Find which segment the point we search for is located on: // Find which segment the point we search for is located on:
late final num segmentPos; late final num segmentPos;
int i = _fractions.bisect(pos); int i = _fractions.bisectRight(pos);
if (i == 0) { if (i == 0) {
segmentPos = pos / _fractions[0]; segmentPos = pos / _fractions[0];
} else { } else {
segmentPos = segmentPos =
(pos - _fractions[i - 1]) / (_fractions[i] - _fractions[i - 1]); (pos - _fractions[i - 1]) / (_fractions[i] - _fractions[i - 1]);
} }
return segments[i].point(segmentPos); return segments[i]!.point(segmentPos);
} }
num size({error = defaultError, minDepth = defaultMinDepth}) { num size({error = defaultError, minDepth = defaultMinDepth}) {

View File

@@ -0,0 +1,18 @@
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();
}
}

View File

@@ -4,11 +4,11 @@ version: 0.0.1
homepage: homepage:
environment: environment:
sdk: ">=2.15.1 <3.0.0" sdk: ">=2.12.0 <3.0.0"
flutter: ">=1.17.0" flutter: ">=1.17.0"
dependencies: dependencies:
bisect: ^1.0.2 bisection: ^0.4.3+1
flutter: flutter:
sdk: flutter sdk: flutter
xml: ^5.3.1 xml: ^5.3.1