Add better timings, and proper configuration

This commit is contained in:
TennesseeTrash 2025-06-08 00:23:46 +02:00
parent 0b32af33b8
commit 5750bb8177
13 changed files with 385 additions and 240 deletions

View file

@ -1,4 +1,3 @@
include(${PROJECT_SOURCE_DIR}/CMake/ctre.cmake)
include(${PROJECT_SOURCE_DIR}/CMake/pugixml.cmake)
add_library(Kanimaji STATIC)
@ -10,7 +9,6 @@ target_compile_features(Kanimaji
target_link_libraries(Kanimaji
PRIVATE
ctre
pugixml
)
@ -21,7 +19,8 @@ target_include_directories(Kanimaji
target_sources(Kanimaji
PRIVATE
"Source/Colour.cpp"
"Source/Error.cpp"
"Source/Kanimaji.cpp"
"Source/Settings.cpp"
"Source/SVG.cpp"
)

View file

@ -1,20 +0,0 @@
#ifndef KANIMAJI_CONFIG_HPP
#define KANIMAJI_CONFIG_HPP
#include "Colour.hpp"
namespace Kanimaji
{
struct GroupConfig
{
double Width;
Colour Colour;
};
struct AnimationSettings
{
};
}
#endif // KANIMAJI_CONFIG_HPP

View file

@ -1,9 +1,32 @@
#ifndef KANIMAJI_ERROR_HPP
#define KANIMAJI_ERROR_HPP
#include <stdexcept>
namespace Kanimaji
{
class Error : public std::runtime_error
{
using std::runtime_error::runtime_error;
};
class FileError : public Error
{
using Error::Error;
};
class ParseError : public Error
{
public:
ParseError(std::size_t current, std::string_view message);
std::size_t Position() const
{
return mPosition;
}
private:
std::size_t mPosition;
};
}
#endif // KANIMAJI_ERROR_HPP

View file

@ -1,16 +1,20 @@
#ifndef KANIMAJI_KANIMAJI_HPP
#define KANIMAJI_KANIMAJI_HPP
#include "Settings.hpp"
#include <filesystem>
#include <string>
namespace Kanimaji
{
bool Animate(const std::string& source, const std::string& destination);
using Path = std::filesystem::path;
constexpr bool Animate(const std::string& filename)
{
return Animate(filename, filename);
}
void AnimateFile(const Path& source, const Path& destination,
const AnimationSettings& settings = AnimationSettings::Default());
std::string Animate(const std::string& source,
const AnimationSettings& settings = AnimationSettings::Default());
}
#endif // KANIMAJI_KANIMAJI_HPP

View file

@ -1,5 +1,5 @@
#ifndef KANIMAJI_COLOUR_HPP
#define KANIMAJI_COLOUR_HPP
#ifndef KANIMAJI_RGB_HPP
#define KANIMAJI_RGB_HPP
#include <cstdint>
#include <string>
@ -56,16 +56,16 @@ namespace Kanimaji
}
}
struct Colour
struct RGB
{
std::uint8_t Red;
std::uint8_t Green;
std::uint8_t Blue;
static constexpr Colour FromHex(std::string_view hex)
static constexpr RGB FromHex(std::string_view hex)
{
if (hex.empty()) {
return Colour { .Red = 0, .Green = 0, .Blue = 0 };
return RGB { .Red = 0, .Green = 0, .Blue = 0 };
}
if (hex[0] == '#') {
@ -74,12 +74,12 @@ namespace Kanimaji
for (char ch : hex) {
if ((ch < '0' || ch > '9') && (ch < 'A' || ch > 'F') && (ch < 'a' && ch > 'f')) {
return Colour { .Red = 0, .Green = 0, .Blue = 0 };
return RGB { .Red = 0, .Green = 0, .Blue = 0 };
}
}
if (hex.length() == 3) {
return Colour {
return RGB {
.Red = Implementation::ToDec(hex[0]),
.Green = Implementation::ToDec(hex[1]),
.Blue = Implementation::ToDec(hex[2]),
@ -87,14 +87,14 @@ namespace Kanimaji
}
if (hex.length() == 6) {
return Colour {
return RGB {
.Red = Implementation::ToDec(hex[0], hex[1]),
.Green = Implementation::ToDec(hex[2], hex[3]),
.Blue = Implementation::ToDec(hex[4], hex[5]),
};
}
return Colour { .Red = 0, .Green = 0, .Blue = 0 };
return RGB { .Red = 0, .Green = 0, .Blue = 0 };
}
constexpr std::string ToHex() const
@ -108,4 +108,4 @@ namespace Kanimaji
};
}
#endif // KANIMAJI_COLOUR_HPP
#endif // KANIMAJI_RGB_HPP

View file

@ -0,0 +1,52 @@
#ifndef KANIMAJI_CONFIG_HPP
#define KANIMAJI_CONFIG_HPP
#include "RGB.hpp"
#include <chrono>
#include <functional>
namespace Kanimaji
{
enum class Flag {
Enable, Disable,
};
enum class Progression {
Linear,
EaseIn,
EaseOut,
EaseInOut,
};
std::string ToString(Progression progression);
struct StrokeStyle
{
double Width;
RGB Colour;
};
using TimeScalingFunc = std::function<double(double)>;
using Duration = std::chrono::duration<double, std::ratio<1>>;
struct AnimationSettings
{
Progression StrokeProgression;
StrokeStyle UnfilledStroke;
StrokeStyle FilledStroke;
RGB StrokeFillingColour;
Flag EnableBrush;
StrokeStyle Brush;
StrokeStyle BrushBorder;
TimeScalingFunc LengthToTimeScaling;
Duration WaitBeforeRepeating;
Duration DelayBetweenStrokes;
static AnimationSettings Default();
};
}
#endif // KANIMAJI_CONFIG_HPP

View file

@ -1 +0,0 @@
#include "Kanimaji/Colour.hpp"

View file

@ -0,0 +1,11 @@
#include "Kanimaji/Error.hpp"
#include <format>
namespace Kanimaji
{
ParseError::ParseError(std::size_t current, std::string_view message)
: Error(std::format("[At: {}] {}", current, message))
, mPosition(current)
{}
}

View file

@ -1,9 +1,7 @@
#include "Kanimaji/Colour.hpp"
#include "Kanimaji/Config.hpp"
#include "Kanimaji/Kanimaji.hpp"
#include "Kanimaji/Settings.hpp"
#include "SVG.hpp"
#include <ctre.hpp>
#include <pugixml.hpp>
#include <format>
@ -13,57 +11,48 @@ namespace Kanimaji
{
namespace
{
constexpr GroupConfig StrokeBorderConfig { .Width = 5, .Colour = Colour::FromHex("#666") };
constexpr GroupConfig StrokeUnfilledConfig{ .Width = 2, .Colour = Colour::FromHex("#EEE") };
constexpr GroupConfig StrokeFilledConfig { .Width = 3.1, .Colour = Colour::FromHex("#000") };
constexpr GroupConfig BrushConfig { .Width = 5.5, .Colour = Colour::FromHex("#F00") };
constexpr GroupConfig BrushBorderConfig { .Width = 7, .Colour = Colour::FromHex("#666") };
constexpr Colour StrokeFillingColour = Colour::FromHex("#F00");
// What the fuck is this?
constexpr double WaitAfter = 1.5;
using namespace std::string_view_literals;
constexpr auto StrokeProgressionFormat = (
" @keyframes stroke-{} {{\n"
" 0.000% {{ stroke-dashoffset: {:.3f}; }}\n"
" {:.3f}% {{ stroke-dashoffset: {:.3f}; }}\n"
" {:.3f}% {{ stroke-dashoffset: 0; }}\n"
" 100.000% {{ stroke-dashoffset: 0; }}\n"
" }}\n"sv
);
constexpr auto AnimationVisibilityFormat = (
" @keyframes showhide-{} {{\n"
" {:.3f}% {{ visibility: hidden; }}\n"
" {:.3f}% {{ stroke: {}; }}\n"
" }}\n"sv
);
constexpr auto AnimationProgressionFormat = (
" #{} {{\n"
" stroke-dasharray: {:.3f} {:.3f};\n"
" stroke-dashoffset: 0;\n"
" animation: stroke-{} {:.3f}s {} infinite,\n"
" showhide-{} {:.3f}s step-start infinite;\n"
" }}\n"sv
);
constexpr auto BrushVisibilityFormat = (
" @keyframes showhide-brush-{} {{\n"
" {:.3f}% {{ visibility: hidden; }}\n"
" {:.3f}% {{ visibility: visible; }}\n"
" 100.000% {{ visibility: hidden; }}\n"
" }}\n"sv
);
constexpr auto BrushProgressionFormat = (
" #{}, #{} {{\n"
" stroke-dasharray: 0 {:.3f};\n"
" animation: stroke-{} {:.3f}s {} infinite,\n"
" showhide-brush-{} {:.3f}s step-start infinite;\n"
" }}\n"sv
);
constexpr auto StrokeProgressionFormat =
" "
"@keyframes stroke-{} {{ "
"0.000% {{ stroke-dashoffset: {:.3f}; }} "
"{:.3f}% {{ stroke-dashoffset: {:.3f}; }} "
"{:.3f}% {{ stroke-dashoffset: 0; }} "
"100.000% {{ stroke-dashoffset: 0; }} }}\n"sv;
constexpr auto AnimationVisibilityFormat =
" "
"@keyframes display-{} {{ "
"{:.3f}% {{ visibility: hidden; }} "
"{:.3f}% {{ stroke: {}; }} }}\n"sv;
constexpr auto AnimationProgressionFormat =
" "
"#{} {{ "
"stroke-dasharray: {:.3f} {:.3f}; "
"stroke-dashoffset: 0; "
"animation: stroke-{} {:.3f}s {} infinite, "
"display-{} {:.3f}s step-start infinite; }}\n"sv;
constexpr auto BrushVisibilityFormat =
" "
"@keyframes display-brush-{} {{ "
"{:.3f}% {{ visibility: hidden; }} "
"{:.3f}% {{ visibility: visible; }} "
"100.000% {{ visibility: hidden; }} }}\n"sv;
constexpr auto BrushProgressionFormat =
" "
"#{}, #{} {{ "
"stroke-dasharray: 0 {:.3f}; "
"animation: stroke-{} {:.3f}s {} infinite, "
"display-brush-{} {:.3f}s step-start infinite; }}\n"sv;
constexpr auto StylesHeader =
"\n "
"/* Styles generated automatically by Kanimaji, please avoid editing manually. */\n"sv;
pugi::xml_node append_group(pugi::xml_node &svg, std::string_view name, GroupConfig config)
double AsSeconds(auto duration) {
return std::max(0.0, duration.count());
}
pugi::xml_node AppendGroup(pugi::xml_node& svg, std::string_view name, StrokeStyle config)
{
pugi::xml_node newGroup = svg.append_child("g");
newGroup.append_attribute("id") = name;
@ -73,81 +62,95 @@ namespace Kanimaji
);
return newGroup;
}
}
bool Animate(const std::string& source, const std::string& destination)
void AmendComment(pugi::xml_document& doc)
{
pugi::xml_document doc;
pugi::xml_parse_result readResult = doc.load_file(source.c_str(), pugi::parse_full);
if (!readResult) {
return false;
}
pugi::xml_node comment = doc.find_child([](pugi::xml_node& node) {
return node.type() == pugi::node_comment;
});
if (comment) {
std::string text = comment.value();
text.append(
"\n===============================================================================\n\n"
"\n"
"===============================================================================\n"
"\n"
"This file has been modified by the Kanimaji tool\n"
"avilable here: (TODO)\n\n"
"avilable here: (Still very WIP, will be available later.)\n"
"\n"
"The Kanimaji tool is based on the original kanimaji.py script.\n"
"Copyright (c) 2016 Maurizio Monge\n"
"The script is available here: https://github.com/maurimo/kanimaji\n"
"Used under the terms of the MIT licence as specified in the project's README.md\n\n"
"Used under the terms of the MIT licence as specified in the project's README.md\n"
"\n"
"Modifications (compared to the original KanjiVG file):\n"
"* The stroke numbers were removed\n"
"* This comment was amended with this section\n"
"* The stroke numbers were removed to prevent decrease clutter\n"
"* Special Styles section (and accompanying <g> and <use> tags) were generated\n"
);
comment.set_value(text);
}
}
void Animate(pugi::xml_document& doc, const AnimationSettings& settings)
{
pugi::xml_node svg = doc.child("svg");
if (!svg) {
throw Error("Unexpected document format: Expected to find a SVG element");
}
svg.remove_child(svg.find_child([](pugi::xml_node& node) {
return std::string_view(node.attribute("id").as_string()).contains("StrokeNumbers");
}));
pugi::xml_node paths = svg.find_child([](pugi::xml_node& node) {
pugi::xml_node pathsContainer = svg.find_child([](pugi::xml_node& node) {
return std::string_view(node.attribute("id").as_string()).contains("StrokePaths");
});
paths.attribute("style") = "fill:none;visibility:hidden;";
std::string baseId = paths.attribute("id").as_string();
pathsContainer.attribute("style") = "fill:none;visibility:hidden;";
std::string baseId = pathsContainer.attribute("id").as_string();
baseId.erase(0, baseId.find('_') + 1);
// 1st pass for getting information
double totalLength = 0.0;
for (const auto& path : svg.select_nodes("//path")) {
pugi::xpath_node_set paths = svg.select_nodes("//path");
for (const auto& path : paths) {
std::string_view d = path.node().attribute("d").as_string();
try {
totalLength += SVG::Path(d).Length();
}
catch (const SVG::ParseError& e) {
return false;
}
totalLength += settings.LengthToTimeScaling(SVG::Path(d).Length());
}
std::string styles =
"\n /* Styles autogenerated by Kanimaji, please do not edit manually. */\n";
std::string strokeKeyframes;
std::string strokeDisplay;
std::string strokeProgress;
std::string brushKeyframes;
std::string brushProgress;
auto background = append_group(svg, "kvg:Kanimaji_Stroke_Background_" + baseId, StrokeUnfilledConfig);
auto brushBorder = append_group(svg, "kvg:Kanimaji_Brush_Background_" + baseId, BrushBorderConfig);
auto animation = append_group(svg, "kvg:Kanimaji_Animation_" + baseId, StrokeFilledConfig);
auto brush = append_group(svg, "kvg:Kanimaji_Brush_" + baseId, BrushConfig);
auto background = AppendGroup(svg, "kvg:Kanimaji_Stroke_Background_" + baseId,
settings.UnfilledStroke);
auto brushBorder = AppendGroup(svg, "kvg:Kanimaji_Brush_Border_" + baseId,
settings.BrushBorder);
auto animation = AppendGroup(svg, "kvg:Kanimaji_Animation_" + baseId,
settings.FilledStroke);
auto brush = AppendGroup(svg, "kvg:Kanimaji_Brush_" + baseId, settings.Brush);
totalLength += paths.size() * AsSeconds(settings.DelayBetweenStrokes);
double totalTime = totalLength
+ AsSeconds(settings.WaitBeforeRepeating)
- AsSeconds(settings.DelayBetweenStrokes);
double previousLength = 0.0;
double currentLength = 0.0;
// 2nd pass to prepare the CSS
for (const auto& xpath : svg.select_nodes("//path")) {
pugi::xml_node path = xpath.node();
for (const auto& path : paths) {
std::string_view d = path.node().attribute("d").as_string();
double segmentLength = SVG::Path(path.attribute("d").as_string()).Length();
double segmentLength = SVG::Path(d).Length();
previousLength = currentLength;
currentLength += segmentLength;
currentLength += settings.LengthToTimeScaling(segmentLength);
double startTime = 100.0 * (previousLength / totalLength);
double endTime = 100.0 * (currentLength / totalLength);
double startTime = 100.0 * (previousLength / totalTime);
double endTime = 100.0 * (currentLength / totalTime);
pugi::xml_attribute idAttr = path.attribute("id");
currentLength += AsSeconds(settings.DelayBetweenStrokes);
pugi::xml_attribute idAttr = path.node().attribute("id");
if (!idAttr) {
continue;
}
@ -163,50 +166,92 @@ namespace Kanimaji
pathId = pathId.substr(4);
}
double totalTime = 4.0;
styles.append(std::format(StrokeProgressionFormat,
strokeKeyframes.append(std::format(StrokeProgressionFormat,
pathId, segmentLength, startTime, segmentLength, endTime));
styles.append(std::format(AnimationVisibilityFormat,
pathId, startTime, endTime, StrokeFillingColour.ToHex()));
styles.append(std::format(AnimationProgressionFormat,
strokeDisplay.append(std::format(AnimationVisibilityFormat,
pathId, startTime, endTime,
settings.StrokeFillingColour.ToHex()));
strokeProgress.append(std::format(AnimationProgressionFormat,
css + "-kanimaji-animation", segmentLength, segmentLength,
pathId, totalTime, "ease-in-out", pathId, totalTime));
styles.append(std::format(BrushVisibilityFormat, pathId, startTime, endTime - 0.001));
styles.append(std::format(BrushProgressionFormat,
css + "-kanimaji-brush", css + "-kanimaji-brush-background",
segmentLength, pathId, totalTime, "ease-in-out", pathId,
pathId, totalTime, ToString(settings.StrokeProgression),
pathId, totalTime));
if (settings.EnableBrush == Flag::Enable) {
brushKeyframes.append(std::format(BrushVisibilityFormat,
pathId, startTime, endTime - 0.001));
brushProgress.append(std::format(BrushProgressionFormat,
css + "-kanimaji-brush",
css + "-kanimaji-brush-border", segmentLength,
pathId, totalTime,
ToString(settings.StrokeProgression), pathId,
totalTime));
}
pugi::xml_node useBackground = background.append_child("use");
useBackground.append_attribute("id") = id + "-kanimaji-background";
useBackground.append_attribute("href") = "#" + id;
pugi::xml_node useBrushBackground = brushBorder.append_child("use");
useBrushBackground.append_attribute("id") = id + "-kanimaji-brush-background";
useBrushBackground.append_attribute("href") = "#" + id;
pugi::xml_node useAnimation = animation.append_child("use");
useAnimation.append_attribute("id") = id + "-kanimaji-animation";
useAnimation.append_attribute("href") = "#" + id;
if (settings.EnableBrush == Flag::Enable) {
pugi::xml_node useBrushBackground = brushBorder.append_child("use");
useBrushBackground.append_attribute("id") = id + "-kanimaji-brush-border";
useBrushBackground.append_attribute("href") = "#" + id;
pugi::xml_node useBrush = brush.append_child("use");
useBrush.append_attribute("id") = id + "-kanimaji-brush";
useBrush.append_attribute("href") = "#" + id;
}
}
// This is how we add styles - prepare the whole thing in a separate buffer first.
std::string styles;
styles.append(StylesHeader);
styles.append(strokeKeyframes);
styles.append(strokeDisplay);
styles.append(strokeProgress);
styles.append(brushKeyframes);
styles.append(brushProgress);
styles.append(" ");
pugi::xml_node style = svg.prepend_child("style");
style.append_attribute("id") = "kvg:Kanimaji_Style";
style.append_child(pugi::node_pcdata).set_value(styles);
bool writeResult = doc.save_file(destination.c_str());
if (!writeResult) {
return false;
}
}
return true;
void AnimateFile(const Path& source, const Path& destination, const AnimationSettings& settings)
{
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file(source.c_str(), pugi::parse_full);
if (!result) {
constexpr std::string_view errorFormat = "Failed to load file from {}, due to: {}";
throw FileError(std::format(errorFormat, source.string(), result.description()));
}
AmendComment(doc);
Animate(doc, settings);
if (!doc.save_file(destination.c_str())) {
throw FileError(std::format("Failed to save file to {}", destination.string()));
}
}
std::string Animate(const std::string& source, const AnimationSettings& settings)
{
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_string(source.c_str(), pugi::parse_full);
if (!result) {
constexpr std::string_view errorFormat = "Failed to parse SVG document: {}";
throw ParseError(result.offset, std::format(errorFormat, result.description()));
}
AmendComment(doc);
Animate(doc, settings);
std::stringstream str;
doc.save(str);
return str.str();
}
}

View file

@ -9,11 +9,6 @@
namespace Kanimaji::SVG
{
ParseError::ParseError(std::size_t current, std::string_view message)
: Error(std::format("[At: {}] {}", current, message))
, mPosition(current)
{}
struct Vec2
{
double x;

View file

@ -1,31 +1,14 @@
#ifndef KANIMAJI_SVG_HPP
#define KANIMAJI_SVG_HPP
#include "Kanimaji/Error.hpp"
#include <memory>
#include <stdexcept>
#include <string_view>
#include <vector>
namespace Kanimaji::SVG
{
class Error : public std::runtime_error
{
using std::runtime_error::runtime_error;
};
class ParseError : public Error
{
public:
ParseError(std::size_t current, std::string_view message);
std::size_t Position() const
{
return mPosition;
}
private:
std::size_t mPosition;
};
class Path
{
public:

View file

@ -0,0 +1,51 @@
#include "Kanimaji/Settings.hpp"
#include <cmath>
#include <utility>
namespace Kanimaji
{
std::string ToString(Progression progression)
{
switch (progression) {
case Progression::Linear: return "linear";
case Progression::EaseIn: return "ease-in";
case Progression::EaseOut: return "ease-out";
case Progression::EaseInOut: return "ease-in-out";
default: std::unreachable();
};
}
AnimationSettings AnimationSettings::Default()
{
using namespace std::chrono_literals;
return AnimationSettings {
.StrokeProgression = Progression::EaseInOut,
.UnfilledStroke = StrokeStyle {
.Width = 2.0,
.Colour = RGB::FromHex("#EEEEEE"),
},
.FilledStroke = StrokeStyle {
.Width = 3.0,
.Colour = RGB::FromHex("#000000"),
},
.StrokeFillingColour = RGB::FromHex("#FF0000"),
.EnableBrush = Flag::Enable,
.Brush = StrokeStyle {
.Width = 5.5,
.Colour = RGB::FromHex("#FF0000"),
},
.BrushBorder = StrokeStyle {
.Width = 7.0,
.Colour = RGB::FromHex("#666666"),
},
.LengthToTimeScaling = [] (double length) -> double {
return std::pow(length, 1.0 / 3.0) / 6.0;
},
.WaitBeforeRepeating = 1s,
.DelayBetweenStrokes = 50ms,
};
}
}

View file

@ -12,8 +12,11 @@ int main(const int argc, const char *argv[])
// return 1;
//}
if (!Kanimaji::Animate("084b8.svg", "084b8-out.svg")) {
std::println("Could not animate the specified file.");
try {
Kanimaji::AnimateFile("084b8.svg", "084b8-out.svg");
}
catch (const std::exception& e) {
std::println("Could not animate the input file");
return 1;
}