Initial commit

This commit is contained in:
2022-01-31 18:10:19 +01:00
commit 1ab1f067b5
16 changed files with 1979 additions and 0 deletions

View File

@@ -0,0 +1,581 @@
/// ignore_for_file: non_constant_identifier_names, avoid_print, unused_local_variable, dead_code, constant_identifier_names
import 'dart:io';
import 'dart:math' show min;
import '../svg/parser.dart';
import 'bezier_cubic.dart' as bezier_cubic;
import 'settings.dart';
import 'package:xml/xml.dart';
import 'package:path/path.dart';
double computePathLength(String path) =>
parse_path(path).size(error: 1e-8).toDouble();
String shescape(path) => "'${path.replace(RegExp(r"(?=['\\\\])"), "\\\\")}'";
String dedent(String s) {
final withoutEmptyLines =
s.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');
}
// ease, ease-in, etc:
// https://developer.mozilla.org/en-US/docs/Web/CSS/timing-function#ease
const pt1 = bezier_cubic.Point(0, 0);
const easeCt1 = bezier_cubic.Point(0.25, 0.1);
const easeCt2 = bezier_cubic.Point(0.25, 1.0);
const easeInCt1 = bezier_cubic.Point(0.42, 0.0);
const easeInCt2 = bezier_cubic.Point(1.0, 1.0);
const easeInOutCt1 = bezier_cubic.Point(0.42, 0.0);
const easeInOutCt2 = bezier_cubic.Point(0.58, 1.0);
const easeOutCt1 = bezier_cubic.Point(0.0, 0.0);
const easeOutCt2 = bezier_cubic.Point(0.58, 1.0);
const pt2 = bezier_cubic.Point(1, 1);
double linear(x) => x;
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 = {
'linear': linear,
'ease': ease,
'ease-in': easeIn,
'ease-in-out': easeInOut,
'ease-out': easeOut
};
final myTimingFunction = timingFunctions[TIMING_FUNCTION]!;
// 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);
void createAnimation(String filename) {
print('processing $filename');
final String filenameNoext = filename.replaceAll(RegExp(r'\.[^\.]+$'), '');
final String baseid = basename(filenameNoext);
// load xml
final XmlDocument doc = XmlDocument.parse(File(filename).readAsStringSync());
// for xlink namespace introduction
doc.rootElement.setAttribute('xmlns:xlink', namespaces['xlink']);
doc.rootElement.setAttribute('xlink:used', '');
// clear all extra elements this program may have previously added
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);
}
}
// 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: STOKE_UNFILLED_COLOR,
width: STOKE_UNFILLED_WIDTH,
);
final animGroup = pathCopyGroup(
id: 'anim',
color: STOKE_FILLED_COLOR,
width: STOKE_FILLED_WIDTH,
);
late final XmlDocumentFragment brushGroup;
late final XmlDocumentFragment brushBrdGroup;
if (SHOW_BRUSH) {
brushGroup = pathCopyGroup(
id: 'brush',
color: BRUSH_COLOR,
width: BRUSH_WIDTH,
);
brushBrdGroup = pathCopyGroup(
id: 'brush-brd',
color: BRUSH_BORDER_COLOR,
width: BRUSH_BORDER_WIDTH,
);
}
// 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 = stroke_length_to_duration(pathlen);
totlen += pathlen;
tottime += duration;
}
}
double animationTime = time_rescale(tottime); // math.pow(3 * tottime, 2.0/3)
tottime += WAIT_AFTER * tottime / animationTime;
final double actualAnimationTime = animationTime;
animationTime += WAIT_AFTER;
final Map<int, String> staticCss = {};
late String animatedCss;
final jsAnimationElements = []; // collect the ids of animating elements
final jsAnimationTimes = []; // the time set (as default) for each animation
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
}
''';
}
if (GENERATE_GIF) {
// final static_css = {};
final last_frame_index = actualAnimationTime ~/ GIF_FRAME_DURATION + 1;
for (int i = 0; i < last_frame_index + 1; i++) {
staticCss[i] = cssHeader;
}
final last_frame_delay =
animationTime - last_frame_index * 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 = dedent('''
#${groupid.replaceAll(':', '\\3a ')} {
display: none;
}
''');
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 = dedent('''
#$gidcss {
stroke-width: ${STOKE_BORDER_WIDTH.toStringAsFixed(1)}px !important;
stroke: $STOKE_BORDER_COLOR !important;
}
''');
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 ');
if (GENERATE_JS_SVG) jsAnimationElements.add({});
void addHref(String suffix, XmlDocumentFragment element) {
final builder = XmlBuilder();
builder.element(
'use',
attributes: {'id': '$pathid-$suffix', 'xlink:href': '#$pathid'},
);
final ref = builder.buildFragment();
element.firstElementChild!.children.add(ref);
if (GENERATE_JS_SVG) jsAnimationElements.last[suffix] = ref;
}
final String bgPathidcss = '$pathidcss-bg';
final String animPathidcss = '$pathidcss-anim';
final String brushPathidcss = '$pathidcss-brush';
final String brushBrdPathidcss = '$pathidcss-brush-brd';
addHref('bg', bgGroup);
addHref('anim', animGroup);
if (SHOW_BRUSH) {
addHref('brush', brushGroup);
addHref('brush-brd', brushBrdGroup);
}
final pathname = pathid.replaceAll(RegExp(r'^kvg:'), '');
final pathlen = computePathLength(p.getAttribute('d') as String);
final duration = stroke_length_to_duration(pathlen);
final relduration = duration * tottime / animationTime; // unscaled time
if (GENERATE_JS_SVG) {
jsAnimationTimes.add(relduration);
}
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 += dedent('''
@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: $STOKE_FILLING_COLOR; }
}
#$animPathidcss {
stroke-dasharray: ${pathlen.toStringAsFixed(3)} ${pathlen.toStringAsFixed(3)};
stroke-dashoffset: 0;
animation: strike-$pathname ${animationTime.toStringAsFixed(3)}s $TIMING_FUNCTION infinite,
showhide-$pathname ${animationTime.toStringAsFixed(3)}s step-start infinite;
}
''');
if (SHOW_BRUSH) {
// brush element visibility
animatedCss += dedent('''
@keyframes showhide-brush-$pathname {
${animStart.toStringAsFixed(3)}% { visibility: hidden; }
${animEnd.toStringAsFixed(3)}% { visibility: visible; }
100% { visibility: hidden; }
}
#$brushPathidcss, #$brushBrdPathidcss {
stroke-dasharray: 0 ${pathlen.toStringAsFixed(3)};
animation: strike-$pathname ${animationTime.toStringAsFixed(3)}s $TIMING_FUNCTION infinite,
showhide-brush-$pathname ${animationTime.toStringAsFixed(3)}s step-start infinite;
}
''');
}
}
if (GENERATE_JS_SVG) {
jsAnimatedCss += '\n/* stroke $pathid */';
// brush and background hidden by default
if (SHOW_BRUSH) {
jsAnimatedCss += dedent('''
#$brushPathidcss, #$brushBrdPathidcss, #$bgPathidcss {
visibility: hidden;
}
''');
}
// hide stroke after current element
const after_curr = '[class *= "current"]';
jsAnimatedCss += dedent('''
$after_curr ~ #$animPathidcss {
visibility: hidden;
}
$after_curr ~ #$bgPathidcss, #$bgPathidcss.animate {
visibility: visible;
}
@keyframes strike-$pathname {
0% { stroke-dashoffset: ${pathlen.toStringAsFixed(3)}; }
100% { stroke-dashoffset: 0; }
}
#$animPathidcss.animate {
stroke: $STOKE_FILLING_COLOR;
stroke-dasharray: ${pathlen.toStringAsFixed(3)} ${pathlen.toStringAsFixed(3)};
visibility: visible;
animation: strike-$pathname ${relduration.toStringAsFixed(3)}s $TIMING_FUNCTION forwards 1;
}
''');
if (SHOW_BRUSH) {
jsAnimatedCss += dedent('''
@keyframes strike-brush-$pathname {
0% { stroke-dashoffset: ${pathlen.toStringAsFixed(3)}; }
100% { stroke-dashoffset: 0.4; }
}
#$brushPathidcss.animate.brush, #$brushBrdPathidcss.animate.brush {
stroke-dasharray: 0 ${pathlen.toStringAsFixed(3)};
visibility: visible;
animation: strike-brush-$pathname ${relduration.toStringAsFixed(3)}s $TIMING_FUNCTION forwards 1;
}
''');
}
}
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 (SHOW_BRUSH) {
rule += ", #$brushPathidcss, #$brushBrdPathidcss";
}
staticCss[k] = staticCss[k]! +
dedent('''
%$rule {
visibility: hidden;
}
''');
} else if (reltime > newelapsedtime) {
// just hide the brush, and bg
rule += "#$bgPathidcss";
if (SHOW_BRUSH) {
rule += ", #$brushPathidcss, #$brushBrdPathidcss";
}
staticCss[k] = staticCss[k]! +
dedent('''
$rule {
visibility: hidden;
}
''');
} else {
final intervalprop =
((reltime - elapsedtime) / (newelapsedtime - elapsedtime));
final progression = myTimingFunction(intervalprop);
staticCss[k] = staticCss[k]! +
dedent('''
#$animPathidcss {
stroke-dasharray: ${pathlen.toStringAsFixed(3)} ${(pathlen + 0.002).toStringAsFixed(3)};
stroke-dashoffset: ${(pathlen * (1 - progression) + 0.0015).toStringAsFixed(4)};
stroke: $STOKE_FILLING_COLOR;
}
''');
if (SHOW_BRUSH) {
staticCss[k] = staticCss[k]! +
dedent('''
#$brushPathidcss, #$brushBrdPathidcss {
stroke-dasharray: 0.001 ${(pathlen + 0.002).toStringAsFixed(3)};
stroke-dashoffset: ${(pathlen * (1 - progression) + 0.0015).toStringAsFixed(4)};
}
''');
}
}
}
}
elapsedlen = newelapsedlen;
elapsedtime = newelapsedtime;
}
}
void addGroup(XmlDocumentFragment g) =>
doc.root.firstElementChild?.children.add(g);
// insert groups
if (SHOW_BRUSH && !SHOW_BRUSH_FRONT_BORDER) addGroup(brushBrdGroup);
addGroup(bgGroup);
if (SHOW_BRUSH && SHOW_BRUSH_FRONT_BORDER) addGroup(brushBrdGroup);
addGroup(animGroup);
if (SHOW_BRUSH) 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);
final svgfile = '${filenameNoext}_anim.svg';
File(svgfile).writeAsStringSync(doc.toXmlString(pretty: true));
doc.root.children.removeAt(0);
print('written $svgfile');
}
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) {
if (!timingFunctions.keys.contains(TIMING_FUNCTION)) {
throw 'Sorry, invalid timing function "$TIMING_FUNCTION"';
}
// createAnimation('assets/kanjivg/kanji/0f9b1.svg');
createAnimation('assets/kanjivg/kanji/04f5c.svg');
}

View File

@@ -0,0 +1,84 @@
import 'dart:math' as math;
class Point {
final double x;
final double y;
const Point(this.x, this.y);
@override
String toString() => '($x,$y)';
}
double thrt(double x) =>
x > 0 ? math.pow(x, 1.0 / 3).toDouble() : -math.pow(-x, 1.0 / 3).toDouble();
double sqrt(double x) => x > 0 ? math.sqrt(x) : 0;
double sq(x) => x * x;
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
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;
final double a = pt1.x - 3 * ct1.x + 3 * ct2.x - pt2.x;
final double b = 3 * ct1.x - 6 * ct2.x + 3 * pt2.x;
final double c = 3 * ct2.x - 3 * pt2.x;
final double d = pt2.x - x;
if (a.abs() < 0.000000001) { // quadratic
if (b.abs() < 0.000000001) return -d / c; // linear
final qb = c / b;
final qc = d / b;
final tmp = sqrt(sq(qb) - 4 * qc);
return (-qb + ((qb > 0 || qc < 0) ? tmp : -tmp)) / 2;
}
final p = -sq(b) / (3 * sq(a)) + c / a;
final q = 2 * cb(b / (3 * a)) - b * c / (3 * sq(a)) + d / a;
final addcoef = -b / (3 * a);
final lmbd = sq(q) / 4 + cb(p) / 27;
if (lmbd >= 0) { // real
final sqlambda = sqrt(lmbd);
final tmp = thrt(-q / 2 + (q < 0 ? sqlambda : -sqlambda));
return tmp - p / (3 * tmp) + addcoef;
}
final norm = sqrt(sq(q) / 4 - lmbd);
if (norm < 0.0000000001) return addcoef;
final angle = math.acos(-q / (2 * norm)) / 3;
final fact = 2 * thrt(norm);
double t = double.infinity;
for (final i in [-1, 0, 1]) {
final tmp = fact * math.cos(angle + i * math.pi * 2 / 3) + addcoef;
if (tmp >= -0.000000001 && tmp < t) t = tmp;
}
return t;
}
double value(Point pt1, Point ct1, Point ct2, Point pt2, double x) {
final t = time(pt1, ct1, ct2, pt2, x);
return cb(t) * pt1.y +
3 * sq(t) * (1 - t) * ct1.y +
3 * t * sq(1 - t) * ct2.y +
cb(1 - t) * pt2.y;
}
// if __name__ == "__main__":
// pt1 = pt(0,0)
// ct1 = pt(0.25, 0.1)
// ct2 = pt(0.25, 1.0)
// pt2 = pt(1,1)
// part = 100
// with open('ease.txt', 'w') as f:
// for i in range(0,part+1,1):
// x = float(i) / part
// y = value(pt1, ct1, ct2, pt2, x)
// f.write("%f %f\n" % (x,y))

View File

@@ -0,0 +1,56 @@
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"