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

0001 /*
0002  * SPDX-FileCopyrightText: 2019-2023 Mattia Basaglia <dev@dragon.best>
0003  *
0004  * SPDX-License-Identifier: GPL-3.0-or-later
0005  */
0006 
0007 #include "avd_renderer.hpp"
0008 
0009 #include "io/svg/detail.hpp"
0010 #include "io/svg/svg_renderer.hpp"
0011 
0012 #include "model/document.hpp"
0013 #include "model/shapes/group.hpp"
0014 #include "model/shapes/trim.hpp"
0015 #include "model/shapes/fill.hpp"
0016 #include "model/shapes/stroke.hpp"
0017 #include "model/shapes/path.hpp"
0018 #include "model/animation/join_animatables.hpp"
0019 
0020 #include <vector>
0021 #include <variant>
0022 
0023 class glaxnimate::io::avd::AvdRenderer::Private
0024 {
0025 public:
0026     using PropRet = std::vector<std::pair<QString, QString>>;
0027 
0028     struct Keyframe
0029     {
0030         QString value;
0031         // TODO interpolators
0032     };
0033 
0034     class AnimationHelper
0035     {
0036     public:
0037         Private* parent = nullptr;
0038         QString name;
0039         std::map<QString, std::map<qreal, Keyframe>> keyframes = {};
0040 
0041         bool animated() const
0042         {
0043             return !keyframes.empty();
0044         }
0045 
0046         static QString attr(const QString& name)
0047         {
0048             return "android:" + name;
0049         }
0050 
0051         qreal frame_to_ms(model::FrameTime f) const
0052         {
0053             return f * 1000 / parent->fps;
0054         }
0055 
0056         template<class Callback>
0057         void render_properties(
0058             QDomElement& element,
0059             std::vector<const model::AnimatableBase*> properties,
0060             const Callback& callback
0061         )
0062         {
0063             model::JoinAnimatables j(std::move(properties), model::JoinAnimatables::Normal);
0064 
0065             auto xml_values = callback(j.current_value());
0066             for ( const auto& p : xml_values )
0067                 element.setAttribute(attr(p.first), p.second);
0068 
0069             if ( j.animated() )
0070             {
0071                 for ( const auto& kf : j )
0072                 {
0073                     xml_values = callback(kf.values);
0074                     for ( const auto& p : xml_values )
0075                     {
0076                         keyframes[p.first][frame_to_ms(kf.time)] = Keyframe{p.second};
0077                     }
0078                 }
0079             }
0080         }
0081 
0082         /// \todo Option for propertyValuesHolder+keyframe?
0083         QDomElement render_object_animators() const
0084         {
0085             QDomElement target = parent->dom.createElement("target");
0086             target.setAttribute("android:name", name);
0087             QDomElement attr = parent->dom.createElement("aapt:attr");
0088             target.appendChild(attr);
0089             attr.setAttribute("name", "android:animation");
0090             QDomElement set = parent->dom.createElement("set");
0091             attr.appendChild(set);
0092 
0093             for ( const auto& prop : keyframes )
0094             {
0095                 QString type;
0096                 if ( prop.first == "pathData" )
0097                     type = "pathType";
0098                 else if ( prop.first.contains("Color") )
0099                     type = "colorType";
0100                 else
0101                     type = "floatType";
0102 
0103                 auto iter = prop.second.begin();
0104 
0105                 while ( iter != prop.second.end() )
0106                 {
0107                     auto start = iter->first;
0108                     QDomElement anim = parent->dom.createElement("objectAnimator");
0109                     anim.setAttribute("android:propertyName", prop.first);
0110                     anim.setAttribute("android:valueType", type);
0111                     anim.setAttribute("android:startOffset", QString::number(start));
0112                     anim.setAttribute("android:valueFrom", iter->second.value);
0113 
0114                     ++iter;
0115                     if ( iter == prop.second.end() )
0116                         break;
0117 
0118                     anim.setAttribute("android:valueTo", iter->second.value);
0119                     anim.setAttribute("android:duration", QString::number(iter->first - start));
0120                     set.appendChild(anim);
0121                 }
0122             }
0123 
0124             return target;
0125         }
0126     };
0127 
0128     void render(model::Composition* comp)
0129     {
0130         fps = comp->fps.get();
0131         vector = dom.createElement("vector");
0132         vector.setAttribute("android:width", QString("%1dp").arg(comp->width.get()));
0133         vector.setAttribute("android:height", QString("%1dp").arg(comp->height.get()));
0134         vector.setAttribute("android:viewportWidth", QString::number(comp->width.get()));
0135         vector.setAttribute("android:viewportHeight", QString::number(comp->height.get()));
0136 
0137         render_comp(comp, vector);
0138     }
0139 
0140     void render_comp(model::Composition* comp, QDomElement& parent)
0141     {
0142         parent.setAttribute("android:name", unique_name(comp, false));
0143         for ( const auto& layer : comp->shapes )
0144             render_element(layer.get(), parent);
0145     }
0146 
0147     void render_element(model::ShapeElement* elm, QDomElement& parent)
0148     {
0149         if ( auto l = elm->cast<model::Layer>() )
0150         {
0151             render_layer(l, parent);
0152         }
0153         else if ( auto g = elm->cast<model::Group>() )
0154         {
0155             render_group(g, parent);
0156         }
0157         else if ( elm->is_instance<model::Shape>() )
0158         {
0159             warning(i18n("%s should be in a group", elm->object_name()));
0160         }
0161         else if ( !elm->is_instance<model::Styler>() && !elm->is_instance<model::Trim>() )
0162         {
0163             warning(i18n("%s is not supported", elm->type_name_human()));
0164         }
0165     }
0166 
0167     void warning(const QString& s)
0168     {
0169         if ( on_warning )
0170             on_warning(s);
0171     }
0172 
0173     QDomElement render_layer_parents(model::Layer* lay, QDomElement& parent)
0174     {
0175         if ( auto parlay = lay->parent.get() )
0176         {
0177             auto p = render_layer_parents(parlay, parent);
0178             QDomElement group = dom.createElement("group");
0179             p.appendChild(group);
0180             render_transform(parlay->transform.get(), group, unique_name(parlay, true) );
0181             return p;
0182         }
0183 
0184         return parent;
0185     }
0186 
0187     void render_layer(model::Layer* lay, QDomElement& parent)
0188     {
0189         auto parent_element = parent;
0190         QDomElement p = render_layer_parents(lay, parent);
0191         auto elm = render_group(lay, p);
0192         if ( lay->mask->mask.get() != model::MaskSettings::NoMask )
0193         {
0194             auto mask = render_clip_path(lay->shapes[0]);
0195             elm.insertBefore(mask, {});
0196         }
0197     }
0198 
0199     QString unique_name(model::DocumentNode* node, bool is_duplicate)
0200     {
0201         QString base = node->name.get();
0202         if ( base.isEmpty() )
0203             base = "item_" + node->uuid.get().toString(QUuid::Id128);
0204 
0205 
0206         QString name = base;
0207         if ( is_duplicate )
0208             name += "_" + QString::number(unique_id++);
0209 
0210         while ( names.count(name) )
0211         {
0212             name = base + "_" + QString::number(unique_id++);
0213         }
0214 
0215         names.insert(name);
0216         return name;
0217     }
0218 
0219     QDomElement render_group(model::Group* group, QDomElement& parent)
0220     {
0221         QDomElement elm = dom.createElement("group");
0222         parent.appendChild(elm);
0223         render_transform(group->transform.get(), elm, unique_name(group, false));
0224 
0225         model::Fill* fill = nullptr;
0226         model::Stroke* stroke = nullptr;
0227         model::Trim* trim = nullptr;
0228         std::vector<std::variant<model::Shape*, model::Group*>> children;
0229         std::vector<model::Shape*> shapes;
0230         std::vector<model::Group*> groups;
0231 
0232         for ( const auto& ch : group->shapes )
0233         {
0234             if ( auto f = ch->cast<model::Fill>() )
0235                 fill = f;
0236             else if ( auto s = ch->cast<model::Stroke>() )
0237                 stroke = s;
0238             else if ( auto t = ch->cast<model::Trim>() )
0239                 trim = t;
0240             else if ( auto g = ch->cast<model::Group>() )
0241             {
0242                 groups.push_back(g);
0243                 children.push_back(g);
0244             }
0245             else if ( auto s = ch->cast<model::Shape>() )
0246             {
0247                 shapes.push_back(s);
0248                 children.push_back(s);
0249             }
0250             else
0251             {
0252                 warning(i18n("%s are not supported", ch->type_name_human()));
0253             }
0254         }
0255 
0256         bool unify_shapes = !groups.empty();
0257         if ( !shapes.empty() )
0258             unify_shapes = true;
0259         else if ( trim )
0260             unify_shapes = trim->multiple.get() == model::Trim::Simultaneously;
0261 
0262 
0263         if ( unify_shapes )
0264         {
0265             for ( const auto& g : groups )
0266                 render_group(g, elm);
0267 
0268             if ( !shapes.empty() )
0269             {
0270                 QString name = shapes.size() == 1 ? unique_name(shapes[0], false) : unique_name(group, true);
0271                 render_shapes(shapes, name, elm, fill, stroke, trim);
0272             }
0273         }
0274         else
0275         {
0276             for ( const auto& ch : children )
0277             {
0278                 if ( ch.index() == 0 )
0279                     render_shapes({std::get<0>(ch)}, unique_name(std::get<0>(ch), false), elm, fill, stroke, trim);
0280                 else
0281                     render_group(std::get<1>(ch), elm);
0282             }
0283         }
0284 
0285         return elm;
0286     }
0287 
0288 
0289     void render_shapes(
0290         const std::vector<model::Shape*>& shapes,
0291         const QString& name,
0292         QDomElement& parent,
0293         model::Fill* fill,
0294         model::Stroke* stroke,
0295         model::Trim* trim
0296     )
0297     {
0298         if ( shapes.empty() )
0299             return;
0300 
0301         auto path = dom.createElement("path");
0302         parent.appendChild(path);
0303         path.setAttribute("android:name", name);
0304         render_shapes_to_path_data(shapes, name, path);
0305         render_fill(fill, name, path);
0306         render_stroke(stroke, name, path);
0307         render_trim(trim, name, path);
0308     }
0309 
0310     void render_shapes_to_path_data(const std::vector<model::Shape*>& shapes, const QString& name, QDomElement& elem)
0311     {
0312         std::vector<std::unique_ptr<model::ShapeElement>> saved;
0313         std::vector<const model::AnimatableBase*> paths;
0314         paths.reserve(shapes.size());
0315 
0316         for ( const auto& sh : shapes )
0317         {
0318             if ( auto p = sh->cast<model::Path>() )
0319             {
0320                 paths.push_back(&p->shape);
0321             }
0322             else
0323             {
0324                 auto conv = sh->to_path();
0325                 collect_paths(conv.get(), paths);
0326                 saved.push_back(std::move(conv));
0327             }
0328         }
0329 
0330         auto& anim = animator(name);
0331         anim.render_properties(elem, paths, [](const std::vector<QVariant>& v) -> PropRet {
0332             return {
0333                 {"pathData", paths_to_path_data(v)},
0334             };
0335         });
0336     }
0337 
0338     void collect_paths(model::ShapeElement* element, std::vector<const model::AnimatableBase*>& paths)
0339     {
0340         if ( auto p = element->cast<model::Path>() )
0341         {
0342             paths.push_back(&p->shape);
0343         }
0344         else if ( auto g = element->cast<model::Group>() )
0345         {
0346             for ( const auto& c : g->shapes )
0347                 collect_paths(c.get(), paths);
0348         }
0349     }
0350 
0351     static QString paths_to_path_data(const std::vector<QVariant>& paths)
0352     {
0353         math::bezier::MultiBezier bez;
0354         for ( const auto& path : paths )
0355             bez.beziers().push_back(path.value<math::bezier::Bezier>());
0356 
0357         return svg::path_data(bez).first;
0358     }
0359 
0360     void render_fill(model::Fill* fill, const QString& name, QDomElement& element)
0361     {
0362         if ( !fill )
0363             return;
0364 
0365         render_styler_color(fill, name, "fillColor", element);
0366 
0367         auto& anim = animator(name);
0368         anim.render_properties(element, {&fill->opacity}, [](const std::vector<QVariant>& v) -> PropRet {
0369             return {
0370                 {"fillAlpha", QString::number(v[0].toDouble())},
0371             };
0372         });
0373         element.setAttribute("android:fillType", fill->fill_rule.get() == model::Fill::EvenOdd ? "evenOdd" : "nonZero");
0374 
0375     }
0376 
0377     void render_stroke(model::Stroke* stroke, const QString& name, QDomElement& element)
0378     {
0379         if ( !stroke )
0380             return;
0381 
0382         render_styler_color(stroke, name, "strokeColor", element);
0383 
0384         auto& anim = animator(name);
0385         anim.render_properties(element, {&stroke->opacity}, [](const std::vector<QVariant>& v) -> PropRet {
0386             return {
0387                 {"strokeAlpha", QString::number(v[0].toDouble())},
0388             };
0389         });
0390         anim.render_properties(element, {&stroke->width}, [](const std::vector<QVariant>& v) -> PropRet {
0391             return {
0392                 {"strokeWidth", QString::number(v[0].toDouble())},
0393             };
0394         });
0395 
0396         element.setAttribute("android:strokeWidth", QString::number(stroke->width.get()));
0397         element.setAttribute("android:strokeMiterLimit", QString::number(stroke->miter_limit.get()));
0398         switch ( stroke->cap.get() )
0399         {
0400             case model::Stroke::RoundCap:
0401                 element.setAttribute("android:strokeLineCap", "round");
0402                 break;
0403             case model::Stroke::ButtCap:
0404                 element.setAttribute("android:strokeLineCap", "butt");
0405                 break;
0406             case model::Stroke::SquareCap:
0407                 element.setAttribute("android:strokeLineCap", "square");
0408                 break;
0409         }
0410         switch ( stroke->join.get() )
0411         {
0412             case model::Stroke::RoundJoin:
0413                 element.setAttribute("android:strokeLineJoin", "round");
0414                 break;
0415             case model::Stroke::MiterJoin:
0416                 element.setAttribute("android:strokeLineJoin", "miter");
0417                 break;
0418             case model::Stroke::BevelJoin:
0419                 element.setAttribute("android:strokeLineJoin", "bevel");
0420                 break;
0421         }
0422     }
0423 
0424     void render_styler_color(model::Styler* styler, const QString& name, const QString& attr, QDomElement& element)
0425     {
0426         auto use = styler->use.get();
0427 
0428         if ( auto color = use->cast<model::NamedColor>() )
0429         {
0430             auto& anim = animator(name);
0431             anim.render_properties(element, {&color->color}, [&attr](const std::vector<QVariant>& v) -> PropRet { return {
0432                 {attr, render_color(v[0].value<QColor>())},
0433             };});
0434         }
0435         else if ( auto gradient = use->cast<model::Gradient>() )
0436         {
0437             render_gradient(attr, gradient, element);
0438         }
0439         else
0440         {
0441             auto& anim = animator(name);
0442             anim.render_properties(element, {&styler->color}, [&attr](const std::vector<QVariant>& v) -> PropRet { return {
0443                 {attr, render_color(v[0].value<QColor>())},
0444             };});
0445         }
0446     }
0447 
0448     void render_gradient(const QString& attr_name, model::Gradient* gradient, QDomElement& element)
0449     {
0450         auto attr = dom.createElement("aapt:attr");
0451         attr.setAttribute("name", "android:" + attr_name);
0452         element.appendChild(attr);
0453         auto gradel = dom.createElement("gradient");
0454         attr.appendChild(gradel);
0455         switch ( gradient->type.get() )
0456         {
0457             case model::Gradient::Linear:
0458                 gradel.setAttribute("android:type", "linear");
0459                 break;
0460             case model::Gradient::Radial:
0461                 gradel.setAttribute("android:type", "radial");
0462                 break;
0463             case model::Gradient::Conical:
0464                 gradel.setAttribute("android:type", "sweep");
0465                 break;
0466         }
0467 
0468         gradel.setAttribute("startX", gradient->start_point.get().x());
0469         gradel.setAttribute("startY", gradient->start_point.get().y());
0470         gradel.setAttribute("endX", gradient->end_point.get().x());
0471         gradel.setAttribute("endY", gradient->end_point.get().y());
0472 
0473         if ( auto cols = gradient->colors.get() )
0474         {
0475             for ( const auto& stop : cols->colors.get() )
0476             {
0477                 auto item = dom.createElement("item");
0478                 item.setAttribute("android:color", render_color(stop.second));
0479                 item.setAttribute("android:offset", QString::number(stop.first));
0480             }
0481         }
0482     }
0483 
0484     void render_trim(model::Trim* trim, const QString& name, QDomElement& element)
0485     {
0486         if ( !trim )
0487             return;
0488 
0489         auto& anim = animator(name);
0490         anim.render_properties(element, {&trim->start}, [](const std::vector<QVariant>& v) -> PropRet { return {
0491             {"trimPathStart", QString::number(v[0].toDouble())},
0492         };});
0493         anim.render_properties(element, {&trim->end}, [](const std::vector<QVariant>& v) -> PropRet { return {
0494             {"trimPathEnd", QString::number(v[0].toDouble())},
0495         };});
0496         anim.render_properties(element, {&trim->offset}, [](const std::vector<QVariant>& v) -> PropRet { return {
0497             {"trimPathOffset", QString::number(v[0].toDouble())},
0498         };});
0499     }
0500 
0501     QDomElement render_clip_path(model::ShapeElement* element)
0502     {
0503         QDomElement clip = dom.createElement("clip-path");
0504         QString name = unique_name(element, false);
0505         clip.setAttribute("android:name", name);
0506 
0507         if ( auto group = element->cast<model::Group>() )
0508         {
0509             std::vector<model::Shape*> shapes = group->docnode_find_by_type<model::Shape>();
0510             render_shapes_to_path_data(shapes, name, clip);
0511         }
0512         else if ( auto shape = element->cast<model::Shape>() )
0513         {
0514             render_shapes_to_path_data({shape}, name, clip);
0515         }
0516         else
0517         {
0518             warning(i18n("%s cannot be a clip path", element->object_name()));
0519             return {};
0520         }
0521 
0522         return clip;
0523     }
0524 
0525     static QString color_comp(int comp)
0526     {
0527         return QString::number(comp, 16).rightJustified(2, '0');
0528     }
0529 
0530     static QString render_color(const QColor& color)
0531     {
0532         return "#" + color_comp(color.alpha()) + color_comp(color.red()) + color_comp(color.green()) + color_comp(color.blue());
0533     }
0534 
0535     void render_transform(model::Transform* trans, QDomElement& elm, const QString& name)
0536     {
0537         auto& anim = animator(name);
0538         anim.render_properties(elm, {&trans->anchor_point, &trans->position}, [](const std::vector<QVariant>& v) -> PropRet {
0539             auto ap = v[0].toPointF();
0540             auto pos = v[1].toPointF() - ap;
0541             return {
0542                 {"pivotX", QString::number(ap.x())},
0543                 {"pivotY", QString::number(ap.y())},
0544                 {"translateX", QString::number(pos.x())},
0545                 {"translateY", QString::number(pos.y())},
0546             };
0547         });
0548         anim.render_properties(elm, {&trans->scale}, [](const std::vector<QVariant>& v) -> PropRet {
0549             auto scale = v[0].value<QVector2D>();
0550             return {
0551                 {"scaleX", QString::number(scale.x())},
0552                 {"scaleY", QString::number(scale.y())},
0553             };
0554         });
0555         anim.render_properties(elm, {&trans->rotation}, [](const std::vector<QVariant>& v) -> PropRet {
0556             return {
0557                 {"rotation", QString::number(v[0].toDouble())},
0558             };
0559         });
0560     }
0561 
0562     void render_anim(QDomElement& container)
0563     {
0564         for ( const auto& p : animations )
0565         {
0566             if ( p.second.animated() )
0567                 container.appendChild(p.second.render_object_animators());
0568         }
0569     }
0570 
0571     AnimationHelper& animator(const QString& name)
0572     {
0573         auto iter = animations.find(name);
0574         if ( iter == animations.end() )
0575             iter = animations.insert({name, {this, name}}).first;
0576         return iter->second;
0577     }
0578 
0579     int fps = 60;
0580     int unique_id = 0;
0581     QDomDocument dom;
0582     QDomElement vector;
0583     std::map<QString, AnimationHelper> animations;
0584     std::function<void (const QString &)> on_warning;
0585     std::unordered_set<QString> names;
0586 };
0587 
0588 glaxnimate::io::avd::AvdRenderer::AvdRenderer(const std::function<void (const QString &)>& on_warning)
0589     : d(std::make_unique<Private>())
0590 {
0591     d->on_warning = on_warning;
0592 }
0593 
0594 glaxnimate::io::avd::AvdRenderer::~AvdRenderer()
0595 {
0596 }
0597 
0598 void glaxnimate::io::avd::AvdRenderer::render(model::Composition* comp)
0599 {
0600     d->render( comp);
0601 }
0602 
0603 QDomElement glaxnimate::io::avd::AvdRenderer::graphics()
0604 {
0605     return d->vector;
0606 }
0607 
0608 
0609 QDomDocument glaxnimate::io::avd::AvdRenderer::single_file()
0610 {
0611     QDomDocument dom;
0612     auto av = dom.createElement("animated-vector");
0613     dom.appendChild(av);
0614     av.setAttribute("xmlns", svg::detail::xmlns.at("android"));
0615     for ( const auto& p : svg::detail::xmlns )
0616     {
0617         if ( p.second.contains("android") )
0618             av.setAttribute("xmlns:" + p.first, p.second);
0619     }
0620 
0621     auto attr = dom.createElement("aapt:attr");
0622     av.appendChild(attr);
0623     attr.setAttribute("name", "android:drawable");
0624     attr.appendChild(graphics());
0625 
0626     d->render_anim(av);
0627 
0628     return dom;
0629 }
0630