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