#include "Kanimaji/Error.hpp" #include "Kanimaji/Kanimaji.hpp" #include "Kanimaji/Settings.hpp" #include "SVG.hpp" #include #include #include 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 and 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(); } }