Initial working Kanimaji implementation
This commit is contained in:
parent
4e87a67296
commit
99901b878a
8 changed files with 588 additions and 83 deletions
|
@ -21,5 +21,7 @@ target_include_directories(Kanimaji
|
||||||
|
|
||||||
target_sources(Kanimaji
|
target_sources(Kanimaji
|
||||||
PRIVATE
|
PRIVATE
|
||||||
|
"Source/Colour.cpp"
|
||||||
"Source/Kanimaji.cpp"
|
"Source/Kanimaji.cpp"
|
||||||
|
"Source/SVG.cpp"
|
||||||
)
|
)
|
||||||
|
|
111
Libraries/Kanimaji/Include/Kanimaji/Colour.hpp
Normal file
111
Libraries/Kanimaji/Include/Kanimaji/Colour.hpp
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
#ifndef KANIMAJI_COLOUR_HPP
|
||||||
|
#define KANIMAJI_COLOUR_HPP
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace Kanimaji
|
||||||
|
{
|
||||||
|
namespace Implementation
|
||||||
|
{
|
||||||
|
constexpr std::uint8_t HexToDec(char ch)
|
||||||
|
{
|
||||||
|
if (ch >= '0' && ch <= '9') {
|
||||||
|
return std::uint8_t(ch - '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch >= 'A' && ch <= 'F') {
|
||||||
|
return std::uint8_t(ch - 'A' + 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch >= 'a' && ch <= 'f') {
|
||||||
|
return std::uint8_t(ch - 'a' + 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr char DecToHex(std::uint8_t val)
|
||||||
|
{
|
||||||
|
if (val < 10) {
|
||||||
|
return char(val + '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val < 16) {
|
||||||
|
return char(val - 10 + 'A');
|
||||||
|
}
|
||||||
|
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr std::uint8_t ToDec(char upper, char lower)
|
||||||
|
{
|
||||||
|
return HexToDec(upper) << 4 | HexToDec(lower);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr std::uint8_t ToDec(char ch)
|
||||||
|
{
|
||||||
|
return ToDec(ch, ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void ChannelToHex(std::uint8_t val, char& ch1, char& ch2)
|
||||||
|
{
|
||||||
|
ch1 = DecToHex(15 & (val >> 4));
|
||||||
|
ch2 = DecToHex(15 & (val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Colour
|
||||||
|
{
|
||||||
|
std::uint8_t Red;
|
||||||
|
std::uint8_t Green;
|
||||||
|
std::uint8_t Blue;
|
||||||
|
|
||||||
|
static constexpr Colour FromHex(std::string_view hex)
|
||||||
|
{
|
||||||
|
if (hex.empty()) {
|
||||||
|
return Colour { .Red = 0, .Green = 0, .Blue = 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hex[0] == '#') {
|
||||||
|
hex = std::string_view(hex.data() + 1, hex.length() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hex.length() == 3) {
|
||||||
|
return Colour {
|
||||||
|
.Red = Implementation::ToDec(hex[0]),
|
||||||
|
.Green = Implementation::ToDec(hex[1]),
|
||||||
|
.Blue = Implementation::ToDec(hex[2]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hex.length() == 6) {
|
||||||
|
return Colour {
|
||||||
|
.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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr std::string ToHex() const
|
||||||
|
{
|
||||||
|
std::string result(7, '#');
|
||||||
|
Implementation::ChannelToHex(Red, result[1], result[2]);
|
||||||
|
Implementation::ChannelToHex(Green, result[3], result[4]);
|
||||||
|
Implementation::ChannelToHex(Blue, result[5], result[6]);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // KANIMAJI_COLOUR_HPP
|
20
Libraries/Kanimaji/Include/Kanimaji/Config.hpp
Normal file
20
Libraries/Kanimaji/Include/Kanimaji/Config.hpp
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
#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
|
9
Libraries/Kanimaji/Include/Kanimaji/Error.hpp
Normal file
9
Libraries/Kanimaji/Include/Kanimaji/Error.hpp
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
#ifndef KANIMAJI_ERROR_HPP
|
||||||
|
#define KANIMAJI_ERROR_HPP
|
||||||
|
|
||||||
|
namespace Kanimaji
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // KANIMAJI_ERROR_HPP
|
1
Libraries/Kanimaji/Source/Colour.cpp
Normal file
1
Libraries/Kanimaji/Source/Colour.cpp
Normal file
|
@ -0,0 +1 @@
|
||||||
|
#include "Kanimaji/Colour.hpp"
|
|
@ -1,29 +1,78 @@
|
||||||
|
#include "Kanimaji/Colour.hpp"
|
||||||
|
#include "Kanimaji/Config.hpp"
|
||||||
#include "Kanimaji/Kanimaji.hpp"
|
#include "Kanimaji/Kanimaji.hpp"
|
||||||
#include "SVG.hpp"
|
#include "SVG.hpp"
|
||||||
|
|
||||||
#include <ctre.hpp>
|
#include <ctre.hpp>
|
||||||
#include <pugixml.hpp>
|
#include <pugixml.hpp>
|
||||||
|
|
||||||
#include <print>
|
#include <format>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
namespace Kanimaji
|
namespace Kanimaji
|
||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr std::string_view strokeBorderWidth = "4.5";
|
constexpr GroupConfig StrokeBorderConfig { .Width = 5, .Colour = Colour::FromHex("#666") };
|
||||||
constexpr std::string_view strokeBorderColour = "#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 std::string_view strokeUnfilledWidth = "3";
|
constexpr Colour StrokeFillingColour = Colour::FromHex("#F00");
|
||||||
constexpr std::string_view strokeUnfilledColour = "#EEE";
|
|
||||||
constexpr std::string_view strokeFillingColour = "#F00";
|
|
||||||
constexpr std::string_view strokeFilledWidth = "3.1";
|
|
||||||
constexpr std::string_view strokeFilledColour = "#000";
|
|
||||||
|
|
||||||
constexpr std::string_view brushColour = "#F00";
|
// What the fuck is this?
|
||||||
constexpr std::string_view brushWidth = "5.5";
|
constexpr double WaitAfter = 1.5;
|
||||||
constexpr std::string_view brushBorderColour = "#666";
|
|
||||||
constexpr std::string_view brushBorderWidth = "7";
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
pugi::xml_node append_group(pugi::xml_node &svg, std::string_view name, GroupConfig config)
|
||||||
|
{
|
||||||
|
pugi::xml_node newGroup = svg.append_child("g");
|
||||||
|
newGroup.append_attribute("id") = name;
|
||||||
|
newGroup.append_attribute("style") = std::format(
|
||||||
|
"fill:none;stroke:{};stroke-width:{};stroke-linecap:round;stroke-linejoin:round;",
|
||||||
|
config.Colour.ToHex(), config.Width
|
||||||
|
);
|
||||||
|
return newGroup;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Animate(const std::string& source, const std::string& destination)
|
bool Animate(const std::string& source, const std::string& destination)
|
||||||
|
@ -39,13 +88,13 @@ namespace Kanimaji
|
||||||
});
|
});
|
||||||
std::string text = comment.value();
|
std::string text = comment.value();
|
||||||
text.append(
|
text.append(
|
||||||
"\n"
|
"\n===============================================================================\n\n"
|
||||||
"===============================================================================\n"
|
|
||||||
"\n"
|
|
||||||
"This file has been modified by the Kanimaji tool\n"
|
"This file has been modified by the Kanimaji tool\n"
|
||||||
"avilable here: (TODO)\n\n"
|
"avilable here: (TODO)\n\n"
|
||||||
"The Kanimaji tool is based on the original kanimaji.py\n"
|
"The Kanimaji tool is based on the original kanimaji.py script.\n"
|
||||||
"script available here: https://github.com/maurimo/kanimaji\n\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"
|
"Modifications (compared to the original KanjiVG file):\n"
|
||||||
"* The stroke numbers were removed\n"
|
"* The stroke numbers were removed\n"
|
||||||
"* Special Styles section (and accompanying <g> and <use> tags) were generated\n"
|
"* Special Styles section (and accompanying <g> and <use> tags) were generated\n"
|
||||||
|
@ -53,9 +102,6 @@ namespace Kanimaji
|
||||||
comment.set_value(text);
|
comment.set_value(text);
|
||||||
|
|
||||||
pugi::xml_node svg = doc.child("svg");
|
pugi::xml_node svg = doc.child("svg");
|
||||||
pugi::xml_attribute xmlns = svg.attribute("xmlns");
|
|
||||||
svg.append_attribute("xmlns:xlink") = "http://www.w3.org/1999/xlink";
|
|
||||||
svg.append_attribute("xlink:used") = "";
|
|
||||||
|
|
||||||
svg.remove_child(svg.find_child([](pugi::xml_node& node) {
|
svg.remove_child(svg.find_child([](pugi::xml_node& node) {
|
||||||
return std::string_view(node.attribute("id").as_string()).contains("StrokeNumbers");
|
return std::string_view(node.attribute("id").as_string()).contains("StrokeNumbers");
|
||||||
|
@ -64,78 +110,94 @@ namespace Kanimaji
|
||||||
pugi::xml_node paths = svg.find_child([](pugi::xml_node& node) {
|
pugi::xml_node paths = svg.find_child([](pugi::xml_node& node) {
|
||||||
return std::string_view(node.attribute("id").as_string()).contains("StrokePaths");
|
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();
|
std::string baseId = paths.attribute("id").as_string();
|
||||||
baseId.erase(0, baseId.find('_') + 1);
|
baseId.erase(0, baseId.find('_') + 1);
|
||||||
std::println("{}", baseId);
|
|
||||||
|
|
||||||
// 1st pass for getting information
|
// 1st pass for getting information
|
||||||
float totalLength = 50;
|
double totalLength = 0.0;
|
||||||
for (const auto& path : svg.select_nodes("//path")) {
|
for (const auto& path : svg.select_nodes("//path")) {
|
||||||
std::println("{}", path.node().attribute("d").as_string());
|
|
||||||
std::string_view d = path.node().attribute("d").as_string();
|
std::string_view d = path.node().attribute("d").as_string();
|
||||||
totalLength += SVG::Path(d).Length();
|
try {
|
||||||
|
totalLength += SVG::Path(d).Length();
|
||||||
|
}
|
||||||
|
catch (const SVG::ParseError& e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string styles = "/* Styles autogenerated by Kanimaji, please do not edit manually. */";
|
std::string styles =
|
||||||
|
"\n /* Styles autogenerated by Kanimaji, please do not edit manually. */\n";
|
||||||
|
|
||||||
pugi::xml_node background = svg.append_child("g");
|
auto background = append_group(svg, "kvg:Kanimaji_Stroke_Background_" + baseId, StrokeUnfilledConfig);
|
||||||
background.append_attribute("id") = "kvg:Kanimaji_Background_" + baseId;
|
auto brushBorder = append_group(svg, "kvg:Kanimaji_Brush_Background_" + baseId, BrushBorderConfig);
|
||||||
background.append_attribute("style") = std::format(
|
auto animation = append_group(svg, "kvg:Kanimaji_Animation_" + baseId, StrokeFilledConfig);
|
||||||
"fill:none;stroke:{};stroke-width:{};stroke-linecap:round;stroke-linejoin:round;",
|
auto brush = append_group(svg, "kvg:Kanimaji_Brush_" + baseId, BrushConfig);
|
||||||
strokeUnfilledColour, strokeUnfilledWidth
|
|
||||||
);
|
|
||||||
|
|
||||||
pugi::xml_node brushBackground = svg.append_child("g");
|
|
||||||
brushBackground.append_attribute("id") = "kvg:Kanimaji_BrushBackground_" + baseId;
|
|
||||||
brushBackground.append_attribute("style") = std::format(
|
|
||||||
"fill:none;stroke:{};stroke-width:{},stroke-linecap:round;stroke-linejoin:round;",
|
|
||||||
brushBorderColour, brushBorderWidth
|
|
||||||
);
|
|
||||||
|
|
||||||
pugi::xml_node animation = svg.append_child("g");
|
|
||||||
animation.append_attribute("id") = "kvg:Kanimaji_Animation_" + baseId;
|
|
||||||
animation.append_attribute("style") = std::format(
|
|
||||||
"fill:none;stroke:{};stroke-width:{};stroke-linecap:round;stroke-linejoin:round;",
|
|
||||||
strokeFilledColour, strokeFilledWidth
|
|
||||||
);
|
|
||||||
|
|
||||||
pugi::xml_node brush = svg.append_child("g");
|
|
||||||
brush.append_attribute("id") = "kvg:Kanimaji_Brush_" + baseId;
|
|
||||||
brush.append_attribute("style") = std::format(
|
|
||||||
"fill:none;stroke:{};stroke-width:{};stroke-linecap:round;stroke-linejoin:round;",
|
|
||||||
brushColour, brushWidth
|
|
||||||
);
|
|
||||||
|
|
||||||
|
double previousLength = 0.0;
|
||||||
|
double currentLength = 0.0;
|
||||||
// 2nd pass to prepare the CSS
|
// 2nd pass to prepare the CSS
|
||||||
for (const auto& xpath : svg.select_nodes("//path")) {
|
for (const auto& xpath : svg.select_nodes("//path")) {
|
||||||
pugi::xml_node path = xpath.node();
|
pugi::xml_node path = xpath.node();
|
||||||
std::string id = path.attribute("id").as_string();
|
|
||||||
std::string css = id;
|
|
||||||
css.replace(css.find(":"), 1, "\\\\3a ");
|
|
||||||
|
|
||||||
// Generate the actual styles
|
double segmentLength = SVG::Path(path.attribute("d").as_string()).Length();
|
||||||
// - A few simplifications for now
|
previousLength = currentLength;
|
||||||
// - Do not use the path lengths and appropriate scaled time,
|
currentLength += segmentLength;
|
||||||
// use a simplified version based solely on the stroke number.
|
|
||||||
|
double startTime = 100.0 * (previousLength / totalLength);
|
||||||
|
double endTime = 100.0 * (currentLength / totalLength);
|
||||||
|
|
||||||
|
pugi::xml_attribute idAttr = path.attribute("id");
|
||||||
|
if (!idAttr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string id = idAttr.as_string();
|
||||||
|
std::string css = id;
|
||||||
|
if (auto colonPos = css.find(':'); colonPos != css.npos) {
|
||||||
|
css.replace(css.find(":"), 1, "\\:");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string pathId = id;
|
||||||
|
if (pathId.starts_with("kvg:")) {
|
||||||
|
pathId = pathId.substr(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
double totalTime = 4.0;
|
||||||
|
|
||||||
|
styles.append(std::format(StrokeProgressionFormat,
|
||||||
|
pathId, segmentLength, startTime, segmentLength, endTime));
|
||||||
|
styles.append(std::format(AnimationVisibilityFormat,
|
||||||
|
pathId, startTime, endTime, StrokeFillingColour.ToHex()));
|
||||||
|
styles.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,
|
||||||
|
totalTime));
|
||||||
|
|
||||||
|
|
||||||
pugi::xml_node useBackground = background.append_child("use");
|
pugi::xml_node useBackground = background.append_child("use");
|
||||||
useBackground.append_attribute("id") = id + "-background";
|
useBackground.append_attribute("id") = id + "-kanimaji-background";
|
||||||
useBackground.append_attribute("xlink:href") = "#" + id;
|
useBackground.append_attribute("href") = "#" + id;
|
||||||
|
|
||||||
pugi::xml_node useBrushBackground = brushBackground.append_child("use");
|
pugi::xml_node useBrushBackground = brushBorder.append_child("use");
|
||||||
useBrushBackground.append_attribute("id") = id + "-brush-background";
|
useBrushBackground.append_attribute("id") = id + "-kanimaji-brush-background";
|
||||||
useBrushBackground.append_attribute("xlink:href") = "#" + id;
|
useBrushBackground.append_attribute("href") = "#" + id;
|
||||||
|
|
||||||
pugi::xml_node useAnimation = animation.append_child("use");
|
pugi::xml_node useAnimation = animation.append_child("use");
|
||||||
useAnimation.append_attribute("id") = id + "-animation";
|
useAnimation.append_attribute("id") = id + "-kanimaji-animation";
|
||||||
useAnimation.append_attribute("xlink:href") = "#" + id;
|
useAnimation.append_attribute("href") = "#" + id;
|
||||||
|
|
||||||
pugi::xml_node useBrush = brush.append_child("use");
|
pugi::xml_node useBrush = brush.append_child("use");
|
||||||
useBrush.append_attribute("id") = id + "-brush";
|
useBrush.append_attribute("id") = id + "-kanimaji-brush";
|
||||||
useBrush.append_attribute("xlink:href") = "#" + id;
|
useBrush.append_attribute("href") = "#" + id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is how we add styles - prepare the whole thing in a separate buffer first.
|
// This is how we add styles - prepare the whole thing in a separate buffer first.
|
||||||
|
styles.append(" ");
|
||||||
pugi::xml_node style = svg.prepend_child("style");
|
pugi::xml_node style = svg.prepend_child("style");
|
||||||
style.append_attribute("id") = "kvg:Kanimaji_Style";
|
style.append_attribute("id") = "kvg:Kanimaji_Style";
|
||||||
style.append_child(pugi::node_pcdata).set_value(styles);
|
style.append_child(pugi::node_pcdata).set_value(styles);
|
||||||
|
|
|
@ -1,14 +1,29 @@
|
||||||
#include "SVG.hpp"
|
#include "SVG.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <charconv>
|
||||||
|
#include <cmath>
|
||||||
#include <format>
|
#include <format>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
namespace Kanimaji::SVG
|
namespace Kanimaji::SVG
|
||||||
{
|
{
|
||||||
|
ParseError::ParseError(std::size_t current, std::string_view message)
|
||||||
|
: Error(std::format("[At: {}] {}", current, message))
|
||||||
|
, mPosition(current)
|
||||||
|
{}
|
||||||
|
|
||||||
struct Vec2
|
struct Vec2
|
||||||
{
|
{
|
||||||
double x;
|
double x;
|
||||||
double y;
|
double y;
|
||||||
|
|
||||||
|
double Length() const
|
||||||
|
{
|
||||||
|
return std::sqrt(x * x + y * y);
|
||||||
|
}
|
||||||
|
|
||||||
friend Vec2 operator+(const Vec2& u, const Vec2& v)
|
friend Vec2 operator+(const Vec2& u, const Vec2& v)
|
||||||
{
|
{
|
||||||
return Vec2 {
|
return Vec2 {
|
||||||
|
@ -16,43 +31,187 @@ namespace Kanimaji::SVG
|
||||||
.y = u.y + v.y,
|
.y = u.y + v.y,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
friend Vec2 operator-(const Vec2& u, const Vec2& v)
|
||||||
|
{
|
||||||
|
return Vec2 {
|
||||||
|
.x = u.x - v.x,
|
||||||
|
.y = u.y - v.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
friend Vec2 operator-(const Vec2& u)
|
||||||
|
{
|
||||||
|
return Vec2 {
|
||||||
|
.x = -u.x,
|
||||||
|
.y = -u.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
friend Vec2 operator*(const Vec2& u, double s)
|
||||||
|
{
|
||||||
|
return Vec2 {
|
||||||
|
.x = u.x * s,
|
||||||
|
.y = u.y * s,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
friend Vec2 operator*(double s, const Vec2& u)
|
||||||
|
{
|
||||||
|
return Vec2 {
|
||||||
|
.x = s * u.x,
|
||||||
|
.y = s * u.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
T Squared(T val)
|
||||||
|
{
|
||||||
|
return val * val;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
T Cubed(T val)
|
||||||
|
{
|
||||||
|
return val * val * val;
|
||||||
|
}
|
||||||
|
|
||||||
class Path::Element
|
class Path::Element
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
enum class Type
|
||||||
|
{
|
||||||
|
Move,
|
||||||
|
CubicBezier,
|
||||||
|
};
|
||||||
|
|
||||||
|
Element(Type type)
|
||||||
|
: mType(type)
|
||||||
|
{}
|
||||||
|
|
||||||
virtual ~Element() = default;
|
virtual ~Element() = default;
|
||||||
|
|
||||||
|
Type GetType() const { return mType; }
|
||||||
|
|
||||||
virtual double Length(Vec2 origin) const = 0;
|
virtual double Length(Vec2 origin) const = 0;
|
||||||
virtual Vec2 Endpoint(Vec2 origin) const = 0;
|
virtual Vec2 Endpoint(Vec2 origin) const = 0;
|
||||||
|
virtual void Serialize(std::string& out) const = 0;
|
||||||
|
protected:
|
||||||
|
Type mType;
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
class ParseError : public Error
|
void SkipWhitespace(std::string_view source, std::size_t& current)
|
||||||
{
|
{
|
||||||
public:
|
while (current < source.size() && std::isspace(source[current])) {
|
||||||
ParseError(std::size_t current, std::string_view message)
|
current++;
|
||||||
: Error(std::format("[At: {}] {}", current, message))
|
}
|
||||||
{}
|
}
|
||||||
};
|
|
||||||
|
void SkipCommaWhitespace(std::string_view source, std::size_t& current)
|
||||||
|
{
|
||||||
|
SkipWhitespace(source, current);
|
||||||
|
if (current < source.size() && source[current] == ',') {
|
||||||
|
current++;
|
||||||
|
}
|
||||||
|
SkipWhitespace(source, current);
|
||||||
|
}
|
||||||
|
|
||||||
|
double ConvertNumber(std::size_t position, std::string_view raw)
|
||||||
|
{
|
||||||
|
double result = 0;
|
||||||
|
auto [ptr, ec] = std::from_chars(raw.data(), raw.data() + raw.size(), result);
|
||||||
|
|
||||||
|
if (ec == std::errc::invalid_argument) {
|
||||||
|
throw ParseError(position, "This is not a number");
|
||||||
|
} else if (ec == std::errc::result_out_of_range) {
|
||||||
|
throw ParseError(position, "This number cannot fit in a double");
|
||||||
|
} else if (ec != std::errc{}) {
|
||||||
|
throw ParseError(position, "Unknown error");
|
||||||
|
} else if (ptr != raw.data() + raw.size()) {
|
||||||
|
throw ParseError(position, "The number does not reach the end of the range");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
double ParseNumber(std::string_view source, std::size_t& current)
|
||||||
|
{
|
||||||
|
constexpr auto isNumeric = [] (char ch) {
|
||||||
|
return std::isdigit(ch) || ch == '.';
|
||||||
|
};
|
||||||
|
|
||||||
|
std::size_t begin = current;
|
||||||
|
if (current < source.size() && source[current] == '-') {
|
||||||
|
current++;
|
||||||
|
}
|
||||||
|
while (current < source.size() && isNumeric(source[current])) {
|
||||||
|
current++;
|
||||||
|
}
|
||||||
|
if (current - begin == 0) {
|
||||||
|
throw ParseError(current, "Numbers cannot be zero width");
|
||||||
|
}
|
||||||
|
return ConvertNumber(begin, { source.data() + begin, current - begin });
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec2 ParseVector(std::string_view source, std::size_t& current)
|
||||||
|
{
|
||||||
|
double x = ParseNumber(source, current);
|
||||||
|
|
||||||
|
SkipCommaWhitespace(source, current);
|
||||||
|
if (current >= source.size()) {
|
||||||
|
throw ParseError(current, "Unexpected input end, a 2D vector needs two numbers");
|
||||||
|
}
|
||||||
|
double y = ParseNumber(source, current);
|
||||||
|
SkipCommaWhitespace(source, current);
|
||||||
|
|
||||||
|
return {
|
||||||
|
.x = x,
|
||||||
|
.y = y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
using ElementPtr = std::unique_ptr<Path::Element>;
|
||||||
|
|
||||||
class Move : public Path::Element
|
class Move : public Path::Element
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Move(bool relative, Vec2 move)
|
Move(bool relative, Vec2 move)
|
||||||
: mRelative(relative), mMove(move)
|
: Element(Type::Move), mRelative(relative), mMove(move)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
double Length(Vec2 origin) const override
|
double Length(Vec2 origin) const override
|
||||||
{
|
{
|
||||||
return 0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Vec2 Endpoint(Vec2 origin) const override
|
Vec2 Endpoint(Vec2 origin) const override
|
||||||
{
|
{
|
||||||
return mRelative ? origin + mMove : mMove;
|
return mRelative ? origin + mMove : mMove;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static ElementPtr Parse(std::string_view source, std::size_t& current)
|
||||||
|
{
|
||||||
|
constexpr auto verify = [] (char ch) {
|
||||||
|
return ch == 'M' || ch == 'm';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (current >= source.size() || !verify(source[current])) {
|
||||||
|
throw ParseError(current, "Invalid move start");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool relative = std::islower(source[current]);
|
||||||
|
current++;
|
||||||
|
SkipWhitespace(source, current);
|
||||||
|
return std::make_unique<Move>(relative, ParseVector(source, current));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Serialize(std::string& out) const override
|
||||||
|
{
|
||||||
|
out.append(std::format("{}{:.3},{:.3}", mRelative ? 'm' : 'M', mMove.x, mMove.y));
|
||||||
|
}
|
||||||
private:
|
private:
|
||||||
bool mRelative; // 7B padding, oof
|
bool mRelative; // 7B padding, oof
|
||||||
Vec2 mMove;
|
Vec2 mMove;
|
||||||
|
@ -62,17 +221,107 @@ namespace Kanimaji::SVG
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
CubicBezier(bool relative, Vec2 control1, Vec2 control2, Vec2 end)
|
CubicBezier(bool relative, Vec2 control1, Vec2 control2, Vec2 end)
|
||||||
: mRelative(relative), mControl1(control1), mControl2(control2), mEnd(end)
|
: Element(Type::CubicBezier)
|
||||||
|
, mRelative(relative)
|
||||||
|
, mControl1(control1)
|
||||||
|
, mControl2(control2)
|
||||||
|
, mEnd(end)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
Vec2 Interpolate(Vec2 origin, double t) const
|
||||||
|
{
|
||||||
|
t = std::clamp(t, 0.0, 1.0);
|
||||||
|
|
||||||
|
Vec2 control1 = mRelative ? origin + mControl1 : mControl1;
|
||||||
|
Vec2 control2 = mRelative ? origin + mControl2 : mControl2;
|
||||||
|
Vec2 end = mRelative ? origin + mEnd : mEnd;
|
||||||
|
|
||||||
|
return origin * Cubed(1.0 - t)
|
||||||
|
+ 3.0 * control1 * Squared(1.0 - t) * t
|
||||||
|
+ 3.0 * control2 * (1.0 - t) * Squared(t)
|
||||||
|
+ end * Cubed(t);
|
||||||
|
}
|
||||||
|
|
||||||
double Length(Vec2 origin) const override
|
double Length(Vec2 origin) const override
|
||||||
{
|
{
|
||||||
throw Error("not implemented");
|
constexpr std::int32_t precision = 128;
|
||||||
|
|
||||||
|
double length = 0.0;
|
||||||
|
Vec2 previous = origin;
|
||||||
|
Vec2 current = origin;
|
||||||
|
for (std::int32_t i = 1; i <= precision; ++i) {
|
||||||
|
double t = static_cast<double>(i) / static_cast<double>(precision);
|
||||||
|
current = Interpolate(origin, t);
|
||||||
|
length += (previous - current).Length();
|
||||||
|
previous = current;
|
||||||
|
}
|
||||||
|
return length;
|
||||||
}
|
}
|
||||||
|
|
||||||
Vec2 Endpoint(Vec2 origin) const override
|
Vec2 Endpoint(Vec2 origin) const override
|
||||||
{
|
{
|
||||||
return mEnd;
|
return mRelative ? origin + mEnd : mEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec2 ReflectControl2(const Vec2& origin, bool relative) const
|
||||||
|
{
|
||||||
|
Vec2 control2 = mRelative ? origin + mControl2 : mControl2;
|
||||||
|
Vec2 end = mRelative ? origin + mEnd : mEnd;
|
||||||
|
Vec2 reflected = relative ? end - control2 : 2.0 * end - control2;
|
||||||
|
return reflected;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ElementPtr Parse(std::string_view source, std::size_t& current)
|
||||||
|
{
|
||||||
|
constexpr auto verify = [] (char ch) {
|
||||||
|
return ch == 'C' || ch == 'c';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (current >= source.size() || !verify(source[current])) {
|
||||||
|
throw ParseError(current, "Invalid Cubic Bezier start");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool relative = std::islower(source[current]);
|
||||||
|
current++;
|
||||||
|
SkipWhitespace(source, current);
|
||||||
|
Vec2 control1 = ParseVector(source, current);
|
||||||
|
SkipCommaWhitespace(source, current);
|
||||||
|
Vec2 control2 = ParseVector(source, current);
|
||||||
|
SkipCommaWhitespace(source, current);
|
||||||
|
Vec2 end = ParseVector(source, current);
|
||||||
|
SkipWhitespace(source, current);
|
||||||
|
return std::make_unique<CubicBezier>(relative, control1, control2, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ElementPtr ParseExtension(std::string_view source, std::size_t& current,
|
||||||
|
const Vec2& previousEnd, const CubicBezier& previous)
|
||||||
|
{
|
||||||
|
constexpr auto verify = [] (char ch) {
|
||||||
|
return ch == 'S' || ch == 's';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (current >= source.size() || !verify(source[current])) {
|
||||||
|
throw ParseError(current, "Invalid Cubic Bezier Extension start");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool relative = std::islower(source[current]);
|
||||||
|
current++;
|
||||||
|
SkipWhitespace(source, current);
|
||||||
|
Vec2 control1 = previous.ReflectControl2(previousEnd, relative);
|
||||||
|
Vec2 control2 = ParseVector(source, current);
|
||||||
|
SkipCommaWhitespace(source, current);
|
||||||
|
Vec2 end = ParseVector(source, current);
|
||||||
|
SkipWhitespace(source, current);
|
||||||
|
return std::make_unique<CubicBezier>(relative, control1, control2, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Serialize(std::string& out) const override
|
||||||
|
{
|
||||||
|
out.append(std::format("{}{:.3},{:.3},{:.3},{:.3},{:.3},{:.3}",
|
||||||
|
mRelative ? 'c' : 'C',
|
||||||
|
mControl1.x, mControl1.y,
|
||||||
|
mControl2.x, mControl2.y,
|
||||||
|
mEnd.x, mEnd.y));
|
||||||
}
|
}
|
||||||
private:
|
private:
|
||||||
bool mRelative; // 7B padding, oof
|
bool mRelative; // 7B padding, oof
|
||||||
|
@ -81,24 +330,47 @@ namespace Kanimaji::SVG
|
||||||
Vec2 mEnd;
|
Vec2 mEnd;
|
||||||
};
|
};
|
||||||
|
|
||||||
using ElementPtr = std::unique_ptr<Path::Element>;
|
|
||||||
void Parse(std::string_view definition, std::vector<ElementPtr>& elements)
|
void Parse(std::string_view definition, std::vector<ElementPtr>& elements)
|
||||||
{
|
{
|
||||||
std::size_t current = 0;
|
std::size_t current = 0;
|
||||||
|
Vec2 previousEnd = { .x = 0.0, .y = 0.0, };
|
||||||
|
Vec2 currentEnd = { .x = 0.0, .y = 0.0, };
|
||||||
|
|
||||||
while (current < definition.size()) {
|
while (current < definition.size()) {
|
||||||
|
SkipWhitespace(definition, current);
|
||||||
switch (definition[current]) {
|
switch (definition[current]) {
|
||||||
case 'M': [[fallthrough]];
|
case 'M': [[fallthrough]];
|
||||||
case 'm':
|
case 'm':
|
||||||
// ParseMove
|
elements.push_back(Move::Parse(definition, current));
|
||||||
break;
|
break;
|
||||||
case 'C': [[fallthrough]];
|
case 'C': [[fallthrough]];
|
||||||
case 'c':
|
case 'c':
|
||||||
// ParseCubicBezier
|
elements.push_back(CubicBezier::Parse(definition, current));
|
||||||
break;
|
break;
|
||||||
|
case 'S': [[fallthrough]];
|
||||||
|
case 's': {
|
||||||
|
if (elements.empty()) {
|
||||||
|
throw ParseError(current, "Cubic Bezier Extensions without predecessors "
|
||||||
|
"are unsupported");
|
||||||
|
}
|
||||||
|
if (elements.back()->GetType() != Path::Element::Type::CubicBezier) {
|
||||||
|
throw ParseError(current, "The predecessor of this Cubic Bezier Extension "
|
||||||
|
"is not a Cubic Bezier itself");
|
||||||
|
}
|
||||||
|
CubicBezier *previous = dynamic_cast<CubicBezier *>(elements.back().get());
|
||||||
|
if (!previous) {
|
||||||
|
throw ParseError(current, "The previous element could not be cast to a "
|
||||||
|
"Cubic Bezier for some reason");
|
||||||
|
}
|
||||||
|
elements.push_back(CubicBezier::ParseExtension(definition, current, previousEnd,
|
||||||
|
*previous));
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw ParseError(current, "Unrecognized character");
|
throw ParseError(current, "Unrecognized character");
|
||||||
}
|
}
|
||||||
|
previousEnd = currentEnd;
|
||||||
|
currentEnd = elements.back()->Endpoint(currentEnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,6 +384,19 @@ namespace Kanimaji::SVG
|
||||||
|
|
||||||
double Path::Length() const
|
double Path::Length() const
|
||||||
{
|
{
|
||||||
return 0;
|
Vec2 origin { .x = 0.0, .y = 0.0, };
|
||||||
|
double length = 0.0;
|
||||||
|
for (const auto& segment : mSegments) {
|
||||||
|
length += segment->Length(origin);
|
||||||
|
origin = segment->Endpoint(origin);
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Path::Serialize(std::string& out) const
|
||||||
|
{
|
||||||
|
for (const auto& segment : mSegments) {
|
||||||
|
segment->Serialize(out);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,19 @@ namespace Kanimaji::SVG
|
||||||
using std::runtime_error::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
|
class Path
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
@ -27,6 +40,8 @@ namespace Kanimaji::SVG
|
||||||
~Path();
|
~Path();
|
||||||
|
|
||||||
double Length() const;
|
double Length() const;
|
||||||
|
|
||||||
|
void Serialize(std::string& out) const;
|
||||||
private:
|
private:
|
||||||
std::vector<std::unique_ptr<Element>> mSegments;
|
std::vector<std::unique_ptr<Element>> mSegments;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue