Initial working Kanimaji implementation

This commit is contained in:
TennesseeTrash 2025-06-06 18:57:42 +02:00
parent 4e87a67296
commit 99901b878a
8 changed files with 588 additions and 83 deletions

View file

@ -21,5 +21,7 @@ target_include_directories(Kanimaji
target_sources(Kanimaji
PRIVATE
"Source/Colour.cpp"
"Source/Kanimaji.cpp"
"Source/SVG.cpp"
)

View 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

View 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

View file

@ -0,0 +1,9 @@
#ifndef KANIMAJI_ERROR_HPP
#define KANIMAJI_ERROR_HPP
namespace Kanimaji
{
}
#endif // KANIMAJI_ERROR_HPP

View file

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

View file

@ -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);

View file

@ -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);
}
}
}

View file

@ -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;
};