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