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
|
||||
PRIVATE
|
||||
"Source/Colour.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 "SVG.hpp"
|
||||
|
||||
#include <ctre.hpp>
|
||||
#include <pugixml.hpp>
|
||||
|
||||
#include <print>
|
||||
#include <format>
|
||||
#include <string_view>
|
||||
|
||||
namespace Kanimaji
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr std::string_view strokeBorderWidth = "4.5";
|
||||
constexpr std::string_view strokeBorderColour = "#666";
|
||||
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 std::string_view strokeUnfilledWidth = "3";
|
||||
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 Colour StrokeFillingColour = Colour::FromHex("#F00");
|
||||
|
||||
constexpr std::string_view brushColour = "#F00";
|
||||
constexpr std::string_view brushWidth = "5.5";
|
||||
constexpr std::string_view brushBorderColour = "#666";
|
||||
constexpr std::string_view brushBorderWidth = "7";
|
||||
// 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
|
||||
);
|
||||
|
||||
|
||||
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)
|
||||
|
@ -39,13 +88,13 @@ namespace Kanimaji
|
|||
});
|
||||
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"
|
||||
"The Kanimaji tool is based on the original kanimaji.py\n"
|
||||
"script available here: https://github.com/maurimo/kanimaji\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"
|
||||
"* The stroke numbers were removed\n"
|
||||
"* Special Styles section (and accompanying <g> and <use> tags) were generated\n"
|
||||
|
@ -53,9 +102,6 @@ namespace Kanimaji
|
|||
comment.set_value(text);
|
||||
|
||||
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) {
|
||||
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) {
|
||||
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();
|
||||
baseId.erase(0, baseId.find('_') + 1);
|
||||
std::println("{}", baseId);
|
||||
|
||||
// 1st pass for getting information
|
||||
float totalLength = 50;
|
||||
double totalLength = 0.0;
|
||||
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();
|
||||
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");
|
||||
background.append_attribute("id") = "kvg:Kanimaji_Background_" + baseId;
|
||||
background.append_attribute("style") = std::format(
|
||||
"fill:none;stroke:{};stroke-width:{};stroke-linecap:round;stroke-linejoin:round;",
|
||||
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
|
||||
);
|
||||
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);
|
||||
|
||||
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();
|
||||
std::string id = path.attribute("id").as_string();
|
||||
std::string css = id;
|
||||
css.replace(css.find(":"), 1, "\\\\3a ");
|
||||
|
||||
// Generate the actual styles
|
||||
// - A few simplifications for now
|
||||
// - Do not use the path lengths and appropriate scaled time,
|
||||
// use a simplified version based solely on the stroke number.
|
||||
double segmentLength = SVG::Path(path.attribute("d").as_string()).Length();
|
||||
previousLength = currentLength;
|
||||
currentLength += segmentLength;
|
||||
|
||||
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");
|
||||
useBackground.append_attribute("id") = id + "-background";
|
||||
useBackground.append_attribute("xlink:href") = "#" + id;
|
||||
useBackground.append_attribute("id") = id + "-kanimaji-background";
|
||||
useBackground.append_attribute("href") = "#" + id;
|
||||
|
||||
pugi::xml_node useBrushBackground = brushBackground.append_child("use");
|
||||
useBrushBackground.append_attribute("id") = id + "-brush-background";
|
||||
useBrushBackground.append_attribute("xlink: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 + "-animation";
|
||||
useAnimation.append_attribute("xlink:href") = "#" + id;
|
||||
useAnimation.append_attribute("id") = id + "-kanimaji-animation";
|
||||
useAnimation.append_attribute("href") = "#" + id;
|
||||
|
||||
pugi::xml_node useBrush = brush.append_child("use");
|
||||
useBrush.append_attribute("id") = id + "-brush";
|
||||
useBrush.append_attribute("xlink:href") = "#" + id;
|
||||
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.
|
||||
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);
|
||||
|
|
|
@ -1,14 +1,29 @@
|
|||
#include "SVG.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <charconv>
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
#include <memory>
|
||||
|
||||
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;
|
||||
double y;
|
||||
|
||||
double Length() const
|
||||
{
|
||||
return std::sqrt(x * x + y * y);
|
||||
}
|
||||
|
||||
friend Vec2 operator+(const Vec2& u, const Vec2& v)
|
||||
{
|
||||
return Vec2 {
|
||||
|
@ -16,43 +31,187 @@ namespace Kanimaji::SVG
|
|||
.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
|
||||
{
|
||||
public:
|
||||
enum class Type
|
||||
{
|
||||
Move,
|
||||
CubicBezier,
|
||||
};
|
||||
|
||||
Element(Type type)
|
||||
: mType(type)
|
||||
{}
|
||||
|
||||
virtual ~Element() = default;
|
||||
|
||||
Type GetType() const { return mType; }
|
||||
|
||||
virtual double Length(Vec2 origin) const = 0;
|
||||
virtual Vec2 Endpoint(Vec2 origin) const = 0;
|
||||
virtual void Serialize(std::string& out) const = 0;
|
||||
protected:
|
||||
Type mType;
|
||||
};
|
||||
|
||||
namespace
|
||||
{
|
||||
class ParseError : public Error
|
||||
void SkipWhitespace(std::string_view source, std::size_t& current)
|
||||
{
|
||||
public:
|
||||
ParseError(std::size_t current, std::string_view message)
|
||||
: Error(std::format("[At: {}] {}", current, message))
|
||||
{}
|
||||
};
|
||||
while (current < source.size() && std::isspace(source[current])) {
|
||||
current++;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
public:
|
||||
Move(bool relative, Vec2 move)
|
||||
: mRelative(relative), mMove(move)
|
||||
: Element(Type::Move), mRelative(relative), mMove(move)
|
||||
{}
|
||||
|
||||
double Length(Vec2 origin) const override
|
||||
{
|
||||
return 0;
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
Vec2 Endpoint(Vec2 origin) const override
|
||||
{
|
||||
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:
|
||||
bool mRelative; // 7B padding, oof
|
||||
Vec2 mMove;
|
||||
|
@ -62,17 +221,107 @@ namespace Kanimaji::SVG
|
|||
{
|
||||
public:
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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:
|
||||
bool mRelative; // 7B padding, oof
|
||||
|
@ -81,24 +330,47 @@ namespace Kanimaji::SVG
|
|||
Vec2 mEnd;
|
||||
};
|
||||
|
||||
using ElementPtr = std::unique_ptr<Path::Element>;
|
||||
void Parse(std::string_view definition, std::vector<ElementPtr>& elements)
|
||||
{
|
||||
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()) {
|
||||
SkipWhitespace(definition, current);
|
||||
switch (definition[current]) {
|
||||
case 'M': [[fallthrough]];
|
||||
case 'm':
|
||||
// ParseMove
|
||||
elements.push_back(Move::Parse(definition, current));
|
||||
break;
|
||||
case 'C': [[fallthrough]];
|
||||
case 'c':
|
||||
// ParseCubicBezier
|
||||
elements.push_back(CubicBezier::Parse(definition, current));
|
||||
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:
|
||||
throw ParseError(current, "Unrecognized character");
|
||||
}
|
||||
previousEnd = currentEnd;
|
||||
currentEnd = elements.back()->Endpoint(currentEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,6 +384,19 @@ namespace Kanimaji::SVG
|
|||
|
||||
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;
|
||||
};
|
||||
|
||||
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:
|
||||
|
@ -27,6 +40,8 @@ namespace Kanimaji::SVG
|
|||
~Path();
|
||||
|
||||
double Length() const;
|
||||
|
||||
void Serialize(std::string& out) const;
|
||||
private:
|
||||
std::vector<std::unique_ptr<Element>> mSegments;
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue