kanjivg-tools/Libraries/Kanimaji/Source/Kanimaji.cpp

341 lines
14 KiB
C++

#include "Kanimaji/Error.hpp"
#include "Kanimaji/Kanimaji.hpp"
#include "Kanimaji/Settings.hpp"
#include "SVG.hpp"
#include <pugixml.hpp>
#include <format>
#include <string_view>
namespace Kanimaji
{
namespace
{
constexpr std::string_view StaticStrokeStyleFormat(
"fill:none;stroke:{};stroke-width:{};stroke-linecap:round;stroke-linejoin:round;"
);
constexpr std::string_view StrokeProgressionFormat(
" "
"@keyframes stroke-{} {{ "
"0.000% {{ stroke-dashoffset: {:.3f}; }} "
"{:.3f}% {{ stroke-dashoffset: {:.3f}; }} "
"{:.3f}% {{ stroke-dashoffset: 0; }} "
"100.000% {{ stroke-dashoffset: 0; }} }}\n"
);
constexpr std::string_view AnimationVisibilityFormat(
" "
"@keyframes display-{} {{ "
"{:.3f}% {{ visibility: hidden; }} "
"{:.3f}% {{ stroke: {}; }} }}\n"
);
constexpr std::string_view AnimationProgressionFormat(
" "
"#{} {{ "
"stroke-dasharray: {:.3f} {:.3f}; "
"stroke-dashoffset: 0; "
"animation: stroke-{} {:.3f}s {} infinite, "
"display-{} {:.3f}s step-start infinite; }}\n"
);
constexpr std::string_view BrushVisibilityFormat(
" "
"@keyframes display-brush-{} {{ "
"{:.3f}% {{ visibility: hidden; }} "
"{:.3f}% {{ visibility: visible; }} "
"100.000% {{ visibility: hidden; }} }}\n"
);
constexpr std::string_view BrushProgressionFormat(
" "
"#{}, #{} {{ "
"stroke-dasharray: 0 {:.3f}; "
"animation: stroke-{} {:.3f}s {} infinite, "
"display-brush-{} {:.3f}s step-start infinite; }}\n"
);
constexpr std::string_view StylesHeader(
"\n "
"/* Styles generated automatically by Kanimaji, please avoid editing manually. */\n"
);
double AsSeconds(auto duration) {
return std::max(0.0, duration.count());
}
void RemoveStrokeNumbers(pugi::xml_node& svg)
{
svg.remove_child(svg.find_child([](pugi::xml_node& node) {
return std::string_view(node.attribute("id").as_string()).contains("StrokeNumbers");
}));
}
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;
newGroup.append_attribute("style") = std::format(
StaticStrokeStyleFormat, config.Colour.ToHex(), config.Width
);
return newGroup;
}
void AmendComment(pugi::xml_document& doc)
{
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"
"This file has been modified by the Kanimaji tool\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"
"Modifications (compared to the original KanjiVG file):\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_node& svg, const AnimationSettings& settings)
{
pugi::xml_node pathsGroup = svg.find_child([](pugi::xml_node& node) {
return std::string_view(node.attribute("id").as_string()).contains("StrokePaths");
});
pathsGroup.attribute("style") = "fill:none;visibility:hidden;";
std::string baseId = pathsGroup.attribute("id").as_string();
if (auto pos = baseId.find('_'); pos != baseId.npos) {
baseId.erase(0, pos + 1);
}
// 1st pass for getting information
double totalLength = 0.0;
pugi::xpath_node_set paths = svg.select_nodes("//path");
for (const auto& path : paths) {
std::string_view d = path.node().attribute("d").as_string();
totalLength += settings.LengthToTimeScaling(SVG::Path(d).Length());
}
std::string strokeKeyframes;
std::string strokeDisplay;
std::string strokeProgress;
std::string brushKeyframes;
std::string brushProgress;
auto background = AppendGroup(svg, "kvg:Kanimaji_StrokeBackground_" + baseId,
settings.UnfilledStroke);
auto brushBorder = AppendGroup(svg, "kvg:Kanimaji_BrushBorder_" + 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& path : paths) {
std::string_view d = path.node().attribute("d").as_string();
double segmentLength = SVG::Path(d).Length();
previousLength = currentLength;
currentLength += settings.LengthToTimeScaling(segmentLength);
double startTime = 100.0 * (previousLength / totalTime);
double endTime = 100.0 * (currentLength / totalTime);
currentLength += AsSeconds(settings.DelayBetweenStrokes);
pugi::xml_attribute idAttr = path.node().attribute("id");
if (!idAttr) {
continue;
}
std::string id = idAttr.as_string();
std::string css = id;
if (auto pos = css.find(':'); pos != css.npos) {
css.replace(pos, 1, "\\:");
}
std::string pathId = id;
if (pathId.starts_with("kvg:")) {
pathId = pathId.substr(4);
}
strokeKeyframes.append(std::format(StrokeProgressionFormat,
pathId, segmentLength, startTime, segmentLength, endTime));
strokeDisplay.append(std::format(AnimationVisibilityFormat,
pathId, startTime, endTime,
settings.StrokeFillingColour.ToHex()));
strokeProgress.append(std::format(AnimationProgressionFormat,
css + "-kanimaji-animation", segmentLength, segmentLength,
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 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_Styles_" + baseId;
style.append_child(pugi::node_pcdata).set_value(styles);
}
}
void AnimateFile(const std::string& source, const std::string& 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, result.description()));
}
pugi::xml_node svg = doc.child("svg");
if (!svg) {
throw Error("Unexpected document format: Expected to find a SVG element");
}
AmendComment(doc);
RemoveStrokeNumbers(svg);
Animate(svg, settings);
if (!doc.save_file(destination.c_str())) {
throw FileError(std::format("Failed to save file to {}", destination));
}
}
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()));
}
pugi::xml_node svg = doc.child("svg");
if (!svg) {
throw Error("Unexpected document format: Expected to find a SVG element");
}
AmendComment(doc);
RemoveStrokeNumbers(svg);
Animate(svg, settings);
std::stringstream str;
doc.save(str);
return str.str();
}
void RemoveStrokeNumbers(const std::string& source, const std::string& destination,
const RemovalSetttings& 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, result.description()));
}
pugi::xml_node svg = doc.child("svg");
if (!svg) {
throw Error("Unexpected document format: Expected to find a SVG element");
}
pugi::xml_node pathsGroup = svg.find_child([](pugi::xml_node& node) {
return std::string_view(node.attribute("id").as_string()).contains("StrokePaths");
});
pathsGroup.attribute("style") = std::format(
StaticStrokeStyleFormat, settings.Style.Colour.ToHex(), settings.Style.Width
);
RemoveStrokeNumbers(svg);
if (!doc.save_file(destination.c_str())) {
throw FileError(std::format("Failed to save file to {}", destination));
}
}
std::string RemoveStrokeNumbers(const std::string& source, const RemovalSetttings& 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()));
}
pugi::xml_node svg = doc.child("svg");
if (!svg) {
throw Error("Unexpected document format: Expected to find a SVG element");
}
pugi::xml_node pathsGroup = svg.find_child([](pugi::xml_node& node) {
return std::string_view(node.attribute("id").as_string()).contains("StrokePaths");
});
pathsGroup.attribute("style") = std::format(
StaticStrokeStyleFormat, settings.Style.Colour.ToHex(), settings.Style.Width
);
RemoveStrokeNumbers(svg);
std::stringstream str;
doc.save(str);
return str.str();
}
}