File indexing completed on 2025-01-05 04:01:21
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 "svg_renderer.hpp" 0008 0009 #include "model/document.hpp" 0010 #include "model/shapes/group.hpp" 0011 #include "model/shapes/layer.hpp" 0012 #include "model/shapes/precomp_layer.hpp" 0013 #include "model/shapes/rect.hpp" 0014 #include "model/shapes/ellipse.hpp" 0015 #include "model/shapes/path.hpp" 0016 #include "model/shapes/polystar.hpp" 0017 #include "model/shapes/fill.hpp" 0018 #include "model/shapes/stroke.hpp" 0019 #include "model/shapes/image.hpp" 0020 #include "model/shapes/text.hpp" 0021 #include "model/shapes/repeater.hpp" 0022 #include "model/animation/join_animatables.hpp" 0023 #include "model/custom_font.hpp" 0024 #include "math/math.hpp" 0025 0026 #include "detail.hpp" 0027 #include "font_weight.hpp" 0028 #include "io/utils.hpp" 0029 0030 using namespace glaxnimate::io::svg::detail; 0031 using namespace glaxnimate; 0032 0033 class io::svg::SvgRenderer::Private 0034 { 0035 public: 0036 void collect_defs(model::Composition* comp) 0037 { 0038 if ( !at_start ) 0039 return; 0040 0041 fps = comp->fps.get(); 0042 ip = comp->animation->first_frame.get(); 0043 op = comp->animation->last_frame.get(); 0044 if ( ip >= op ) 0045 animated = NotAnimated; 0046 0047 at_start = false; 0048 defs = element(svg, "defs"); 0049 for ( const auto& color : comp->document()->assets()->colors->values ) 0050 write_named_color(defs, color.get()); 0051 for ( const auto& color : comp->document()->assets()->gradient_colors->values ) 0052 write_gradient_colors(defs, color.get()); 0053 for ( const auto& gradient : comp->document()->assets()->gradients->values ) 0054 write_gradient(defs, gradient.get()); 0055 0056 auto view = element(svg, "sodipodi:namedview"); 0057 view.setAttribute("inkscape:pagecheckerboard", "true"); 0058 view.setAttribute("borderlayer", "true"); 0059 view.setAttribute("bordercolor", "#666666"); 0060 view.setAttribute("pagecolor", "#ffffff"); 0061 view.setAttribute("inkscape:document-units", "px"); 0062 0063 add_fonts(comp->document()); 0064 0065 write_meta(comp); 0066 } 0067 0068 void write_meta(model::Composition* comp) 0069 { 0070 auto rdf = element(element(svg, "metadata"), "rdf:RDF"); 0071 auto work = element(rdf, "cc:Work"); 0072 element(work, "dc:format").appendChild(dom.createTextNode("image/svg+xml")); 0073 QString dc_type = animated ? "MovingImage" : "StillImage"; 0074 element(work, "dc:type").setAttribute("rdf:resource", "http://purl.org/dc/dcmitype/" + dc_type); 0075 element(work, "dc:title").appendChild(dom.createTextNode(comp->name.get())); 0076 auto document = comp->document(); 0077 0078 if ( document->info().empty() ) 0079 return; 0080 0081 if ( !document->info().author.isEmpty() ) 0082 element(element(element(work, "dc:creator"), "cc:Agent"), "dc:title").appendChild(dom.createTextNode(document->info().author)); 0083 0084 if ( !document->info().description.isEmpty() ) 0085 element(work, "dc:description").appendChild(dom.createTextNode(document->info().description)); 0086 0087 if ( !document->info().keywords.empty() ) 0088 { 0089 auto bag = element(element(work, "dc:subject"), "rdf:Bag"); 0090 for ( const auto& kw: document->info().keywords ) 0091 element(bag, "rdf:li").appendChild(dom.createTextNode(kw)); 0092 } 0093 } 0094 0095 void add_fonts(model::Document* document) 0096 { 0097 if ( font_type == CssFontType::None ) 0098 return; 0099 0100 QString css; 0101 static QString font_face = R"( 0102 @font-face { 0103 font-family: '%1'; 0104 font-style: %2; 0105 font-weight: %3; 0106 src: url(%4); 0107 } 0108 )"; 0109 0110 for ( const auto & font : document->assets()->fonts->values ) 0111 { 0112 auto custom = font->custom_font(); 0113 if ( !custom.is_valid() ) 0114 continue; 0115 0116 QRawFont raw = custom.raw_font(); 0117 auto type = qMin(suggested_type(font.get()), font_type); 0118 0119 if ( type == CssFontType::Link ) 0120 { 0121 auto link = element(svg, "link"); 0122 link.setAttribute("xmlns", "http://www.w3.org/1999/xhtml"); 0123 link.setAttribute("rel", "stylesheet"); 0124 link.setAttribute("href", font->css_url.get()); 0125 link.setAttribute("type", "text/css"); 0126 } 0127 else if ( type == CssFontType::FontFace ) 0128 { 0129 css += font_face 0130 .arg(custom.family()) 0131 .arg(WeightConverter::convert(raw.weight(), WeightConverter::qt, WeightConverter::css)) 0132 .arg(raw.style() == QFont::StyleNormal ? 0 : 1) 0133 .arg(font->source_url.get()) 0134 ; 0135 } 0136 else if ( type == CssFontType::Embedded ) 0137 { 0138 QString base64_encoded = font->data.get().toBase64(QByteArray::Base64UrlEncoding); 0139 QString format = model::CustomFontDatabase::font_data_format(font->data.get()) == model::FontFileFormat::OpenType ? "opentype" : "ttf"; 0140 0141 css += font_face 0142 .arg(custom.family()) 0143 .arg(WeightConverter::convert(raw.weight(), WeightConverter::qt, WeightConverter::css)) 0144 .arg(raw.style() == QFont::StyleNormal ? 0 : 1) 0145 .arg("data:application/x-font-" + format + ";charset=utf-8;base64," + base64_encoded) 0146 ; 0147 } 0148 } 0149 0150 if ( !css.isEmpty() ) 0151 element(svg, "style").appendChild(dom.createTextNode(css)); 0152 } 0153 0154 QDomElement element(QDomNode parent, const char* tag) 0155 { 0156 QDomElement e = dom.createElement(tag); 0157 parent.appendChild(e); 0158 return e; 0159 } 0160 0161 void write_composition(QDomElement& parent, model::Composition* comp) 0162 { 0163 for ( const auto& lay : comp->shapes ) 0164 write_shape(parent, lay.get(), false); 0165 } 0166 0167 void write_visibility_attributes(QDomElement& parent, model::VisualNode* node) 0168 { 0169 if ( !node->visible.get() ) 0170 parent.setAttribute("display", "none"); 0171 if ( node->locked.get() ) 0172 parent.setAttribute("sodipodi:insensitive", "true"); 0173 } 0174 0175 void write_shapes(QDomElement& parent, const model::ShapeListProperty& shapes, bool has_mask = false) 0176 { 0177 if ( shapes.empty() ) 0178 return; 0179 0180 auto it = shapes.begin(); 0181 if ( has_mask ) 0182 ++it; 0183 0184 for ( ; it != shapes.end(); ++it ) 0185 write_shape(parent, it->get(), false); 0186 } 0187 0188 0189 QString styler_to_css(model::Styler* styler) 0190 { 0191 if ( styler->use.get() ) 0192 return "url(#" + non_uuid_ids_map[styler->use.get()] + ")"; 0193 if ( styler->color.get().alpha() == 0 ) 0194 return "transparent"; 0195 return styler->color.get().name(); 0196 } 0197 0198 QDomElement write_styler_shapes(QDomElement& parent, model::Styler* styler, const Style::Map& style) 0199 { 0200 if ( styler->affected().size() == 1 ) 0201 { 0202 write_shape_shape(parent, styler->affected()[0], style); 0203 write_visibility_attributes(parent, styler); 0204 parent.setAttribute("id", id(styler)); 0205 return parent; 0206 } 0207 0208 auto g = start_group(parent, styler); 0209 write_style(g, style); 0210 write_visibility_attributes(g, styler); 0211 g.setAttribute("id", id(styler)); 0212 0213 for ( model::ShapeElement* subshape : styler->affected() ) 0214 { 0215 write_shape_shape(g, subshape, style); 0216 } 0217 0218 return g; 0219 } 0220 0221 QString unlerp_time(model::FrameTime time) const 0222 { 0223 return QString::number(math::unlerp(ip, op, time), 'f'); 0224 } 0225 0226 struct AnimationData 0227 { 0228 struct Attribute 0229 { 0230 QString attribute; 0231 QStringList values = {}; 0232 }; 0233 0234 AnimationData(SvgRenderer::Private* parent, const std::vector<QString>& attrs, int n_keyframes, 0235 qreal time_stretch, model::FrameTime time_start) 0236 : parent(parent), time_stretch(time_stretch), time_start(time_start) 0237 { 0238 attributes.reserve(attrs.size()); 0239 for ( const auto& attr : attrs ) 0240 { 0241 attributes.push_back({attr}); 0242 attributes.back().values.reserve(n_keyframes); 0243 } 0244 } 0245 0246 QString key_spline(const model::KeyframeTransition& trans) 0247 { 0248 return QString("%1 %2 %3 %4") 0249 .arg(trans.before().x(), 0, 'f') 0250 .arg(trans.before().y(), 0, 'f') 0251 .arg(trans.after().x(), 0, 'f') 0252 .arg(trans.after().y(), 0, 'f') 0253 ; 0254 } 0255 0256 void add_values(const std::vector<QString>& vals) 0257 { 0258 for ( std::size_t i = 0; i != attributes.size(); i++ ) 0259 attributes[i].values.push_back(vals[i]); 0260 } 0261 0262 void add_keyframe(model::FrameTime time, const std::vector<QString>& vals, 0263 const model::KeyframeTransition& trans) 0264 { 0265 if ( time < parent->ip || time > parent->op ) 0266 return; 0267 0268 if ( key_times.empty() && time > parent->ip ) 0269 { 0270 key_times.push_back("0"); 0271 key_splines.push_back("0 0 1 1"); 0272 add_values(vals); 0273 } 0274 else if ( hold && last + 1 < time ) 0275 { 0276 0277 key_times.push_back(parent->unlerp_time(time - 1)); 0278 key_splines.push_back("0 0 1 1"); 0279 for ( std::size_t i = 0; i != attributes.size(); i++ ) 0280 attributes[i].values.push_back(attributes[i].values.back()); 0281 0282 } 0283 0284 key_times.push_back(parent->unlerp_time(time)); 0285 key_splines.push_back(key_spline(trans)); 0286 0287 for ( std::size_t i = 0; i != attributes.size(); i++ ) 0288 attributes[i].values.push_back(vals[i]); 0289 0290 last = time; 0291 hold = trans.hold(); 0292 } 0293 0294 void add_dom( 0295 QDomElement& element, const char* tag = "animate", const QString& type = {}, 0296 const QString& path = {}, bool auto_orient = false 0297 ) 0298 { 0299 if ( last < parent->op && path.isEmpty() ) 0300 { 0301 key_times.push_back("1"); 0302 for ( auto& attr : attributes ) 0303 { 0304 if ( !attr.values.empty() ) 0305 attr.values.push_back(attr.values.back()); 0306 } 0307 } 0308 else 0309 { 0310 key_splines.pop_back(); 0311 } 0312 0313 QString key_times_str = key_times.join("; "); 0314 QString key_splines_str = key_splines.join("; "); 0315 for ( const auto& data : attributes ) 0316 { 0317 QDomElement animation = parent->element(element, tag); 0318 animation.setAttribute("begin", parent->clock(time_start + time_stretch * parent->ip)); 0319 animation.setAttribute("dur", parent->clock(time_start + time_stretch * parent->op-parent->ip)); 0320 animation.setAttribute("attributeName", data.attribute); 0321 animation.setAttribute("calcMode", "spline"); 0322 if ( !path.isEmpty() ) 0323 { 0324 animation.setAttribute("path", path); 0325 if ( auto_orient ) 0326 animation.setAttribute("rotate", "auto"); 0327 } 0328 animation.setAttribute("keyTimes", key_times_str); 0329 animation.setAttribute("keySplines", key_splines_str); 0330 animation.setAttribute("repeatCount", "indefinite"); 0331 if ( !type.isEmpty() ) 0332 animation.setAttribute("type", type); 0333 } 0334 } 0335 0336 SvgRenderer::Private* parent; 0337 std::vector<Attribute> attributes; 0338 QStringList key_times = {}; 0339 QStringList key_splines = {}; 0340 model::FrameTime last = 0; 0341 bool hold = false; 0342 qreal time_stretch = 1; 0343 model::FrameTime time_start = 0; 0344 }; 0345 0346 void write_property( 0347 QDomElement& element, 0348 model::AnimatableBase* property, 0349 const QString& attr 0350 ) 0351 { 0352 element.setAttribute(attr, property->value().toString()); 0353 0354 if ( animated ) 0355 { 0356 if ( property->keyframe_count() < 2 ) 0357 return; 0358 0359 auto keyframes = split_keyframes(property); 0360 0361 AnimationData data(this, {attr}, keyframes.size(), time_stretch, time_start); 0362 0363 for ( int i = 0; i < int(keyframes.size()); i++ ) 0364 { 0365 auto kf = keyframes[i].get(); 0366 data.add_keyframe(time_to_global(kf->time()), {kf->value().toString()}, kf->transition()); 0367 } 0368 0369 data.add_dom(element); 0370 } 0371 } 0372 0373 qreal time_to_global(qreal time) 0374 { 0375 for ( auto it = timing.rbegin(), end = timing.rend(); it != end; ++it ) 0376 time = (*it)->time_from_local(time); 0377 return time; 0378 } 0379 0380 template<class Callback> 0381 void write_properties( 0382 QDomElement& element, 0383 std::vector<const model::AnimatableBase*> properties, 0384 const std::vector<QString>& attrs, 0385 const Callback& callback 0386 ) 0387 { 0388 auto jflags = animated == NotAnimated ? model::JoinAnimatables::NoKeyframes : model::JoinAnimatables::Normal; 0389 model::JoinedAnimatable j(std::move(properties), {}, jflags); 0390 0391 { 0392 auto vals = callback(j.current_value()); 0393 for ( std::size_t i = 0; i != attrs.size(); i++ ) 0394 element.setAttribute(attrs[i], vals[i]); 0395 } 0396 0397 if ( j.animated() && animated ) 0398 { 0399 auto keys = split_keyframes(&j); 0400 AnimationData data(this, attrs, keys.size(), time_stretch, time_start); 0401 0402 for ( const auto& kf : keys ) 0403 data.add_keyframe(time_to_global(kf->time()), callback(j.value_at(kf->time())), kf->transition()); 0404 0405 data.add_dom(element); 0406 } 0407 } 0408 0409 static std::vector<QString> callback_point(const std::vector<QVariant>& values) 0410 { 0411 return callback_point_result(values[0].toPointF()); 0412 } 0413 0414 static std::vector<QString> callback_point_result(const QPointF& c) 0415 { 0416 return std::vector<QString>{ 0417 QString::number(c.x()), 0418 QString::number(c.y()) 0419 }; 0420 } 0421 0422 void write_shape_rect(QDomElement& parent, model::Rect* rect, const Style::Map& style) 0423 { 0424 auto e = element(parent, "rect"); 0425 write_style(e, style); 0426 write_properties(e, {&rect->position, &rect->size}, {"x", "y"}, 0427 [](const std::vector<QVariant>& values){ 0428 QPointF c = values[0].toPointF(); 0429 QSizeF s = values[1].toSizeF(); 0430 return std::vector<QString>{ 0431 QString::number(c.x() - s.width()/2), 0432 QString::number(c.y() - s.height()/2) 0433 }; 0434 } 0435 ); 0436 write_properties(e, {&rect->size}, {"width", "height"}, 0437 [](const std::vector<QVariant>& values){ 0438 QSizeF s = values[0].toSizeF(); 0439 return std::vector<QString>{ 0440 QString::number(s.width()), 0441 QString::number(s.height()) 0442 }; 0443 } 0444 ); 0445 write_property(e, &rect->rounded, "ry"); 0446 } 0447 0448 void write_shape_ellipse(QDomElement& parent, model::Ellipse* ellipse, const Style::Map& style) 0449 { 0450 auto e = element(parent, "ellipse"); 0451 write_style(e, style); 0452 write_properties(e, {&ellipse->position}, {"cx", "cy"}, &Private::callback_point); 0453 write_properties(e, {&ellipse->size}, {"rx", "ry"}, 0454 [](const std::vector<QVariant>& values){ 0455 QSizeF s = values[0].toSizeF(); 0456 return std::vector<QString>{ 0457 QString::number(s.width() / 2), 0458 QString::number(s.height() / 2) 0459 }; 0460 } 0461 ); 0462 } 0463 0464 void write_shape_star(QDomElement& parent, model::PolyStar* star, const Style::Map& style) 0465 { 0466 model::FrameTime time = star->time(); 0467 0468 auto e = write_bezier(parent, star, style); 0469 0470 if ( star->outer_roundness.animated() || !qFuzzyIsNull(star->outer_roundness.get()) || 0471 star->inner_roundness.animated() || !qFuzzyIsNull(star->inner_roundness.get()) ) 0472 return; 0473 0474 set_attribute(e, "sodipodi:type", "star"); 0475 set_attribute(e, "inkscape:randomized", "0"); 0476 // inkscape:rounded Works differently than lottie so we leave it as 0 0477 set_attribute(e, "inkscape:rounded", "0"); 0478 int sides = star->points.get_at(time); 0479 set_attribute(e, "sodipodi:sides", sides); 0480 set_attribute(e, "inkscape:flatsided", star->type.get() == model::PolyStar::Polygon); 0481 QPointF c = star->position.get_at(time); 0482 set_attribute(e, "sodipodi:cx", c.x()); 0483 set_attribute(e, "sodipodi:cy", c.y()); 0484 set_attribute(e, "sodipodi:r1", star->outer_radius.get_at(time)); 0485 set_attribute(e, "sodipodi:r2", star->inner_radius.get_at(time)); 0486 qreal angle = math::deg2rad(star->angle.get_at(time) - 90); 0487 set_attribute(e, "sodipodi:arg1", angle); 0488 set_attribute(e, "sodipodi:arg2", angle + math::pi / sides); 0489 } 0490 0491 void write_shape_text(QDomElement& parent, model::TextShape* text, Style::Map style) 0492 { 0493 QFontInfo font_info(text->font->query()); 0494 0495 // QFontInfo is broken, so we do something else 0496 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 0497 int weight = QFontDatabase::weight(font_info.family(), font_info.styleName()); 0498 QFont::Style font_style = QFontDatabase::italic(font_info.family(), font_info.styleName()) ? QFont::StyleItalic : QFont::StyleNormal; 0499 #else 0500 QFontDatabase db; 0501 int weight = db.weight(font_info.family(), font_info.styleName()); 0502 QFont::Style font_style = db.italic(font_info.family(), font_info.styleName()) ? QFont::StyleItalic : QFont::StyleNormal; 0503 #endif 0504 0505 // Convert weight 0506 weight = WeightConverter::convert(weight, WeightConverter::qt, WeightConverter::css); 0507 0508 style["font-family"] = font_info.family(); 0509 style["font-size"] = QString("%1pt").arg(font_info.pointSizeF()); 0510 style["line-height"] = QString("%1px").arg(text->font->line_spacing()); 0511 style["font-weight"] = QString::number(weight); 0512 switch ( font_style ) 0513 { 0514 case QFont::StyleNormal: style["font-style"] = "normal"; break; 0515 case QFont::StyleItalic: style["font-style"] = "italic"; break; 0516 case QFont::StyleOblique: style["font-style"] = "oblique"; break; 0517 } 0518 0519 auto e = element(parent, "text"); 0520 write_style(e, style); 0521 write_properties(e, {&text->position}, {"x", "y"}, &Private::callback_point); 0522 0523 model::Font::CharDataCache cache; 0524 for ( const auto& line : text->font->layout(text->text.get()) ) 0525 { 0526 auto tspan = element(e, "tspan"); 0527 tspan.appendChild(dom.createTextNode(line.text)); 0528 set_attribute(tspan, "sodipodi:role", "line"); 0529 0530 write_properties(tspan, {&text->position}, {"x", "y"}, [base=line.baseline](const std::vector<QVariant>& values){ 0531 return callback_point_result(values[0].toPointF() + base); 0532 }); 0533 tspan.setAttribute("xml:space", "preserve"); 0534 } 0535 } 0536 0537 void write_shape_shape(QDomElement& parent, model::ShapeElement* shape, const Style::Map& style) 0538 { 0539 if ( auto rect = qobject_cast<model::Rect*>(shape) ) 0540 { 0541 write_shape_rect(parent, rect, style); 0542 } 0543 else if ( auto ellipse = qobject_cast<model::Ellipse*>(shape) ) 0544 { 0545 write_shape_ellipse(parent, ellipse, style); 0546 } 0547 else if ( auto star = qobject_cast<model::PolyStar*>(shape) ) 0548 { 0549 write_shape_star(parent, star, style); 0550 } 0551 else if ( auto text = shape->cast<model::TextShape>() ) 0552 { 0553 write_shape_text(parent, text, style); 0554 } 0555 else if ( !qobject_cast<model::Styler*>(shape) ) 0556 { 0557 write_bezier(parent, shape, style); 0558 } 0559 } 0560 0561 void write_styler_attrs(QDomElement& element, model::Styler* styler, const QString& attr) 0562 { 0563 if ( styler->use.get() ) 0564 { 0565 element.setAttribute(attr, "url(#" + non_uuid_ids_map[styler->use.get()] + ")"); 0566 return; 0567 } 0568 0569 write_property(element, &styler->color, attr); 0570 write_property(element, &styler->opacity, attr+"-opacity"); 0571 } 0572 void write_image(model::Image* img, QDomElement& parent) 0573 { 0574 if ( img->image.get() ) 0575 { 0576 auto e = element(parent, "image"); 0577 set_attribute(e, "x", 0); 0578 set_attribute(e, "y", 0); 0579 set_attribute(e, "width", img->image->width.get()); 0580 set_attribute(e, "height", img->image->height.get()); 0581 transform_to_attr(e, img->transform.get()); 0582 set_attribute(e, "xlink:href", img->image->to_url().toString()); 0583 } 0584 } 0585 0586 void write_stroke(model::Stroke* stroke, QDomElement& parent) 0587 { 0588 Style::Map style; 0589 style["fill"] = "none"; 0590 if ( !animated ) 0591 { 0592 style["stroke"] = styler_to_css(stroke); 0593 style["stroke-opacity"] = QString::number(stroke->opacity.get()); 0594 style["stroke-width"] = QString::number(stroke->width.get()); 0595 } 0596 switch ( stroke->cap.get() ) 0597 { 0598 case model::Stroke::Cap::ButtCap: 0599 style["stroke-linecap"] = "butt"; 0600 break; 0601 case model::Stroke::Cap::RoundCap: 0602 style["stroke-linecap"] = "round"; 0603 break; 0604 case model::Stroke::Cap::SquareCap: 0605 style["stroke-linecap"] = "square"; 0606 break; 0607 0608 } 0609 switch ( stroke->join.get() ) 0610 { 0611 case model::Stroke::Join::BevelJoin: 0612 style["stroke-linejoin"] = "bevel"; 0613 break; 0614 case model::Stroke::Join::RoundJoin: 0615 style["stroke-linejoin"] = "round"; 0616 break; 0617 case model::Stroke::Join::MiterJoin: 0618 style["stroke-linejoin"] = "miter"; 0619 style["stroke-miterlimit"] = QString::number(stroke->miter_limit.get()); 0620 break; 0621 } 0622 style["stroke-dasharray"] = "none"; 0623 QDomElement g = write_styler_shapes(parent, stroke, style); 0624 if ( animated ) 0625 { 0626 write_styler_attrs(g, stroke, "stroke"); 0627 write_property(g, &stroke->width, "stroke-width"); 0628 } 0629 } 0630 0631 void write_fill(model::Fill* fill, QDomElement& parent) 0632 { 0633 Style::Map style; 0634 if ( !animated ) 0635 { 0636 style["fill"] = styler_to_css(fill); 0637 style["fill-opacity"] = QString::number(fill->opacity.get()); 0638 } 0639 style["stroke"] = "none"; 0640 QDomElement g = write_styler_shapes(parent, fill, style); 0641 if ( animated ) 0642 write_styler_attrs(g, fill, "fill"); 0643 } 0644 0645 void write_precomp_layer(model::PreCompLayer* layer, QDomElement& parent) 0646 { 0647 if ( layer->composition.get() ) 0648 { 0649 timing.push_back(layer->timing.get()); 0650 auto clip = element(defs, "clipPath"); 0651 set_attribute(clip, "id", "clip_" + id(layer)); 0652 set_attribute(clip, "clipPathUnits", "userSpaceOnUse"); 0653 auto clip_rect = element(clip, "rect"); 0654 set_attribute(clip_rect, "x", "0"); 0655 set_attribute(clip_rect, "y", "0"); 0656 set_attribute(clip_rect, "width", layer->size.get().width()); 0657 set_attribute(clip_rect, "height", layer->size.get().height()); 0658 0659 auto e = start_layer(parent, layer); 0660 transform_to_attr(e, layer->transform.get()); 0661 write_property(e, &layer->opacity, "opacity"); 0662 write_visibility_attributes(parent, layer); 0663 time_stretch = layer->timing->stretch.get(); 0664 time_start = layer->timing->start_time.get(); 0665 write_composition(e, layer->composition.get()); 0666 time_stretch = 1; 0667 time_start = 0; 0668 timing.pop_back(); 0669 } 0670 } 0671 0672 void write_repeater_vis(QDomElement& element, model::Repeater* repeater, int index, int n_copies) 0673 { 0674 element.setAttribute("display", index < repeater->copies.get() ? "block" : "none"); 0675 0676 float alpha_lerp = float(index) / (n_copies == 1 ? 1 : n_copies - 1); 0677 model::JoinAnimatables opacity({&repeater->start_opacity, &repeater->end_opacity}, model::JoinAnimatables::NoValues); 0678 auto opacity_func = [&alpha_lerp](float a, float b){ 0679 return math::lerp(a, b, alpha_lerp); 0680 }; 0681 set_attribute(element, "opacity", opacity.combine_current_value<float, float>(opacity_func)); 0682 0683 if ( animated ) 0684 { 0685 int kf_count = repeater->copies.keyframe_count(); 0686 if ( kf_count >= 2 ) 0687 { 0688 AnimationData anim_display(this, {"display"}, kf_count, time_stretch, time_start); 0689 0690 for ( int i = 0; i < kf_count; i++ ) 0691 { 0692 auto kf = repeater->copies.keyframe(i); 0693 anim_display.add_keyframe(time_to_global(kf->time()), {index < kf->get() ? "block" : "none"}, kf->transition()); 0694 } 0695 0696 anim_display.add_dom(element); 0697 } 0698 0699 if ( opacity.animated() ) 0700 { 0701 AnimationData anim_opacity(this, {"opacity"}, opacity.keyframes().size(), time_stretch, time_start); 0702 for ( const auto& keyframe : opacity.keyframes() ) 0703 { 0704 anim_opacity.add_keyframe( 0705 time_to_global(keyframe.time), 0706 {QString::number(opacity.combine_value_at<float, float>(keyframe.time, opacity_func))}, 0707 keyframe.transition() 0708 ); 0709 } 0710 } 0711 } 0712 } 0713 0714 void write_repeater(model::Repeater* repeater, QDomElement& parent, bool force_draw) 0715 { 0716 int n_copies = repeater->max_copies(); 0717 if ( n_copies < 1 ) 0718 return; 0719 0720 QDomElement container = start_group(parent, repeater); 0721 QString base_id = id(repeater); 0722 QString prev_clone_id = base_id + "_0"; 0723 QDomElement og = element(container, "g"); 0724 og.setAttribute("id", prev_clone_id); 0725 for ( const auto& sib : repeater->affected() ) 0726 write_shape(og, sib, force_draw); 0727 0728 write_repeater_vis(og, repeater, 0, n_copies); 0729 0730 for ( int i = 1; i < n_copies; i++ ) 0731 { 0732 QString clone_id = base_id + "_" + QString::number(i);; 0733 QDomElement use = element(container, "use"); 0734 use.setAttribute("xlink:href", "#" + prev_clone_id); 0735 use.setAttribute("id", clone_id); 0736 write_repeater_vis(use, repeater, i, n_copies); 0737 transform_to_attr(use, repeater->transform.get()); 0738 prev_clone_id = clone_id; 0739 } 0740 } 0741 0742 void write_shape(QDomElement& parent, model::ShapeElement* shape, bool force_draw) 0743 { 0744 if ( auto grp = qobject_cast<model::Group*>(shape) ) 0745 { 0746 write_group_shape(parent, grp); 0747 } 0748 else if ( auto stroke = qobject_cast<model::Stroke*>(shape) ) 0749 { 0750 if ( stroke->visible.get() ) 0751 write_stroke(stroke, parent); 0752 } 0753 else if ( auto fill = qobject_cast<model::Fill*>(shape) ) 0754 { 0755 if ( fill->visible.get() ) 0756 write_fill(fill, parent); 0757 } 0758 else if ( auto img = qobject_cast<model::Image*>(shape) ) 0759 { 0760 write_image(img, parent); 0761 } 0762 else if ( auto layer = qobject_cast<model::PreCompLayer*>(shape) ) 0763 { 0764 write_precomp_layer(layer, parent); 0765 } 0766 else if ( auto repeater = qobject_cast<model::Repeater*>(shape) ) 0767 { 0768 write_repeater(repeater, parent, force_draw); 0769 } 0770 else if ( force_draw ) 0771 { 0772 write_shape_shape(parent, shape, {}); 0773 write_visibility_attributes(parent, shape); 0774 set_attribute(parent, "id", id(shape)); 0775 } 0776 } 0777 0778 QDomElement write_bezier(QDomElement& parent, model::ShapeElement* shape, const Style::Map& style) 0779 { 0780 QDomElement path = element(parent, "path"); 0781 write_style(path, style); 0782 QString d; 0783 QString nodetypes; 0784 std::tie(d, nodetypes) = path_data(shape->shapes(shape->time())); 0785 set_attribute(path, "d", d); 0786 set_attribute(path, "sodipodi:nodetypes", nodetypes); 0787 0788 if ( animated ) 0789 { 0790 std::vector<const model::AnimatableBase*> props; 0791 for ( auto prop : shape->properties() ) 0792 { 0793 if ( prop->traits().flags & model::PropertyTraits::Animated ) 0794 props.push_back(static_cast<model::AnimatableBase*>(prop)); 0795 } 0796 0797 model::JoinAnimatables j(std::move(props), model::JoinAnimatables::NoValues); 0798 0799 if ( j.animated() ) 0800 { 0801 AnimationData data(this, {"d"}, j.keyframes().size(), time_stretch, time_start); 0802 0803 for ( const auto& kf : j ) 0804 data.add_keyframe(time_to_global(kf.time), {path_data(shape->shapes(kf.time)).first}, kf.transition()); 0805 0806 data.add_dom(path); 0807 } 0808 } 0809 return path; 0810 } 0811 0812 /** 0813 * \brief Creates a <g> element for recurse_parents 0814 * \param parent DOM element to add the <g> into 0815 * \param ancestor Ancestor layer (to create the <g> for) 0816 * \param descendant Descendant layer 0817 */ 0818 QDomElement start_layer_recurse_parents(const QDomElement& parent, model::Layer* ancestor, model::Layer* descendant) 0819 { 0820 QDomElement g = element(parent, "g"); 0821 g.setAttribute("id", id(descendant) + "_" + id(ancestor)); 0822 g.setAttribute("inkscape:label", i18n("%1 (%2)", descendant->object_name(), ancestor->object_name())); 0823 g.setAttribute("inkscape:groupmode", "layer"); 0824 transform_to_attr(g, ancestor->transform.get()); 0825 return g; 0826 } 0827 0828 /** 0829 * \brief Creates nested <g> elements for each layer parent (using the parent property) 0830 * \param parent DOM element to add the elements into 0831 * \param ancestor Ancestor layer (searched recursively for parents) 0832 * \param descendant Descendant layer 0833 */ 0834 QDomElement recurse_parents(const QDomElement& parent, model::Layer* ancestor, model::Layer* descendant) 0835 { 0836 if ( !ancestor->parent.get() ) 0837 return start_layer_recurse_parents(parent, ancestor, descendant); 0838 return start_layer_recurse_parents(recurse_parents(parent, ancestor->parent.get(), descendant), ancestor, descendant); 0839 } 0840 0841 void write_group_shape(QDomElement& parent, model::Group* group) 0842 { 0843 QDomElement g; 0844 bool has_mask = false; 0845 if ( auto layer = group->cast<model::Layer>() ) 0846 { 0847 if ( !layer->render.get() ) 0848 return; 0849 0850 if ( layer->parent.get() ) 0851 { 0852 QDomElement parent_g = recurse_parents(parent, layer->parent.get(), layer); 0853 g = start_layer(parent_g, group); 0854 } 0855 else 0856 { 0857 g = start_layer(parent, group); 0858 } 0859 0860 if ( layer->mask->has_mask() ) 0861 { 0862 has_mask = true; 0863 0864 QDomElement clip = element(defs, "mask"); 0865 QString mask_id = "clip_" + id(layer); 0866 clip.setAttribute("id", mask_id); 0867 clip.setAttribute("mask-type", "alpha"); 0868 if ( layer->shapes.size() > 1 ) 0869 write_shape(clip, layer->shapes[0], false); 0870 0871 g.setAttribute("mask", "url(#" + mask_id + ")"); 0872 } 0873 0874 if ( animated && layer->visible.get() ) 0875 { 0876 auto* lay_range = layer->animation.get(); 0877 auto* doc_range = layer->owner_composition()->animation.get(); 0878 bool has_start = lay_range->first_frame.get() > doc_range->first_frame.get(); 0879 bool has_end = lay_range->last_frame.get() < doc_range->last_frame.get(); 0880 0881 if ( has_start || has_end ) 0882 { 0883 QDomElement animation = element(g, "animate"); 0884 animation.setAttribute("begin", clock(ip)); 0885 animation.setAttribute("dur", clock(op-ip)); 0886 animation.setAttribute("calcMode", "discrete"); 0887 animation.setAttribute("attributeName", "display"); 0888 animation.setAttribute("repeatCount", "indefinite"); 0889 QString times; 0890 QString vals; 0891 0892 times += "0;"; 0893 0894 if ( has_start ) 0895 { 0896 vals += "none;inline;"; 0897 times += unlerp_time(lay_range->first_frame.get()) + ";"; 0898 } 0899 else 0900 { 0901 vals += "inline;"; 0902 } 0903 0904 if ( has_end ) 0905 { 0906 vals += "none;"; 0907 times += unlerp_time(lay_range->last_frame.get()) + ";"; 0908 } 0909 0910 animation.setAttribute("values", vals); 0911 animation.setAttribute("keyTimes", times); 0912 } 0913 } 0914 } 0915 else 0916 { 0917 g = start_group(parent, group); 0918 } 0919 0920 transform_to_attr(g, group->transform.get(), group->auto_orient.get()); 0921 write_property(g, &group->opacity, "opacity"); 0922 write_visibility_attributes(g, group); 0923 write_shapes(g, group->shapes, has_mask); 0924 } 0925 0926 template<class PropT, class Callback> 0927 QDomElement transform_property( 0928 QDomElement& e, const char* name, PropT* prop, const Callback& callback, 0929 const QString& path = {}, bool auto_orient = false 0930 ) 0931 { 0932 model::JoinAnimatables j({prop}, model::JoinAnimatables::NoValues); 0933 0934 auto parent = e.parentNode(); 0935 QDomElement g = dom.createElement("g"); 0936 parent.insertBefore(g, e); 0937 parent.removeChild(e); 0938 g.appendChild(e); 0939 0940 if ( j.animated() ) 0941 { 0942 AnimationData data(this, {"transform"}, j.keyframes().size(), time_stretch, time_start); 0943 0944 if ( !path.isEmpty() ) 0945 { 0946 for ( const auto& kf : j ) 0947 data.add_keyframe(time_to_global(kf.time), {""}, kf.transition()); 0948 data.add_dom(g, "animateMotion", "", path, auto_orient); 0949 } 0950 else 0951 { 0952 0953 for ( const auto& kf : j ) 0954 data.add_keyframe(time_to_global(kf.time), {callback(prop->get_at(kf.time))}, kf.transition()); 0955 data.add_dom(g, "animateTransform", name); 0956 } 0957 } 0958 0959 g.setAttribute("transform", QString("%1(%2)").arg(name).arg(callback(prop->get()))); 0960 return g; 0961 } 0962 0963 void transform_to_attr(QDomElement& parent, model::Transform* transf, bool auto_orient = false) 0964 { 0965 if ( animated && (transf->position.animated() || transf->scale.animated() || transf->rotation.animated() || transf->anchor_point.animated()) ) 0966 { 0967 QDomElement subject = parent; 0968 subject = transform_property(subject, "translate", &transf->anchor_point, [](const QPointF& val){ 0969 return QString("%1 %2").arg(-val.x()).arg(-val.y()); 0970 }); 0971 subject = transform_property(subject, "scale", &transf->scale, [](const QVector2D& val){ 0972 return QString("%1 %2").arg(val.x()).arg(val.y()); 0973 }); 0974 subject = transform_property(subject, "rotate", &transf->rotation, [](qreal val){ 0975 return QString::number(val); 0976 }); 0977 math::bezier::MultiBezier mb; 0978 mb.beziers().push_back(transf->position.bezier()); 0979 subject = transform_property(subject, "translate", &transf->position, [](const QPointF& val){ 0980 return QString("%1 %2").arg(val.x()).arg(val.y()); 0981 }, path_data(mb).first, auto_orient); 0982 } 0983 else 0984 { 0985 auto matr = transf->transform_matrix(transf->time()); 0986 parent.setAttribute("transform", QString("matrix(%1, %2, %3, %4, %5, %6)") 0987 .arg(matr.m11()) 0988 .arg(matr.m12()) 0989 .arg(matr.m21()) 0990 .arg(matr.m22()) 0991 .arg(matr.m31()) 0992 .arg(matr.m32()) 0993 ); 0994 } 0995 } 0996 0997 void write_style(QDomElement& element, const Style::Map& s) 0998 { 0999 QString st; 1000 for ( auto it : s ) 1001 { 1002 st.append(it.first); 1003 st.append(':'); 1004 st.append(it.second); 1005 st.append(';'); 1006 } 1007 element.setAttribute("style", st); 1008 } 1009 1010 QDomElement start_group(QDomElement& parent, model::DocumentNode* node) 1011 { 1012 QDomElement g = element(parent, "g"); 1013 g.setAttribute("id", id(node)); 1014 g.setAttribute("inkscape:label", node->object_name()); 1015 return g; 1016 } 1017 1018 QDomElement start_layer(QDomElement& parent, model::DocumentNode* node) 1019 { 1020 auto g = start_group(parent, node); 1021 g.setAttribute("inkscape:groupmode", "layer"); 1022 return g; 1023 } 1024 1025 QString id(model::DocumentNode* node) 1026 { 1027 return node->type_name() + "_" + node->uuid.get().toString(QUuid::Id128); 1028 } 1029 1030 /// Avoid locale nonsense by defining these functions (on ASCII chars) manually 1031 static constexpr bool valid_id_start(char c) noexcept 1032 { 1033 return ( c >= 'a' && c <= 'z') || 1034 ( c >= 'A' && c <= 'Z') || 1035 c == '_'; 1036 } 1037 1038 static constexpr bool valid_id(char c) noexcept 1039 { 1040 return valid_id_start(c) || 1041 ( c >= '0' && c <= '9') || 1042 c == '-'; 1043 } 1044 1045 void write_named_color(QDomElement& parent, model::NamedColor* color) 1046 { 1047 auto gradient = element(parent, "linearGradient"); 1048 gradient.setAttribute("osb:paint", "solid"); 1049 QString id = pretty_id(color->name.get(), color); 1050 non_uuid_ids_map[color] = id; 1051 gradient.setAttribute("id", id); 1052 1053 auto stop = element(gradient, "stop"); 1054 stop.setAttribute("offset", "0"); 1055 write_property(stop, &color->color, "stop-color"); 1056 } 1057 1058 QString pretty_id(const QString& s, model::DocumentNode* node) 1059 { 1060 if ( s.isEmpty() ) 1061 return id(node); 1062 1063 QByteArray str = s.toLatin1(); 1064 QString id_attempt; 1065 if ( !valid_id_start(str[0]) ) 1066 id_attempt.push_back('_'); 1067 1068 for ( char c : str ) 1069 { 1070 if ( c == ' ' ) 1071 id_attempt.push_back('_'); 1072 else if ( valid_id(c) ) 1073 id_attempt.push_back(c); 1074 } 1075 1076 if ( id_attempt.isEmpty() ) 1077 return id(node); 1078 1079 QString id_final = id_attempt; 1080 int i = 1; 1081 while ( non_uuid_ids.count(id_final) ) 1082 id_final = id_attempt + QString::number(i++); 1083 1084 return id_final; 1085 } 1086 1087 template<class T> 1088 std::enable_if_t<std::is_arithmetic_v<T> && !std::is_same_v<T, bool>> 1089 set_attribute(QDomElement& e, const QString& name, T val) 1090 { 1091 // not using e.setAttribute overloads to bypass locale settings 1092 e.setAttribute(name, QString::number(val)); 1093 } 1094 1095 void set_attribute(QDomElement& e, const QString& name, bool val) 1096 { 1097 e.setAttribute(name, val ? "true" : "false"); 1098 } 1099 1100 void set_attribute(QDomElement& e, const QString& name, const char* val) 1101 { 1102 e.setAttribute(name, val); 1103 } 1104 1105 void set_attribute(QDomElement& e, const QString& name, const QString& val) 1106 { 1107 e.setAttribute(name, val); 1108 } 1109 1110 1111 void write_gradient_colors(QDomElement& parent, model::GradientColors* gradient) 1112 { 1113 auto e = element(parent, "linearGradient"); 1114 QString id = pretty_id(gradient->name.get(), gradient); 1115 non_uuid_ids_map[gradient] = id; 1116 e.setAttribute("id", id); 1117 1118 if ( animated && gradient->colors.keyframe_count() > 1 ) 1119 { 1120 int n_stops = std::numeric_limits<int>::max(); 1121 for ( const auto& kf : gradient->colors ) 1122 if ( kf.get().size() < n_stops ) 1123 n_stops = kf.get().size(); 1124 1125 auto stops = gradient->colors.get(); 1126 for ( int i = 0; i < n_stops; i++ ) 1127 { 1128 AnimationData data(this, {"offset", "stop-color"}, gradient->colors.keyframe_count(), time_stretch, time_start); 1129 for ( const auto& kf : gradient->colors ) 1130 { 1131 auto stop = kf.get()[i]; 1132 data.add_keyframe( 1133 time_to_global(kf.time()), 1134 {QString::number(stop.first), stop.second.name()}, 1135 kf.transition() 1136 ); 1137 } 1138 1139 auto s = element(e, "stop"); 1140 s.setAttribute("stop-opacity", "1"); 1141 set_attribute(s, "offset", stops[i].first); 1142 s.setAttribute("stop-color", stops[i].second.name()); 1143 data.add_dom(s); 1144 } 1145 } 1146 else 1147 { 1148 for ( const auto& stop : gradient->colors.get() ) 1149 { 1150 auto s = element(e, "stop"); 1151 s.setAttribute("stop-opacity", "1"); 1152 set_attribute(s, "offset", stop.first); 1153 s.setAttribute("stop-color", stop.second.name()); 1154 } 1155 } 1156 } 1157 1158 void write_gradient(QDomElement& parent, model::Gradient* gradient) 1159 { 1160 QDomElement e; 1161 if ( gradient->type.get() == model::Gradient::Radial || gradient->type.get() == model::Gradient::Conical ) 1162 { 1163 e = element(parent, "radialGradient"); 1164 write_properties(e, {&gradient->start_point}, {"cx", "cy"}, &Private::callback_point); 1165 write_properties(e, {&gradient->highlight}, {"fx", "fy"}, &Private::callback_point); 1166 1167 write_properties(e, {&gradient->start_point, &gradient->end_point}, {"r"}, 1168 [](const std::vector<QVariant>& values) -> std::vector<QString> { 1169 return { QString::number( 1170 math::length(values[1].toPointF() - values[0].toPointF()) 1171 )}; 1172 }); 1173 } 1174 else 1175 { 1176 e = element(parent, "linearGradient"); 1177 write_properties(e, {&gradient->start_point}, {"x1", "y1"}, &Private::callback_point); 1178 write_properties(e, {&gradient->end_point}, {"x2", "y2"}, &Private::callback_point); 1179 } 1180 1181 QString id = pretty_id(gradient->name.get(), gradient); 1182 non_uuid_ids_map[gradient] = id; 1183 e.setAttribute("id", id); 1184 e.setAttribute("gradientUnits", "userSpaceOnUse"); 1185 1186 auto it = non_uuid_ids_map.find(gradient->colors.get()); 1187 if ( it != non_uuid_ids_map.end() ) 1188 e.setAttribute("xlink:href", "#" + it->second); 1189 } 1190 1191 QString clock(model::FrameTime time) 1192 { 1193 return QString::number(time / fps, 'f'); 1194 } 1195 1196 std::vector<model::StretchableTime*> timing; 1197 QDomDocument dom; 1198 qreal fps = 60; 1199 qreal ip = 0; 1200 qreal op = 60; 1201 bool at_start = true; 1202 std::set<QString> non_uuid_ids; 1203 std::map<model::DocumentNode*, QString> non_uuid_ids_map; 1204 AnimationType animated; 1205 QDomElement svg; 1206 QDomElement defs; 1207 CssFontType font_type; 1208 qreal time_stretch = 1; 1209 model::FrameTime time_start = 0; 1210 }; 1211 1212 1213 io::svg::SvgRenderer::SvgRenderer(AnimationType animated, CssFontType font_type) 1214 : d(std::make_unique<Private>()) 1215 { 1216 d->animated = animated; 1217 d->font_type = font_type; 1218 d->svg = d->dom.createElement("svg"); 1219 d->dom.appendChild(d->svg); 1220 d->svg.setAttribute("xmlns", detail::xmlns.at("svg")); 1221 for ( const auto& p : detail::xmlns ) 1222 { 1223 if ( !p.second.contains("android") ) 1224 d->svg.setAttribute("xmlns:" + p.first, p.second); 1225 } 1226 1227 d->write_style(d->svg, { 1228 {"fill", "none"}, 1229 {"stroke", "none"} 1230 }); 1231 d->svg.setAttribute("inkscape:export-xdpi", "96"); 1232 d->svg.setAttribute("inkscape:export-ydpi", "96"); 1233 d->svg.setAttribute("version", "1.1"); 1234 } 1235 1236 io::svg::SvgRenderer::~SvgRenderer() 1237 { 1238 } 1239 1240 void io::svg::SvgRenderer::write_composition(model::Composition* comp) 1241 { 1242 d->collect_defs(comp); 1243 auto g = d->start_layer(d->svg, comp); 1244 d->write_composition(g, comp); 1245 } 1246 1247 1248 void io::svg::SvgRenderer::write_main(model::Composition* comp) 1249 { 1250 if ( d->at_start ) 1251 { 1252 QString w = QString::number(comp->width.get()); 1253 QString h = QString::number(comp->height.get()); 1254 d->svg.setAttribute("width", w); 1255 d->svg.setAttribute("height", h); 1256 d->svg.setAttribute("viewBox", QString("0 0 %1 %2").arg(w).arg(h)); 1257 d->svg.appendChild(d->dom.createElement("title")).appendChild(d->dom.createTextNode(comp->name.get())); 1258 write_composition(comp); 1259 } 1260 else 1261 { 1262 write_composition(comp); 1263 } 1264 } 1265 1266 void io::svg::SvgRenderer::write_shape(model::ShapeElement* shape) 1267 { 1268 d->collect_defs(shape->owner_composition()); 1269 d->write_shape(d->svg, shape, true); 1270 } 1271 1272 void io::svg::SvgRenderer::write_node(model::DocumentNode* node) 1273 { 1274 if ( auto co = qobject_cast<model::Composition*>(node) ) 1275 write_main(co); 1276 else if ( auto sh = qobject_cast<model::ShapeElement*>(node) ) 1277 write_shape(sh); 1278 } 1279 1280 QDomDocument io::svg::SvgRenderer::dom() const 1281 { 1282 return d->dom; 1283 } 1284 1285 void io::svg::SvgRenderer::write(QIODevice* device, bool indent) 1286 { 1287 device->write(d->dom.toByteArray(indent ? 4 : -1)); 1288 } 1289 1290 glaxnimate::io::svg::CssFontType glaxnimate::io::svg::SvgRenderer::suggested_type(model::EmbeddedFont* font) 1291 { 1292 if ( !font->css_url.get().isEmpty() ) 1293 return CssFontType::Link; 1294 if ( !font->source_url.get().isEmpty() ) 1295 return CssFontType::FontFace; 1296 if ( !font->data.get().isEmpty() ) 1297 return CssFontType::Embedded; 1298 return CssFontType::None; 1299 } 1300 1301 static char bezier_node_type(const math::bezier::Point& p) 1302 { 1303 switch ( p.type ) 1304 { 1305 case math::bezier::PointType::Smooth: 1306 return 's'; 1307 case math::bezier::PointType::Symmetrical: 1308 return 'z'; 1309 case math::bezier::PointType::Corner: 1310 default: 1311 return 'c'; 1312 } 1313 } 1314 1315 std::pair<QString, QString> glaxnimate::io::svg::path_data(const math::bezier::MultiBezier& shape) 1316 { 1317 QString d; 1318 QString nodetypes; 1319 for ( const math::bezier::Bezier& b : shape.beziers() ) 1320 { 1321 if ( b.empty() ) 1322 continue; 1323 1324 d += QString("M %1,%2 C").arg(b[0].pos.x()).arg(b[0].pos.y()); 1325 nodetypes += bezier_node_type(b[0]); 1326 1327 for ( int i = 1; i < b.size(); i++ ) 1328 { 1329 d += QString(" %1,%2 %3,%4 %5,%6") 1330 .arg(b[i-1].tan_out.x()).arg(b[i-1].tan_out.y()) 1331 .arg(b[i].tan_in.x()).arg(b[i].tan_in.y()) 1332 .arg(b[i].pos.x()).arg(b[i].pos.y()) 1333 ; 1334 nodetypes += bezier_node_type(b[i]); 1335 } 1336 1337 if ( b.closed() ) 1338 { 1339 d += QString(" %1,%2 %3,%4 %5,%6") 1340 .arg(b.back().tan_out.x()).arg(b.back().tan_out.y()) 1341 .arg(b[0].tan_in.x()).arg(b[0].tan_in.y()) 1342 .arg(b[0].pos.x()).arg(b[0].pos.y()) 1343 ; 1344 d += " Z"; 1345 } 1346 } 1347 return {d, nodetypes}; 1348 }