File indexing completed on 2025-01-05 04:01:18

0001 /*
0002  * SPDX-FileCopyrightText: 2019-2023 Mattia Basaglia <dev@dragon.best>
0003  *
0004  * SPDX-License-Identifier: GPL-3.0-or-later
0005  */
0006 
0007 #pragma once
0008 
0009 #include <variant>
0010 
0011 #include <QDomElement>
0012 #include <QRegularExpression>
0013 
0014 #include "model/animation/keyframe_transition.hpp"
0015 #include "model/animation/frame_time.hpp"
0016 #include "model/animation/join_animatables.hpp"
0017 #include "math/bezier/bezier.hpp"
0018 #include "app/utils/string_view.hpp"
0019 
0020 #include "path_parser.hpp"
0021 #include "detail.hpp"
0022 #include "io/animated_properties.hpp"
0023 
0024 namespace glaxnimate::io::svg {
0025 
0026 QColor parse_color(const QString& string);
0027 
0028 } // namespace glaxnimate::io::svg
0029 
0030 namespace glaxnimate::io::svg::detail {
0031 
0032 using namespace glaxnimate::io::detail;
0033 
0034 class AnimateParser
0035 {
0036 public:
0037     struct AnimatedProperties : public glaxnimate::io::detail::AnimatedProperties
0038     {
0039         QDomElement element;
0040 
0041         bool apply_motion(model::AnimatedProperty<QPointF>& prop, const QPointF& delta_pos = {}, model::Property<bool>* auto_orient = nullptr) const
0042         {
0043             auto motion = properties.find("motion");
0044             if ( motion == properties.end() )
0045                 return false;
0046 
0047             if ( auto_orient )
0048                 auto_orient->set(motion->second.auto_orient);
0049 
0050             for ( const auto& kf : motion->second.keyframes )
0051                 prop.set_keyframe(kf.time, QPointF())->set_transition(kf.transition);
0052 
0053             if ( qFuzzyIsNull(math::length(delta_pos)) )
0054             {
0055                 prop.set_bezier(motion->second.motion);
0056             }
0057             else
0058             {
0059                 math::bezier::Bezier bez = motion->second.motion;
0060                 for ( auto& p : bez )
0061                     p.translate(delta_pos);
0062 
0063                 prop.set_bezier(bez);
0064             }
0065 
0066             return true;
0067         }
0068 
0069         bool prepare_joined(std::vector<JoinedProperty>& props) const override
0070         {
0071             for ( auto& p : props )
0072             {
0073                 if ( p.prop.index() == 1 )
0074                 {
0075                     if ( !element.hasAttribute(*p.get<1>()) )
0076                         return false;
0077                     p.prop = split_values(element.attribute(*p.get<1>()));
0078                 }
0079             }
0080 
0081             return true;
0082         }
0083     };
0084 
0085     static std::vector<qreal> split_values(const QString& v)
0086     {
0087         if ( !v.contains(separator) )
0088         {
0089             bool ok = false;
0090             qreal val = v.toDouble(&ok);
0091             if ( ok )
0092                 return {val};
0093 
0094             QColor c(v);
0095             if ( c.isValid() )
0096                 return {c.redF(), c.greenF(), c.blueF(), c.alphaF()};
0097             return {};
0098         }
0099 
0100         auto split = ::utils::split_ref(v, separator,
0101 #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
0102         Qt::SkipEmptyParts
0103 #else
0104         QString::SkipEmptyParts
0105 #endif
0106         );
0107         std::vector<qreal> values;
0108         values.reserve(split.size());
0109         for ( const auto& r : split )
0110             values.push_back(r.toDouble());
0111         return values;
0112     }
0113 
0114     model::FrameTime clock_to_frame(const QString& clock)
0115     {
0116         auto match = clock_re.match(clock, 0, QRegularExpression::PartialPreferCompleteMatch);
0117         if ( !match.hasMatch() )
0118             return 0;
0119 
0120         static constexpr const qreal minutes = 60;
0121         static constexpr const qreal hours = 60*60;
0122         static const std::map<QString, qreal> units = {
0123             {"ms", 0.001},
0124             {"s", 1.0},
0125             {"min", minutes},
0126             {"h", hours}
0127         };
0128 
0129         if ( !match.captured("unit").isEmpty() )
0130             return match.captured("timecount").toDouble() * units.at(match.captured("unit")) * fps;
0131 
0132         return (
0133             match.captured("hours").toDouble() * hours +
0134             match.captured("minutes").toDouble() * minutes +
0135             match.captured("seconds").toDouble()
0136         ) * fps;
0137     }
0138 
0139     ValueVariant parse_value(const QString& str, ValueVariant::Type type) const
0140     {
0141         switch ( type )
0142         {
0143             case ValueVariant::Vector:
0144                 return split_values(str);
0145             case ValueVariant::Bezier:
0146                 return PathDParser(str).parse();
0147             case ValueVariant::String:
0148                 return str;
0149             case ValueVariant::Color:
0150                 return parse_color(str);
0151         }
0152 
0153         return {};
0154     }
0155 
0156     std::vector<ValueVariant> get_values(const QDomElement& animate)
0157     {
0158         QString attr = animate.attribute("attributeName");
0159         ValueVariant::Type type = ValueVariant::Vector;
0160         if ( attr == "d" )
0161             type = ValueVariant::Bezier;
0162         else if ( attr == "display" )
0163             type = ValueVariant::String;
0164         else if ( attr == "fill" || attr == "stroke" || attr == "stop-color" )
0165             type = ValueVariant::Color;
0166 
0167         std::vector<ValueVariant> values;
0168 
0169         if ( animate.hasAttribute("values") )
0170         {
0171             auto val_str = animate.attribute("values").split(frame_separator_re,
0172 #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
0173         Qt::SkipEmptyParts
0174 #else
0175         QString::SkipEmptyParts
0176 #endif
0177             );
0178             values.reserve(val_str.size());
0179             for ( const auto& val : val_str )
0180                 values.push_back(parse_value(val, type));
0181 
0182             if ( values.size() < 2 )
0183             {
0184                 warning("Not enough values in `animate`");
0185                 return {};
0186             }
0187 
0188             for ( uint i = 1; i < values.size(); i++ )
0189             {
0190                 if ( !values[i].compatible(values[0]) )
0191                 {
0192                     warning("Mismatching `values` in `animate`");
0193                     return {};
0194                 }
0195             }
0196         }
0197         else
0198         {
0199             if ( animate.hasAttribute("from") )
0200             {
0201                 values.push_back(parse_value(animate.attribute("from"), type));
0202             }
0203             else
0204             {
0205                 if ( attr == "transform" )
0206                 {
0207                     warning("You need to set `values` or `from` in `animateTransform`");
0208                     return {};
0209                 }
0210                 else if ( !animate.hasAttribute(attr) )
0211                 {
0212                     warning("Missing `from` in `animate`");
0213                     return {};
0214                 }
0215 
0216                 values.push_back(parse_value(animate.attribute(attr), type));
0217             }
0218 
0219             if ( animate.hasAttribute("to") )
0220             {
0221                 values.push_back(parse_value(animate.attribute("to"), type));
0222             }
0223             else if ( type == ValueVariant::Vector && animate.hasAttribute("by") )
0224             {
0225                 auto by = split_values(animate.attribute("to"));
0226                 if ( by.size() != values[0].vector().size() )
0227                 {
0228                     warning("Mismatching `by` and `from` in `animate`");
0229                     return {};
0230                 }
0231 
0232                 for ( uint i = 0; i < by.size(); i++ )
0233                     by[i] += values[0].vector()[i];
0234 
0235                 values.push_back(std::move(by));
0236             }
0237             else
0238             {
0239                 warning("Missing `to` or `by` in `animate`");
0240                 return {};
0241             }
0242         }
0243 
0244         if ( type == ValueVariant::Bezier )
0245         {
0246             for ( const auto& v : values )
0247             {
0248                 if ( v.bezier().size() != 1 )
0249                 {
0250                     warning("Can only load animated `d` if each keyframe has exactly 1 path");
0251                     return {};
0252                 }
0253             }
0254         }
0255 
0256         return values;
0257     }
0258 
0259     void register_time_range(model::FrameTime start_time, model::FrameTime end_time)
0260     {
0261         if ( !kf_range_initialized )
0262         {
0263             kf_range_initialized = true;
0264             min_kf = start_time;
0265             max_kf = end_time;
0266         }
0267         else
0268         {
0269             if ( min_kf > start_time )
0270                 min_kf = start_time;
0271             if ( max_kf < end_time )
0272                 max_kf = end_time;
0273         }
0274     }
0275 
0276     void parse_animate(const QDomElement& animate, AnimatedProperty& prop, bool motion)
0277     {
0278         if ( !prop.keyframes.empty() )
0279         {
0280             warning("Multiple `animate` for the same property");
0281             return;
0282         }
0283 
0284         model::FrameTime start_time = 0;
0285         model::FrameTime end_time = 0;
0286 
0287         if ( animate.hasAttribute("begin") )
0288             start_time = clock_to_frame(animate.attribute("begin"));
0289 
0290         if ( animate.hasAttribute("dur") )
0291             end_time = start_time + clock_to_frame(animate.attribute("dur"));
0292         else if ( animate.hasAttribute("end") )
0293             end_time = clock_to_frame(animate.attribute("end"));
0294 
0295         if ( start_time >= end_time )
0296         {
0297             warning("Invalid timings in `animate`");
0298             return;
0299         }
0300 
0301         register_time_range(start_time, end_time);
0302 
0303         std::vector<ValueVariant> values;
0304 
0305         if ( motion )
0306         {
0307             if ( !animate.hasAttribute("path") )
0308             {
0309                 warning("Missing path for animateMotion");
0310                 return;
0311             }
0312 
0313             if ( animate.hasAttribute("rotate") )
0314             {
0315                 QString rotate = animate.attribute("rotate");
0316                 if ( rotate == "auto" )
0317                 {
0318                     prop.auto_orient = true;
0319                 }
0320                 else
0321                 {
0322                     bool is_number = false;
0323                     int degrees = rotate.toInt(&is_number) % 360;
0324                     if ( !is_number || degrees != 0 )
0325                     {
0326                         warning("The only supported values for animateMotion.rotate are auto or 0");
0327                         prop.auto_orient = rotate == "auto-reverse";
0328                     }
0329                 }
0330             }
0331 
0332             auto mbez = PathDParser(animate.attribute("path")).parse();
0333 
0334             /// \todo Automatically add Hold transitions for sub-bezier end points
0335             for ( const auto& b : mbez.beziers() )
0336             {
0337                 for ( const auto& p : b )
0338                 {
0339                     values.push_back(std::vector<qreal>{p.pos.x(), p.pos.y()});
0340                     prop.motion.push_back(p);
0341                 }
0342             }
0343 
0344             /// \todo keyPoints
0345         }
0346         else
0347         {
0348             values = get_values(animate);
0349         }
0350 
0351         if ( values.empty() )
0352             return;
0353 
0354         std::vector<model::FrameTime> times;
0355         times.reserve(values.size());
0356 
0357         if ( animate.hasAttribute("keyTimes") )
0358         {
0359             auto strings = animate.attribute("keyTimes").split(frame_separator_re,
0360 #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
0361         Qt::SkipEmptyParts
0362 #else
0363         QString::SkipEmptyParts
0364 #endif
0365             );
0366             if ( strings.size() != int(values.size()) )
0367             {
0368                 warning(QString("`keyTimes` (%1) and `values` (%2) mismatch").arg(strings.size()).arg(int(values.size())));
0369                 return;
0370             }
0371 
0372             for ( const auto& s : strings )
0373                 times.push_back(math::lerp(start_time, end_time, s.toDouble()));
0374         }
0375         else
0376         {
0377             for ( qreal i = 0; i < values.size(); i++ )
0378                 times.push_back(math::lerp(start_time, end_time, i/(values.size()-1)));
0379         }
0380 
0381         std::vector<model::KeyframeTransition> transitions;
0382         QString calc = animate.attribute("calcMode", "linear");
0383         if ( calc == "spline" )
0384         {
0385             if ( !animate.hasAttribute("keySplines") )
0386             {
0387                 warning("Missing `keySplines`");
0388                 return;
0389             }
0390 
0391             auto splines = animate.attribute("keySplines").split(frame_separator_re,
0392 #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
0393         Qt::SkipEmptyParts
0394 #else
0395         QString::SkipEmptyParts
0396 #endif
0397             );
0398             if ( splines.size() != int(values.size()) - 1 )
0399             {
0400                 warning("Wrong number of `keySplines` values");
0401                 return;
0402             }
0403 
0404             transitions.reserve(values.size());
0405             for ( const auto& spline : splines )
0406             {
0407                 auto params = split_values(spline);
0408                 if ( params.size() != 4 )
0409                 {
0410                     warning("Invalid value for `keySplines`");
0411                     return;
0412                 }
0413                 transitions.push_back({{params[0], params[1]}, {params[2], params[3]}});
0414             }
0415             transitions.emplace_back();
0416         }
0417         else
0418         {
0419             model::KeyframeTransition def;
0420             if ( calc == "discrete" )
0421                 def.set_hold(true);
0422             transitions = std::vector<model::KeyframeTransition>(values.size(), def);
0423         }
0424 
0425         prop.keyframes.reserve(values.size());
0426 
0427         for ( uint i = 0; i < values.size(); i++ )
0428             prop.keyframes.push_back({times[i], values[i], transitions[i]});
0429     }
0430 
0431     void store_animate(const QString& target, const QDomElement& animate)
0432     {
0433         stored_animate[target].push_back(animate);
0434     }
0435 
0436     template<class FuncT>
0437     AnimatedProperties parse_animated_elements(const QDomElement& parent, const FuncT& func)
0438     {
0439         AnimatedProperties props;
0440         props.element = parent;
0441 
0442         for ( const auto& child: ElementRange(parent) )
0443             func(child, props);
0444 
0445         if ( parent.hasAttribute("id") )
0446         {
0447             auto it = stored_animate.find(parent.attribute("id"));
0448             if ( it != stored_animate.end() )
0449             {
0450                 for ( const auto& child: it->second )
0451                     func(child, props);
0452             }
0453         }
0454 
0455         return props;
0456     }
0457 
0458 
0459     AnimatedProperties parse_animated_properties(const QDomElement& parent)
0460     {
0461         return parse_animated_elements(parent, [this](const QDomElement& child, AnimatedProperties& props){
0462             if ( child.tagName() == "animate" && child.hasAttribute("attributeName") )
0463                 parse_animate(child, props.properties[child.attribute("attributeName")], false);
0464             else if ( child.tagName() == "animateMotion" )
0465                 parse_animate(child, props.properties["motion"], true);
0466         });
0467     }
0468 
0469     AnimatedProperties parse_animated_transform(const QDomElement& parent)
0470     {
0471         return parse_animated_elements(parent, [this](const QDomElement& child, AnimatedProperties& props){
0472             if ( child.tagName() == "animateTransform" && child.hasAttribute("type") && child.attribute("attributeName") == "transform" )
0473                 parse_animate(child, props.properties[child.attribute("type")], false);
0474             else if ( child.tagName() == "animateMotion" )
0475                 parse_animate(child, props.properties["motion"], true);
0476         });
0477     }
0478 
0479     void warning(const QString& msg)
0480     {
0481         if ( on_warning )
0482             on_warning(msg);
0483     }
0484 
0485     qreal fps = 60;
0486     qreal min_kf = 0;
0487     qreal max_kf = 0;
0488     bool kf_range_initialized = false;
0489     std::function<void(const QString&)> on_warning;
0490     std::unordered_map<QString, std::vector<QDomElement>> stored_animate;
0491     static const QRegularExpression separator;
0492     static const QRegularExpression clock_re;
0493     static const QRegularExpression frame_separator_re;
0494 };
0495 
0496 } // namespace glaxnimate::io::svg::detail